Graph data formats
The knowledge map of a case is served as four static files under /v1/graph/{case}/. All binary files are
little-endian and are cached with Cache-Control: public, max-age=86400, immutable plus an ETag.
Use graphVersion from /v1/cases or version from meta as a cache key.
{ "case": "papers", "version": "ragflow_v2", "nodes": 7656, "links": 61248, "bounds": { "min": [-512.3, -498.1, -470.9], "max": [506.7, 515.2, 489.4] }, "clusters": 42}positions.bin
Section titled “positions.bin”float32[nodes * 3] — x, y, z for each node, in nodeIndex order. Byte size is nodes × 12.
offset 0: x0 y0 z0 | 12: x1 y1 z1 | 24: x2 y2 z2 | …links.bin (ngraph format)
Section titled “links.bin (ngraph format)”int32[] adjacency list. A negative value starts a new source node and encodes it as -(id + 1);
the positive values that follow are its neighbours encoded as id + 1. Ids are nodeIndex values.
[-1, 2, 3, -2, 3, -4, 1] └ node 0 → 1, 2 └ node 1 → 2 └ node 3 → 0Links are undirected for visualisation; a pair may appear once or in both directions.
const positions = await vol.graph.positions("papers"); // Float32Arrayconst links = await vol.graph.links("papers"); // Int32Array
const xyz = (i: number) => positions.subarray(i * 3, i * 3 + 3);
function* edges(links: Int32Array): Generator<[number, number]> { let src = -1; for (const v of links) { if (v < 0) src = -v - 1; else if (src >= 0) yield [src, v - 1]; }}
// CSR adjacency (fast neighbour lookup for 80k+ nodes)function toCsr(links: Int32Array, n: number) { const deg = new Uint32Array(n); for (const [a, b] of edges(links)) { deg[a]!++; deg[b]!++; } const offsets = new Uint32Array(n + 1); for (let i = 0; i < n; i++) offsets[i + 1] = offsets[i]! + deg[i]!; const cols = new Uint32Array(offsets[n]!); const fill = offsets.slice(0, n); for (const [a, b] of edges(links)) { cols[fill[a]!++] = b; cols[fill[b]!++] = a; } return { offsets, cols };}import numpy as np, requests
base = f"{BASE}/v1/graph/papers"pos = np.frombuffer(requests.get(f"{base}/positions.bin", headers=H).content, dtype="<f4").reshape(-1, 3)raw = np.frombuffer(requests.get(f"{base}/links.bin", headers=H).content, dtype="<i4")
edges, src = [], -1for v in raw: if v < 0: src = -int(v) - 1 else: edges.append((src, int(v) - 1))edges = np.array(edges, dtype=np.int32)labels.json
Section titled “labels.json”string[] with one title per node, in nodeIndex order. Use it for tooltips; fetch full metadata lazily
with GET /v1/graph/{case}/nodes/{nodeIndex}.
clusters.json
Section titled “clusters.json”interface ClustersFile { clusters: { id: number; label: string; color: string; centroid: [number, number, number]; size: number }[]; assignment: number[]; // nodeIndex -> cluster id}color is #rrggbb. assignment.length === meta.nodes. Cluster ids are not guaranteed to be contiguous,
so index clusters by id, not by array position.