fix(falkordb): hoist LIMIT before per-row MATCH to avoid query timeout#1507
Closed
brentkearney wants to merge 2 commits into
Closed
fix(falkordb): hoist LIMIT before per-row MATCH to avoid query timeout#1507brentkearney wants to merge 2 commits into
brentkearney wants to merge 2 commits into
Conversation
brentkearney
force-pushed
the
fix/falkordb-edge-search-timeout
branch
from
May 25, 2026 06:08
fa3b74f to
6af7117
Compare
BaiYouQing
added a commit
to BaiYouQing/graphiti
that referenced
this pull request
May 25, 2026
…korDB fulltext query Two fixes for FalkorDB search operations: 1. **PR getzep#1507 - edge_fulltext_search LIMIT optimization**: Hoist the LIMIT clause before per-row MATCH to prevent FalkorDB timeout when fulltext search returns hundreds of candidates. Apply 4x buffer so post-MATCH WHERE filters still fill the requested limit. 2. **Issue getzep#1269 - Remove group_id from fulltext query**: FalkorDB treats hyphens as NOT operators even inside double quotes, which breaks group_ids containing hyphens. The group_id filter is already applied at the Cypher level, making the fulltext query filter redundant and harmful.
Member
|
Thanks for this, and for the thorough benchmarking. Consolidating on #1500, which removes the per-row re-MATCH entirely via |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1506.
Summary
Fix a FalkorDB-specific query-timeout bug in
edge_fulltext_searchthat causes silent ingestion failures on non-trivially-sized graphs.The defective Cypher template exists in two functions: the provider-agnostic
edge_fulltext_searchingraphiti_core/search/search_utils.py(the path exercised during episode ingestion and dedup) and the FalkorDB driver's copy ingraphiti_core/driver/falkordb/operations/search_ops.py. Both apply theirLIMITafter a per-rowMATCHre-lookup of every relationship returned bydb.idx.fulltext.queryRelationships. With a broad-tokened OR query the fulltext call yields hundreds of candidate edges, the planner runs the MATCH for each one, and total execution exceeds FalkorDB's default 1-second module timeout. The fulltext call itself is sub-millisecond; the per-row MATCH dominates the cost.This PR hoists
ORDER BY score / LIMITbetweenYIELDand the per-rowMATCHin both functions, with a ×4 multiplier so the post-MATCHWHEREclause (which can filter on node labels vian/m) still has enough candidates to fill the requested limit in the common case.Neo4j and Kuzu are unaffected — Neo4j's
db.index.fulltext.queryRelationshipsaccepts{limit: $limit}inline (graph_queries.py:175) and Kuzu usesTOP := $limit, so the limit is already applied inside the procedure. The hoist is gated to the FalkorDB provider in both functions.Changes
graphiti_core/search/search_utils.py— inedge_fulltext_search, add a FalkorDB branch tomatch_queryplacingWITH rel, score / ORDER BY score DESC / LIMIT $limit_with_bufferbetweenYIELDandMATCH; passlimit_with_buffer = limit * 4(FalkorDB only). This is the path used during ingestion/dedup.graphiti_core/driver/falkordb/operations/search_ops.py— the same hoist in the driver'sedge_fulltext_searchcopy.Both keep the final
ORDER BY score DESC / LIMIT $limitafter theWHERE, so each returns exactly$limitresults. No changes to public API, return shape, or behavior on the Neo4j / Kuzu / Neptune paths.Reproduction & benchmark
Against a FalkorDB graph with 1,117 nodes and 909 RELATES_TO edges (FalkorDB 4.18.8, Redis 8.6.3), running the captured failing query:
db.idx.fulltext.queryRelationships(...)alone (count + max score)TIMEOUT_DEFAULT=30000End-to-end verified through the live
search_utils.pyingestion/dedup path: an MCPadd_memorycall that previously dropped silently withQuery timed outnow completes ingestion successfully (entity/edge extraction, embeddings, dedup, summarization), and the edge-dedup search that drives temporal invalidation runs without FalkorDB timeouts.Why ×4 multiplier rather than ×1
edge_search_filter_query_constructor(graphiti_core/search/search_filters.py:120) can append filters that referencen/m(node label filter at:146:n:label AND m:label), so theWHEREclause must run afterMATCH. The multiplier ensures theWHEREhas enough candidates to fill$limitin the common case.×4 is conservative — at the default
limit=10, we pull 40 fulltext candidates and run 40 MATCHes (~5 ms each = ~200 ms). If filters are very narrow and reduce >75%, the function returns fewer than$limit— same behavior as today when filters happen to filter most matches out. No regression vs. current semantics; just a cap on how many candidates we examine.If maintainers prefer a different multiplier or want it configurable, happy to adjust.
Tests
tests/test_search.pyand FalkorDB integration tests should pass unchanged. (CI will confirm.)Out of scope (potential follow-ups)
edge_bfs_search(search_ops.py:413-428) has a similar per-row MATCH-after-YIELD shape but the upstreamYIELDcomes from a path expansion rather than fulltext, so the planner behavior differs. Worth auditing in a separate PR.services.queue_servicelogs the timeout butadd_memorystill returns "queued for processing" to the caller — same shape as the recently-fixed GeminiEmbedder batch silent drop. Worth surfacing queue errors back to the caller, but a separate concern.valid_edgesis empty. Callers ingraphiti.py:1727andutils/maintenance/edge_operations.py:399constructSearchFilters(edge_uuids=[edge.uuid for edge in valid_edges]); whenvalid_edgesis empty this becomesedge_uuids=[], which producesWHERE e.uuid IN []— always false. Result is semantically correct (no related edges to dedup against) but wastes the fulltext lookup + per-row MATCH + reranker LLM call. After this PR the wasted query is fast, but the cleanest fix is at the call sites: skip the search entirely whenvalid_edgesis empty. Worth a separate perf-opt PR; tempting in-constructor fixes (truthiness check) actually break dedup semantics because they make the search unconstrained instead of returning empty.