10. louvain - modularity community detection
← Graph exampleswhat this does
louvain runs two-phase modularity-greedy community detection over one table's relation graph. GET /v1/tenants/:t/graph/:schema/louvain returns a communities envelope with one entry per row: the node's primary key and the integer id of the community it settled into. Where components only separates nodes that cannot reach each other at all, louvain finds the densely connected groups inside a single connected island.
when to use it
- Segmenting an interaction graph into groups that genuinely talk to each other, rather than groups that merely happen to be reachable.
- Topic clustering over a citation or co-occurrence graph, where the grouping should come from the edges rather than from a label you already assigned.
- A first pass before an expensive per-group model: run louvain, then treat each community as an independent unit of work.
schema requirement
The relation must be declared in the schema's [[relations]] block and must target the table it is declared on. louvain refuses a relation that points at another table, and the error names both tables. See schemas/reference#relations.
the request
rel is the only required parameter. tolerance (default 1e-4) is the modularity delta below which the outer aggregation loop stops, and max_levels (default 10) caps that loop. Real graphs converge in a handful of levels - the cap is a guard against pathological input, not a knob you normally touch.
curl -G "https://$OC_HOST/v1/tenants/$OC_TENANT/graph/social.users/louvain" \
--data-urlencode "rel=follows" \
--data-urlencode "tolerance=1e-4" \
--data-urlencode "max_levels=10" \
-H "Authorization: Bearer $OC_TOKEN"communities = db.graph.louvain(
"social.users",
rel="follows",
tolerance=1e-4,
max_levels=10,
)
# The SDK flattens the envelope to {pk: community_id}:
# {"alice": 0, "bob": 0, "carol": 0, "heidi": 1, ...}// The TypeScript SDK does not wrap the graph algorithms - use fetch.
const qs = new URLSearchParams({ rel: "follows", max_levels: "10" });
const res = await fetch(
`https://${OC_HOST}/v1/tenants/${OC_TENANT}/graph/social.users/louvain?${qs}`,
{ headers: { Authorization: `Bearer ${OC_TOKEN}` } },
);
const { communities } = await res.json();// The Go SDK does not wrap the graph algorithms - use net/http.
url := "https://" + OC_HOST + "/v1/tenants/" + OC_TENANT +
"/graph/social.users/louvain?rel=follows&max_levels=10"
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+OC_TOKEN)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var out struct {
Communities []struct {
PK string `json:"pk"`
Community uint32 `json:"community"`
} `json:"communities"`
}
json.NewDecoder(resp.Body).Decode(&out)what you get back
{
"communities": [
{ "pk": "alice", "community": 0 },
{ "pk": "bob", "community": 0 },
{ "pk": "carol", "community": 0 },
{ "pk": "dave", "community": 0 },
{ "pk": "heidi", "community": 1 },
{ "pk": "ivan", "community": 1 },
{ "pk": "judy", "community": 1 },
{ "pk": "mallory", "community": 2 }
]
}
An object with a single communities array - not a bare array, and not the row shape the other algorithm endpoints use. Here pk is a plain string and community is a dense integer in the range zero to k. Ids are assigned in order of first appearance by lexicographic primary key, so the lexicographically smallest key is always in community 0 and a rerun over an unchanged graph produces the same numbering.
how it works
- Phase one moves each node into whichever neighbouring community gives the largest modularity gain. Phase two collapses each community into a single node and repeats.
- The loop stops when the modularity delta between levels falls below
tolerance, when phase one stops moving nodes, or whenmax_levelsis reached. - Community ids are dense and deterministic, so two runs over the same graph are directly comparable and can be stored as-is.
- The endpoint refuses graphs above its 500,000-node ceiling. The per-level pass is linear in nodes plus edges, and a multi-million-node call will not land inside a request budget.
common mistakes
- Reading the response as an array. It is
{ "communities": [ ... ] }. Indexing the body directly gets you nothing. - Expecting
pkto be an array here. louvain returns a plain string, whilecomponents,betweenness,label_propagationandeigenvector_centralityall return a primary-key array. The response grammar genuinely differs per endpoint - write the parser per endpoint. - Comparing community ids across graphs. The numbering comes from this graph's own key order. Ids from a different graph, or from the same graph after new rows land, are not comparable.