Skip to content

fix(graph-node): batchInsert nodes missing from label index - #616

Merged
ruvnet merged 2 commits into
mainfrom
fix/graph-node-batchinsert-label-index
Jun 28, 2026
Merged

fix(graph-node): batchInsert nodes missing from label index#616
ruvnet merged 2 commits into
mainfrom
fix/graph-node-batchinsert-label-index

Conversation

@ruvnet

@ruvnet ruvnet commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

@ruvector/graph-node's batchInsert produced query-invisible nodes: nodes inserted via the fast bulk path were counted in stats() and were traversable by kHopNeighbors, but were not added to the property-graph label index that the only working Cypher pattern (MATCH (n:Label) RETURN n) reads. A user who bulk-loads and then queries by label gets silent empty results — a correctness bug in the fastest ingest path.

Reproduction (before fix)

const { GraphDatabase } = require('@ruvector/graph-node');
const db = new GraphDatabase({ distanceMetric: 'Cosine', dimensions: 4 });
const nodes = [], edges = [];
for (let i = 0; i < 50; i++)
  nodes.push({ id: `p${i}`, embedding: new Float32Array([i,i,i,i]), labels: ['Person'], properties: { name: `n${i}` } });
for (let i = 0; i < 49; i++)
  edges.push({ from: `p${i}`, to: `p${i+1}`, description: 'knows', embedding: new Float32Array([0,0,0,1]) });

await db.batchInsert({ nodes, edges });
console.log((await db.stats()).totalNodes);                 // 50  ✅
console.log((await db.kHopNeighbors('p0', 2)).length);      // 3   ✅
console.log((await db.query('MATCH (n:Person) RETURN n')).nodes.length); // 0  ❌ (expected 50)

stats() and kHopNeighbors see the nodes; the label scan returns 0.

Root cause

In crates/ruvector-graph-node/src/lib.rs:

  • create_node inserted into both backing indexes: the hypergraph adjacency/vector index (hg.add_entity, used by kHopNeighbors/stats/searchHyperedges) and the property graph + label index (gdb.create_node, read by the Cypher label scan).
  • batch_insert only called hg.add_entity per node — it never called gdb.create_node, and it silently ignored each node's labels/properties.

So the two indexes diverged for bulk-loaded data.

Fix

Extract the shared registration logic into a single register_node helper (single source of truth) that populates the hypergraph index, the property graph + label index, and optional persistent storage. Both create_node and batch_insert now call it, so all read surfaces stay consistent. batch_insert now also honors per-node labels/properties (previously dropped).

Test

Added a Rust regression test (cargo test -p ruvector-graph-node) asserting that nodes registered via the shared path are consistently visible through all three read surfaces — label-scoped scan (get_nodes_by_label), kHop adjacency (k_hop_neighbors), and stats() entity count.

running 3 tests
test transactions::tests::test_transaction_lifecycle ... ok
test transactions::tests::test_transaction_rollback ... ok
test tests::batch_inserted_nodes_are_visible_to_label_scan_khop_and_stats ... ok
test result: ok. 3 passed; 0 failed

Verified end-to-end after rebuilding the NAPI binding (napi build --platform --release): the repro above now returns MATCH (n:Person) = 50, plus a mixed batchInsert + createNode case returns the correct per-label counts, with stats/kHop unchanged.

Out of scope / known issues (filed, not fixed here)

Surfaced by the same benchmarking but deliberately not addressed in this focused fix:

  • Inline property filter silently ignoredMATCH (n:Person {name:"Alice"}) returns all Person nodes instead of filtering. The query executor reads only the label, not the inline property map.
  • Relationship / path / WHERE / aggregation patterns return empty — e.g. MATCH (a)-[r]->(b) RETURN a,r,b, MATCH (a)-[*1..2]->(b), MATCH (n) WHERE n.name="x", RETURN count(n). The supported Cypher subset is effectively just MATCH (n:Label) RETURN n. This is a larger feature gap, not a one-line correctness fix, and is left as a separate effort.
  • Bare MATCH (n) RETURN n (no label) returns 0 — the executor only scans by label.

Provenance

Discovered via agent-harness-generator ruvector benchmarking — see GRAPH-ANALYTICS-PROOF.md §5 (the batchInsert ↔ Cypher consistency bug, proven with a known-answer table). $0 to find and fix (local Rust build + test, no LLM/paid API).

🤖 Generated with claude-flow

ruvnet and others added 2 commits June 28, 2026 12:02
batchInsert only populated the hypergraph adjacency/vector index
(used by kHopNeighbors and stats) but never inserted nodes into the
property graph + label index that the Cypher `MATCH (n:Label) RETURN n`
scan reads. As a result, the fastest ingest path produced
query-invisible nodes: they were counted in stats() and traversable by
kHopNeighbors, but a label-scoped MATCH returned 0.

createNode did both; batchInsert did not. Extract the shared
index-registration logic into a single `register_node` helper (single
source of truth) and call it from both createNode and batchInsert so the
hypergraph index, property graph + label index, and optional storage all
stay consistent. batchInsert now also honors per-node labels/properties
(previously ignored).

Adds a Rust regression test asserting that nodes registered via the
shared path are consistently visible through all three read surfaces:
label-scoped scan (get_nodes_by_label), kHop adjacency (k_hop_neighbors),
and stats() entity counts.

Discovered via agent-harness-generator ruvector benchmarking
(GRAPH-ANALYTICS-PROOF §5).

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_019rVRYrRDKyxYK18kuVrDSf
….2.1

- cargo fmt on crates/ruvector-graph-node/src/lib.rs (Rustfmt CI)
- regenerate Cargo.lock so the local ruvector-sona workspace member
  resolves at 0.2.1 (offline, no external version bumps) — fixes
  `cargo metadata --locked` lockfile-integrity check

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_019rVRYrRDKyxYK18kuVrDSf
@ruvnet
ruvnet merged commit 3ae7e5f into main Jun 28, 2026
60 of 62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant