1. Product catalog (row + vector + FTS + graph edge)
← Atomic multi-shape
Save one product across four shapes - the structured row in shop.products, an embedding of the name + description for similarity search, a BM25 full-text index on the description for keyword search, and a graph edge to its supplier. The supplier edge is the only one you don't write by hand: declaring supplier_id as a [[relations]] column in the schema makes the row write also create the edge in the same commit.
- E-commerce catalogs where one product needs to be findable by keyword, by similarity, and by a graph walk (e.g. "all products from suppliers in this region").
- Any domain object you want to query in more than one shape - the four calls run once per object and then every read path works.
Push this once with /v1/schemas before the first write. Note the [[relations]] block - that's what makes the supplier edge automatic.
# shop/products.toml
namespace = "shop"
table = "products"
primary_key = ["id"]
[[columns]]
name = "id"
ty = "str"
required = true
[[columns]]
name = "name"
ty = "str"
required = true
[[columns]]
name = "description"
ty = "str"
[[columns]]
name = "price_cents"
ty = "i64"
[[columns]]
name = "category"
ty = "str"
[[columns]]
name = "supplier_id"
ty = "str"
required = true
[[indexes]]
name = "by_category"
columns = ["category"]
# Declaring supplier_id as a relation makes the row write also
# create a graph edge from this product to its supplier.
[[relations]]
name = "supplied_by"
from_col = "supplier_id"
bidirectional = true
[relations.target]
namespace = "shop"
table = "suppliers"
pk = "id" curl -X POST "$ORIGINCHAIN_URL/v1/tenants/$T/rows/shop.products" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "sku-9281",
"name": "Trailblazer 2",
"description": "All-terrain running shoe with carbon-plate forefoot.",
"price_cents": 12900,
"category": "running-shoes",
"supplier_id": "sup-acme-shoes"
}'db.rows.put("shop.products", {
"id": "sku-9281",
"name": "Trailblazer 2",
"description": "All-terrain running shoe with carbon-plate forefoot.",
"price_cents": 12900,
"category": "running-shoes",
"supplier_id": "sup-acme-shoes",
})
# The supplied_by edge to sup-acme-shoes is written in the same commit -
# the [[relations]] block in the schema makes that automatic.// The TypeScript SDK does not wrap row writes yet
// (shipping in the next release). Use `fetch` for now -
// it is exactly the same HTTP call the SDK will make.
await fetch(`${BASE_URL}/v1/tenants/${TENANT}/rows/shop.products`, {
method: "POST",
headers: {
"Authorization": `Bearer ${OC_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "sku-9281",
name: "Trailblazer 2",
description: "All-terrain running shoe with carbon-plate forefoot.",
price_cents: 12900,
category: "running-shoes",
supplier_id: "sup-acme-shoes",
}),
});// The Go SDK does not wrap row writes yet
// (shipping in the next release). Use net/http for now.
body, _ := json.Marshal(map[string]any{
"id": "sku-9281",
"name": "Trailblazer 2",
"description": "All-terrain running shoe with carbon-plate forefoot.",
"price_cents": 12900,
"category": "running-shoes",
"supplier_id": "sup-acme-shoes",
})
req, _ := http.NewRequestWithContext(ctx, "POST",
BASE_URL+"/v1/tenants/"+TENANT+"/rows/shop.products",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+OC_TOKEN)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
The id here is the same primary key as the row. Same key = same logical product across shapes.
curl -X POST "$ORIGINCHAIN_URL/v1/tenants/$T/vector/shop.products/put" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "sku-9281",
"embedding": [0.0124, -0.0883, 0.0451, /* ... 768 floats ... */],
"dim": 768,
"metric": "cosine",
"metadata": { "category": "running-shoes" }
}'db.vector.put(
"shop.products",
"sku-9281",
embedding_768d, # name+description embedded together
metadata={ "category": "running-shoes" },
)await db.vectorPut("shop.products", {
id: "sku-9281",
embedding: embedding768d,
dim: 768,
metric: "cosine",
metadata: { category: "running-shoes" },
});err := db.VectorPut(ctx, "shop.products", originchain.VectorPutRequest{
ID: "sku-9281",
Embedding: embedding768d,
Dim: 768,
Metric: "cosine",
Metadata: map[string]any{
"category": "running-shoes",
},
}) doc_id matches the row's primary key so search results can be joined back to the row.
curl -X POST "$ORIGINCHAIN_URL/v1/tenants/$T/fts/shop.products/index" \
-H "Authorization: Bearer $OC_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"field": "description",
"doc_id": "sku-9281",
"text": "Trailblazer 2 all-terrain running shoe with carbon-plate forefoot"
}'db.fts.index(
"shop.products",
"description",
doc_id="sku-9281",
text="Trailblazer 2 all-terrain running shoe with carbon-plate forefoot",
)await db.ftsIndex("shop.products", {
field: "description",
docId: "sku-9281",
text: "Trailblazer 2 all-terrain running shoe with carbon-plate forefoot",
});err := db.FTSIndex(ctx, "shop.products", originchain.FTSIndexRequest{
Field: "description",
DocID: "sku-9281",
Text: "Trailblazer 2 all-terrain running shoe with carbon-plate forefoot",
})
The three calls are separate. There is no single "write everything" endpoint. Each call is atomic by itself - the row write either commits or doesn't, the vector put either commits or doesn't. Every mutating call gets an auto-attached Idempotency-Key from the SDK, so if the FTS call fails after the row and vector succeeded, you can safely retry just the FTS one without re-writing the others.
- Forgetting one of the three calls. If you skip the vector put, similarity search won't return this product. If you skip the FTS index, keyword search won't. The row write doesn't fan out to the other shapes.
- Updating one without re-doing the others. If you re-put the row with a new description, the vector and FTS index still point at the old text until you re-put / re-index them too.
- Mismatched IDs. The
idin the row write, the vector put, and the FTSdoc_idmust all be the same string. If they drift, you can't join search hits back to the row. - Writing the supplier edge by hand. Don't. Let the
[[relations]]block do it. If you also POST to/graph/edges, you'll get duplicate edges on every row update.