Summary
_query_graph_text() currently performs T+1 graph-scoring passes for a query with T unique terms:
This means every query rescans the graph once for the combined query and then once again for every distinct search term.
Proposal
Replace the current T+1 scoring flow with a single candidate-scoring traversal.
Introduce a private helper (for example _score_query()) that computes both:
- the existing combined ranking;
- the best matching node for each normalized query term.
_pick_seeds() would then consume the precomputed per-term winners instead of rescoring the graph once per token.
The goal is to preserve existing behaviour while eliminating the redundant scoring passes.
Why this should be equivalent
The combined trigram candidate set (norm_terms + [joined]) is a superset of every individual per-token candidate set.
Therefore every node that could receive a non-zero singleton score is already visited during the combined scoring traversal, allowing the per-term winners to be computed without additional graph scans.
The intention is to preserve:
- combined ranking
- per-term seed selection
- existing tie-breaking behaviour
- existing seed deduplication
- existing trigram prefilter behaviour
while changing only the implementation.
Prototype results
I implemented a prototype to validate the approach.
Across synthetic graphs ranging from 1k to 100k nodes, together with Graphify's own ~11k node graph, I observed:
| Scenario |
Improvement |
| 1k–100k synthetic graphs |
1.4×–2.4× faster |
| 100k graph (3-term query) |
~41% lower latency |
| 100k graph (10-term query) |
~35% lower latency |
| 11k real graph |
1.8×–2.0× faster |
Single-term queries also improve because the current implementation still performs two scoring passes.
Scope
This would be an internal refactor only.
No intended changes to:
- CLI
- MCP
- graph format
- public API
- query behaviour
Current PR
#1918
Summary
_query_graph_text()currently performs T+1 graph-scoring passes for a query with T unique terms:_score_nodes()call to produce the ranked results;_score_nodes([term])call per unique token inside_pick_seeds()to guarantee per-term seed coverage (introduced in fix: guarantee per-term BFS seed diversity in query (fixes #1445) #1596).This means every query rescans the graph once for the combined query and then once again for every distinct search term.
Proposal
Replace the current T+1 scoring flow with a single candidate-scoring traversal.
Introduce a private helper (for example
_score_query()) that computes both:_pick_seeds()would then consume the precomputed per-term winners instead of rescoring the graph once per token.The goal is to preserve existing behaviour while eliminating the redundant scoring passes.
Why this should be equivalent
The combined trigram candidate set (
norm_terms + [joined]) is a superset of every individual per-token candidate set.Therefore every node that could receive a non-zero singleton score is already visited during the combined scoring traversal, allowing the per-term winners to be computed without additional graph scans.
The intention is to preserve:
while changing only the implementation.
Prototype results
I implemented a prototype to validate the approach.
Across synthetic graphs ranging from 1k to 100k nodes, together with Graphify's own ~11k node graph, I observed:
Single-term queries also improve because the current implementation still performs two scoring passes.
Scope
This would be an internal refactor only.
No intended changes to:
Current PR
#1918