Skip to content

feat(neo4j): route similarity search through HNSW vector indexes - #1796

Draft
xiaoyaner0201 wants to merge 6 commits into
getzep:mainfrom
xiaoyaner0201:feat/neo4j-hnsw-vector-index
Draft

xiaoyaner0201 wants to merge 6 commits into
getzep:mainfrom
xiaoyaner0201:feat/neo4j-hnsw-vector-index

Conversation

@xiaoyaner0201

@xiaoyaner0201 xiaoyaner0201 commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Refs #1793.

Problem

Neo4j similarity search computes vector.similarity.cosine() against every matching node/edge. On larger graphs this makes dedup/search take minutes and can leave long-running transactions behind.

What this does

  • Creates opt-in Neo4j vector indexes for Entity.name_embedding, Community.name_embedding, and RELATES_TO.fact_embedding, using the configured embedding dimension rather than a hard-coded dimension.
  • Routes both node_similarity_search and edge_similarity_search through db.index.vector.queryNodes / queryRelationships when use_vector_index is enabled.
  • Keeps the exact cosine path as the default and as a fallback when the index is unavailable, offline, populating, mismatched, or otherwise cannot be used.
  • Applies group_id, search, and endpoint filters after a bounded ANN over-fetch. If the filtered candidate window is sparse, it reruns the exact query so the old result-count contract is preserved.
  • Plumbs the opt-in through Neo4jDriver and the public Graphiti constructor; indirect MCP construction can use GRAPHITI_NEO4J_USE_VECTOR_INDEX.
  • Fixes the pre-existing edge bug where source/target UUID filters were silently ignored when group_ids was None.
  • Leaves community_similarity_search and non-Neo4j providers unchanged.

Why this is not a blind #859 revert

#859 previously added an HNSW path but used a hard-coded dimension and a simpler opt-in. This implementation keeps indexing disabled by default, couples the index dimension to the configured embedder, preserves exact fallback, validates query dimensions, bounds lifecycle fallback to known index errors, and covers both node and edge search paths.

Evidence

A live local Neo4j 5.26 graph with approximately 52k Entity nodes and 2560-dimensional embeddings was used for an independent node canary. For the same real embedding vector:

  • HNSW queryNodes top-10: approximately 4.9 seconds under host load
  • Existing brute-force cosine top-10: approximately 155 seconds
  • Top-10 set and order: identical in the single-seed comparison

A second live filtered probe used 12 random xiaoyaner-core seeds. The HNSW candidate path returned 10 rows for every seed and reduced the measured query time from 490.5 seconds total to 6.6 seconds total. Six of twelve had identical set and order; the remaining results had the same row count but ANN ranking/set differences, which is expected from approximate search and is why exact fallback remains available for sparse windows and the feature is opt-in.

A sparse-group probe showed the bounded candidate window can produce fewer than the requested limit. The implementation detects that case and reruns the exact filtered query; a focused regression test covers this fallback.

Testing

  • 85 passed focused tests covering node/edge routing, filters, over-fetch, sparse-window exact fallback, lifecycle fallback, dimension validation, result shape, public wiring, and provider isolation.
  • ruff check clean.
  • ruff format --check clean.
  • git diff --check clean.

The repository-wide non-integration suite was attempted but cannot be used as a green gate in this environment: unrelated integration-marked tests try to connect to localhost:7687, while the live Neo4j instance is isolated inside Docker and is not exposed on that host port. The run reached existing database-dependent errors; no code failure from this patch was established.

Scope / trade-off

HNSW is approximate. Results may differ from exact cosine even when the candidate window is full. Operators who require exact recall can leave the opt-in disabled. Operators enabling it should create/verify the indexes and compare their workload before making it the default in their deployment.

Index creation remains opt-in to avoid silently starting a potentially expensive index population during upgrade. The current local deployment has the indexes online and has separately verified the live node path, but this PR does not change upstream defaults.

Neo4j similarity search computes vector.similarity.cosine() against every
matching node/edge, with no vector index. On a 76k-edge graph with 2560-dim
embeddings a single edge_similarity_search takes ~41s; the same top-20
result set via an HNSW index returns in ~82ms.

HNSW support existed briefly: getzep#859 added a USE_HNSW flag and a Neo4j
vector-index branch, and getzep#894 removed it during cleanup. This reintroduces
it without the parts that made the original fragile.

- get_vector_indices() emits CREATE VECTOR INDEX DDL for Entity.name_embedding,
  Community.name_embedding and RELATES_TO.fact_embedding. The dimension is
  supplied by the caller rather than hardcoded (getzep#859 pinned 1024, which breaks
  any non-1024 embedder).
- get_vector_similarity_query() emits db.index.vector.queryNodes /
  queryRelationships, over-fetching limit*3 candidates so group_id and
  SearchFilters post-filtering cannot truncate below the caller's limit.
- Neo4jDriver gains embedding_dim and use_vector_index constructor arguments.
  Both default to preserving current behaviour: index DDL is only emitted when
  use_vector_index=True, so existing deployments keep the exact-recall
  brute-force path and are not surprised by a one-time background index build.
- edge_similarity_search routes through the index when enabled and falls back
  to the brute-force query otherwise.

Recall was verified against the brute-force path on a real 76,333-edge graph:
top-20 result sets were identical (20/20 intersection, no misses).

Refs getzep#1793
Deployments that construct Graphiti indirectly (the MCP server passes
uri/user/password to Graphiti and never touches Neo4jDriver) cannot supply
use_vector_index=True. Read GRAPHITI_NEO4J_USE_VECTOR_INDEX and
GRAPHITI_NEO4J_EMBEDDING_DIM as fallbacks so the feature is reachable from
configuration; explicit constructor arguments still take precedence.
Route Neo4j entity similarity searches through the native entity_name_embedding vector index when opt-in is enabled. Apply group and search filters after a bounded over-fetch, preserving the exact cosine path when the candidate window is sparse or the index is unavailable. Share dimension and lifecycle fallback handling with edge searches and cover both paths with contract tests.
@xiaoyaner0201
xiaoyaner0201 force-pushed the feat/neo4j-hnsw-vector-index branch from 7b00896 to eaea277 Compare September 2, 2026 19:21

This branch has not been deployed

No deployments
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