Repository navigation
feat(fulltext2): parallel segment search, token_filter, df cutoff and score-LIMIT pushdown (#30402) - #30618
feat(fulltext2): parallel segment search, token_filter, df cutoff and score-LIMIT pushdown (#30402)#30618cpegeric wants to merge 37 commits into
Conversation
…search (matrixorigin#30402) FULLTEXT2 top-k search (SearchBoolean, the BM25 bag-of-words / IN BM25 MODE path) walked the index's base+tail segments serially, leaving the per-segment BlockMax-WAND work single-threaded on a CPU-bound multi-segment query. Parallelize the multi-segment SearchBoolean across a worker pool sized by the new fulltext2_threads_search session variable (0 = GOMAXPROCS, mirroring hnsw_threads_search). Each segment is an independent top-k search (own dense ord space, own WAND threshold, own per-segment heap); each worker writes only its own per-segment result slot and the merge runs serially in segment order, so the merged top-k is identical to the serial walk -- the thread count is a perf-only hint. The shared globalStats df/phraseDf caches are mutex-guarded. The value rides IndexSearchScan algo_options (ScanOptions.ThreadsSearch), resolved from the session variable at plan time and gated on MORPCVersion123 so a mixed-version cluster stays consistent. It is baked onto the loaded Index at Load (HNSW-parity: a warm cache keeps the first-load value; results are identical regardless of thread count, so it is safe). Benchmark (200k wiki docs, 23 base segments, gojieba, BM25 top-k=1000, warm): ~1.7x avg, ~2.3x p95/p99, identical result sets across thread counts. SearchPhrase (positional NL) stays serial for now. Concurrent build and the token-filter interface (stop words) are separate follow-ups. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
mo-bot review · Commit: "interrupted" |
aunjgr
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES at b84cf63940640aaa242a6eeb35a839de9f63ded6: the required design-first gate is not closed.
This is a feature despite the small production diff: it adds a public session/global configuration contract, changes default CPU concurrency and shared-cache synchronization, and carries new plugin scan options with a capability epoch. Those are explicit complexity triggers in the repository's feature-design-review contract. The PR links no exact, separately approved design revision, and the delivered change contains no such artifact. The PR description and benchmark table are useful evidence, but not a traceable design approval.
Please provide/review the bounded execution and configuration design before implementation approval, covering:
- Per-query and concurrent-query CPU/result-memory bounds and cancellation/error drainage.
SearchBooleannow retains every segment result inperSegbefore merging, including when threads=1; this is a changed peak-lifetime model, not merely simultaneous execution of the old loop. I am not claiming a leak or demonstrated OOM. - The intended session/prepared/warm-cache semantics: the first cache load owns the knob for subsequent callers. Decide and document that operational contract rather than treating identical results as proof that the tuning control takes effect per execution.
- Actual activation/compatibility semantics. The planner comment says below123 returns0 “serial”, but search interprets0 as GOMAXPROCS. Specify whether this is an ungated performance hint or a gated default change, and make code/tests agree.
- Decisive truncated-top-k/Boolean/phrase/filter/liveness/error controls. The new unit comparison requests100 of60 matches, so it does not exercise a competitive truncated heap; the BVT changes the knob on the same warm cache and does not prove serial-vs-parallel execution when that cache retains its first-load setting.
These are concrete decisions/evidence required by the triggered design gate, not objections to parallel segment search itself or claims that existing CI/race results failed. I inspected the full ten-file change map, producer/cache/search/synchronization paths and added tests, then stopped full implementation approval at this upstream gate. No fresh local benchmark or runtime failure is claimed.
Add a Lucene-style token_filter WITH option to CREATE FULLTEXT2 INDEX. Step 1 supports the "stop" filter: it drops stopwords (gojieba's builtin English+Chinese stop_words.utf8) at the build, CDC and BM25 query paths, so all three tokenize identically. It requires POSITION_FREE=TRUE (a bag-of-words index, which also requires the gojieba parser) -- removing a stopword's postings leaves a positional gap a phrase query cannot reason about, so NL/BOOLEAN are unsupported (already rejected by the POSITION_FREE check). Gated at MORPCVersion123. - new pkg/fulltext2/tokenfilter: Analyzer/TokenFilter + StopFilter + builtin stopword loader (dict/stop_words.utf8 via JiebaDictDir) - TableConfig.TokenFilters threaded through build/CDC/query from the token_filter algo_param; Index.tokenFilters baked at Load and applied in SearchBagOfWords/StreamBagOfWords - grammar: TOKEN_FILTER keyword + tree.IndexOption.TokenFilters; SHOW CREATE render; catalog IndexAlgoParamTokenFilter - schema validation (requires POSITION_FREE); MORPC CREATE gate - ValidateReindexParams rejects turning POSITION_FREE off on a token_filter index (closes a REINDEX bypass of the CREATE guard) - unit tests + BVT fulltext2_token_filter Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Update on P1 (@XuPeng-SH, @aptend): I've reversed my earlier reply. As of
Validation: |
|
mo-bot review · Commit: Review completed. Read the assessment. |
XuPeng-SH
left a comment
There was a problem hiding this comment.
mo-bot review · mo-bot-local · review
Commit: d5adc1464f8eeb309155773e2061b6d2cb185e80
Assessment: request_changes
The secondary-sort-key defect is fixed. Design revision 5 still permits incorrect short results from the new bounded base-table join; deferring index freshness to #30853 does not close this regression.
Model: gpt-6-astra / xhigh
Session: 01a13dcf-1517-7f90-b3f9-c5f9f9e7031b · slot 0 · generation 1
- P2
pkg/sql/plan/apply_indices_fulltext.go:1605: Index two matching rows with different scores, then delete the highest-scoring row inside a transaction. Run ORDER BY score DESC LIMIT 1 while projecting a non-covered column so the query requires a base-table join. The asynchronous index still contains the deleted row.
Impact: The pushed limit selects only the deleted candidate, which the base-table join removes, returning no rows despite a surviving match. The previous explicit-order path was unbounded. Decline this bound unless current-row membership is proven, or implement exact filtering/refill before truncation. Add a competitive transaction regression for deletion and stale INCLUDE predicates.
Evidence: The planner records the join as boundable at lines 1083–1090 and installs CandidateLimit here without proving base-row visibility. Eligibility is also computed before restoring INCLUDE predicates at lines 708–715. The reader performs bounded top-k before the base join, without ordinary-MATCH refill. Reconciled retained SearchQuery evidence demonstrates the same truncation-before-recheck failure: top-1 followed by recheck returns empty, while recheck-before-limit returns pk=2. That probe models the recheck rather than executing a live transaction. Design lines 434–449 explicitly acknowledge and defer the short-result behavior.
Validation:
-
Current-head evidence audit passed through check-run: checks/1792759445202487951-3.json records exit 0 and the clean assigned head; output is retained in evidence-audit.log.
-
Verified seven retained artifact hashes, command selection, terminal receipt and failure logs. The fulltext2 owner tree and four relevant planner functions are unchanged from the probe revision; the join helper only adds the single-sort-key restriction. Details and artifact copies are in review-evidence.json and retained-evidence/.
-
Reviewed current single-key guards and competitive secondary-key BVT additions. The INCLUDE recheck BVT uses LIMIT 10 on four rows without explicit score ordering, so it does not cover competitive truncation.
-
Frozen exact-head CI metadata reports successful build, UT, SCA, coverage, proxy BVT and pessimistic BVT. local_validation is empty; raw CI case logs were not supplied.
Limitations:
-
No new native tests or live SQL transaction ran in this attempt. Historical probes support the failure mechanism; they are not presented as current-head runtime execution.
-
Following mo-self-review’s instruction that failed design “short-circuits full implementation polishing” (.agents/skills/mo-self-review/SKILL.md), exhaustive remaining concurrency, analyzer/protocol/parser and performance verification stops at this concrete design blocker. The 71-file inventory, design assessment and remaining scope are recorded in design-review.json.
A candidate limit pushed below the base-table join counts index candidates, so a document deleted or changed in the transaction (or not yet applied by ISCP) took a slot, the join dropped it, and LIMIT returned too few rows. The planner marks a bound below that join for post-filter over-fetch, on the implicit relevance order and the explicit ORDER BY score DESC path. fulltext2 sizes the candidate budget like the vector indexes do and searches that many candidates; the sort above applies the exact LIMIT and OFFSET. Covered searches have no join and are unchanged. BVT: competing scores under LIMIT 1 with an in-transaction DELETE and INCLUDE UPDATE, explicit and implicit order, and OFFSET. Design doc records the decision; covered-search staleness stays with matrixorigin#30853. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@XuPeng-SH, P2 (a bounded search below the base-table join returns too few rows while the index lags): fixed in A search bounded below the base-table join now over-fetches, the same way a post-filtered vector search does. Previously it kept exactly
Regression test (
With the search-side change reverted, all five transaction queries fail by returning too few rows. Unit tests also assert the over-fetch flag on the bounded join cases (a new implicit-order case included) and its absence on the covered ones. Removing the planner change fails four of the join cases. The guarantee is the same as a post-filtered vector search: stale candidates within the over-fetch headroom no longer shorten the result. More stale candidates than the headroom can still return fewer rows than exist. That applies equally to lag after commit, which a check of the transaction's own writes could not see. Covered searches, answered from the index alone, can still return a stale row while the index lags. That, and an exact freshness contract, stay with #30853. Design doc revision 5's "Index lag (decision)" section records this and the rejected alternatives: a per-query freshness check with a full-scan fallback, dropping the bound on transaction-local writes, and counting the transaction's deletes. Validation: |
XuPeng-SH
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES at exact head 372c6f4bab366347cd1137c1bdc06545b392d505
The secondary-order fix remains intact, and this commit improves the stale-candidate case, but the base-table-join LIMIT regression is still possible.
P2 — bounded search can still return fewer rows than SQL LIMIT when index lag exceeds the heuristic over-fetch budget. The new planner marks the scan PostFilterOverFetch; the FULLTEXT2 reader then uses the shared finite overfetch.PostFilterLimit(k) budget (for LIMIT 1, 11 candidates). There is no refill after the base-table join drops candidates. If the highest-ranked 11 indexed documents are deleted or no longer satisfy the current INCLUDE predicate while a lower-ranked live match remains, the scan returns only those 11, the join drops all of them, and SQL returns an empty page. The prior explicit-score join path was unbounded and would have reached the live match.
The new pessimistic-transaction regression establishes the one-stale-candidate case, but not a stale run longer than the over-fetch headroom. The design now explicitly accepts short results beyond that headroom and refers to #30853; that does not close #30853's stated expected behavior (“no fewer rows than LIMIT allows”) or avoid a regression in this newly bounded path.
Please preserve completeness for this exact full-text query path (for example, refill after recheck or decline the bound when it cannot be made exact), and add a competitive test with more stale top candidates than the budget plus a surviving lower-ranked match. I did not run a live transaction or local test in this workspace; this finding is from the exact-head planner/reader code and the stated finite budget.
…ded join search A search bounded below the base-table join used a fixed over-fetch, so when the transaction had deleted or updated more top candidates than its headroom, the join dropped them all and LIMIT returned too few rows. The search is now bounded to the larger of that over-fetch and LIMIT+OFFSET plus the rows the transaction deleted or updated in the source table, counted from its workspace (row batches and tombstones already written to S3) through a new optional engine capability. When they cannot be counted (an object-level delete or ALTER in the transaction) the search runs unbounded. Read-only transactions and historical reads skip the lookup. Commits by other transactions that the async index has not applied are eventual consistency and stay with matrixorigin#30853. BVT: 15 top rows deleted in the transaction under LIMIT 1 return the live lower-ranked match, explicit and implicit order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@XuPeng-SH, P2 follow-up (a fixed over-fetch still returns too few rows once more top candidates are stale than its headroom): fixed in A search bounded below the base-table join is now bounded to the larger of the post-filter over-fetch and
Regression test (
Commits by other transactions that ISCP hasn't applied yet are deliberately not counted. In that window the index also lacks newly inserted rows and the new versions of updated rows, so the query is eventually consistent whatever the bound. Counting only those deletes would repair half the window at a cost on every query. That window, the covered search, and an exact freshness contract stay with #30853. Design doc revision 5's "Index lag (decision)" section records this and the alternatives considered. Validation:
|
XuPeng-SH
left a comment
There was a problem hiding this comment.
Retaining the existing P2 at exact head d05accd2f677f9966177201b5d536d7fb549d233. The new transaction-local delete count and the 15-deleted-top-rows regression address the previous same-transaction over-fetch gap. Static inspection confirms the reader now widens to LIMIT+OFFSET plus counted local deletes, and falls back to unbounded when the provider cannot count them.
The short-page case remains when ISCP has not applied committed source deletes/updates: TransactionLocalDeletedRows only counts the current transaction workspace, so this path falls back to the finite post-filter candidate budget. The search truncates before the base-table join/recheck; if more stale high-ranking rows than that budget precede a surviving match, SQL can still return fewer rows than LIMIT allows. This is also a regression from the previously unbounded base-table-join path and is the unresolved behavior tracked by #30853.
Please check index coverage and fall back to an unbounded/current-row path when the generation is behind, or defer the bound until completeness is guaranteed. Add a regression for more than the over-fetch headroom of committed-but-not-yet-indexed deletes plus a lower-ranked live match. Validation boundary: this is static review of the exact head; this workspace has no MatrixOne checkout or Go toolchain, so I did not run unit/BVT tests or a live transaction.
…index Decision log for PR matrixorigin#30618 review P2: a generation behind the source does not fall back to an unbounded search. A lagging index is stale whatever the bound, and an unbounded search over a large index (88M rows) streams millions of candidates into the base-table join. Recorded at the bound in the reader and in the design doc's rejected alternatives; index freshness stays with matrixorigin#30853. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@XuPeng-SH, P2 (committed but not yet indexed rows beyond the over-fetch headroom): rejected, by design.
The transaction's own deletes and updates are exact as of The decision is recorded in // WARNING: TOXIC. Do not fall back to an unbounded search when the index lags the source.
//
// Decision (PR #30618 review P2, @cpegeric, 2026-10-23): commits by other transactions that
// the async index has not applied yet are not counted, and a generation behind the source
// does not fall back to an unbounded search. A lagging index is stale whatever the bound
// (it lacks inserted rows and new versions), and an unbounded search over a large index
// (88M rows) streams millions of candidates into the join. That fallback is refused; index
// freshness is tracked by #30853.It is also listed under rejected alternatives in design doc revision 5, "Index lag (decision)". |
|
mo-bot review · Commit: Review completed. Read the assessment. |
XuPeng-SH
left a comment
There was a problem hiding this comment.
mo-bot review · mo-bot-local · review
Commit: c258f1d84fe41f190763add1bfcc00f865c3b7fb
Assessment: request_changes
The design gate remains blocked: the new score-LIMIT pushdown can discard an unchanged, already-indexed matching row during committed-delete lag. Transaction-local deletion accounting addresses only part of the visibility problem.
Model: gpt-6-astra / xhigh
Session: 01a13dcf-1517-7f90-b3f9-c5f9f9e7031b · slot 0 · generation 1
- P2
pkg/sql/plan/apply_indices_fulltext.go:1606: Run a non-covered MATCH query with ORDER BY score DESC LIMIT 1 after another transaction commits deletion of the 11 highest-scoring indexed matches, before asynchronous index maintenance applies those deletes. A lower-ranked, unchanged indexed match remains visible.
Impact: The new rewrite fetches 11 candidates, all subsequently rejected by the base-table join, and returns no row. The previous unbounded explicit-sort path retains the surviving match. This loses an existing match independently of missing newly inserted index entries. Preserve completion through visibility-aware admission or refill, or decline this rewrite until safe; add a regression with controlled committed-delete lag.
Evidence: The reader's read-only transaction guard at search.go:878 leaves the budget unchanged, and ordinary MATCH has no refill after exhausting it. The current-head white-box probe exercised the actual budget hook, transaction guard, search and reader paging: budget=11, bounded_after_visibility=[], unbounded_after_visibility=[1]. lag-probe-v2.log and receipt checks/1792775348513027563-4.json retain the failing assertion. The probe models base-table visibility; it is not a live SQL reproduction.
Validation:
-
Verified the frozen head and unchanged source; inventoried all 75 changed files and reviewed the feature design, relevant comments, LIMIT ownership chain, tests and baseline planner behavior.
-
Isolation preflight, diagnostic native build and repository native-provenance verification passed with matching receipts.
-
TestTransactionLocalBound and TestApplyTransactionLocalBound passed within the first diagnostic run. Its probe stopped on an incorrect ordering assumption and was excluded as defect evidence.
-
The corrected focused probe failed the survivor-preservation assertion through the supplied test-exec. Commands, source fingerprints, exits, logs and overlay hashes were reconciled in /home/xupeng/.local/state/mo-bot/jobs/a41b12478286155dd8a85a4a/attempt-1/review-evidence.json.
-
Frozen evidence reports successful CI, but local_validation is empty and the supplied tests do not cover this committed-delete lag case.
Limitations:
-
No live SQL/CDC lag reproduction was executed; the finding combines executable component evidence with the traced planner and join behavior.
-
The confirmed design failure short-circuits full implementation approval of parallel search/build, token filters, df cutoff, generated parser changes and their remaining lifecycle and compatibility closures.
-
No fresh performance measurements were taken. Historical author benchmarks describe potential gains at earlier revisions and do not establish current-head performance.
gouhongshen
left a comment
There was a problem hiding this comment.
Codex automated review
Reviewed all 75 changed files of the fulltext2 change at head c258f1d against origin/main, after loading all fifteen existing reviews, the issue comments and the five inline threads, including the newest bot review published at this exact head. The previously blocking findings are resolved in the code and re-verified: the analyzer and scan-option gates use the still-unallocated MORPCVersion130 (live upstream main ends at 129, checked via the GitHub API), the dense full-Boolean evaluator stays serial so the O(segments x k) result-memory bound holds, the df cutoff is a plan-dependency session variable that invalidates ordinary and prepared plans, the score-LIMIT pushdown declines any secondary ORDER BY key on both the covered and base-table join paths, peeled INCLUDE and pk predicates are re-checked on the base scans so in-transaction updates cannot surface stale rows, bounded join searches over-fetch through the shared overfetch.PostFilterLimit hook, and the transaction's own uncommitted deletes widen the bound via the new engine.TransactionLocalDeletesProvider, falling back to an unbounded search only when they cannot be counted. The Bloom membership probe now stays on the join's probe spine, fixing the multi-MATCH hang. The cross-segment WAND floor and ceiling pre-pass use strict skip comparisons and an h.Len() >= k publish guard, so pruning stays results-identical at any worker count; workers are semaphore-bounded, per-segment error slots drain before return, and worker panics re-raise on the calling goroutine. The pipelined build feeds the builder serially in source order with bounded background finishing and spill-file discarding, so built segments are identical to serial builds, and token_filter is reconstructed identically on create, CDC, compaction and query, with the REINDEX position_free-off bypass closed. One item remains disputed and is not re-emitted as a finding because it was raised by earlier reviewers and the author answered it with a recorded design decision: committed-but-not-yet-indexed deletes beyond the over-fetch headroom can shorten an explicitly score-ordered LIMIT page while the asynchronous index lags; the author refused the unbounded fallback with a code comment and design revision 5, and issue #30853 (filed by the same author against main, where the implicit candidate limit already produces this short page) tracks the exact freshness contract, consistent with how post-filtered vector searches already behave. Verification in this checkout: native prerequisites were built with its documented make cgo jieba-dict target, then go test -race passed for pkg/fulltext2 and its plugin plan, search and compile packages; focused pkg/sql/plan fulltext and candidate-limit tests passed including every decline shape; pkg/frontend plan-invalidation tests, disttae TransactionLocalDeletedRows tests, the parser suites and the reader transaction-bound tests passed, re-run at this head. Limitations: no live cluster run in this workspace, so the distributed BVT suites (threads_search, threads_build, search_df_cutoff, token_filter, candidate_limit, pessimistic include_recheck) were reviewed statically against their recorded results; an initial pkg/fulltext2 attempt failed only because the native headers were absent and passed after the documented setup was built.
|
@XuPeng-SH, P2 at This is the case refused in
The transaction's own deletes and updates are exact as of |
…tes cannot be counted An object-level delete or an ALTER in the transaction made the bounded search below the base-table join run unbounded. It now keeps the post-filter over-fetch and accepts a possibly short page, the same as HNSW. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@XuPeng-SH, as of
The accepted consequence is the same in both bounded cases: a page can return fewer than It is recorded at // WARNING: TOXIC. Do not fall back to an unbounded search when the index lags the source,
// or when the transaction's own deletes cannot be counted.
// ...
// Accepted consequence: while the index lags, or after an uncounted delete in the
// transaction, a page can return fewer than LIMIT rows.Design doc revision 5, "Index lag (decision)", lists both unbounded fallbacks as rejected alternatives. Any finding that asks to reinstate an unbounded or brute-force fallback is rejected by this decision. |
|
mo-bot review · Commit: Review completed. Read the assessment. |
XuPeng-SH
left a comment
There was a problem hiding this comment.
mo-bot review · mo-bot-local · review
Commit: 492773d874ce50ba33a13f8dd0f6c1161813006b
Assessment: request_changes
The design gate remains blocked: the new explicit score-LIMIT rewrite can discard an unchanged, already-indexed matching row. Bounded search reduces work, but transaction-local deletion accounting does not preserve results during committed-delete lag.
Model: gpt-6-astra / xhigh
Session: 01a13dcf-1586-7a01-9000-d45e20995cf8 · slot 1 · generation 1
- P2
pkg/sql/plan/apply_indices_fulltext.go:1606: Run a non-covered MATCH query with ORDER BY score DESC LIMIT 1 after another transaction deletes the 11 highest-scoring indexed matches, before asynchronous index maintenance applies those deletes. A lower-ranked indexed match remains visible.
Impact: The new rewrite fetches 11 candidates, the base-table join rejects all of them, and the query returns nothing. The previous explicit-sort streaming path returns the surviving match. Preserve completion after visibility filtering or decline this rewrite when unsafe, and add a controlled committed-delete-lag regression. This does not require an unconditional brute-force fallback.
Evidence: search.go:876 leaves the budget unchanged for a read-only transaction; ordinary MATCH exhausts that budget without refill. The retained component probe produced budget=11, bounded_after_visibility=[], and unbounded_after_visibility=[1], failing its survivor assertion. Its execution at c258f1d was reconciled with receipts and logs, and the exercised path is semantically unchanged at this head. The author's documented short-page tradeoff acknowledges this outcome; the preexisting implicit-order limitation does not remove the explicit-order regression.
Validation:
-
Verified the frozen head and clean source; inventoried all 75 changed files and reviewed the design, relevant discussion, LIMIT ownership chain, tests and baseline behavior.
-
Current evidence audit passed: checks/1792784145796695093-3.json records the assigned head and exit 0, matching evidence-audit.log.
-
Reconciled five retained command/source/exit receipts, logs and artifact hashes, including native provenance and the corrected failing probe. Excluded the earlier probe with an invalid ordering assumption.
-
Audit and recovered evidence are retained under /home/xupeng/.local/state/mo-bot/jobs/9fa8a0ff8b474644748e2b96/attempt-1/evidence-audit.json and recovered-c258/. Frozen local_validation is empty; successful CI statuses do not cover this lag case.
Limitations:
-
The retained probe models base-table visibility; no live SQL/CDC reproduction or fresh native test was executed.
-
The design blocker short-circuits full implementation approval of parallel search/build, analyzer persistence, df cutoff, parser changes and their remaining concurrency, compatibility and test closures.
-
No fresh performance measurements were taken; historical author benchmarks do not establish current-head performance.
|
@fengttt please help force merge this PR. Codex alreadt got lost and insist fallback to table scan/full index scan without limit/brute force search. |
The bounded search below the base-table join keeps the post-filter over-fetch and no longer widens by the transaction's own deletes, so the storage-engine capability is removed. Design revision 6 states what the search returns while the index lags, and new BVT checks each statement from SQL: the joined and index-only plans in a transaction, the df cutoff never-empty, boolean/NL, filter, range and prepared-plan rules, the pushdown declines, and stopword stripping on build, CDC and REINDEX. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@XuPeng-SH @gouhongshen, as of Change in Behaviour until ISCP applies a write. This applies to the reading transaction's own uncommitted writes and to other transactions' commits alike:
These are accepted. #30853 was rewritten to this contract (its earlier "no fewer rows than LIMIT allows" wording is withdrawn). Short or stale results while the index lags are not a query defect. Any finding that asks for an unbounded search, a full-scan fallback, or a coverage gate is rejected ( Black-box tests of the design doc. Each statement in revision 6 is checked from SQL with an oracle that would fail if the statement were false:
Validation: |
|
mo-bot review · Commit: Review completed. Read the assessment. |
XuPeng-SH
left a comment
There was a problem hiding this comment.
mo-bot review · mo-bot-local · review
Commit: 21a1d5727c1a058baf4728d5d220b86fe33a9b70
Assessment: request_changes
Reviewed the complete 74-file delta and supplied concerns. Responsibility ownership, serial fallbacks and the revised asynchronous-index contract are reasonable, but the parallel build violates its documented memory bound.
Model: gpt-6-astra / xhigh
Session: 01a13dcf-1517-7f90-b3f9-c5f9f9e7031b · slot 0 · generation 1
- P2
pkg/fulltext2/build_pipeline.go:115: CREATE/REINDEX using the json_value parser with multiple build threads and large JSON string values. rowContent returns populated Terms with nil Content, and addToPipeline forwards them to Add.
Impact: Counting only Content lets these rows bypass byte backpressure entirely. They accumulate until the row limit—up to 512 rows—regardless of segment capacity, introducing substantial memory retention. Account for owned variable-sized payload, including pre-tokenized terms, or feed direct-token rows immediately; add a regression covering this parser path.
Evidence: On the unchanged candidate, twelve 1 MiB terms remain queued with chunkBytes=0 against a 4 MiB budget. A CREATE-operator probe with Capacity=1 writes twelve segments with one thread but zero with two threads before closing. A diagnostic-only control counting term bytes flushes successfully. Retained logs: pipeline-budget.log, tvf-budget.log and pipeline-budget-control.log.
Validation:
-
Preflight passed. Repository-native provenance verification and 192 matching input hashes qualified the reused native library for this head.
-
34 focused top-level tests passed under the race detector across fulltext2, tokenfilter, SQL planning and three plugin packages. Coverage includes concurrency failures, serial equivalence, candidate-limit barriers, INCLUDE rechecks and bloom placement.
-
Both diagnostic regression probes reproduced the finding; the serial operator control and diagnostic accounting control passed. Source remained unchanged.
-
Generated SQL parser matched regeneration after command-header/source-path normalization. Frozen patch matched the committed delta except its omitted terminal newline; the initial strict-comparison diagnostic and successful corrected check were retained.
-
Commands, source fingerprints, exits, logs and overlay hashes were reconciled in /home/xupeng/.local/state/mo-bot/jobs/d60a6d92d67dece8cd3b2cd4/attempt-1/validation-audit.json. Audit receipt: checks/1792838981944733528-4.json. Detailed design and concern assessment: review-analysis.json in the same directory.
Limitations:
-
The CREATE probe exercises the production parser/operator/pipeline with mocked persistence, not a running SQL service.
-
Excess retained payload was demonstrated; no production OOM or end-to-end throughput measurement is claimed.
-
Supplied frozen-head CI reports successful build, unit and BVT checks, but lacks raw case-selection logs. Full SQL suites and skipped upgrade-compatibility jobs were not rerun locally.
…ild chunk budget Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@XuPeng-SH Fixed in The chunk budget counted only Regression: |
# Conflicts: # pkg/defines/const.go # pkg/sql/parsers/dialect/mysql/mysql_sql.go
What type of PR is this?
Which issue(s) this PR fixes:
issue #30402
What this PR does / why we need it:
FULLTEXT2 query-performance work for #30402: three opt-in search/build improvements plus two planner fixes. Design doc (contract, decisions, invariants for the opt-in features):
docs/design/20261019-fulltext2-parallel-search-and-token-filter.md.1. Parallel segment search (
fulltext2_threads_search)The multi-segment BM25 top-k walk was serial; each per-segment search is independent, so they now run concurrently. It is performance-only — the merged top-k (ids, scores, order) is identical at any worker count.
Session/global
fulltext2_threads_search, mirroringhnsw_threads_search. The neutral default0resolves to the deployment's effective parallelism (GOMAXPROCS);N>0caps the workers. Worker goroutines are scheduler-multiplexed onto GOMAXPROCS, so concurrent queries share one CPU budget.Each worker writes only its own per-segment slot; the merge is serial. Peak extra result memory is
O(segments × k)small records (id+score+include), independent of posting/corpus size. Every worker is semaphore-bounded and drains (slot or error) before return; a per-segment error is surfaced after drain.Gated on a cluster-wide capability epoch: it only withholds an explicit operator value until every node understands the scan option (identical results make a mixed cluster safe regardless).
Cross-segment pruning: a shared, certified lower bound on the global k-th best score plus a per-segment score ceiling lets uncompetitive segments be skipped whole; results-identical to the single-segment oracle.
A worker panic is re-raised on the calling goroutine, so the pipeline turns it into a query error (as the serial loop did) instead of crashing the CN.
~1.9–2.1× latency improvement (serial → all-cores) on a 20k/7-segment corpus.
2.
token_filteranalyzer —stop(bag-of-words)A Lucene-style analyzer chain on the index. Step 1 is the
stopfilter: it drops stopwords (gojieba's builtin English+Chinesestop_words.utf8) at the build, CDC, and BM25 query paths, symmetrically, so the index never stores their postings.POSITION_FREE=TRUE(a bag-of-words index, which also requires the gojieba parser): removing a stopword's postings leaves a positional gap a phrase query cannot reason about, so NL/BOOLEAN are rejected (by the existing POSITION_FREE check). An all-stopword query returns empty, not an error.token_filteris an immutable, persisted index property, reconstructed identically on every build path; REINDEX/idxcron rebuild and merge preserve it. TurningPOSITION_FREEoff on atoken_filterindex via REINDEX is rejected (would bypass the CREATE guard).Measured on 20k wiki (POSITION_FREE gojieba): index −12.5% bytes / −41% postings. Query latency on realistic stopword-bearing queries ~5.7× faster serial (766µs → 134µs), ~11× with parallel search on top (766µs → 68µs); stopwords are the most compressible to store but the most expensive to walk at query time.
3. High-df term cutoff (
fulltext2_search_df_cutoff)Session/global fraction
c(default0= off). ForIN BM25 MODEonly, query terms present in ≥cof the live corpus are dropped before the search (near-zero idf, dominant posting work). This changes results (fewer matches, slightly different scores), so it is off by default, gated on the capability epoch, and never applied to natural-language or boolean mode. It never empties a query: single-term queries, all-terms-cut, and survivors that occur in no document fall back to the uncut query. A change of value invalidates cached and prepared plans. Contract in the design doc (revision 3).4. Push
ORDER BY score DESC LIMIT kinto the fulltext2 search (non-covered)Root cause of the SQL-pipeline gap in #30402: with an explicit
ORDER BY score DESC LIMIT kover base-table columns, the limit sat on the outer sort, so the search streamed every match, the base table was fully joined, and the sort ran over the whole matched set. The search's WAND top-k now receivesk(+offset) when the plan is exactly the rewrite's own INNER join of the filterless base table with the search onpk = doc_idand the sort has a single score-DESC key. Any other shape declines (user join, residual filter, aggregate, second MATCH, secondary ORDER BY key such as, id). The explicit form follows the same admissibility rule as the implicit relevance order (shared code): with only INCLUDE-column filters, both forms bound the search identically. Measured: search output 45,688 → 1,000 rows (50k docs, LIMIT 1000) and 5,000 → 10 (5k docs, LIMIT 10).5. Fix: multi-MATCH hang with
fulltext_bloom_filter_pushdown=1A multi-MATCH query with a residual filter hung forever: the rewrite put a membership probe on every fulltext search, including one on the build side of the MATCH join chain, which on the TP sequential path runs before the producer of that filter. Probes now go only to searches on the probe side (the optimizer's own rule). The variable stays default-off: a 500k-row benchmark showed it 20–43% faster with selective filters but up to 5.6× slower with non-selective ones.
6. Fix: rows violating INCLUDE-column filters on the base-table join path
INCLUDE-column and pk predicates were moved off the base table scan (and the membership build scan) into the fulltext2 search, which evaluates them on index-time values. With an UPDATE the index has not seen (same transaction, or async index lag), the join returned rows violating the WHERE clause, e.g.
BEGIN; UPDATE t SET category=2 WHERE id=4; SELECT ... WHERE MATCH(...) AND category=1returned row 4 withcategory = 2. Both base scans now re-check every filter; the predicates still ride the search as a pruning prefilter, and the membership-pushdown and candidate-limit decisions still use only the residual filters, so INCLUDE-only queries keepCandidate Limitand skip the membership scan. One problem remains, and it already exists on main: while the index lags (ISCP catch-up, or the transaction's own uncommitted writes), a covered search returns deleted or stale rows and a bounded top-k can return too few rows. It is tracked as #30853 and will be fixed there with the coverage check #30117 added for HNSW, not in this PR.Tests
Unit: stop filter (drop/case-fold/position/builtin load), BM25 query stripping, schema validation (requires POSITION_FREE, rejects non-word parser / unknown filter), grammar round-trip, REINDEX guard, and a competitive truncated-top-k parallel==serial equality (explicit 1 vs N, k<matches, deterministic order) run under
-race. BVT:fulltext2_token_filter(reject paths, SHOW CREATE round-trip, BM25 stopword behavior, NL/boolean rejection, REINDEX guard) +fulltext2_threads_search; fullfulltext2suite green.Added for 3–5: df-cutoff unit tests (strict neutrality at
c=1, mid-frequency drop, disabled values, boolean untouched, never-empty incl. a df-0 survivor) +fulltext2_search_df_cutoffBVT + frontend plan-invalidation test; score-LIMIT pushdown plan tests (positive and every decline shape) + BVT assertingCandidate Limitand same-rows-as-declined oracle; probe-side invariant test for 2/3 MATCHes + BVT with bloom on. Added for 6 and the parity fix: base scans re-check peeled INCLUDE predicates (3 shapes incl. membership) and implicit/explicit score-order parity with INCLUDE filters (plus residual,SQL_CALC_FOUND_ROWS, tie-break declines), both mutation-checked; BVTpessimistic_transaction/fulltext2/fulltext2_include_recheck(in-transaction UPDATE, bloom on/off, LIMIT) and EXPLAIN parity checks infulltext2_candidate_limit. Verified atf3cf5a996e:fulltext2/1150/1150,pessimistic_transaction/fulltext2/918/918,fulltext/green (3 known ignores), all fulltext2 suites also green with bloom forced on; touched packages pass,pkg/fulltext2under-race.🤖 Generated with Claude Code