Skip to content

[fix](be) Support BM25 scoring on keyword SEARCH indexes - #69051

Merged
airborne12 merged 10 commits into
apache:masterfrom
airborne12:foreman/T-75
Oct 20, 2026
Merged

airborne12 merged 10 commits into
apache:masterfrom
airborne12:foreman/T-75

Conversation

@airborne12

@airborne12 airborne12 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: None

Related PR: #68758

Problem Summary: SEARCH can match a keyword inverted index, but projecting score() can drop matched rows or report an unsupported index. Keyword indexes omit norms and may omit frequencies; they still need usable BM25 defaults.

Preserve whole keyword values, including case and whitespace. Use frequency and encoded norm 1 for docs-only/no-norm postings. Legacy indexes without total-term-frequency metadata use average length 1; SNII keeps its physical statistics. Analyzed segments missing required scoring metadata and numeric BKD scoring retain their existing restrictions.

This branch incorporates master's unified MATCH/SEARCH engine from #68758. Keyword norm handling lives in the CLucene postings cursor, including partition switches and WAND bounds; SNII cursors provide docs-only frequencies and norms. The obsolete query_v2/SNII scoring implementations are removed with master.

Keyword matching and missing-metadata behavior follow Elasticsearch while retaining Doris's existing BM25 formula and collection-statistics conventions.

P1 follow-up: keyword filters and internal conjunctions

  1. A keyword index explicitly declared with parser=none can inherit support_phrase=true from FE defaults. Ordinary MATCH filters then entered scoring although the statistics collector skipped that field. Both IndexReaderHelper query-type overloads now share the canonical analyzer-and-phrase eligibility check used by statistics collection. The implementation is out of line to keep analyzer dependencies out of the header. Named-analyzer and normalizer behavior is preserved. Equality and keyword MATCH filters keep the SEARCH-only baseline score.
  2. Scored SNII AND grouped a docs-only keyword term into a path that counts positions. Such sources now use the existing individual term scorer with implicit unit frequency; position-bearing sources retain batched scoring. Internal SEARCH('body:apple AND tag:x') continues to score both fields.

Release note

SEARCH with score() supports keyword inverted indexes with valid BM25 scores, including conjunctions, TopN ordering and WAND execution. Keyword MATCH filters do not request uncollected statistics when phrase support is inherited.

Check List (For Author)

  • Test
    • Unit Test: 365 targeted ASAN tests passed, including keyword defaults/partition switches, physical postings fixtures, SNII sources, SEARCH execution/lowering, batched-versus-streamed conjunction scores and both scoring-eligibility overloads. The final designated-initializer style edit was separately rechecked with all 20 IndexReaderHelper tests.
    • Regression test: 60 keyword checks across V2/V3/SNII, WAND on/off, implicit/explicit-none indexes, internal SEARCH AND and external equality/MATCH filters; all outputs generated with -forceGenOut and replayed. The original 24 output blocks retain exactly the same values. All 54 raw-index SQL checks also pass.
    • Manual test: Full ASAN BE build with ./build.sh --be -j16 and live SQL replay on an isolated instance passed. Both reported P1 scenarios reproduced on the prior binaries. The final BE ELF requires no GLIBC version newer than 2.10 (baseline 2.17).
    • No need to test or manual test.
  • Behavior changed:
    • No.
    • Yes, as described above.
  • Does this need documentation?
    • No. No syntax or configuration changed.
    • Yes.

Validation

  • PASS: header hygiene, modified-line clang-tidy and independent Standards/Spec self-review.
  • PASS: whole candidate merged with master 8f43208c770fab8a697d4ec71cf615154e4c0878 under clang-format 16, and the complete PR English code gate.
  • The temporary local CMake test-source selection was restored and is excluded from this PR; the local unit-only GLIBC_COMPATIBILITY=OFF avoids duplicate compatibility symbols in the test link.
  • For head 1c146c172aaf4c308563b9aa0abe2943b6d086e0, TeamCity compilation 1072159, BE UT 1072160 (15,254 passed, 114 ignored), P0 1072181 (6,204 passed, 1 muted), Cloud P0 1072183 (5,016 passed, 3 muted), External 1072182 (761 passed), and Vault P0 1072184 (21 passed) finished successfully. Automatic review 38923289710 reports zero P0/P1 findings and confirms both prior P1 issues are addressed. NonConcurrent retry 1072283 finished successfully on the same head and configuration (548 passed, 1 muted). The original first-failing index-compaction fault-injection test passed without muting. The source of the earlier memory pressure remains unconfirmed. Compilation, BE UT and the core regression configurations now have current-head finished successes. The latest coverage aggregation 1072314 and FE coverage check 1072315 also finished successfully. Compilation, BE UT, P0, Cloud P0, External, Vault, NonConcurrent and both coverage checks have current-head finished SUCCESS results, with no running or queued builds at verification. Performance 1071833 ran the older head 46e15ab and is excluded from this validation. GitHub checks were complete with success or workflow-defined skips, and the PR had no merge conflict at verification. Maintainer approval is still required.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@airborne12 airborne12 changed the title [fix](be) Reject BM25 scoring on non-tokenized SEARCH indexes [fix](be) Support BM25 scoring on keyword SEARCH indexes Oct 15, 2026
@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@airborne12

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes for one P1: the shared SNII scoring gate now enables scoring for ordinary indexed keyword equality/MATCH filters, although their field statistics are not collected. A score() query with matching keyword filter rows can therefore fail with Not such column. The inline comment gives a concrete supported query and the complete caller/collector mismatch. No existing inline findings need to be carried forward.

Reviewed all 16 changed files at f22f161. The review converged in 2 rounds: both complete reviewers and the separate risk reviewer returned NO_NEW_VALUABLE_FINDINGS in the latest round, and every candidate/risk has an explicit disposition. No additional user focus was supplied.

The relevant execution flow is:

score runtime -> shared index context -> every index iterator
SEARCH -> collected stats -> private native score sink -> scored bitmap -> outer collector
keyword equality/MATCH -> shared SNII scorer -> missing keyword stats -> P1 failure

Checkpoint conclusions:

  1. Goal and evidence: intended keyword SEARCH scoring is implemented across CLucene V2/V3 and SNII, with unit/regression fixtures, but the ordinary-predicate regression requires correction before merge.
  2. Scope/reuse: changes are focused on existing BM25, postings, statistics and native decoding. Reusing the eligibility predicate needs a narrower caller boundary, as described inline.
  3. Concurrency/locking: no new threads, locks or changed lock order. Scored native calls bypass shared bitmap caching/single-flight; no additional concurrency defect found.
  4. Lifecycle/static initialization: existing cache handles and local ownership retain readers through decoding; private score maps move into query-owned immutable storage. No new owner cycle or cross-translation-unit initialization dependency found.
  5. Configuration: no new configuration; the existing WAND option has true/false regression pairs.
  6. Compatibility: no storage writer or FE/BE protocol change. Legacy keyword indexes use unit defaults for omitted metadata; analyzed indexes still reject missing scoring metadata. No new libc API is introduced; final binary ABI was not audited in this static review.
  7. Parallel paths: traced V2/V3/SNII, docs-only/positioned, term/phrase, inline/slim/windowed, candidate/exhaustive, cache, scalar/array/null and compound/TopN/ascending paths. Ordinary equality/MATCH eligibility is the one verified mismatch.
  8. Special conditions/error handling: position/norm guards match persisted capabilities and preserve decode-error propagation. Pinned CLucene source confirms absent frequency pointers and negative missing-block-stat sentinels, making the unit-frequency and conservative unit-norm WAND fallback sound.
  9. Test coverage: added fixtures cover scalar keyword case/whitespace, null/empty/missing values, multiple segments, dual indexes and rankings. Missing SNII score()+ordinary keyword equality/MATCH coverage explains the P1. The new docs-only unit uses df 1/10/501, below the 512 windowed threshold; docs-only windowed, positional keyword arrays, large real CLucene skip blocks and mixed historical segments have static path evidence without new execution coverage.
  10. Golden outputs: a temporary static audit checked all 54 golden hit sets and BM25 values, plus all 24 WAND on/off pairs. Native physical-token statistics explain its different scores from legacy average-length defaults. Golden generation and author-reported executions were not independently verified.
  11. Observability: existing field/segment errors, collection-statistics VLOGs and query timers cover these branches; no additional metric requirement identified.
  12. Transactions/persistence: no edit-log, commit/publish, failover, version-map or persistent metadata mutation; existing visibility and delete-bitmap filtering are retained.
  13. Data writes/atomicity: no production writer changes. Writer inspection established the reader's keyword, array, null and frequency invariants; no new atomicity/crash path found.
  14. FE/BE variables: none added. Existing regression query shapes establish that SEARCH/score with ordinary indexed filters is supported.
  15. Performance/memory: docs-only decoding skips absent PRX/norm reads and preserves candidate-window selection. Existing buffers/cache ownership are reused; no additional verified allocation, complexity or redundant-scan defect found.
  16. Other checks: no new hub-header dependency, hygiene-table/PCH change, explicit-instantiation family or src-.cpp test inclusion. No second independent finding; equality/MATCH and related uncollected-clause observations are one root cause, deduplicated into the single inline.

Validation was static only, as required: no builds, Doris tests, source edits or submodule initialization. All changed checkout blobs match the reviewed head. The listed runtime test gaps are validation limits; no unresolved correctness candidate or unreviewed changed-file scope remains.

Review converged after 2 round(s).

const bool actual_similarity =
context->collection_similarity &&
IndexReaderHelper::is_need_similarity_score(query_type, &_index_meta);
context->collection_similarity && IndexReaderHelper::is_need_snii_similarity_score(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep uncollected keyword filters out of the scoring path

_query() is also used by ordinary SQL filters, and SegmentIterator::_init_index_iterators() attaches the same non-null collection_similarity to every iterator in a score query. On an SNII table with an analyzed body index and a keyword tag index, SELECT id, score() AS s FROM t WHERE SEARCH('body:apple') AND tag = 'x' ORDER BY s DESC LIMIT 10 now sends the tag's EQUAL_QUERY through score_plain_term_candidates(). However, CollectionStatistics::extract_collect_info() only collects MATCH/SEARCH leaves, so no statistics exist for the equality filter's tag field: get_or_calculate_avg_dl() throws Index statistics collection failed: Not such column ..., failing a query that previously worked. Keyword MATCH predicates have the same mismatch because MatchPredicateCollector explicitly skips non-analyzed indexes. Please distinguish SEARCH leaf scoring from ordinary filters, and keep reader eligibility consistent with the statistics collected for each caller; add an SNII regression combining SEARCH/score() with an indexed keyword equality or MATCH filter.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29865 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit f22f16113644b4bbe254765329c92bdf8095593e, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17599	5722	5597	5597
q2	2087	306	258	258
q3	10247	1467	878	878
q4	4681	480	343	343
q5	7444	813	547	547
q6	198	194	171	171
q7	805	818	621	621
q8	9254	1385	1241	1241
q9	5695	4343	4344	4343
q10	6839	1289	1039	1039
q11	447	265	242	242
q12	634	409	306	306
q13	18089	5629	2982	2982
q14	289	271	245	245
q15	q16	745	735	678	678
q17	1233	779	574	574
q18	7116	6334	6225	6225
q19	1257	981	626	626
q20	414	347	231	231
q21	6167	2445	2514	2445
q22	389	332	273	273
Total cold run time: 101629 ms
Total hot run time: 29865 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6278	6250	6199	6199
q2	770	576	550	550
q3	5294	5423	4903	4903
q4	2048	2144	1637	1637
q5	5497	6087	5821	5821
q6	267	197	157	157
q7	2423	2089	2019	2019
q8	3360	2851	2917	2851
q9	8118	8032	8247	8032
q10	3756	3808	3405	3405
q11	667	447	436	436
q12	723	723	540	540
q13	3682	4174	3211	3211
q14	317	333	296	296
q15	q16	719	727	673	673
q17	8514	7755	7500	7500
q18	13138	12317	13137	12317
q19	990	865	858	858
q20	2199	2197	1929	1929
q21	6137	5053	5092	5053
q22	514	459	417	417
Total cold run time: 75411 ms
Total hot run time: 68804 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 153423 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit f22f16113644b4bbe254765329c92bdf8095593e, data reload: false

query5	4319	596	456	456
query6	423	210	190	190
query7	4810	478	227	227
query8	324	173	170	170
query9	8751	4019	4033	4019
query10	458	314	254	254
query11	6022	3816	3585	3585
query12	142	90	86	86
query13	1268	457	339	339
query14	6128	4155	3873	3873
query14_1	3750	3559	3600	3559
query15	210	208	180	180
query16	943	440	422	422
query17	867	664	543	543
query18	2432	444	325	325
query19	204	169	141	141
query20	87	88	85	85
query21	209	141	118	118
query22	13042	13134	12827	12827
query23	13595	12949	12507	12507
query23_1	12701	12769	12723	12723
query24	7082	881	478	478
query24_1	482	490	487	487
query25	545	423	373	373
query26	1323	272	150	150
query27	2742	459	281	281
query28	4582	2000	1989	1989
query29	1599	638	465	465
query30	310	220	205	205
query31	1013	942	819	819
query32	154	111	88	88
query33	534	318	250	250
query34	917	889	515	515
query35	758	779	656	656
query36	836	788	731	731
query37	129	108	95	95
query38	1816	1778	1705	1705
query39	805	795	762	762
query39_1	745	754	750	750
query40	221	124	104	104
query41	75	70	69	69
query42	87	87	83	83
query43	376	392	343	343
query44	1371	708	706	706
query45	183	218	166	166
query46	893	1006	591	591
query47	3022	2947	2889	2889
query48	303	315	218	218
query49	573	405	296	296
query50	719	288	222	222
query51	10572	10497	10465	10465
query52	79	81	71	71
query53	193	225	157	157
query54	237	190	174	174
query55	72	69	68	68
query56	245	223	201	201
query57	1609	1582	1587	1582
query58	279	264	269	264
query59	2184	2145	1946	1946
query60	277	241	222	222
query61	146	148	149	148
query62	399	344	287	287
query63	201	172	178	172
query64	2731	959	777	777
query65	3335	3272	3287	3272
query66	1807	453	298	298
query67	20062	20385	19882	19882
query68	2959	1027	644	644
query69	391	297	251	251
query70	912	840	820	820
query71	294	245	215	215
query72	2690	2566	2329	2329
query73	540	564	305	305
query74	4601	4503	4293	4293
query75	2359	2307	1973	1973
query76	2296	1058	633	633
query77	359	396	289	289
query78	9821	9750	9170	9170
query79	996	867	551	551
query80	1150	450	357	357
query81	586	320	268	268
query82	576	141	112	112
query83	308	195	191	191
query84	318	119	110	110
query85	865	433	382	382
query86	369	241	224	224
query87	2025	1986	1838	1838
query88	3663	2736	2724	2724
query89	348	305	271	271
query90	1813	185	185	185
query91	182	154	129	129
query92	102	88	80	80
query93	1052	981	588	588
query94	558	306	279	279
query95	586	344	326	326
query96	676	548	238	238
query97	2491	2437	2311	2311
query98	171	153	166	153
query99	777	760	649	649
Total cold run time: 235937 ms
Total hot run time: 153423 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.77 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit f22f16113644b4bbe254765329c92bdf8095593e, data reload: false

query1	0.01	0.00	0.01
query2	0.09	0.05	0.05
query3	0.26	0.12	0.12
query4	1.61	0.13	0.13
query5	0.29	0.27	0.26
query6	1.17	0.69	0.68
query7	0.04	0.00	0.01
query8	0.05	0.04	0.03
query9	0.42	0.36	0.35
query10	0.59	0.61	0.63
query11	0.21	0.15	0.14
query12	0.19	0.14	0.15
query13	0.50	0.50	0.52
query14	1.02	1.05	1.02
query15	0.66	0.61	0.64
query16	0.36	0.32	0.34
query17	1.17	1.16	1.12
query18	0.23	0.22	0.21
query19	2.09	2.05	2.02
query20	0.02	0.01	0.01
query21	15.48	0.26	0.15
query22	4.40	0.05	0.06
query23	16.26	0.33	0.12
query24	3.15	0.63	0.36
query25	0.13	0.05	0.06
query26	1.22	0.24	0.14
query27	0.05	0.03	0.04
query28	1.96	0.69	0.44
query29	12.53	4.23	3.36
query30	0.29	0.15	0.13
query31	2.78	0.58	0.34
query32	3.22	0.63	0.52
query33	3.23	3.21	3.24
query34	15.54	4.02	3.42
query35	3.36	3.36	3.33
query36	0.56	0.46	0.44
query37	0.09	0.06	0.06
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.18	0.15	0.14
query41	0.08	0.03	0.03
query42	0.03	0.03	0.03
query43	0.04	0.03	0.04
Total cold run time: 95.65 s
Total hot run time: 24.77 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (30/30) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.10% (35633/46218)
Line Coverage 62.35% (403400/646976)
Region Coverage 58.74% (340580/579805)
Branch Coverage 59.65% (156889/263033)

Problem Summary:
Resolve conflicts with the unified MATCH and SEARCH index execution refactor.
Preserve this PR's scoring behavior through the new query and reader interfaces.

Related PR: apache#68758
Release note: Preserve search scoring fixes after index execution unification.
Tests: ASAN unit validation in progress; header hygiene and clang-format 16 passed.
Behavior changed: No additional behavior beyond the existing scoring fix.
Documentation: Not required; no new syntax or settings.
Problem Summary: The general postings mock inherits unsupported multi-reader block-bound methods. Use a leaf mock to validate keyword norm and WAND defaults across partitions. Tests: affected ASAN suites; production code unchanged. Release note: none. Behavior changed: no. Documentation: not required.
@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@airborne12

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all nine changed files at 46e15ab, including the SEARCH collector/lowering, both index formats, postings, BM25 and WAND consumers. No new distinct inline findings were confirmed. Changes are requested because an existing P1 remains applicable.

Existing finding disposition: 4278499777 is partially addressed. Ordinary keyword equality now stays out of scoring. However, a keyword index configured with support_phrase=true still lets an ordinary MATCH predicate score, while MatchPredicateCollector skips that non-analyzed index. For example, with an analyzed body index and keyword tag index carrying that property, SELECT id, score() AS s FROM t WHERE SEARCH('body:apple') AND tag MATCH_ANY 'x' ORDER BY s DESC LIMIT 10 collects only body statistics, then the tag reader calls BM25Similarity::for_one_term and fails in get_or_calculate_avg_dl(tag) with a missing-column error. FE property validation permits this configuration. This is the keyword MATCH concern already stated in the existing thread, so it is retained without another inline comment. Default keyword indexes without support_phrase=true do not take this path.

Critical checkpoint conclusions:

  1. Goal and evidence: keyword SEARCH now admits optional metadata and exposes unit norms consistently; analyzed missing-metadata restrictions remain. Unit and regression cases support the intended behavior, but were not executed during this review. The existing mixed SEARCH/MATCH failure remains above.
  2. Scope and reuse: focused changes reuse current format adapters, statistics admission and scoring machinery; no unrelated implementation change found.
  3. Concurrency and locks: no threads, locks or lock-order changes. Statistics are collected during scanner preparation before reader open; new state is a cursor-local scalar or classification parameter.
  4. Lifecycle, memory and initialization: iterator/cache-handle ownership is preserved, the partition flag follows the owned leaf iterator, and no new allocating container, owner cycle or cross-TU static is introduced.
  5. Configuration: no configuration item added.
  6. Compatibility: no persisted format, public protocol or rolling-upgrade representation changes. Existing keyword metadata omissions are interpreted at read time.
  7. Thrift/protobuf: untouched.
  8. Parallel paths: CLucene and SNII, scored/unscored access, single/partitioned readers, term/phrase lowering and scalar/WAND consumers were traced. Keyword PHRASE lowers to a raw term. Norm one is used for rows and block bounds; unknown frequency keeps the saturation upper bound.
  9. Conditional checks and comments: analyzed-only rejection and keyword defaults have concrete metadata-omission explanations. No speculative error recovery was added; existing error propagation is preserved.
  10. Test coverage: new cases cover TERM/PHRASE/EXACT, multiple inserts, NULL/empty input, case sensitivity, missing terms, OR TopN, ascending order, WAND on/off and dual indexes. Existing analyzed rejection tests remain. The untested extensions are listed below.
  11. Changed results: all 54 golden labels and result IDs were checked statically. The distinct BM25 values match the implementation: CLucene uses avgdl=1; SNII keeps ten physical tokens across eleven rows, including one NULL, giving avgdl=10/11. Dual-index exact scoring also matches. Output ordering is deterministic for these inputs; generation provenance was not independently established.
  12. Observability: existing collection logs and failure diagnostics remain suitable; no new distributed operation or metric requirement identified.
  13. Transactions and persistence: query-only change; version visibility, delete bitmaps, edit-log replay and failover state are unaffected by the modified code.
  14. Writes and atomicity: no production write path changed. Existing SNII segment staging/commit and failure cleanup remain intact.
  15. FE/BE variable passing: no new transmitted variable or send path.
  16. Performance: constant-size per-leaf norm metadata and per-field analyzer classification add no extra posting scan or large allocation; no material new performance defect identified.
  17. Additional risks: collector/index selection, metadata defaults, partition/shallow-seek invalidation, conservative pruning and writer token conventions were checked. Every investigated candidate has a verified duplicate or dismissal disposition; no unresolved concrete candidate remains.

User focus: no additional focus was supplied; the complete PR scope was reviewed.

Coverage limits: this was static inspection only, with no builds, tests or live SQL replay. Author-reported test runs were not treated as independently executed evidence. The external CLucene decoder implementation is absent from this checkout, so its unchanged internals could not be re-audited. The added tests do not directly force large multi-block WAND, heterogeneous norm partitions with shallow seeks, keyword arrays, ignore_above, or positional keyword MATCH. Repository adapter/writer contracts were inspected for these cases; these limits did not establish a distinct new bug.

All three assigned reviewers returned NO_NEW_VALUABLE_FINDINGS in round 1; the full changed-file scope and all initial risk questions have explicit dispositions. Review convergence does not clear the independently confirmed existing P1.

Review converged after 1 round(s).

Existing P0/P1 findings confirmed for this head: #69051 (comment)

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29766 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 46e15abe616fc95d28205580115148c0e0750d37, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17590	5698	5545	5545
q2	2095	327	258	258
q3	10241	1435	845	845
q4	4683	476	337	337
q5	7426	833	542	542
q6	200	194	160	160
q7	791	801	601	601
q8	9257	1389	1115	1115
q9	5560	4285	4277	4277
q10	6844	1296	1034	1034
q11	453	256	243	243
q12	642	404	294	294
q13	18065	3668	3015	3015
q14	277	267	251	251
q15	q16	731	730	664	664
q17	1203	748	577	577
q18	7139	6242	6170	6170
q19	1099	969	632	632
q20	391	342	227	227
q21	5784	2670	2687	2670
q22	394	354	309	309
Total cold run time: 100865 ms
Total hot run time: 29766 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6697	6676	6826	6676
q2	745	608	582	582
q3	5637	5703	5299	5299
q4	2076	2160	1707	1707
q5	5687	5490	5769	5490
q6	249	197	154	154
q7	2248	2023	1846	1846
q8	3284	2790	2785	2785
q9	8138	8007	7943	7943
q10	3642	3580	3230	3230
q11	595	418	396	396
q12	659	692	500	500
q13	3354	3608	2990	2990
q14	285	289	274	274
q15	q16	673	686	624	624
q17	7828	7037	6979	6979
q18	13260	12306	13161	12306
q19	963	853	849	849
q20	2185	2171	1935	1935
q21	6140	5133	5172	5133
q22	531	470	402	402
Total cold run time: 74876 ms
Total hot run time: 68100 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152347 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 46e15abe616fc95d28205580115148c0e0750d37, data reload: false

query5	4317	598	449	449
query6	433	244	226	226
query7	4817	460	256	256
query8	346	180	161	161
query9	8776	4035	4026	4026
query10	468	312	262	262
query11	6024	3790	3553	3553
query12	142	92	90	90
query13	1238	431	313	313
query14	6141	4029	3800	3800
query14_1	3517	3512	3568	3512
query15	206	197	177	177
query16	950	440	411	411
query17	830	633	524	524
query18	2411	425	298	298
query19	204	188	139	139
query20	88	84	81	81
query21	215	140	117	117
query22	12955	12986	12826	12826
query23	13335	12772	12468	12468
query23_1	12507	12465	12505	12465
query24	7135	819	461	461
query24_1	475	471	482	471
query25	534	442	363	363
query26	1278	261	140	140
query27	2761	439	274	274
query28	4665	1968	1956	1956
query29	1591	607	457	457
query30	308	224	190	190
query31	1015	934	807	807
query32	152	95	96	95
query33	533	315	256	256
query34	1030	856	504	504
query35	753	752	638	638
query36	816	814	731	731
query37	135	109	90	90
query38	1780	1765	1709	1709
query39	801	817	751	751
query39_1	742	740	750	740
query40	221	116	98	98
query41	74	67	72	67
query42	86	86	84	84
query43	374	382	338	338
query44	1363	710	715	710
query45	185	182	166	166
query46	851	966	565	565
query47	2964	3009	2933	2933
query48	303	320	223	223
query49	577	409	301	301
query50	673	284	218	218
query51	10340	10251	10529	10251
query52	80	82	71	71
query53	191	213	161	161
query54	262	208	183	183
query55	74	76	67	67
query56	234	235	216	216
query57	1630	1585	1558	1558
query58	298	275	267	267
query59	2142	2146	1937	1937
query60	284	251	235	235
query61	166	201	141	141
query62	383	339	285	285
query63	209	162	172	162
query64	2733	954	775	775
query65	3311	3250	3240	3240
query66	1790	413	305	305
query67	20158	19987	19925	19925
query68	2941	1015	617	617
query69	380	291	254	254
query70	863	808	794	794
query71	289	231	210	210
query72	2699	2546	2342	2342
query73	526	543	298	298
query74	4607	4461	4306	4306
query75	2348	2321	1966	1966
query76	2292	1028	625	625
query77	349	381	288	288
query78	9734	9821	9114	9114
query79	974	912	520	520
query80	656	422	369	369
query81	554	316	279	279
query82	673	139	116	116
query83	269	195	183	183
query84	315	127	100	100
query85	867	426	364	364
query86	351	241	232	232
query87	2009	1953	1830	1830
query88	3655	2714	2679	2679
query89	346	310	270	270
query90	1789	188	191	188
query91	165	156	128	128
query92	104	92	89	89
query93	1020	1005	582	582
query94	493	320	308	308
query95	596	348	371	348
query96	670	529	233	233
query97	2506	2406	2311	2311
query98	163	153	147	147
query99	779	767	656	656
Total cold run time: 234087 ms
Total hot run time: 152347 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.75 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 46e15abe616fc95d28205580115148c0e0750d37, data reload: false

query1	0.01	0.01	0.01
query2	0.10	0.05	0.04
query3	0.27	0.12	0.11
query4	1.60	0.13	0.12
query5	0.25	0.27	0.25
query6	1.17	0.69	0.68
query7	0.04	0.01	0.00
query8	0.05	0.04	0.04
query9	0.40	0.32	0.34
query10	0.57	0.57	0.59
query11	0.22	0.15	0.13
query12	0.19	0.15	0.14
query13	0.50	0.51	0.49
query14	1.03	1.01	1.00
query15	0.65	0.60	0.62
query16	0.36	0.35	0.35
query17	1.18	1.17	1.16
query18	0.23	0.21	0.24
query19	2.12	2.01	1.99
query20	0.01	0.01	0.02
query21	15.53	0.26	0.14
query22	4.49	0.06	0.06
query23	16.22	0.32	0.12
query24	3.04	1.10	0.45
query25	0.10	0.04	0.05
query26	1.68	0.27	0.15
query27	0.05	0.05	0.06
query28	2.04	0.68	0.42
query29	12.48	4.26	3.35
query30	0.28	0.14	0.14
query31	2.79	0.59	0.34
query32	3.22	0.62	0.52
query33	3.16	3.18	3.19
query34	15.51	4.03	3.40
query35	3.33	3.37	3.36
query36	0.55	0.43	0.44
query37	0.10	0.06	0.07
query38	0.05	0.04	0.04
query39	0.03	0.03	0.03
query40	0.18	0.15	0.14
query41	0.09	0.04	0.04
query42	0.04	0.03	0.03
query43	0.04	0.04	0.04
Total cold run time: 95.95 s
Total hot run time: 24.75 s

### What problem does this PR solve?

Issue Number: None
Related PR: apache#69051

Problem Summary: Physical CLucene fixtures omit norms and now correctly read unit document length, but their Boolean and multi-segment expectations still assumed zero. Update those expectations, including benchmark checksums. Add V2/V3/SNII SQL coverage for SEARCH scores combined with keyword equality and MATCH filters, with WAND enabled and disabled. The current unified reader and predicate collector already share scoring eligibility; the new regression verifies that ordinary keyword filters neither request uncollected statistics nor add scores. Pin the SEARCH execution prerequisites under CI session fuzzing.

### Release note

None. This updates test expectations and regression coverage.

### Check List (For Author)

- Test: 324 ASAN unit tests, changed-line clang-tidy and header hygiene passed. Full ASAN FE/BE build passed. Generated and replayed the 24-query keyword filter suite; all filters match baseline IDs and scores. Replayed test_search_score_raw_index successfully.
    - Regression test / Unit Test
- Behavior changed: No production behavior change
- Does this need documentation: No

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested at reviewed head 34616deab2616a225ffa110bd7547ffda473325f: one new P1 for scored SNII SEARCH conjunctions, and existing P1 4278499777 remains applicable. All 13 changed files were reviewed against the supplied aggregate diff, including upstream/downstream execution, unit tests and both regression goldens. This was static review only; no builds or tests were run and no repository source was edited.

The new finding is an internal SEARCH AND requesting positions from a docs-only keyword source after its statistics are admitted. The existing finding is different: ordinary keyword MATCH can request statistics the collector skipped. Equality and default keyword MATCH are fixed, but INDEX idx_tag(tag) USING INVERTED PROPERTIES("parser"="none") gets support_phrase=true from FE catalog defaults. WHERE SEARCH('body:apple') AND tag MATCH_ANY 'x' with score()/TopN then passes the reader's phrase-support scoring gate while MatchPredicateCollector skips the non-analyzed tag. BM25 requests the missing tag statistics. Keep the existing P1 as blocking; it is not reposted.

Concrete execution reviewed:

Scanner preparation: SEARCH predicates -> collection statistics
Scan execution: SEARCH lowering/bindings -> weights -> source -> postings -> scorer -> TopN
                                                     SNII scored AND -> positions (new P1)

Critical checkpoint conclusions:

  1. Goal and proof: scalar keyword TERM/PHRASE/EXACT/OR scoring is supported by the inspected implementation and supplied tests; the scored SNII AND path remains incomplete.
  2. Scope and reuse: focused admission/default-norm changes reuse PostingsBlock and the existing BM25 formula.
  3. Concurrency and locks: statistics are created per scanner during preparation; new norm state belongs to each cursor. No new thread, shared mutation, lock or lock-order change.
  4. Lifecycle and static initialization: iterator ownership, borrowed positions and next-block span validity remain intact. No new static/global initializer or ownership cycle.
  5. Configuration: no new configuration item; tests use existing execution and WAND variables.
  6. Compatibility: V2/V3 share CLucene handling; SNII retains physical statistics. No persisted format change, new libc API or FE/BE protocol change. Historical analyzed segments missing required metadata remain explicitly rejected. No release ELF was built or audited here.
  7. Thrift/protobuf: no changed definitions or field IDs.
  8. Parallel paths: ordinary/seek partition transitions, WAND, term/phrase/Boolean and format-specific paths were traced. New P1 identifies the missed positional conjunction assumption; existing P1 covers the residual filter eligibility mismatch.
  9. Conditional checks: analyzer classification matches writers and the norm fallback is documented; analyzed missing metadata still produces an error.
  10. Test coverage: inspected no-norm and partition unit cases, physical fixture updates, collection admission/negative cases, and regressions across V2/V3/SNII with WAND on/off, multiple inserts, NULL/empty/case values, absent terms, dual-index EXACT and TopN. Missing internal SEARCH AND and parser=none keyword MATCH variants correspond to the two blockers. ARRAY and mixed-norm/shallow-seek behavior were inspected statically without new dedicated regression coverage.
  11. Changed results: all distinct golden scores and row memberships were independently checked against BM25 math, including legacy avgdl=1 versus SNII physical avgdl=10/11 and the analyzed filter fixture. Ordering is deterministic for the supplied limits. Golden generation provenance and author-reported runtime results were not independently verified.
  12. Observability: existing segment/field errors, timing and collection VLOG remain adequate for these local paths; no new mandatory metric identified.
  13. Transactions and persistence: no changed transaction, EditLog, failover or persistence behavior.
  14. Data mutation and crashes: no production write path changed; visible-rowset selection, physical segment identity, delete bitmap handling and failure clearing remain unchanged.
  15. FE/BE variables: none added; no serialization/send sites require changes.
  16. Performance and memory: one norm-capability check per scoring term opening and one boolean per partition; no new per-row allocation, scan or large buffer. Existing ownership/accounting is unchanged.
  17. Other concerns and user focus: no additional user focus was supplied. Remaining code concerns are the new inline P1 and the independently confirmed existing P1 above; no unresolved candidate or unreviewed changed-file scope remains. Execution and artifact validation remain outside this static review's evidence.

Review converged after 2 round(s).

Existing P0/P1 findings confirmed for this head: #69051 (comment)

bool has_positions, bool has_norms,
bool is_analyzed) {
// A keyword posting has implicit frequency and length one, without either section.
if (is_analyzed && (!has_positions || !has_norms)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Support docs-only keywords in scored SNII conjunctions

This now admits keyword SNII indexes without positions, but the scored AND path still requires them. On the SNII schema from the new keyword-filter regression, SELECT id, score() AS s FROM t WHERE SEARCH('body:apple AND tag:x', '{"mode":"standard"}') ORDER BY s DESC LIMIT 10 reaches OperatorBooleanWeight::listed_terms. SNII sources always batch reads, including a single tag term, so ListedTerms::open(true, true) calls open_terms(..., positions=true, scoring=false); SniiIndexSource rejects the keyword source with snii: the index holds no positions. Support implicit unit frequencies in this conjunction path or use the individual term scorers, and add an internal SEARCH AND regression with WAND on/off. The new SQL AND filter cases do not exercise this path.

### What problem does this PR solve?

Issue Number: None
Related PR: apache#69051

Problem Summary: A docs-only SNII keyword term in a scored SEARCH conjunction entered a batched path that requires positions. Keep it on the existing term scorer with implicit unit frequency. Separately, parser=none may inherit support_phrase=true; ordinary keyword MATCH filters then requested statistics that collection skipped. Share the analyzer-and-phrase eligibility rule between both query-type overloads, keeping heavy analyzer dependencies out of the header.

### Release note

Scored SEARCH conjunctions support docs-only keyword indexes. Keyword MATCH filters do not request uncollected statistics even with inherited phrase support.

### Check List (For Author)

- Test: 365 targeted ASAN BE unit tests, complete ASAN BE build, 60 keyword SQL output checks and 54 raw-index SQL checks passed. Both reported scenarios reproduce on the previous binary. Output is generated with forceGenOut and replayed; the original 24 output blocks remain unchanged.
- Behavior changed: Yes, as described above.
- Does this need documentation: No.
yiguolei pushed a commit that referenced this pull request Oct 16, 2026
…69051 (#69159)

### What problem does this PR solve?

Related PR: #69051

Problem Summary: Backport #69051 to `branch-4.1`.

Legacy keyword SEARCH indexes lack norms and total-term-frequency
metadata. Scoring could use a zero average length or zero norm and
return invalid scores. Preserve whole keyword values, use unit document
length when metadata is absent, and keep WAND upper bounds conservative.
Numeric BKD scoring remains unsupported.

#### Branch adaptations

Backport the final net change from
f22f161. Keep the branch-4.1
collection-statistics and predicate-collector paths and TermInfo
representation. Select the same reader for plain-field statistics and
execution when keyword and analyzed indexes coexist; retain the
branch-specific variant binding path. The branch has no SNII, so this PR
includes V2/V3 coverage and omits SNII-only changes.

### Release note

SEARCH on legacy keyword indexes can return valid BM25 scores.

### Check List (For Author)

- Test
    - [x] Regression test
    - [x] Unit Test
    - [ ] Manual test
- Behavior changed:
- [x] Yes. SEARCH on legacy keyword indexes can return valid BM25
scores.
- Does this need documentation?
    - [x] No.

### Validation

- PASS: Clang 16 across the complete candidate merged with
`origin/branch-4.1` (`0993d5e5512b3d27649e6c44f38b13836e03ee97`).
- PASS: English code check for the complete PR diff.
- PASS: 69 targeted ASAN BE unit tests from 5 suites on this backport.
- PASS: ASAN BE/FE build with `./build.sh --be --fe -j48`.
- PASS: `test_search_score_raw_index` and `test_search_score_cache`,
each replayed on this backport with one suite executed and zero
failures, fatal scripts, or skips.
- PASS: the BE binary requires GLIBC no newer than 2.10 (baseline 2.17).
- PASS: the two backport commits merge together without conflicts.

Validation completed on the committed backport head. The temporary test
instances are shut down after the run.

The unit run selected relevant test sources and existing repository
helpers in a temporary local CMake manifest. The original manifest has
been restored and is not included in this PR.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all 19 changed files at 1c146c172aaf4c308563b9aa0abe2943b6d086e0, including upstream collectors/readers/writers and downstream scoring/TopN paths. No new actionable findings.

Both previously reported P1 mechanisms are addressed at this head:

  • 4278499777: the shared reader eligibility gate keeps equality and non-analyzed keyword MATCH filters out of scoring, consistent with collected statistics, including explicit parser=none with inherited phrase support.
  • 4295241438: scored AND leaves docs-only sources on individual term scorers instead of requesting positions through ListedTerms. Their postings supply unit frequency/norm.

Neither remains an existing blocker; neither is reposted. No additional user review focus was supplied.

The reviewed execution chain is:

SEARCH/MATCH predicates -> physical-index/term statistics -> BM25 similarity
  -> CLucene/SNII source -> streamed postings or ListedTerms -> score collection/TopN/WAND

Critical checkpoint conclusions:

Checkpoint Conclusion
Goal and proof Keyword SEARCH receives usable scoring defaults; unit/regression cases cover intended behavior. This invocation provides static evidence only.
Scope and clarity Changes are focused on eligibility, norms, statistics and conjunction selection; existing analyzer predicates and term scorers are reused.
Concurrency and locks No new thread, shared mutable state or lock-order edge. The new norm flag is local to its cursor/partition.
Lifecycle and static initialization Reader/iterator ownership and cache-handle lifetimes remain intact; no new global initializer or cross-TU dependency.
Configuration No configuration item added.
Compatibility and rolling upgrades No storage/wire format change. Legacy keyword metadata gets defaults; analyzed missing-metadata rejection remains explicit. Release ELF/glibc compatibility was not executed/audited here.
Thrift/protobuf IDs No definition changes; branch comparison is inapplicable.
Parallel paths Checked V2/V3/SNII, SQL filters versus SEARCH leaves, streamed/batched execution, AND/OR/NOT and occurrence booleans, phrase/exact lowering, partitions and WAND.
Conditional branches Comments explain the concrete omitted-norm and missing-position scenarios; canonical analyzer classification matches writers and collectors.
Functional coverage New suites cover three formats, WAND on/off, internal AND, external equality/MATCH filters, TERM/PHRASE/EXACT/OR, case/whitespace, NULL/empty scalar values, absent terms, multiple rowsets, TopN directions and dual indexes.
Negative/unit coverage Inspected analyzed missing-position/norm errors, numeric BKD rejection, index-binding conflicts, cleanup, unit defaults, partition switches, and docs-only conjunction tests.
Changed results All 60 keyword-filter and 54 raw-index golden blocks match static row-set/BM25 arithmetic. WAND pairs agree; SQL keyword filters retain baseline scores. Generation provenance was not independently verified.
Observability Existing query timers, statistics VLOG and error propagation cover these read-side branches; no new telemetry requirement identified.
Transactions and persistence No transaction, EditLog, failover or persisted-metadata path changes. Read versions and delete-bitmap mutation are unaffected.
Data writes and crash behavior No new write path or ownership edge requiring recovery handling.
FE/BE variable passing No new transmitted variable or send path.
Performance and memory New production state is a cursor/partition boolean and stack capability data; SNII capability probes are constant time. No significant new allocation or asymptotic regression identified.
Other risks The exact pinned CLucene source confirms docs-only max frequency stays unknown (-1), preserving the conservative WAND saturation bound. No additional verified issue remains.

Review convergence: converged after 1 round. Both complete-scope reviewers and the separate risk reviewer returned NO_NEW_VALUABLE_FINDINGS; all initial risks have evidence-backed dispositions and the final sweep found no unresolved candidate or unreviewed changed file.

Validation limits: static-only as required; no builds, unit/regression execution, sanitizers, runtime pruning checks, format/hygiene executables or final release-artifact audit. The added SQL suites do not exercise arrays/repeated keyword elements, ignore_above, wholly NULL raw fields or mixed-age analyzed metadata collections; relevant source paths and existing negative tests were inspected. Author-reported runs were not treated as executed evidence from this review.

Review converged after 1 round(s).

@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@airborne12

Copy link
Copy Markdown
Member Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29549 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit c52cdfb6a0640884b46fad7b00b6c10a032a4e9f, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17593	5626	5517	5517
q2	2105	352	248	248
q3	10223	1401	843	843
q4	4684	478	350	350
q5	7443	793	530	530
q6	193	190	156	156
q7	746	802	606	606
q8	9253	1318	1072	1072
q9	5520	4278	4259	4259
q10	6824	1303	1037	1037
q11	448	250	238	238
q12	635	396	291	291
q13	18100	3651	2996	2996
q14	267	273	247	247
q15	q16	744	726	679	679
q17	1195	764	567	567
q18	7154	6256	6208	6208
q19	1265	958	598	598
q20	400	352	225	225
q21	5531	2584	2616	2584
q22	399	362	298	298
Total cold run time: 100722 ms
Total hot run time: 29549 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6508	6655	6366	6366
q2	725	610	600	600
q3	5678	5753	5251	5251
q4	2043	2098	1594	1594
q5	5772	5796	5566	5566
q6	248	192	141	141
q7	2229	2059	1770	1770
q8	3026	2720	2706	2706
q9	8080	7698	7542	7542
q10	3659	3611	3226	3226
q11	596	406	374	374
q12	669	696	499	499
q13	3274	3602	3025	3025
q14	291	296	262	262
q15	q16	666	690	633	633
q17	7746	6988	6855	6855
q18	13045	12213	13078	12213
q19	916	783	780	780
q20	2173	2154	1958	1958
q21	5762	4672	4804	4672
q22	512	455	388	388
Total cold run time: 73618 ms
Total hot run time: 66421 ms

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all 21 changed paths at head 9b356cb594f60b802de117ff9b5915b60500d424. No new actionable findings. Three independent reviewers returned NO_NEW_VALUABLE_FINDINGS; the static review converged after one round.

Both existing P1 concerns are addressed at this head:

  • 4278499777: shared MATCH eligibility excludes external keyword equality/MATCH filters from scoring while SEARCH retains its separate keyword statistics and scorer path.
  • 4295241438: docs-only SNII AND terms retain individual scorers with positions=false; positional sources retain batching. Both original triggers have direct regressions. Neither remains in the blocking-comment set.

Critical checkpoint conclusions:

  • Goal and evidence: keyword SEARCH uses compatible scoring defaults while preserving raw values, case and whitespace; source tracing and targeted tests support the intended behavior.
  • Scope and reuse: production changes are focused and reuse existing eligibility, statistics and term scorers. Three variant fixture edits remove an option absent from the current encoder API.
  • Concurrency and locks: no new threads, shared state, locks or lock-order changes.
  • Lifecycle and initialization: cursor/source ownership and block validity remain coherent; no new static initialization dependencies or cleanup hazards.
  • Configuration: none added; dynamic reload is not applicable.
  • Compatibility: reader-side defaults support existing keyword indexes without changing disk/wire formats or dependencies; analyzed missing-metadata and numeric BKD restrictions remain. The new translation unit is included by existing source discovery.
  • Thrift/protobuf: unchanged; field-ID comparison is not applicable.
  • Parallel paths: checked both helper overloads, MATCH/SEARCH, CLucene V2/V3 and SNII, normalizers, positional/docs-only postings, AND/OR/NOT, partition/shallow-seek transitions and WAND. Unit norm bounds agree with scored postings; unavailable frequency bounds remain conservative.
  • Conditions and errors: comments explain inherited phrase metadata and missing positions; failures propagate through the existing Status/exception boundary.
  • Test coverage: added unit and regression cases cover explicit/implicit keyword metadata, internal conjunctions, external filters, exact/phrase/term matching, TopN, ascending scores, case, missing values and WAND on/off. Existing analyzed-metadata negative cases remain.
  • Expected results: all 114 output markers are accounted for; every distinct BM25 result was independently recomputed. Ordering and bounded ties are deterministic.
  • Observability: existing errors, collection logging and SEARCH timers remain sufficient for this local change.
  • Transactions and persistence: no EditLog, version, recovery or failover changes.
  • Data writes and atomicity: no production write-path changes.
  • FE/BE variables: none added or transmitted.
  • Performance and memory: the capability probe reads SNII metadata; existing streaming fallback adds no new data-sized allocation or lock work.
  • Other core invariants: no new visibility, delete-bitmap, nullable-column or ignored-status issue identified. No additional user focus was supplied.

Validation limits: this was static review only, as required; no builds or tests were run. Runtime success, output-generation provenance and final ELF compatibility were not independently verified. Larger postings, array/deletion combinations and additional nesting were not executed. No unresolved static candidate or assigned coverage gap remains.

Review converged after 1 round(s).

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29326 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 9b356cb594f60b802de117ff9b5915b60500d424, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17589	5668	5540	5540
q2	2120	331	271	271
q3	10210	1412	856	856
q4	4684	475	340	340
q5	7436	800	541	541
q6	200	195	158	158
q7	766	802	613	613
q8	10018	1360	1111	1111
q9	5730	4294	4284	4284
q10	6901	1303	1030	1030
q11	451	256	243	243
q12	636	408	284	284
q13	18109	3562	2993	2993
q14	279	276	244	244
q15	q16	730	731	687	687
q17	1221	770	585	585
q18	7069	6242	6181	6181
q19	1115	973	595	595
q20	397	350	237	237
q21	4894	2269	2404	2269
q22	369	321	264	264
Total cold run time: 100924 ms
Total hot run time: 29326 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6156	6158	6147	6147
q2	700	568	536	536
q3	5209	5462	4984	4984
q4	2017	2115	1549	1549
q5	5333	5200	5172	5172
q6	246	185	144	144
q7	2121	1858	1698	1698
q8	2962	2560	2571	2560
q9	7675	7622	7631	7622
q10	3691	3594	3250	3250
q11	605	426	379	379
q12	673	701	505	505
q13	3270	3705	2967	2967
q14	291	291	287	287
q15	q16	662	697	624	624
q17	7869	7059	6894	6894
q18	13129	12207	13129	12207
q19	935	808	819	808
q20	2185	2172	1941	1941
q21	5853	4744	4818	4744
q22	504	459	413	413
Total cold run time: 72086 ms
Total hot run time: 65431 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152370 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 9b356cb594f60b802de117ff9b5915b60500d424, data reload: false

query5	4285	596	470	470
query6	431	232	200	200
query7	4795	443	232	232
query8	316	167	154	154
query9	8766	3975	3985	3975
query10	473	310	257	257
query11	6078	3771	3537	3537
query12	147	92	90	90
query13	1256	456	331	331
query14	6129	4028	3764	3764
query14_1	3509	3474	3477	3474
query15	211	198	187	187
query16	961	448	421	421
query17	843	638	526	526
query18	2464	424	302	302
query19	196	157	126	126
query20	86	86	84	84
query21	222	131	111	111
query22	12901	13108	12678	12678
query23	13549	12885	12454	12454
query23_1	12749	12655	12663	12655
query24	7080	851	452	452
query24_1	477	456	472	456
query25	518	396	337	337
query26	1197	251	135	135
query27	2782	437	267	267
query28	4625	1975	1944	1944
query29	1523	595	451	451
query30	294	229	189	189
query31	1032	933	815	815
query32	155	99	96	96
query33	514	310	243	243
query34	919	866	498	498
query35	764	781	658	658
query36	812	829	709	709
query37	131	104	91	91
query38	1806	1732	1670	1670
query39	808	826	752	752
query39_1	736	737	757	737
query40	231	131	98	98
query41	75	70	68	68
query42	91	89	84	84
query43	367	376	341	341
query44	1284	714	702	702
query45	185	179	162	162
query46	855	965	575	575
query47	2971	2966	2896	2896
query48	306	321	228	228
query49	579	407	306	306
query50	702	287	222	222
query51	10654	10509	10281	10281
query52	84	91	73	73
query53	188	217	155	155
query54	247	206	210	206
query55	78	71	68	68
query56	242	246	224	224
query57	1583	1589	1571	1571
query58	300	277	268	268
query59	2206	2114	2044	2044
query60	305	245	240	240
query61	173	170	164	164
query62	396	349	294	294
query63	199	168	173	168
query64	2860	983	763	763
query65	3286	3255	3254	3254
query66	1767	415	323	323
query67	20384	20159	20198	20159
query68	3279	989	607	607
query69	389	277	240	240
query70	851	768	864	768
query71	298	229	191	191
query72	2794	2584	2340	2340
query73	525	533	292	292
query74	4597	4463	4343	4343
query75	2347	2288	1928	1928
query76	2370	1023	606	606
query77	356	419	277	277
query78	9842	9761	9171	9171
query79	993	856	520	520
query80	1273	421	345	345
query81	593	315	274	274
query82	770	134	107	107
query83	300	195	188	188
query84	311	120	98	98
query85	887	431	383	383
query86	405	231	231	231
query87	2009	1933	1826	1826
query88	3616	2721	2697	2697
query89	338	296	259	259
query90	1920	180	179	179
query91	164	153	131	131
query92	94	88	93	88
query93	985	983	565	565
query94	643	321	269	269
query95	582	401	317	317
query96	668	536	233	233
query97	2500	2447	2337	2337
query98	170	152	148	148
query99	787	759	650	650
Total cold run time: 236020 ms
Total hot run time: 152370 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.61 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 9b356cb594f60b802de117ff9b5915b60500d424, data reload: false

query1	0.01	0.00	0.01
query2	0.10	0.05	0.05
query3	0.26	0.12	0.11
query4	1.61	0.12	0.12
query5	0.24	0.21	0.24
query6	1.17	0.69	0.67
query7	0.03	0.00	0.00
query8	0.05	0.03	0.04
query9	0.41	0.34	0.35
query10	0.58	0.60	0.57
query11	0.19	0.14	0.13
query12	0.18	0.14	0.14
query13	0.48	0.48	0.48
query14	1.04	1.01	1.01
query15	0.64	0.60	0.59
query16	0.33	0.35	0.35
query17	1.11	1.14	1.10
query18	0.23	0.21	0.21
query19	2.14	1.99	1.94
query20	0.02	0.02	0.01
query21	15.47	0.28	0.14
query22	4.36	0.05	0.05
query23	16.27	0.32	0.12
query24	2.77	0.73	0.41
query25	0.09	0.06	0.04
query26	1.74	0.25	0.16
query27	0.05	0.03	0.03
query28	2.48	0.63	0.40
query29	12.52	4.28	3.40
query30	0.29	0.14	0.15
query31	2.77	0.57	0.34
query32	3.22	0.61	0.52
query33	3.29	3.22	3.22
query34	15.44	4.03	3.41
query35	3.36	3.36	3.35
query36	0.56	0.47	0.47
query37	0.09	0.06	0.06
query38	0.05	0.03	0.03
query39	0.03	0.03	0.03
query40	0.17	0.14	0.15
query41	0.09	0.03	0.03
query42	0.03	0.04	0.03
query43	0.04	0.04	0.03
Total cold run time: 96 s
Total hot run time: 24.61 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (7/7) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.26% (35797/46336)
Line Coverage 62.47% (404027/646735)
Region Coverage 58.91% (341148/579066)
Branch Coverage 59.88% (157695/263364)

@airborne12
airborne12 merged commit 4016f20 into apache:master Oct 20, 2026
37 checks passed
@airborne12
airborne12 deleted the foreman/T-75 branch October 20, 2026 02:29
@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

yiguolei pushed a commit that referenced this pull request Oct 21, 2026
…69051 (#69318)

### What problem does this PR solve?

Issue Number: None

Related PR: #69051

Problem Summary: Backport the merged keyword SEARCH BM25 fix to
branch-4.2. Projecting `score()` on keyword indexes can drop matching
rows or reject missing scoring metadata. Keyword MATCH filters can also
request statistics that were not collected, and scored SNII conjunctions
can incorrectly require positions from docs-only terms.

Use unit frequency and encoded norm 1 for keyword postings, preserve
whole keyword values, and score docs-only terms through the existing
individual term scorer. Legacy indexes without norms use average length
1; SNII retains its physical collection statistics. The BM25 formula and
the restrictions for analyzed indexes remain unchanged.

Source commit: `4016f207bfe7eff77f56f4d98d23629888539462`.

#### Target branch adaptations

- branch-4.2 already contains the unified MATCH/SEARCH engine. It does
not contain master's normalizer-only scoring exception, so this pick
retains the target analyzer-and-phrase eligibility policy and
analyzed-index metadata requirements.
- The older CLucene interface lacks norm-presence propagation. Pass
`reader->hasNorms()` through posting partitions and restore it on
partition switches, including WAND bounds.
- Retain the target compile-check wrappers and adapt the statistics
helper signatures without weakening assertions. Both source regression
suites and their expected outputs are preserved verbatim.
- GitHub's historical PR-head diff also shows three Variant
parser-option compatibility files. They are absent from the final merged
squash, and branch-4.2 still has that parser option. They are excluded.
Merging the PR head `9b356cb594f60b802de117ff9b5915b60500d424` into the
landing parent reproduces the exact landed tree
`57f5e5d63fafe808a83bc393e36ea59a6e367ca0`.

### Release note

SEARCH with `score()` supports keyword inverted indexes, including
conjunctions, TopN ordering, WAND execution, and keyword MATCH filters.

### Check List (For Author)

- Test
- [x] Regression test: both source suites passed; all 114 expected tags
ran exactly once (54 raw-index and 60 keyword-filter checks).
- [x] Unit Test: 369 affected ASAN tests passed; 3 pre-existing
benchmarks are disabled. All 7 source-added or renamed cases were
discovered and executed.
    - [ ] Manual test
    - [ ] No need to test or manual test.
- Behavior changed:
    - [ ] No.
    - [x] Yes, as described above.
- Does this need documentation?
    - [x] No. No syntax or configuration changes.
    - [ ] Yes.

### Backport validation

- PASS: `source ./custom_env.sh && bash build.sh --be --fe -j 24` (ASAN
BE, FE and packaged output; 45m 19s).
- PASS: full ASAN UT binary build with `bash run-be-ut.sh -j 48`,
default `GLIBC_COMPATIBILITY=ON`; no local CMake test-source selection.
- PASS: `bash run-be-ut.sh --run
--filter="BooleanPostingsBench.*:CluceneIndexSourceTest.*:CollectionStatisticsDetailedTest.*:CollectionStatisticsSniiScoringTest.*:CollectionStatisticsTest.*:CursorChainedPostings.*:DocSetCollectorTest.*:ExpansionLeafTest.*:FunctionSearchNullTest.*:FunctionSearchTest.*:IndexReaderHelperTest.*:ListedTermsTest.*:ListedTermsWalkTest.*:MultiSegmentCollectorTest.*:PhraseQueryV2Test.*:PhraseScorerStreamTest.*:PrefixQueryV2Test.*:SegmentPostingsTest.*"
-j 48`; 369 passed, 3 existing disabled benchmarks, zero failures or
sanitizer reports. Coverage includes postings/norms, collection
statistics, MATCH eligibility, conjunctions, prefix/phrase queries,
cursor chaining, and SEARCH execution/NULL handling.
- PASS: `bash run-regression-test.sh --run -d search -s
test_search_score_raw_index,test_search_score_keyword_filters -parallel
1` on an isolated ASAN FE/BE instance. Both suites passed with zero
failed/fatal/skipped scripts. All 114 tags ran exactly once; source
expected files are byte-identical. Coverage includes V2/V3/SNII, WAND
on/off, whole keyword values, TopN/ascending order, internal AND,
external equality/MATCH filters, and implicit/explicit `parser=none`.
- PASS: complete merged-candidate format preflight for head
`809eb7498a0fdfce905bdde7a350f3ba207947bd` against latest branch-4.2
`bcaaa5f293674f1adba0dfa52c8f8f57fb5e9549`; complete committed PR
English check (17 source files, zero exceptions).
- Builds and execution used target base
`62cb1df3a7f8124ad338e8788e4faaf71f030b07`. The subsequent
commit-message amendment preserved the exact tested code tree. The newer
target commit changes JSON_SEARCH path escaping and has no changed-file
overlap with this pick.
- Modified-line clang-tidy 20.1.8 processed five production and six test
translation units with the branch configuration and current compilation
database. It emitted no changed-line diagnostics. The command exits with
the pre-existing unmatched `NOLINTEND` in `be/src/core/types.h:578`. The
same diagnostic was reproduced with all modified existing production
files restored to target-base contents through a VFS overlay; the
offending header is byte-identical to base
`62cb1df3a7f8124ad338e8788e4faaf71f030b07`. No unrelated baseline fix is
included.

<details>
<summary>Source hunk audit</summary>

| File | Source hunk | Disposition |
| --- | --- | --- |
| `be/src/storage/index/index_reader_helper.cpp` | `@@ -0,0 +1,54 @@` |
Adapted: use the target canonical analyzer-and-phrase policy; no
master-only normalizer exception. |
| `be/src/storage/index/index_reader_helper.h` | `@@ -103,36 +103,12 @@
public:` | Adapted: retain target compile-check wrappers and phrase-only
MATCH eligibility. |
| `be/src/storage/index/inverted/clucene/spi/clucene_postings_cursor.h`
| `@@ -155,7 +155,7 @@ public:` | Adapted: preserve source unit norm and
WAND bound behavior, adding missing norm-presence partition fields from
the older target interface. |
| `be/src/storage/index/inverted/clucene/spi/clucene_postings_cursor.h`
| `@@ -305,6 +305,8 @@ public:` | Adapted: preserve source unit norm and
WAND bound behavior, adding missing norm-presence partition fields from
the older target interface. |
| `be/src/storage/index/inverted/clucene/spi/clucene_postings_cursor.h`
| `@@ -314,8 +316,8 @@ private:` | Adapted: preserve source unit norm
and WAND bound behavior, adding missing norm-presence partition fields
from the older target interface. |
|
`be/src/storage/index/inverted/query/engine/boolean_query/operator_boolean_weight.h`
| `complete postimage` | Ported (verbatim) |
| `be/src/storage/index/inverted/query/scoring/bm25_similarity.h` | `@@
-24,14 +24,8 @@` | Ported: source change retained with target-only
surrounding context. |
| `be/src/storage/index/inverted/reader/collection_statistics.cpp` | `@@
-44,9 +44,11 @@ namespace collection_statistics_detail {` | Adapted: add
keyword/analyzed distinction; keep target analyzed-index metadata
restrictions and omit unavailable normalizer-only exception. |
| `be/src/storage/index/inverted/reader/collection_statistics.cpp` | `@@
-264,10 +266,11 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset,` | Adapted: add keyword/analyzed distinction;
keep target analyzed-index metadata restrictions and omit unavailable
normalizer-only exception. |
| `be/src/storage/index/inverted/reader/collection_statistics.cpp` | `@@
-337,12 +340,11 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset,` | Adapted: add keyword/analyzed distinction;
keep target analyzed-index metadata restrictions and omit unavailable
normalizer-only exception. |
| `be/src/storage/index/inverted/reader/collection_statistics.cpp` | `@@
-351,10 +353,8 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset,` | Adapted: add keyword/analyzed distinction;
keep target analyzed-index metadata restrictions and omit unavailable
normalizer-only exception. |
| `be/src/storage/index/inverted/reader/collection_statistics.cpp` | `@@
-375,11 +375,12 @@ Status CollectionStatistics::process_segment(const
RowsetSharedPtr& rowset,` | Adapted: add keyword/analyzed distinction;
keep target analyzed-index metadata restrictions and omit unavailable
normalizer-only exception. |
| `be/src/storage/index/inverted/reader/collection_statistics.h` | `@@
-86,8 +86,9 @@ private:` | Adapted: target helper gains only
is_analyzed; retain compile-check wrappers. |
| `be/src/storage/index/inverted/reader/collection_statistics.h` | `@@
-121,11 +122,12 @@ struct SniiScoringSegmentStats {` | Adapted: target
helper gains only is_analyzed; retain compile-check wrappers. |
|
`be/test/storage/index/inverted/query/engine/boolean_postings_bench_test.cpp`
| `complete postimage` | Ported (verbatim) |
|
`be/test/storage/index/inverted/query/engine/boolean_query/listed_terms_test.cpp`
| `complete postimage` | Ported (verbatim) |
|
`be/test/storage/index/inverted/query/engine/multi_segment_collector_test.cpp`
| `complete postimage` | Ported (verbatim) |
|
`be/test/storage/index/inverted/query/engine/segment_postings_test.cpp`
| `complete postimage` | Ported (verbatim) |
| `be/test/storage/index/inverted/query/spi/fake_index_source.h` |
`complete postimage` | Ported (verbatim) |
| `be/test/storage/index/inverted/reader/collection_statistics_test.cpp`
| `@@ -667,6 +667,7 @@ protected:` | Adapted: keep keyword tests and
target fixtures; omit signature update for the normalizer-only test that
does not exist on 4.2. |
| `be/test/storage/index/inverted/reader/collection_statistics_test.cpp`
| `@@ -676,8 +677,8 @@ protected:` | Adapted: keep keyword tests and
target fixtures; omit signature update for the normalizer-only test that
does not exist on 4.2. |
| `be/test/storage/index/inverted/reader/collection_statistics_test.cpp`
| `@@ -975,26 +976,45 @@ TEST_F(CollectionStatisticsTest,
LegacyV3RejectsSegmentWrittenWithoutNorms) {` | Adapted: keep keyword
tests and target fixtures; omit signature update for the normalizer-only
test that does not exist on 4.2. |
| `be/test/storage/index/inverted/reader/collection_statistics_test.cpp`
| `@@ -1230,9 +1250,10 @@ TEST(CollectionStatisticsSniiScoringTest,
ResolveRejectsSegmentWithoutPositions)` | N-A: this hunk only updates
the pre-existing normalizer-only exception test; that path/test does not
exist on branch-4.2. Keyword coverage is retained. |
| `be/test/storage/index/inverted/reader/collection_statistics_test.cpp`
| `@@ -1939,7 +1960,9 @@ TEST_F(CollectionStatisticsTest,
CollectSkipsIndexWithoutAnalyzer) {` | Adapted: keep keyword tests and
target fixtures; omit signature update for the normalizer-only test that
does not exist on 4.2. |
| `be/test/storage/index/inverted/reader/collection_statistics_test.cpp`
| `@@ -2377,7 +2400,7 @@ TEST_F(CollectionStatisticsTest,
SearchArrayStringSelectsFulltextLeafIndex) {` | Adapted: keep keyword
tests and target fixtures; omit signature update for the normalizer-only
test that does not exist on 4.2. |
| `be/test/storage/index/inverted/reader/collection_statistics_test.cpp`
| `@@ -2397,8 +2420,8 @@ TEST_F(CollectionStatisticsTest,
SearchExactIgnoresAnalyzedBindingHint) {` | Adapted: keep keyword tests
and target fixtures; omit signature update for the normalizer-only test
that does not exist on 4.2. |
| `be/test/storage/segment/index_reader_helper_test.cpp` | `@@ -22,6
+22,7 @@` | Ported: source change retained with target-only surrounding
context. |
| `be/test/storage/segment/index_reader_helper_test.cpp` | `@@ -380,6
+381,7 @@ TEST_F(IndexReaderHelperTest,
IsNeedSimilarityScoreWithInvertedIndexQueryTypeTes` | Ported: source
change retained with target-only surrounding context. |
| `be/test/storage/segment/index_reader_helper_test.cpp` | `@@ -448,6
+450,7 @@ TEST_F(IndexReaderHelperTest,
IsNeedSimilarityScoreWithTExprOpcodeTest) {` | Ported: source change
retained with target-only surrounding context. |
| `be/test/storage/segment/index_reader_helper_test.cpp` | `@@ -460,3
+463,51 @@ TEST_F(IndexReaderHelperTest,
IsNeedSimilarityScoreWithTExprOpcodeTest) {` | Ported: source change
retained with target-only surrounding context. |
| `regression-test/data/search/test_search_score_keyword_filters.out` |
`complete postimage` | Ported (verbatim) |
| `regression-test/data/search/test_search_score_raw_index.out` |
`complete postimage` | Ported (verbatim) |
|
`regression-test/suites/search/test_search_score_keyword_filters.groovy`
| `complete postimage` | Ported (verbatim) |
| `regression-test/suites/search/test_search_score_raw_index.groovy` |
`complete postimage` | Ported (verbatim) |
| `be/src/storage/index/inverted/clucene/spi/clucene_index_source.cpp` |
`additional target adaptation` | Adapted: pass reader.hasNorms into the
older postings partition so keyword unit norms work. Source parent
already contains this plumbing. |
| `be/test/core/data_type_serde/data_type_serde_arrow_test.cpp` | `@@
-957,9 +957,8 @@ TEST(DataTypeSerDeArrowTest,
PlainArrowConverterDoesNotInferIcebergUuid) {` | N-A: historical PR-head
compatibility edit, absent from the final landed squash. The target API
still has check_duplicate_json_path. |
| `be/test/core/data_type_serde/data_type_serde_arrow_test.cpp` | `@@
-1054,9 +1053,8 @@ TEST(DataTypeSerDeArrowTest,
IcebergVariantExtensionAndParquetSchema) {` | N-A: historical PR-head
compatibility edit, absent from the final landed squash. The target API
still has check_duplicate_json_path. |
|
`be/test/core/data_type_serde/data_type_variant_v2_serde_jsonb_test.cpp`
| `@@ -112,9 +112,8 @@ std::string json_at(const DataTypeVariantV2SerDe&
serde, const IColumn& column,` | N-A: historical PR-head compatibility
edit, absent from the final landed squash. The target API still has
check_duplicate_json_path. |
| `be/test/exprs/variant_v2_execution_test.cpp` | `@@ -49,9 +49,8 @@
TEST(VariantV2ExecutionTest, TypePropertiesDoNotAffectEquality) {` |
N-A: historical PR-head compatibility edit, absent from the final landed
squash. The target API still has check_duplicate_json_path. |

</details>

#### Range-diff review

Compared the landed source commit against the target pick with `git
range-diff 4016f20^..4016f20 HEAD^..HEAD`.

Representative differences:

```diff
      ## be/src/storage/index/index_reader_helper.cpp (new) ##
     @@
    @@ be/src/storage/index/index_reader_helper.cpp (new)
     +    // parser=none can inherit support_phrase=true from FE defaults. MATCH statistics skip
     +    // these keyword indexes, so their filters must not request a scoring context either.
     +    return inverted_index::InvertedIndexAnalyzer::should_analyzer(properties) &&
    -+           (get_parser_phrase_support_string_from_properties(properties) ==
    -+                    INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES ||
    -+            IndexReaderHelper::is_normalizer_only(properties));
    ++           get_parser_phrase_support_string_from_properties(properties) ==
    ++                   INVERTED_INDEX_PARSER_PHRASE_SUPPORT_YES;
     +}
     +
     +} // namespace
    @@ be/src/storage/index/index_reader_helper.h: public:
              return iter->get_reader(InvertedIndexReaderType::STRING_TYPE) != nullptr;
          }
      
    -+    // MATCH filters score analyzed phrase indexes and single-term normalizers.
    ++    // MATCH filters score analyzed indexes with phrase support.
          static bool is_need_similarity_score(InvertedIndexQueryType query_type,
     -                                         const TabletIndex* index_meta) {
     -        if (query_type == InvertedIndexQueryType::MATCH_ANY_QUERY ||

    + ## be/src/storage/index/inverted/clucene/spi/clucene_index_source.cpp ##
    +@@ be/src/storage/index/inverted/clucene/spi/clucene_index_source.cpp: ClucenePostingsCursor::Partition open_postings(lucene::index::IndexReader* reade
    +                                                lucene::index::Term* term, bool positions,
    +                                                bool scoring, const io::IOContext* io_ctx) {
    +     ClucenePostingsCursor::Partition result;
    ++    result.has_norms = reader->hasNorms(term->field());
    +     if (positions) {
    +         auto iter = make_term_positions_ptr(reader, scoring, io_ctx);
    +         result.positions = iter.get();
     

      ## be/src/storage/index/inverted/reader/collection_statistics.cpp ##
     @@ be/src/storage/index/inverted/reader/collection_statistics.cpp: namespace collection_statistics_detail {
    + 
      Result<SniiScoringSegmentStats> resolve_snii_scoring_segment(uint64_t index_doc_count,
                                                                   uint64_t sum_total_term_freq,
    -                                                              bool has_positions, bool has_norms,
    -+                                                             bool is_analyzed,
    -                                                              bool single_term_normalizer) {
    --    // Lowercase-normalized docs-only postings use unit frequency and length for scoring.
    --    if ((!has_positions || !has_norms) && !single_term_normalizer) {
    -+    // Keywords and lowercase-only normalizers can omit both sections: a matched value
    -+    // contributes one term with unit frequency and length. Other analyzed sources need both.
    -+    if (is_analyzed && !single_term_normalizer && (!has_positions || !has_norms)) {
    +-                                                             bool has_positions, bool has_norms) {
    +-    if (!has_positions || !has_norms) {
    ++                                                             bool has_positions, bool has_norms,
    ++                                                             bool is_analyzed) {
    ++    // Keyword terms use unit frequency and length without positions or norms.
    ++    // Analyzed sources still require both sections.
    ++    if (is_analyzed && (!has_positions || !has_norms)) {
              return ResultError(Status::Error<ErrorCode::INVERTED_INDEX_NOT_SUPPORTED, false>(
                      "SNII scoring requires positions and norms; this segment was written without "
                      "positions or without norms. Norms are left out when the index sets \"norms\" = "
```

The first and third groups omit the master-only normalizer exception and
preserve branch-4.2's existing analyzed-index restrictions. The second
group supplies the older target's missing norm-presence plumbing; the
landed source parent already has it. Other differences are the
corresponding helper signatures and tests, retained compile-check
context, and backport commit metadata. The source hunk that updates the
normalizer-only test is inapplicable because that feature and test do
not exist on branch-4.2. The complete audit above accounts for all
source hunks and the extra target plumbing file.

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
airborne12 added a commit to airborne12/apache-doris that referenced this pull request Oct 29, 2026
…oks up

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69051

Problem Summary: the previous commit collects BM25 statistics for MATCH on
a keyword index. Two inputs made that collection disagree with the reader,
and the score() query failed where it used to run with a score of 0:

1. Phrase slop. A keyword MATCH_PHRASE reads a trailing " ~N" or " ~N+" as
   its slop: lower_match strips it, so tag MATCH_PHRASE 'BP ~1' looks up
   the term BP. The collector stored "BP ~1", and the scorer then failed
   with "Index statistics collection failed: Not such term BP".
   The collector now strips the slop with the same take_slop, as the
   no-index MATCH_PHRASE path in match.cpp already does.

2. A number cast to text. With enable_match_without_inverted_index (on by
   default), body MATCH 'apple' AND CAST(n AS STRING) MATCH '1' runs the
   second predicate as a scalar filter. The collector looks through the
   cast to n, selects its BKD index, and, because a BKD index has no
   analyzer, the keyword rule admitted it; collecting statistics then
   opened the BKD index as a text index and the query failed with
   "segment: logical index is not a text inverted index". Only a text
   index is read with a score, so a MATCH on a non-string leaf now
   collects nothing.

### Release note

None

### Check List (For Author)

- Test: Regression test / Unit Test
    - Unit Test: CollectionStatisticsTest (new
      KeywordPhraseCollectsTermWithoutSlop and
      MatchOnNumericColumnCollectsNothing), IndexReaderHelperTest
    - Regression test: test_match_keyword_score gains phrase slop and
      CAST(n AS STRING) MATCH cases on V2/V3/SNII (.out generated, only
      new blocks added); test_omit_norms, test_match_phrase_prefix_score,
      test_bm25_score, test_search_score_keyword_filters and
      test_search_score_raw_index pass
    - Mutation check: with the collector change reverted, both new unit
      tests fail, the suite fails with "Not such term BP", and the
      CAST(n AS STRING) MATCH query fails with the error above
- Behavior changed: No (fixes the previous commit before release)
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
airborne12 added a commit that referenced this pull request Oct 30, 2026
…sticsearch (#69815)

### What problem does this PR solve?

Issue Number: None

Related PR: #69052

Problem Summary: score() of pattern queries disagrees with Elasticsearch
and with itself:

    -- 'Inverted indexes are suitable for full text search',
    -- 'The parser splits text into multiple terms', ...
SELECT score() FROM document WHERE SEARCH('content:/.*ext/'); -- BM25
sum
SELECT score() FROM document WHERE SEARCH('content:*ext'); -- BM25 sum
    SELECT score() FROM document WHERE SEARCH('content:te*');      -- 1
    SELECT score() FROM document WHERE content MATCH_REGEXP 'ext'; -- 0

Elasticsearch scores a query_string /regexp/, wildcard or prefix, and a
regexp, wildcard or prefix query, with a constant 1 per matching row
(default rewrite constant_score_blended); measured on 9.5.5 with the
rows above: 1.0 for both matching rows. Only an explicit
"rewrite": "scoring_boolean" sums the BM25 of the expanded terms. Next
to a scored clause in a bool must, a regexp adds exactly 1.

#69052 made SEARCH regexp and wildcard sum the BM25 of every expanded
term, i.e. scoring_boolean always on, while prefix stayed constant. That
sum also takes idf and avgdl from the current segment, not from the
tablet statistics MATCH uses: two inserts of the same text scored
0.2876821 and 1.3862942 for identical 'apple' rows, while MATCH 'apple'
scored 0.6931471 for both. MATCH_REGEXP never requested a scoring
context, so it scored 0 like a filter.

Fix:
1. Revert the scoring part of #69052: ExpandWeight scores every pattern
   with the constant scorer again. The test fixture extensions that
   #69051 built on are kept.
2. MATCH_REGEXP requests scoring on every index; the regexp expansion
   then publishes the constant 1. The MATCH statistics collector skips
   it, since a constant reads no statistics and the pattern is not text
   to analyze.
github-actions Bot pushed a commit that referenced this pull request Nov 1, 2026
…sticsearch (#69815)

### What problem does this PR solve?

Issue Number: None

Related PR: #69052

Problem Summary: score() of pattern queries disagrees with Elasticsearch
and with itself:

    -- 'Inverted indexes are suitable for full text search',
    -- 'The parser splits text into multiple terms', ...
SELECT score() FROM document WHERE SEARCH('content:/.*ext/'); -- BM25
sum
SELECT score() FROM document WHERE SEARCH('content:*ext'); -- BM25 sum
    SELECT score() FROM document WHERE SEARCH('content:te*');      -- 1
    SELECT score() FROM document WHERE content MATCH_REGEXP 'ext'; -- 0

Elasticsearch scores a query_string /regexp/, wildcard or prefix, and a
regexp, wildcard or prefix query, with a constant 1 per matching row
(default rewrite constant_score_blended); measured on 9.5.5 with the
rows above: 1.0 for both matching rows. Only an explicit
"rewrite": "scoring_boolean" sums the BM25 of the expanded terms. Next
to a scored clause in a bool must, a regexp adds exactly 1.

#69052 made SEARCH regexp and wildcard sum the BM25 of every expanded
term, i.e. scoring_boolean always on, while prefix stayed constant. That
sum also takes idf and avgdl from the current segment, not from the
tablet statistics MATCH uses: two inserts of the same text scored
0.2876821 and 1.3862942 for identical 'apple' rows, while MATCH 'apple'
scored 0.6931471 for both. MATCH_REGEXP never requested a scoring
context, so it scored 0 like a filter.

Fix:
1. Revert the scoring part of #69052: ExpandWeight scores every pattern
   with the constant scorer again. The test fixture extensions that
   #69051 built on are kept.
2. MATCH_REGEXP requests scoring on every index; the regexp expansion
   then publishes the constant 1. The MATCH statistics collector skips
   it, since a constant reads no statistics and the pattern is not text
   to analyze.
airborne12 added a commit to airborne12/apache-doris that referenced this pull request Nov 2, 2026
…tory

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69051

Problem Summary: while collecting BM25 statistics, snii_field_has_no_terms
checks whether a field is a BKD index or a VARIANT path the segment does
not hold. It called IndexFileReader::snii_logical_indexes(), which returns
the directory by value, so every scored field of every segment deep-copied
all directory entries, with their suffix strings and BKD file tables. The
check runs for every SNII score query, including analyzed MATCH and SEARCH
on a table where a VARIANT index adds thousands of unrelated paths. A
standalone estimate with the same entry layout gave about 260 us per copy
at 2048 entries, against about 4 us for the scan alone.

Add SniiSegmentReader::find_logical_index, which exposes the
MetadataDirectory::find lookup open_index already uses, and
IndexFileReader::snii_logical_index_kind on top of it. The check now reads
the kind of one entry and copies nothing. Results do not change.

### Release note

None

### Check List (For Author)

- Test: Regression test / Unit Test
    - Unit Test: new SniiSegmentReaderLogicalIndexesTest
      .FindsDirectoryEntryWithoutCopyingIt; 1620 tests in
      IndexReaderHelperTest, CollectionStatistics* and *Snii* pass, among
      them SniiScoringSkipsVariantPathMissingFromSegment and
      SniiScoringRejectsMissingLogicalIndexForWholeCollection
    - Regression test: test_match_keyword_score (its BKD and sparse VARIANT
      cases on V2/V3/SNII), test_omit_norms, test_match_phrase_prefix_score,
      test_bm25_score, test_search_score_keyword_filters,
      test_search_score_raw_index, test_search_score_pattern_constant pass
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
airborne12 added a commit that referenced this pull request Nov 3, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: #69051

Problem Summary: score() of a MATCH on an untokenized (keyword) inverted
index is always 0, while SEARCH on the same field scores BM25:

    INDEX idx_redirect (redirect) USING INVERTED
    -- one row ('BP')
    SELECT score() FROM t WHERE redirect MATCH 'BP';  -- 0
    SELECT score() FROM t WHERE SEARCH('redirect:BP'); -- 0.2876821

Elasticsearch scores a match, term or match_phrase query on a keyword
field with BM25 in query context: no norms and no frequencies, so
freq = dl = avgdl = 1 and the score is the idf of the whole value
(0.2876821 above, 0.6931471 for N=4, n=2). Only a bool filter scores 0.
SEARCH already gives exactly these values. MATCH on keyword behaved like
a filter, so `body MATCH 'apple' AND tag MATCH 'x'` ranked as if the tag
clause were not there.

Root cause: #69051 made the engine score keyword postings (unit
frequency
and norm) but kept MATCH out on purpose: can_score_match required an
analyzed index, and MatchPredicateCollector returned early for indexes
without an analyzer, so no statistics were collected for them.

Fix: a keyword (non-analyzed) index is eligible for MATCH scoring
regardless of support_phrase, and MatchPredicateCollector collects its
whole value as one term (case and spaces kept), the same term the reader
looks up and the same as SearchPredicateCollector collects. Analyzed
indexes keep the existing support_phrase / lowercase-normalizer rule.
Equality and IN stay pure filters. A keyword MATCH_PHRASE_PREFIX scores
the constant 1 of a single-term prefix; MATCH_REGEXP is unchanged.
yiguolei pushed a commit that referenced this pull request Nov 5, 2026
…ARCH #69790 (#70008)

Backport of #69790 to branch-4.2.

### What problem does this PR solve?

Issue Number: None

Related PR: #69790, #69051

Problem Summary: score() of a MATCH on an untokenized (keyword) inverted
index is always 0, while SEARCH on the same field scores BM25:

    INDEX idx_redirect (redirect) USING INVERTED
    -- one row ('BP')
    SELECT score() FROM t WHERE redirect MATCH 'BP';  -- 0
    SELECT score() FROM t WHERE SEARCH('redirect:BP'); -- 0.2876821

Elasticsearch scores match, term and match_phrase on a keyword field
with BM25 in query context, so the score is the idf of the whole value.
#69051 (already on 4.2) made the engine score keyword postings but kept
MATCH out. This makes a keyword index eligible for MATCH scoring and
collects its whole value as one term, the one the reader looks up. It
also carries the follow-up fixes from #69790's review:
- A keyword MATCH_PHRASE drops its ` ~N` slop before collecting.
- A MATCH on a non-string leaf such as `CAST(n AS STRING)` collects
nothing.
- A VARIANT path that a segment indexed as BKD, or does not hold, adds
no statistics. This covers the BKD searcher coming from the searcher
cache, and SNII.
- A SNII index kind is looked up without copying the directory.

### Branch adaptations

- `can_score_match` keeps 4.2's analyzed rule (support_phrase only);
branch-4.2 has no lowercase-normalizer clause.
- `test_omit_norms` does not exist on 4.2, so its `.out` change is
dropped. The branch was cut before #69922 (the 4.2 backport of #69815)
and then merged with branch-4.2 to resolve its conflict in
`index_reader_helper_test.cpp`: both tests are kept, as on master.
- `test_match_keyword_score` inserts its VARIANT rows as JSON strings:
`parse_to_variant` in `VALUES` fails on 4.2 with a max-subcolumns
conversion error. The expected output is master's file, unchanged.
- **#69901 (BM25 statistics per index) is not on 4.2.** A MATCH on a
keyword index and a MATCH on an analyzed index of the same column in one
`score()` query (`name MATCH 'apple' USING ANALYZER english AND name
MATCH 'apple' USING ANALYZER none`) now fails with "Scoring predicates
for one field select different inverted indexes". Master behaved the
same before #69901. Before this change, that query ran with the keyword
clause scoring 0. Each clause alone scores fine. Two analyzed indexes on
one column already fail this way on 4.2. Backporting #69901 lifts this.

### Validation

On this branch merged with branch-4.2 c2aaed9, ASAN build:
- BE UT: 1611 tests in `IndexReaderHelperTest`, `CollectionStatistics*`
and `*Snii*` pass.
- Regression: `test_match_keyword_score` passes against master's `.out`
on V2/V3/SNII. The following also pass:
`test_match_phrase_prefix_score`, `test_bm25_score`,
`test_score_filter_matrix`, `test_score_filter_usage`,
`test_search_score_topn_predicates`, `test_multi_analyzer_query`,
`test_match_using_analyzer`, `test_single_column_multi_index` (with
debug points on), `test_search_score_keyword_filters`,
`test_search_score_raw_index`, `test_search_score_pattern_constant`.
- Local note: the local thirdparty predates the lance-c upgrade, so
`lance_reader.cpp`'s `lance_scanner_set_scalar_index_auto_selection`
call was stubbed for the local build only. It is not part of this PR.

### Release note

score() of MATCH / MATCH_ALL / MATCH_PHRASE on a keyword (untokenized)
inverted index now returns the BM25 score of the value, the same as
SEARCH, instead of 0.

### Check List (For Author)

- Test: Regression test / Unit Test (see Validation)
- Behavior changed: Yes. Keyword MATCH contributes its BM25 score to
score() instead of 0.
- Does this need documentation: No

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. dev/4.1.6-merged dev/4.2.0-merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants