OriginChainDB docs
examples · graph · 16 / 17

16. node2vec - train embeddings, then query them

← Graph examples

what this does

node2vec learns a vector for every node from biased random walks, so nodes sitting in similar parts of the graph end up close together. It is two calls: POST /v1/tenants/:t/graph/:schema/node2vec trains, and GET /v1/tenants/:t/graph/:schema/node2vec/:rel/topk answers similarity queries. The second only works if the first was made with persist: true - without it the vectors come back in the response and nothing is stored.

when to use it

  • "Nodes like this one", where similarity should come from graph position rather than from text or image content.
  • Link prediction and recommendation over a relation you already model as a foreign key.
  • Feeding graph structure into a downstream model as a fixed-width vector, without writing a walk sampler yourself.

schema requirement

The relation must be declared in the schema's [[relations]] block. node2vec needs no feature columns - it learns from structure alone. See schemas/reference#relations.

step 1 of 2 - train and persist

rel is the only required field; every other knob has a default, so a body of just the relation name is a complete request. Set persist: true to store the vectors under the schema and relation so the topk route becomes available. Without it the call is a pure computation: it trains in memory, returns the vectors and writes nothing.

POST /v1/tenants/:t/graph/:schema/node2vec
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/graph/social.users/node2vec" \
  -H "Authorization: Bearer $OC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "rel":            "follows",
    "dim":            64,
    "walks_per_node": 10,
    "walk_length":    40,
    "window_size":    5,
    "epochs":         5,
    "seed":           42,
    "persist":        true
  }'

what training returns

{
  "embeddings": {
    "alice": [0.0142, -0.0318, 0.0091],
    "bob":   [-0.0077, 0.0264, -0.0155],
    "carol": [0.0203, -0.0119, 0.0288]
  },
  "vocab_size":     11,
  "training_pairs": 4820,
  "final_loss":     0.6931,
  "dim":            64,
  "persisted":      true
}

embeddings maps each node's primary key to its vector, and each vector is dim floats long - they are shortened above to keep the example readable. vocab_size is how many nodes appeared in a walk, training_pairs how many skip-gram pairs were consumed, and final_loss the loss at the end of the last epoch. persisted mirrors the request flag and is true only when the blob was actually written, so you can tell whether the next call will work without probing for it.

step 2 of 2 - ask for similar nodes

The relation name moves into the path here, because that is how the persisted blob is keyed. query is the node you are asking about and k is how many neighbours you want. metric accepts cosine, dot, l2 or manhattan and defaults to cosine.

GET /v1/tenants/:t/graph/:schema/node2vec/:rel/topk
curl -G "https://$OC_HOST/v1/tenants/$OC_TENANT/graph/social.users/node2vec/follows/topk" \
  --data-urlencode "query=alice" \
  --data-urlencode "k=5" \
  --data-urlencode "metric=cosine" \
  -H "Authorization: Bearer $OC_TOKEN"

what the query returns

[
  { "pk": "bob",   "score": 0.94 },
  { "pk": "carol", "score": 0.91 },
  { "pk": "dave",  "score": 0.72 }
]

A flat array of { pk, score }, best first, with the query node itself excluded. pk is a plain string here, not the primary-key array the algorithm endpoints return. Fewer than k hits means the persisted set is smaller than k; asking for more than the engine's node ceiling truncates the result rather than failing the call.

how it works

  • Training samples biased random walks from every node, then runs skip-gram with negative sampling over the resulting sequences. p and q bias the walks exactly as they do on the random-walk endpoint.
  • The same seed, graph and configuration produce byte-identical vectors, so a retrain is reproducible.
  • persist: true writes one blob per schema and relation, and the next persist replaces it atomically. What is stored is the trained vectors themselves rather than the recipe - replaying training elsewhere would not land on the same numbers.
  • The response is capped at one million floats, which is vocab_size times dim. Above that the call is refused with a 400, because a body that large stops being something a normal gateway will carry.
  • dim above 1024 is refused, and so is a walk budget above ten million, where the budget is walks_per_node multiplied by walk_length.

common mistakes

  • Forgetting persist: true. Training without it returns the vectors and stores nothing, and the topk route then answers 503 telling you to post with persist first. That 503 is a missing prerequisite, not an outage.
  • Putting the relation in the query string on the topk call. It belongs in the path, as .../node2vec/<rel>/topk.
  • Misspelling a knob. Unknown request fields are dropped rather than rejected by default, so walk_len instead of walk_length trains at the default and still returns 200. Check the names against the body above.
  • Comparing scores across metrics. A cosine similarity and an l2 distance are not on the same scale. Pick one metric and stay with it.