9. components - split the graph into islands
← Graph exampleswhat this does
components partitions every row under a schema into connected islands. GET /v1/tenants/:t/graph/:schema/components walks rel as undirected, runs union-find over the whole node universe, and returns one entry per node carrying the id of the component it landed in. Every row comes back, including rows with no edges at all - those are components of one.
when to use it
- "Is this one network or several?" - the first thing to establish before running any global algorithm over the graph.
- Orphan detection. A node alone in its component has no relationships at all, which is usually either a data-quality problem or the answer you were looking for.
- Partitioning work. Run an expensive per-island analysis on each component independently instead of on the whole graph at once.
schema requirement
The relation must be declared in the schema's [[relations]] block and must target the table it is declared on. See schemas/reference#relations.
the request
rel names the relation. The optional include_rows=true switch also echoes each row's full body alongside its component id; leave it off unless you need the columns, because the default response carries only what the algorithm actually computed.
curl -G "https://$OC_HOST/v1/tenants/$OC_TENANT/graph/social.users/components" \
--data-urlencode "rel=follows" \
-H "Authorization: Bearer $OC_TOKEN"# The Python SDK does not wrap this endpoint - call it directly.
import httpx
r = httpx.get(
f"https://{OC_HOST}/v1/tenants/{OC_TENANT}/graph/social.users/components",
params={"rel": "follows"},
headers={"Authorization": f"Bearer {OC_TOKEN}"},
)
for row in r.json():
print(row["pk"][0], "->", row["_component"])// The TypeScript SDK does not wrap the graph algorithms - use fetch.
const qs = new URLSearchParams({ rel: "follows" });
const res = await fetch(
`https://${OC_HOST}/v1/tenants/${OC_TENANT}/graph/social.users/components?${qs}`,
{ headers: { Authorization: `Bearer ${OC_TOKEN}` } },
);
const islands = new Map<string, string[]>();
for (const row of await res.json()) {
const key = row._component;
islands.set(key, [...(islands.get(key) ?? []), row.pk[0]]);
}// The Go SDK does not wrap the graph algorithms - use net/http.
url := "https://" + OC_HOST + "/v1/tenants/" + OC_TENANT +
"/graph/social.users/components?rel=follows"
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+OC_TOKEN)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var rows []struct {
PK []string `json:"pk"`
Component string `json:"_component"`
}
json.NewDecoder(resp.Body).Decode(&rows)what you get back
[
{ "pk": ["alice"], "_component": "[\"alice\"]" },
{ "pk": ["bob"], "_component": "[\"alice\"]" },
{ "pk": ["carol"], "_component": "[\"alice\"]" },
{ "pk": ["heidi"], "_component": "[\"heidi\"]" },
{ "pk": ["ivan"], "_component": "[\"heidi\"]" },
{ "pk": ["judy"], "_component": "[\"heidi\"]" },
{ "pk": ["mallory"], "_component": "[\"mallory\"]" }
]
One entry per node. pk is the node's primary-key array. _component is the canonical JSON encoding of the component root's primary key, which makes it a string containing a JSON array, not an array. Treat it as an opaque grouping key: nodes that share it are in the same island. With include_rows=true each entry additionally carries the row's own columns, next to the same _component field.
how it works
- Union-find over the schema's whole node universe, with every
reledge treated as undirected. - Each row is emitted exactly once whether or not it has edges, so the number of distinct
_componentvalues is the number of islands. - The compact
{pk, _component}shape is the default because the algorithm's entire answer is one id per node. Echoing row bodies made the response grow with row width without adding information. include_rows=truerestores the earlier verbatim-row shape for callers that depended on it. The partitioning is identical either way - only the payload changes.
common mistakes
- Treating
_componentas a key you can look up. It is a canonical-JSON string, not a bare primary key and not an integer. Group on it; do not try to resolve it back to a row. - Reaching for
include_rows=trueby habit. It multiplies the response by your row width. Get the partition first, then fetch only the rows you actually want by primary key. - Expecting isolated rows to be absent. A row with no edges is a component of one and is present in the response.