Flat BM25 queries of the form:
SELECT ... FROM nodes_fts JOIN nodes WHERE MATCH ? AND project=? ORDER BY bm25() LIMIT N
block FTS5 WAND/MaxScore early-exit — the outer JOIN+WHERE is invisible to
the FTS5 planner, so it scores every matching document before any filter fires.
On a large codebase with 100K+ matches this causes 2–16 minute queries.
Fix: two-step subquery. The inner FTS5-only query:
SELECT rowid, bm25(nodes_fts) FROM nodes_fts WHERE MATCH ? ORDER BY bm25() LIMIT 2000
can early-terminate because no outer predicate blocks it. The outer query
then joins and filters at most BM25_INNER_LIMIT (2000) candidates.
The count query uses the identical inner-limit subquery, so it benefits too.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>