# `aborist.search` The retrieval primitive. Today's only backend is FTS5 over the chunks table; the `SearchBackend` ABC is in place so additional backends (BM25 over titles, embedding-based vector search) can be added without touching the rest of the QA pipeline. ## `SearchBackend` ABC ```python from aborist.search import SearchBackend, AuditMode, Hit class SearchBackend(ABC): @abstractmethod def search(self, query: str, limit: int = 20) -> list[Hit]: ... ``` Each `Hit` carries `(document_root, document_uri, chunk_idx, snippet, score, audit_mode, title)`. `audit_mode` is the sticky provenance label that tracks how the chunk made it into the index; FTS5 backend always sets `UNGROUNDED` (search itself doesn't verify anything โ€” that's the QA pipeline's job). ## `FTS5Backend` Wraps the contentless `chunks_fts` virtual table. Two-mode query: - **AND-mode (strict, primary):** every content token must appear in the doc. Keeps unrelated docs out of the context window. - **OR-mode (fallback):** when AND returns 0 hits, fall back to OR but **capped to top-5 longest tokens** (proxy for rarity). Long topical synonyms fed via `extra_or_tokens` join the pool โ€” `neurotechnology` (15 chars) outranks `thoughts` (8) by length and surfaces brain-tech titles for vocabulary-mismatch queries. ```python from aborist.search import FTS5Backend backend = FTS5Backend(conn) # Plain search hits = backend.search("permacomputer", limit=32) # Search with synonym pool injection (used by qa.query._search_corpus) hits = backend.search( long_query, limit=32, extra_or_tokens=synonym_expand(qtokens, shards_dir=shards_dir), ) ``` ## Stopword & stopword-cap discipline `_FTS5_STOPWORDS` filters question words (`what`, `tell`, `please`) + generic connectors (`one`, `some`, `another`, `without`, `soon`, `currently`) before AND/OR construction. Two principles: - **Stay in sync with `_TITLE_STOPWORDS`** in `qa/query.py`. A token filtered at retrieval time but kept at title-relevance check (or vice versa) creates ranking incoherence. - **`_OR_FALLBACK_MAX_TOKENS=5`** caps the OR-mode pool. Without this, a 19-token OR clause matches millions of docs and forces BM25 to rank them all โ€” 13s/shard observed pre-cap. Now 0.25s/shard. ## Snippet building FTS5 contentless mode means SQLite's built-in `snippet()` and `highlight()` return empty. Aborist builds snippets in Python by joining `chunks_fts.rowid = chunks.chunk_id`, decompressing the chunk content, and locating query tokens locally (`_build_snippet`). ## Source [`aborist/search/fts5.py`](../../aborist/search/fts5.py) ยท [`aborist/search/__init__.py`](../../aborist/search/__init__.py)