Take the write and the 643-entity fan-out off the search path - #121
Merged
Conversation
…h path vault_search took 7 to 227 seconds and MCP clients gave up at 30. Entity extraction matched substrings anywhere, so a 4-word query pulled 643 entities and every stage after it was sized by that number. The co-occurrence boost then ran two lookups per entity, 1286 round trips. And every search wrote reinforcement inline, which serialized overlapping searches on the single SQLite writer and grew the table 250k rows a day. Extraction is now one statement, anchored at a word start, capped at 50 and keeping the least frequent entities: a term filling hundreds of triple slots boosts every candidate equally and separates none of them. The boost lookup is one chunked IN query. Reinforcement buffers in memory and drains on a background thread, so the search call issues no writes at all. Co-occurrence stays: its read path feeds the ranking boost and the attractor's clustering blend, and accumulated values are untouched. Three consecutive searches go from 41s, 7.4s, 226.8s to 0.61s, 0.63s, 0.77s. Four threads of overlapping searches no longer serialize. Closes #120
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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 #120.
What was wrong
vault_searchtook 7 to 227 seconds and MCP clients gave up at ~30s with "No response received". Three compounding causes, measured against the live store (695 notes, 9627 triples, 12.85M co-occurrence pairs of which 12.72M were search-generated):LIKE '%word%'per query word meant%agent%matched most of the graph. The 4-word query "azure foundry knowledge agent" produced 643 query entities, which are not the query's entities, and every stage after it was sized by that number.fetchallcalls costing 9.2s of 14.5s.The fix
Extraction is one statement for the whole query, anchored at a word start (the entity begins with the word, or a word inside it does), with hyphens and underscores normalized to spaces so
azure-foundry-hubstill reads as words. Wildcards inside a query word are escaped. The result is capped atMAX_QUERY_ENTITIES(50), keeping the least frequent entities: a term filling hundreds of triple slots boosts every candidate note equally and so separates none of them, which makes the cap a statement about signal rather than a latency patch.The boost lookup is one chunked
INquery instead of one per entity. The bounded-boost arithmetic below it is byte-identical and regression-tested against a re-implementation of the old per-entity loop.Reinforcement is buffered in memory and drained off the request path: at 5000 pending pairs a short-lived daemon thread writes with its own connection, and an
atexithook flushes the remainder. The search call itself now issues zero writes toentity_cooccurrence. Per-search fan-out is bounded at 50 query entities x 40 result entities = 2000 pairs, with result entities taken in rank order so the top note's associations survive the cap.Co-occurrence stays a feature. Its read path earns its place in the ranking boost and in
attractor.py's clustering blend, and existing accumulated values are untouched.Measured
Timings after are on a synthetic 700-note / 10k-triple store with a stubbed embedder; the before figures are the live store. Live verification against the deployed service follows the merge.
Ranking changes, stated deliberately
Notes boosted only because an unrelated entity happened to contain a query word as a substring lose that boost. When the cap bites, common entities drop out of the boost input, so notes associated with them lose it too — intentional sharpening, and the most visible thing to check live. Back-to-back identical searches no longer see each other's reinforcement, since the write now lands after the drain. Reinforcement accumulates far more slowly, and a pair repeated inside one flush window is bumped once rather than once per search.
Gate
uv run ruff check src/ tests/exit 0.uv run pytest -qexit 0, 828 passed (+20), reproduced cold after the build.One existing test changed:
test_reinforce_from_searchasserted that a search writes reinforcement, which is exactly the behaviour this issue removes. It now asserts the search writes nothing, the pair is buffered, and the row lands with the same increased value after a flush. Its siblingtest_reinforce_noop_no_entities_in_searchpasses unmodified.