Skip to content

perf(arcadedb): score vectors in the engine, not in Python - #5

Open
g33kroid wants to merge 1 commit into
ArcadeData:feat/arcadedb-backendfrom
g33kroid:fix/arcadedb-server-side-cosine
Open

g33kroid wants to merge 1 commit into
ArcadeData:feat/arcadedb-backendfrom
g33kroid:fix/arcadedb-server-side-cosine

Conversation

@g33kroid

@g33kroid g33kroid commented Sep 8, 2026

Copy link
Copy Markdown

The three similarity searches (node, edge, community) fetch every candidate row with its embedding over Bolt, then compute cosine in Python with numpy, sort, and truncate to the limit.

For a 384-dimension embedder that is ~1.5KB per candidate row shipped across the wire to be discarded, and the cost grows with the tenant's node count rather than with the limit.

ArcadeDB supports the same vector.similarity.cosine() the Neo4j path uses — I verified it over Bolt against 26.9.1 — so get_vector_cosine_func_query() already returns the right expression for this provider through its fallthrough. Nothing in graph_queries.py needed changing. Scoring, min_score, ordering and the limit all move into the query, matching what the Neo4j, FalkorDB and Kuzu drivers already do. The Python-side _cosine_similarity() and the numpy import go with it.

Measured

ArcadeDB 26.9.1 (arcadedata/arcadedb@sha256:02a1a74f…), 1800 nodes, fastembed BGE-small (384-dim), graphiti's own load test:

before after
vector search p50 330.3 ms 79.9 ms
vector search p95 354.9 ms 133.5 ms

Results are unchanged — the same exact cosine over the same candidate set, computed one hop earlier. Functional suite 15/15 on ArcadeDB with this applied.

On the LSM_VECTOR index — deliberately not in this PR

The obvious next step is db.index.vector.queryNodes against an LSM_VECTOR index, which does work over Bolt on 26.9.1. I benchmarked it rather than assuming, 1800 nodes across 3 tenants, querying one tenant:

in-query cosine, tenant-filtered : p50  35.1ms  rows=10
in-query cosine, index present   : p50  17.7ms  rows=10
index ANN k=10   + tenant filter : p50   7.3ms  rows=2   <- want 10
index ANN k=30   + tenant filter : p50   6.5ms  rows=10
index ANN k=100  + tenant filter : p50   6.1ms  rows=10

It is roughly 3x faster again — but the index returns the global top-k before the tenant filter is applied, so the natural k = limit silently returns 2 rows where 10 were asked for. Not an error, just a short result, which is the failure mode hardest to notice in a hybrid search that fuses three legs.

Making that safe needs an over-fetch factor tied to tenant selectivity, and with enough tenants the required k grows without a good bound. That is a design decision for this driver's owners rather than something to slip into a perf patch, so this PR keeps exact semantics and leaves the index alone. Happy to implement the over-fetch version — behind a flag, or with a fallback to the exact path when the filtered result comes up short — if you want it.

🤖 Generated with Claude Code

The three similarity searches fetched every candidate row *with its
embedding* over Bolt, then computed cosine in Python with numpy, sorted, and
truncated to the limit. For a 384-dimension embedder that ships ~1.5KB per
candidate row across the wire to discard almost all of it, and it grows with
the tenant's node count rather than with the limit.

ArcadeDB supports the same `vector.similarity.cosine()` that the Neo4j path
uses, so `get_vector_cosine_func_query()` already returns the correct
expression for this provider through its fallthrough — nothing there needed
changing. Scoring, `min_score` filtering, ordering and the limit all move
into the query, which is what the Neo4j, FalkorDB and Kuzu drivers already
do. The Python-side `_cosine_similarity()` helper and the numpy import go
with it.

Measured on ArcadeDB 26.9.1 (digest 02a1a74f), 1800 nodes, fastembed
BGE-small (384-dim), graphiti's own load test:

    vector search p50   330.3ms -> 79.9ms
    vector search p95   354.9ms -> 133.5ms

Results are unchanged: the same exact cosine over the same candidate set,
computed one hop earlier. The functional suite is 15/15 on ArcadeDB with
this applied.

This also folds the embedding guard into the filter list rather than
emitting it as a second WHERE, because the rewritten queries build one
WHERE. That happens to fix the `WHERE ... WHERE ...` syntax error that made
every filtered similarity search fail, which getzep#2 fixes separately and for
which getzep#2 should get the credit.
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