feat(search): index markdown section bodies for BM25 full-text search (#518) - #1778
feat(search): index markdown section bodies for BM25 full-text search (#518)#1778ShauryaaSharma wants to merge 2 commits into
Conversation
Section nodes exposed only their heading text to BM25, so search_graph could not match the prose beneath a heading. Index that body so markdown content is searchable. - store: add a `body` column to the nodes_fts FTS5 table; new cbm_store_fts_rebuild() drops+recreates the table (upgrading legacy 4-column databases) and backfills `body` from each node's docstring, guarded by json_valid() against malformed-JSON rows - store: expose CBM_SQL_FTS_BODY_EXPR so every nodes_fts write site feeds `body` through one shared expression - pipeline: the wholesale backfill now delegates to cbm_store_fts_rebuild(); the row-level delta-merge insert writes `body` through the same shared expression, so prose arriving incrementally is searchable too - mcp: stop excluding Section from BM25 results. Section falls in the unboosted ELSE bucket of the label CASE, so code symbols keep their ranking advantage by construction rather than by exclusion - internal/cbm: capture the markdown section body beneath each heading, stopping at the first nested subsection, capped at MAX_COMMENT_LEN with a UTF-8-safe backoff, reusing the existing docstring property - tests: 3 extraction cases + 4 store FTS cases, including a delta-path guard — a four-column INSERT into the five-column table is still valid SQL that silently leaves `body` NULL, so the incremental path needs its own assertion The DROP+recreate opens a brief window, one-time per database during the schema upgrade, where a concurrent bm25_search finds no table and degrades to the regex path. Closes DeusData#518 Refs DeusData#519 (Module description promotion follows in a stacked PR) Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
Standalone harness measuring what adding `body` to nodes_fts actually costs, so the sizing question on DeusData#518 rests on measurement rather than estimate. The full-index path already did delete-all plus a full re-INSERT before this change, so `body` does not add a pass over the graph — it makes an existing rebuild index more text. The harness therefore isolates the two things that genuinely change: per-row body tokenisation time, and the storage the column adds to the FTS index. Builds the same synthetic corpus three ways and reports the deltas: A 4-column FTS (pre-DeusData#518 baseline) B 5-column FTS, body for every node (what DeusData#518 ships) C 5-column FTS, body only for Section/Module rows (the WHERE lever) Methodology notes, because both mistakes produce confident-looking nonsense: - variants are interleaved (A,B,C, A,B,C, ...) rather than grouped, so drift over the life of the process cannot land entirely on whichever variant runs last. Grouped runs reported variant C as slower than B despite C doing strictly less work. - the minimum is reported, not the mean: backfill time has a hard floor and an unbounded tail, so the minimum is the closest observable approximation of the real work. - run-to-run spread is printed, and a warning fires when noise is large relative to the delta being reported, rather than letting a noisy timing column pass as authoritative. Uses the real CBM_SQL_FTS_BODY_EXPR including the json_valid() guard, and seeds a fixed-width 64-bit PRNG reset per variant so all three variants see a byte-identical corpus. Not a replacement for scripts/benchmark-index.sh, which measures real end-to-end indexing on a real repository; this deliberately strips parsing and I/O so the FTS write is visible. Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
|
Read in full. The structure is right and slice 1 is complete with no slice-2 leakage. But the delta test has exactly the hole it was written to close, and there is a second behavioural change the description does not mention. Both are fixable; neither is a rethink. The delta test does not guard the delta call siteThis is the important one, and I checked it rather than reading the name and moving on.
So: revert The fix is to drive the production delta path: build a base index, add nodes, run the real delta merge, then assert A fourth write site exists
Not a correctness bug today, but the BM25 ranking changes for every query, including nodes with no body
The PR body says "backward compatible — additive column". That is true of the schema and not of the ranking. This is worth an explicit decision rather than an accident: either accept the shift and say so, or pass explicit column weights to Alongside that: Two test weaknesses
Mechanical
Worth knowing that this is currently blocking everything else: the What is right, and it is most of it
Scope is clean: On the benchmark harness — you committed it about twenty minutes after asking whether it was wanted. I have since said yes on #617, so keep it; I mention it only because "asked, then shipped before the answer" is a habit worth not forming. It is 477 of the 859 lines here, which is also worth a sentence in the description so a reviewer knows 55% of the diff is a measurement tool rather than the feature. Fix the delta test, decide the BM25 weights question, tighten the two tests and split that header line, and I will merge this. Then I will run the real-corpus benchmark on my side as promised. |
Slice 1 of the #617 split, per the review guidance there: the FTS body infrastructure plus Markdown
Sectionindexing. #617 stays open as the ledger; the #519 YAML/JSON Module description promotion follows as a second PR stacked on this one.Rebuilt against current
mainrather than rebased — the original branch was ~1100 commits behind andpipeline.c,mcp.candstore.chad all been reworked around the touched regions, so the old diff served as the design spec.What & why
search_graphBM25 only matched node names and headings, so it was blind to markdown prose.Sectionnodes exposed only their heading text and were excluded from BM25 results entirely. This indexes the section body so content is searchable.Closes #518.
Changes
store — adds a
bodycolumn to thenodes_ftsFTS5 table. Newcbm_store_fts_rebuild()drops and recreates the table (which is what upgrades legacy 4-column databases) and backfillsbodyfrom each node's docstring, guarded byjson_valid()so malformed-JSON rows degrade to name-only indexing instead of aborting the wholeINSERT...SELECT.CBM_SQL_FTS_BODY_EXPRis exported fromstore.hso everynodes_ftswrite site feedsbodythrough one expression.pipeline — the wholesale backfill delegates to
cbm_store_fts_rebuild()(net −17 lines). The row-level delta-merge insert inpipeline_delta.cwritesbodythrough the same shared expression.That third write site is the one worth reviewing closely.
nodes_ftsnow has five columns, and an INSERT naming only the original four is still perfectly valid SQL that silently leavesbodyNULL — prose arriving through delta merge would have been unsearchable while a full reindex looked correct. It has its own test rather than relying on the full-index path.mcp — stops excluding
Sectionfrom BM25 results in both the search and count queries.Sectionfalls in the unboostedELSE 0.0bucket of the label-boost CASE, so code symbols keep their ranking advantage by construction rather than by exclusion.Modulestays excluded here and is handled in the #519 slice.internal/cbm — captures the markdown section body beneath each heading, stopping at the first nested subsection (each subsection gets its own
Sectionnode and its own body). Capped at the existingMAX_COMMENT_LEN(500) with a UTF-8-safe backoff, reusing the existing docstring property.Testing
3 extraction cases and 4 store FTS cases:
markdown_section_body_captured— body captured, sibling section excludedmarkdown_section_no_body— empty heading yields no docstringmarkdown_section_body_capped— body ≤ 500 bytesfts_rebuild_indexes_body_content— body tokens searchable, names still searchablefts_rebuild_upgrades_legacy_schema— legacy 4-column table upgraded in placefts_rebuild_tolerates_malformed_properties—json_valid()guard holdsfts_delta_insert_populates_body— the delta-path guard described aboveThe extractor's grammar assumption was verified directly against
tree_sitter_markdownrather than assumed: for nested headings the parser producessection [0-81]containing the heading, a paragraph, and a nestedsection [48-81], and the helper correctly stops at byte 48.Notes
Backward compatible — additive column, legacy databases keep working read-only and are upgraded by the DROP+recreate at the next index run, which was already a full FTS rebuild.
One operational caveat worth stating rather than leaving to be discovered: the DROP+recreate opens a brief window during the schema upgrade where a concurrent
bm25_searchfinds no table and degrades to the regex path. It is one-time per database and degrades gracefully, but with daemon-era concurrent sessions it is observable.No MCP tool changes, no new dependencies, no new
system()/popen()/network calls.Performance evidence — still outstanding
I owe measured numbers for this slice and do not have them yet.
scripts/benchmark-index.shneeds a built product binary and shells out topython3; my local toolchain is 32-bit MinGW.org GCC 6.3.0 with nomake, and a 32-bit address space will not hold anything near the reference corpus, so any figure I produced here would be the wrong measurement at the wrong scale.What I can measure is the narrowed question: since the full-index path already did delete-all plus a full re-INSERT, this change makes an existing rebuild index more text rather than adding a pass, so the real delta is per-row body tokenisation and the storage the column adds. I can build a standalone harness against the vendored SQLite with FTS5 measuring index size and tokenisation wall-clock with and without the
bodycolumn, and the same pair with aWHERE label IN (...)lever applied, and commit it so it is reproducible.Happy to build that if it is the evidence you want, or to defer to a run of
benchmark-index.shon the reference corpus or in CI. Flagging it up front rather than having it discovered in review.