OriginChain docs
examples · atomic · 3 / 5

3. Social user (row + vector + graph self-relation)

← Atomic multi-shape
what this does

Save a user as a row in social.users, embed their bio for "find similar users", and model the follow graph as a follows list column on that same row. A [[relations]] self-relation turns each element of the list into a graph edge (alice → bob, alice → carol), and bidirectional = true maintains the reverse side so "who follows me?" is a graph read.

when to use it
  • "People you may know" - similar-user lookup over bio embeddings, intersected with friends-of-friends from the follow graph.
  • Reverse-adjacency reads like "who follows me?" are a graph walk, not an index scan, because the bidirectional relation maintains both sides.
  • Any social, professional, or collaboration network with self-relations between users.
the schema (one table)

Push this once. The follows list column plus the self-relation is the whole graph - no separate edge table to keep in sync.

# social/users.toml - one table models the profile AND the graph.
namespace   = "social"
table       = "users"
primary_key = ["id"]

[[columns]]
name = "id"
ty   = "str"
required = true

[[columns]]
name = "handle"
ty   = "str"
required = true

[[columns]]
name = "bio"
ty   = "str"

[[columns]]
name = "joined_ms"
ty   = "u64"

# The follow graph lives in a list column on the row. Each element is a
# user id this person follows.
[[columns]]
name    = "follows"
ty      = "list"
element = "str"

[[indexes]]
name    = "by_handle"
columns = ["handle"]

# A self-relation turns the 'follows' list into graph edges: one forward
# edge per element (alice -> bob, alice -> carol). bidirectional = true also
# maintains the reverse side, so "who follows bob?" is a graph read, not a scan.
[[relations]]
name          = "following"
from_col      = "follows"
bidirectional = true

[relations.target]
namespace = "social"
table     = "users"
pk        = "id"
call 1 of 3 - the user row (profile + follows)

The follows array lands as graph edges in the same write. To follow or unfollow later, re-put the row with the updated list - the edges are recomputed from the new list on each write.

POST /v1/tenants/:t/rows/social.users
curl -X POST "$ORIGINCHAIN_URL/v1/tenants/$T/rows/social.users" \
  -H "Authorization: Bearer $OC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id":        "alice",
    "handle":    "@alice",
    "bio":       "Mechanical engineer. Trail runner. Reading sci-fi.",
    "joined_ms": 1746180851000,
    "follows":   ["bob", "carol"]
  }'
call 2 of 3 - the profile embedding

Use the same id as the user row. Now /vector/social.users/topk returns similar users by bio.

POST /v1/tenants/:t/vector/social.users/put
curl -X POST "$ORIGINCHAIN_URL/v1/tenants/$T/vector/social.users/put" \
  -H "Authorization: Bearer $OC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id":        "alice",
    "embedding": [0.0182, -0.0712, 0.0419, /* ... 768 floats ... */],
    "dim":       768,
    "metric":    "cosine"
  }'
call 3 of 3 - traverse the follow graph

neighbors walks one hop forward along the relation (who alice follows); reverse walks the back-edge (who follows bob). Both are direct adjacency-list lookups - no scan, no join.

GET /v1/tenants/:t/graph/social.users/{neighbors,reverse}?rel=following&pk=...
# Who does alice follow? (one hop forward along 'following')
curl "$ORIGINCHAIN_URL/v1/tenants/$T/graph/social.users/neighbors?rel=following&pk=alice" \
  -H "Authorization: Bearer $OC_TOKEN"
# → ["bob", "carol"]

# Who follows bob? (reverse side, maintained by bidirectional = true)
curl "$ORIGINCHAIN_URL/v1/tenants/$T/graph/social.users/reverse?rel=following&pk=bob" \
  -H "Authorization: Bearer $OC_TOKEN"
# → ["alice"]
about atomicity

The user row and the profile embedding are separate calls - each atomic by itself. There is no single "write everything" endpoint. The row write (including its follows edges) commits atomically, and the SDKs auto-attach an Idempotency-Key on every mutating call, so if the vector put fails after the row succeeded, retry just that one.

common mistakes
  • Modeling follows as a composite-PK edge table. A follows table keyed [follower_id, followee_id] can't be traversed with neighbors(pk="alice") - the source of each edge is the whole composite key, not a single user. Put the relation on the users row via a follows list instead.
  • Forgetting the reverse direction. Set bidirectional = true so "who follows bob?" is a reverse read. Without it you can only walk forward.
  • Partial re-put on follow/unfollow. Edges are recomputed from the follows list on each write, so re-put the row with the full new list, not just the delta.
  • Updating handle without re-embedding. If you embed the handle and let users edit it, the vector goes stale until you re-put. Either re-put on every profile edit or only embed the bio.