Combine Boolean filtering with BM25 ranking - #491
the-osiris wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved correctness, memory-bounding, and planner-handling issues must be addressed before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Combines Boolean filtering with BM25 ranking in a single index scan, including adaptive CTID handling and zero-score Boolean results.
Changes:
- Adds combined planner and scan execution.
- Adds regression coverage and expected outputs.
- Documents combined-query behavior.
File summaries
| File | Reviewed changes |
|---|---|
test/sql/boolean_queries.sql |
Combined-scan regression tests |
test/expected/boolean_queries.out |
Updated expected results |
src/planner/cost.c |
Combined-path planning support |
src/access/scan.c |
Ranking, filtering, deduplication, and Boolean-tail handling |
src/access/am.h |
Combined-scan state |
README.md |
Combined-query documentation |
Review details
Suppressed comments (2)
src/access/scan.c:539
- The new
result_count > max_entriesbranch is not exercised by the added combined-scan tests, which all use small match sets and therefore build the hash lookup. Please add a regression with a reducedwork_memand a match set above the threshold, checking both ranked results and the zero-score Boolean tail; otherwise the heap-recheck/dedup fallback can regress without detection.
max_entries = (long)work_mem * 1024L / TP_BOOLEAN_FILTER_ENTRY_BYTES;
if (so->result_count > max_entries)
return;
src/planner/cost.c:204
- boolean_full_scan is only computed in the !has_orderby branch, so combined queries such as WHERE body @@ '!term' ORDER BY ... are costed as ordinary top-k scans even though tp_boolean_execute() must enumerate and materialize the full Boolean universe when the ranked batch is rejected. The planner can therefore choose this path for a small LIMIT while paying for a full index scan and temporary result file; include the full-scan cost/selectivity for these combined predicates or keep them on the fallback path.
if (has_boolean && !has_orderby)
{
if (!tp_boolean_get_constant_query(path, &boolean_query))
boolean_full_scan = true;
- Files reviewed: 6/6 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Beyond the two inline findings, I also checked whether tp_boolean_filter_prepare/tp_begin_combined_boolean_tail could silently miss rows committed concurrently while the combined scan is in flight — that path follows the same snapshot/MVCC semantics as any other index scan re-execution here, so it's not a new correctness regression introduced by this diff.
Extended reasoning...
Two confirmed findings are already queued as inline comments: an unbounded exact-operand-count guard now reachable through the combined Boolean+ORDER BY path in src/planner/cost.c, and an unbounded materialization loop in the combined-scan tail of src/access/scan.c that can be driven by LIMIT-less queries (exactly the idiom CLAUDE.md recommends for counting matches). Given findings exist, this is not an approve. I additionally looked at the concurrent-write visibility angle for the newly-introduced Boolean-tail replay path (tp_boolean_filter_prepare re-executing/reusing the materialized match set, then tp_begin_combined_boolean_tail replaying it) and concluded it relies on ordinary index-scan snapshot semantics rather than introducing a new staleness or omission bug, so I'm not raising it as a separate finding. That check is concrete enough to be worth surfacing per the narrow exception for findings-present runs, but doesn't change the overall need for a human look given the two confirmed issues already flagged inline.
|
@the-osiris Check the AI feedback on this one, I'll take a look once the robots are happy :) |
Allow one @@ predicate and one BM25 ORDER BY to share an index scan. Ranked candidates use heap rechecks, widening preserves LIMIT results, and Boolean-only matches are appended with zero score so the result set remains complete. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f218d1e2-89d8-47a4-932c-a5db3c632a1a
Materialize Boolean matches after ranked backoff and reuse them to reject non-matching CTIDs before heap access. This preserves rechecks and the zero-score tail while avoiding repeated rejected heap fetches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f218d1e2-89d8-47a4-932c-a5db3c632a1a
637dcfb to
223aca0
Compare
Keep combined ranking correct and memory-bounded at the candidate ceiling, apply planner safeguards consistently, and cover the low-work_mem fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 317257d6-b169-41f8-9fe8-75eec673de6c
|
Also added low-work_mem fallback coverage and full-materialization costing for negative, prefix, weight, and parameterized queries. Fixed in e0b7e10. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🟡 Changes recommended
The planner’s cap fallback ignores unflushed memtable-chain documents and can allow a combined scan to hit a runtime candidate-limit error.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
Keep the PostgreSQL 18 disabled-node regression focused on a path the index still rejects now that single-key Boolean ranking scans are supported. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 317257d6-b169-41f8-9fe8-75eec673de6c
Include active memtable-chain documents in the combined Boolean-ranking safety threshold and cover both unflushed and persisted corpora. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 317257d6-b169-41f8-9fe8-75eec673de6c
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The planner's shared-lock chain count can miss concurrent appends, allowing a combined plan to hit the runtime candidate cap and error instead of safely falling back.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Resolved since last review (1)
| tp_acquire_index_lock(index_state, LW_SHARED); | ||
|
|
||
| metap = tp_get_metapage(index_rel); | ||
| total_docs = metap->total_docs; | ||
| walker = tp_chain_walker_open( | ||
| index_rel, metap->memtable_head_blkno, 0, CurrentMemoryContext); | ||
| pfree(metap); | ||
|
|
||
| while (total_docs < TP_MAX_QUERY_LIMIT && | ||
| tp_chain_walker_next(walker, &record)) |


Summary
@@filtering and BM25 ordering in one index scanPerformance
On a deterministic 200k-row corpus with five interleaved warm-cache runs:
The combined scan returned the same top 20 as GIN + BM25.
Validation
boolean_queries,rescan, andfiltered_seedregressionsDepends on #480.