Skip to content

feat(fts): 8a — standalone algorithms (tokenizer + BM25 + posting list)#78

Merged
joaoh82 merged 1 commit into
mainfrom
feat/fts-algorithms
May 3, 2026
Merged

feat(fts): 8a — standalone algorithms (tokenizer + BM25 + posting list)#78
joaoh82 merged 1 commit into
mainfrom
feat/fts-algorithms

Conversation

@joaoh82

@joaoh82 joaoh82 commented May 3, 2026

Copy link
Copy Markdown
Owner

Summary

First sub-phase of Phase 8 — full-text search. 8a ships the standalone algorithms only, no SQL integration yet. Mirrors the shape of Phase 7d.1's src/sql/hnsw.rs: pure algorithm, infallible API, zero project deps.

  • New module src/sql/fts/ with three files
    • tokenizer.rs — ASCII split + lowercase (Q3: ASCII MVP)
    • bm25.rs — BM25+ scoring (k1=1.5, b=0.75 fixed; Q4 + Q5: no stemming, no stop list)
    • posting_list.rs — in-memory BTreeMap<term, BTreeMap<rowid, freq>> index with insert / remove / query / matches / score
  • Single-line wire-up in src/sql/mod.rs (pub mod fts;)
  • 22 inline unit tests (~370 LOC); whole module is ~510 LOC of code

The public surface is shaped to be directly callable from 8b without rework: PostingList::matches / score back the per-row fts_match / bm25_score scalar fns, and PostingList::query powers the bulk try_fts_probe optimizer hook.

Out of scope (later sub-phases)

Concern Lands in
IndexMethod::Fts enum arm + executor dispatch 8b
fts_match / bm25_score scalar fn dispatch 8b
try_fts_probe optimizer hook 8b
fts_indexes: Vec<FtsIndex> on Table + INSERT/DELETE wiring 8b
KIND_FTS_POSTING cell encoding + v4→v5 file-format bump 8c
fts cargo feature gate 8e (where it gates the MCP bm25_search tool)
Hybrid retrieval worked example 8d
Docs sweep (docs/fts.md, supported-sql.md, etc.) 8f

Test plan

  • cargo build --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --all-targets — clean
  • cargo test sql::fts22 / 22 passing
  • cargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs — full suite green
  • cargo fmt --all -- --check — no diff
  • cargo clippy --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --all-targets — no new warnings on FTS code
  • cargo doc --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs --no-deps — module docs build

Notable test coverage

  • BM25 numeric reproducibility — hand-computed reference on a 3-doc corpus, asserted within f64::EPSILON * 16. Catches any future formula drift.
  • Length normalization sanity — same TF in a longer doc strictly scores lower (b > 0 working).
  • IDF behavior — rare term (1 of 1000 docs) outscores common term (1000 of 1000) by ≥5×.
  • 1k-doc deterministic synthetic corpus — top-10 query for a rare term surfaces exactly the 5 inserted matches, in stable order across re-runs.
  • Tie-break — equal-score docs come back in ascending rowid order.

🤖 Generated with Claude Code

Phase 8 sub-phase 8a per docs/phase-8-plan.md. Adds the standalone
inverted-index trio (no SQL integration, no persistence) under
src/sql/fts/, mirroring the role Phase 7d.1's src/sql/hnsw.rs played
for vector search:

- tokenizer.rs   — ASCII split + lowercase (Q3: ASCII MVP)
- bm25.rs        — BM25+ scoring, k1=1.5 / b=0.75 fixed (Q4 + Q5: no
                   stemming, no stop list)
- posting_list.rs — in-memory inverted index keyed on i64 rowid;
                   insert / remove / query / matches / score

Public surface is infallible (no Result wrappers, no project-error
import) and depends only on std — direct reuse target for 8b's
fts_match / bm25_score scalar fns and the try_fts_probe optimizer
hook. Single-line wire-up in src/sql/mod.rs (pub mod fts;).

22 new unit tests, all inline #[cfg(test)]:
- tokenizer: empty/punctuation/lowercase/alphanumeric/non-ASCII
- bm25: zero-cases, TF monotonicity, length normalization, IDF
        domination, hand-computed 3-doc reference, query-token
        compounding
- posting_list: empty/insert/remove/reinsert idempotence, multi-term
                any-term semantics, deterministic 1k-doc corpus,
                tie-break by rowid asc

Out of scope (later sub-phases):
- IndexMethod::Fts arm + executor dispatch         → 8b
- fts_match / bm25_score scalar fns                → 8b
- try_fts_probe optimizer hook                     → 8b
- KIND_FTS_POSTING cell + v4→v5 file-format bump   → 8c
- fts cargo feature gate (defer until MCP tool)    → 8e
- docs sweep                                       → 8f

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joaoh82 joaoh82 merged commit 500c088 into main May 3, 2026
16 checks passed
joaoh82 added a commit that referenced this pull request May 3, 2026
First major-version bump of SQLRite. The 0.1.x cycle covered Phase 0 →
Phase 7 (modernization → AI-era extensions). v0.2.0 marks Phase 8 —
full-text search + hybrid retrieval — and the on-disk file-format change
that comes with it.

What ships in v0.2.0
====================

Phase 8 — Full-text search + hybrid retrieval (PRs #78#83):

- 8a — Standalone FTS algorithms (tokenizer + BM25 + posting list)
       under src/sql/fts/, mirroring Phase 7d.1's HNSW shape.
- 8b — SQL surface: CREATE INDEX … USING fts (col), fts_match(col, 'q'),
       bm25_score(col, 'q'), the try_fts_probe optimizer hook, and
       INSERT/DELETE/UPDATE lifecycle wiring (rebuild-on-save).
- 8c — Cell-encoded persistence (KIND_FTS_POSTING, 0x06), stage_fts_btree
       / load_fts_postings, and the on-demand v4 → v5 file-format bump.
       Existing v4 databases without FTS keep writing v4; the first save
       with an FTS index attached promotes to v5. Decoders accept both.
- 8d — Hybrid retrieval worked example at examples/hybrid-retrieval/ —
       BM25 + vector cosine via raw arithmetic (no new typed function),
       hand-baked vectors so it stays reproducible without a model.
- 8e — sqlrite-mcp bm25_search tool, symmetric with vector_search. The
       MCP server now exposes 8 tools (was 7).
- 8f — Final docs sweep: new docs/fts.md canonical reference; FTS
       sections added to supported-sql.md, architecture.md, file-format.md
       (KIND_FTS_POSTING layout + v5 history entry), sql-engine.md,
       mcp.md (count bump 7→8), smoke-test.md; _index.md and roadmap.md
       updated to mark Phase 8 complete.

File-format change
==================

This is the headline reason for the major bump:

- v4 (Phase 7) — value block dispatch gained the Vector tag (Phase 7a).
  Phase 7's JSON, HNSW, and ask additions all lived inside the v4
  envelope.
- v5 (Phase 8c) — adds KIND_FTS_POSTING (0x06). On-demand: only
  written when the database has at least one FTS index attached.
  Decoders accept v4 and v5; v0.2.0 readers can open v0.1.x files
  without surprise. v0.1.x readers cannot open v5 files (clean
  "unsupported version" error).

Testing
=======

Engine 303 + MCP 19 + 73 across the other crates — all green at the new
version. Inter-crate deps bumped from 0.1 to 0.2 in three places
(Cargo.toml's optional sqlrite-ask, sqlrite-mcp's sqlrite-engine ref,
sdk/wasm's sqlrite-ask ref); cargo build refreshed Cargo.lock.

Manifests bumped (12 files via scripts/bump-version.sh):

- Cargo.toml
- sqlrite-ffi/Cargo.toml
- sqlrite-ask/Cargo.toml
- sqlrite-mcp/Cargo.toml
- sdk/python/Cargo.toml + pyproject.toml
- sdk/nodejs/Cargo.toml + package.json
- sdk/wasm/Cargo.toml
- desktop/src-tauri/Cargo.toml + tauri.conf.json
- desktop/package.json

Plus inter-crate dep refs, the Cargo.toml comment block, and
sqlrite-ask/README.md's installation snippet.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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