Monday, July 21, 2025

Finding Watchers


Now I want to turn it around the other way and consider what happens when a stock price changes.

On this occasion, I have a fixed stock in mind, and want to find all the users who are watching that stock so that I can notify them that the price has changed.

Going back to curl, we can come up with a suitable query:
curl https://user-stocks.cluster-ckgvna81hufy.us-east-1.neptune.amazonaws.com:8182/openCypher -d 'query=MATCH (u:User)-[r]->(s:Stock {symbol:$symbol}) RETURN u.username, s.symbol' -d 'parameters={"symbol": "UPM6"}'
{
  "results": [{
      "u.username": "user003",
      "s.symbol": "UPM6"
    }, {
      "u.username": "user015",
      "s.symbol": "UPM6"
    }]
}
If you're surprised that user003 showed up here, don't be. I obtained this stock from the list we generated in the last episode - which was a list of stocks being watched by user003. It would be very strange if it did not show up.

So let's repeat our trick from last time and put that into code, starting with a main:
package main

import (
    "fmt"
    "log"
    "os"
    "slices"

    "github.com/gmmapowell/ignorance/neptune/internal/neptune"
)

func main() {
    if len(os.Args) < 2 {
        log.Printf("Usage: watchers <stock>")
        return
    }
    stock := os.Args[1]
    watchers, err := neptune.FindStockWatchers("user-stocks", stock)
    if err != nil {
        panic(err)
    }
    if len(watchers) == 0 {
        fmt.Printf("no watchers found\n")
        return
    }
    slices.Sort(watchers)
    fmt.Printf("Stock %s watched by:\n", stock)
    for _, w := range watchers {
        fmt.Printf("  %s\n", w)
    }
}

NEPTUNE_FIND_WATCHERS:neptune/cmd/watchers/main.go

This is simpler than before, since we have no need to go to dynamo to find out more information about the stock - since we already started with the stock in hand.

The code to query Neptune is basically just a copy-and-paste of the query to go the other way with the appropriate changes for the query above. I have highlighted the lines that are different.
package neptune

import (
    "context"
    "fmt"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/service/neptunedata"
)

func FindStockWatchers(db string, stock string) ([]string, error) {
    svc, err := openNeptune(db)
    if err != nil {
        return nil, err
    }
    query := `
    MATCH (u:User)-[r]->(s:Stock {symbol:$symbol})
    RETURN u.username, s.symbol
    `
    params := fmt.Sprintf(`{"symbol": "%s"}`, stock)
    linkQuery := neptunedata.ExecuteOpenCypherQueryInput{OpenCypherQuery: aws.String(query), Parameters: aws.String(params)}
    out, err := svc.ExecuteOpenCypherQuery(context.TODO(), &linkQuery)
    if err != nil {
        return nil, err
    }

    results, err := unpack(out.Results)
    if err != nil {
        return nil, err
    }
    var ret []string
    for _, m := range results {
        ret = append(ret, m["u.username"].(string))
    }

    return ret, nil
}

NEPTUNE_FIND_WATCHERS:neptune/internal/neptune/findWatchers.go

Yes, I agree: if they are so similar, we should probably refactor them to extract the commonalities. On this occasion, I would rather not do that because I'm not exactly sure what the commonalities are and what patterns I would use to put them back together. In short, I'm not sure how much shorter I could make it, and I think the loss of clarity would not be worth it.

And when we're done we end up with:
$ AWS_PROFILE=ziniki-admin cmd/watchers/watchers UPM6
Stock UPM6 watched by:
  user003
  user015

Conclusion

Once you've got things started, it's really easy to run Neptune queries. I'm still struggling with identifying and fixing syntax errors in the openCypher language, mainly because the error messages seem opaque. But as my understanding of the language grows, I'm getting better at it.

No comments:

Post a Comment