Skip to content

[fix](be) Score MATCH on keyword inverted indexes like SEARCH - #69790

Merged
airborne12 merged 7 commits into
apache:masterfrom
airborne12:fix-keyword-match-score
Nov 3, 2026
Merged

airborne12 merged 7 commits into
apache:masterfrom
airborne12:fix-keyword-match-score

Conversation

@airborne12

Copy link
Copy Markdown
Member

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.

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 and Elasticsearch, instead of 0.

Check List (For Author)

  • Test: Regression test / Unit Test / Manual test
    • Regression test: new test_match_keyword_score (V2/V3/SNII, implicit
      and explicit parser=none); test_search_score_keyword_filters .out
      regenerated: only the keyword MATCH blocks change, now adding idf(x)
      and equal to the SEARCH AND blocks; test_omit_norms .out regenerated:
      its queries are MATCH_PHRASE_PREFIX on keyword indexes, now 1.0
      instead of 0.0. 25 score-related suites in inverted_index_p0, search
      and variant_p0 pass
    • Unit Test: IndexReaderHelperTest, CollectionStatisticsTest
    • Manual test: the reported table on an ASAN build; MATCH equals
      SEARCH and Elasticsearch 9.5.5 to 7 digits, also for ARRAY,
      a column with both a keyword and an analyzed index, and OR/AND mixes
  • Behavior changed: Yes. Keyword MATCH contributes its BM25 score to
    score() instead of 0.
  • Does this need documentation: No

🤖 Generated with Claude Code

### What problem does this PR solve?

Issue Number: None

Related PR: apache#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: apache#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.

### 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 and Elasticsearch, instead of 0.

### Check List (For Author)

- Test: Regression test / Unit Test / Manual test
    - Regression test: new test_match_keyword_score (V2/V3/SNII, implicit
      and explicit parser=none); test_search_score_keyword_filters .out
      regenerated: only the keyword MATCH blocks change, now adding idf(x)
      and equal to the SEARCH AND blocks; test_omit_norms .out regenerated:
      its queries are MATCH_PHRASE_PREFIX on keyword indexes, now 1.0
      instead of 0.0. 25 score-related suites in inverted_index_p0, search
      and variant_p0 pass
    - Unit Test: IndexReaderHelperTest, CollectionStatisticsTest
    - Manual test: the reported table on an ASAN build; MATCH equals
      SEARCH and Elasticsearch 9.5.5 to 7 digits, also for ARRAY<STRING>,
      a column with both a keyword and an analyzed index, and OR/AND mixes
- Behavior changed: Yes. Keyword MATCH contributes its BM25 score to
  score() instead of 0.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@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

Copy link
Copy Markdown
Member Author

run buildall

@airborne12

Copy link
Copy Markdown
Member Author

/review

@airborne12
airborne12 marked this pull request as ready for review October 29, 2026 01:53
@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: 28396 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit ae2b73761bea48c2e0f78c0af88ade9b9ba2469f, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17584	5168	5075	5075
q2	2140	316	256	256
q3	10254	1361	813	813
q4	4678	467	336	336
q5	7434	792	525	525
q6	182	183	151	151
q7	748	803	601	601
q8	9239	1330	1059	1059
q9	5445	4196	4186	4186
q10	6807	1293	1022	1022
q11	442	246	228	228
q12	645	393	285	285
q13	18072	3590	3012	3012
q14	278	269	246	246
q15	q16	718	724	667	667
q17	1191	751	572	572
q18	6929	6194	6119	6119
q19	1094	943	591	591
q20	379	328	226	226
q21	5313	2170	2273	2170
q22	347	292	256	256
Total cold run time: 99919 ms
Total hot run time: 28396 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5594	5614	5602	5602
q2	701	573	554	554
q3	5219	5352	4926	4926
q4	1990	2164	1542	1542
q5	5252	5180	5501	5180
q6	251	199	161	161
q7	2349	2083	1871	1871
q8	2998	2675	2720	2675
q9	8083	7991	7993	7991
q10	3854	3687	3262	3262
q11	655	461	449	449
q12	726	686	546	546
q13	3619	3973	3305	3305
q14	321	310	305	305
q15	q16	710	726	660	660
q17	8348	7024	6916	6916
q18	13026	12180	12803	12180
q19	920	819	831	819
q20	2138	2152	1906	1906
q21	5559	4654	4748	4654
q22	515	471	391	391
Total cold run time: 72828 ms
Total hot run time: 65895 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151421 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 ae2b73761bea48c2e0f78c0af88ade9b9ba2469f, data reload: false

query5	4302	587	485	485
query6	428	209	189	189
query7	4808	453	227	227
query8	322	165	151	151
query9	8790	3206	3208	3206
query10	474	320	269	269
query11	6065	3950	3554	3554
query12	149	91	88	88
query13	1263	454	338	338
query14	6073	3926	3602	3602
query14_1	3470	3433	3410	3410
query15	207	193	177	177
query16	947	454	407	407
query17	893	710	543	543
query18	2424	454	333	333
query19	200	179	146	146
query20	87	88	93	88
query21	225	144	122	122
query22	13087	13079	12738	12738
query23	13424	12820	12510	12510
query23_1	12526	12575	12618	12575
query24	7386	849	491	491
query24_1	488	493	493	493
query25	539	423	365	365
query26	1257	253	141	141
query27	2763	446	279	279
query28	4607	2029	2024	2024
query29	1582	604	475	475
query30	297	218	185	185
query31	1024	941	816	816
query32	141	95	92	92
query33	537	306	251	251
query34	892	850	509	509
query35	745	753	666	666
query36	840	792	733	733
query37	128	97	86	86
query38	1772	1736	1669	1669
query39	793	789	742	742
query39_1	734	734	737	734
query40	218	125	96	96
query41	72	71	69	69
query42	89	88	86	86
query43	355	369	316	316
query44	1342	713	713	713
query45	181	176	167	167
query46	850	978	586	586
query47	2786	2882	2609	2609
query48	336	316	221	221
query49	573	406	288	288
query50	688	287	219	219
query51	10656	10623	10401	10401
query52	83	79	71	71
query53	192	208	151	151
query54	244	193	190	190
query55	71	68	65	65
query56	245	213	212	212
query57	1659	1619	1579	1579
query58	274	266	261	261
query59	2128	2122	2011	2011
query60	287	250	223	223
query61	157	147	141	141
query62	392	335	279	279
query63	186	157	161	157
query64	2732	1001	759	759
query65	3323	3277	3253	3253
query66	1766	416	304	304
query67	20194	20207	19982	19982
query68	2965	1027	627	627
query69	386	288	250	250
query70	862	780	835	780
query71	301	249	213	213
query72	2733	2827	2324	2324
query73	525	536	303	303
query74	4614	4529	4352	4352
query75	2317	2301	1911	1911
query76	2316	1016	602	602
query77	337	377	276	276
query78	9843	9773	9161	9161
query79	1027	856	548	548
query80	829	428	344	344
query81	583	321	281	281
query82	577	151	110	110
query83	305	200	199	199
query84	308	124	106	106
query85	857	430	373	373
query86	362	246	231	231
query87	2029	1942	1787	1787
query88	3839	2788	2773	2773
query89	334	277	245	245
query90	1809	167	171	167
query91	156	157	127	127
query92	97	90	81	81
query93	1036	1021	587	587
query94	525	320	282	282
query95	588	406	301	301
query96	704	492	241	241
query97	2466	2423	2323	2323
query98	181	155	168	155
query99	784	755	644	644
Total cold run time: 235138 ms
Total hot run time: 151421 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.

Static review of PR 69790 at exact head ae2b737. I verified one new P1 correctness issue in keyword MATCH_PHRASE scoring: the reader strips supported trailing phrase slop (~N/~N+) before executing the keyword term, while the new collection path records the unstripped literal and therefore computes IDF for a different term. The remaining changed code was traced through index selection, CLucene/SNII no-norm handling, NULL/hidden-row accounting, and array/variant candidate resolution without another verified defect. Changed tests and expected-output labels are structurally aligned; tests were inspected but not executed under the static-only contract. There were no existing inline P0/P1 findings to carry forward, and no additional user focus was supplied. The review did not converge because all assigned subagent launches terminated with the recorded temporary model-capacity error before returning NO_NEW_VALUABLE_FINDINGS; the main-agent recovery and final sweeps completed, but delegated convergence evidence is missing.

Review did not converge after 3 rounds. This automated review cannot pass the review check.

Convergence failed at the three-round cap because every assigned subagent launch terminated with the recorded temporary model-capacity error before appending a result or returning NO_NEW_VALUABLE_FINDINGS. The main agent independently completed the full changed-file, mechanism, test-structure, anchor, and live-state sweeps, but delegated convergence evidence remains unavailable.

@hello-stephen

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

query1	0.01	0.00	0.00
query2	0.09	0.04	0.05
query3	0.26	0.09	0.09
query4	1.62	0.09	0.11
query5	0.24	0.25	0.24
query6	1.17	0.69	0.69
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.40	0.35	0.35
query10	0.58	0.61	0.63
query11	0.20	0.15	0.15
query12	0.20	0.16	0.15
query13	0.51	0.49	0.50
query14	1.04	1.04	1.01
query15	0.62	0.62	0.62
query16	0.36	0.32	0.35
query17	1.14	1.14	1.15
query18	0.21	0.19	0.20
query19	2.14	2.06	1.97
query20	0.02	0.01	0.01
query21	15.48	0.18	0.13
query22	4.91	0.05	0.06
query23	16.28	0.32	0.12
query24	3.07	0.58	0.51
query25	0.12	0.07	0.05
query26	1.50	0.23	0.16
query27	0.05	0.05	0.03
query28	2.94	0.76	0.39
query29	12.47	4.10	3.27
query30	0.28	0.13	0.09
query31	2.77	0.56	0.31
query32	3.23	0.60	0.51
query33	3.12	3.14	3.25
query34	15.20	4.00	3.39
query35	3.33	3.33	3.36
query36	0.55	0.45	0.42
query37	0.09	0.06	0.06
query38	0.04	0.04	0.03
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.02	0.02
query43	0.04	0.04	0.03
Total cold run time: 96.71 s
Total hot run time: 24.44 s

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Review recovery stopped: No final review submission was declared
Workflow run: https://github.com/apache/doris/actions/runs/40803783396

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@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 at head ae2b737. Two new P1 regressions abort supported scored queries: mixing keyword and analyzed indexes on the same column, and using an indexed numeric-to-string MATCH as a scalar filter beside a scored text MATCH. Both are detailed inline.

Existing P1 4408148364 remains applicable and is retained without reposting. Keyword MATCH_PHRASE slop is stripped by query lowering but retained in the collected term; the scorer can fail with missing term statistics. This is distinct from the two new findings.

Critical checkpoint conclusions from static inspection:

  • Goal and proof: ordinary keyword MATCH/MATCH_ALL/MATCH_PHRASE now collect a whole-value term and request BM25, agreeing with SEARCH on the covered cases. The new BE unit tests and regression cases exercise this, but the three reported boundary cases remain uncovered.
  • Scope and reuse: the two production changes are small and focused; analyzed collection reuses analyze_plain_query. The new raw-term helper still needs query-lowering alignment, physical-index identity and reader-type constraints as described in the findings.
  • Concurrency: no new threads, mutable globals, atomics or lock ordering. Existing tablet collection is serialized by TabletCollectionStatistics::_mutex, with statistics copied into each scanner; score accumulation stays per scanner. No additional concurrency defect found.
  • Lifecycle: TermInfo owns its value, selected variant metadata retains ownership, and cache/searcher handles keep their existing lifetimes. No new static initialization dependency or ownership cycle.
  • Configuration: no configuration added or changed; dynamic update behavior is unaffected.
  • Compatibility: no function signatures, wire definitions or storage formats changed. Existing V2/V3/SNII adapters consume the new keyword score requests; no added rolling-upgrade protocol requirement found.
  • Thrift/protobuf IDs: no gensrc changes.
  • Parallel paths: reviewed MATCH versus SEARCH, analyzed and keyword indexes, string arrays/variant resolution, multiple-index selection, scalar fallback, boolean/NULL handling, no-score queries, prefixes and regexp. The two new failures are at those selection/fallback boundaries. Keyword phrase prefixes correctly keep constant score 1; regexp and phrase-edge eligibility stays unchanged.
  • Conditions and comments: the new eligibility and term branches explain whole-value semantics with examples, but non-analyzed properties alone do not prove a compatible text reader. The missing distinctions are covered inline.
  • Test coverage: cases cover implicit/explicit parser=none, inherited phrase support, case/spaces, NULL rows, mixed fields, equality/IN controls, V2/V3/SNII and WAND on/off. Add same-column mixed-analyzer, numeric-cast filter and phrase-slop regressions. New queries order results and preserve the final fixture.
  • Changed results: all changed row sets and distinct numeric outputs agree with the traced formulas. SNII's lower nullable-keyword values reflect existing physical token-count/average-length behavior already present in SEARCH; the prefix outputs correctly change from 0 to 1.
  • Observability: existing statistics logs, scan profile timers and propagated error messages cover this path; no new logging or metrics requirement identified.
  • Transactions/persistence: no transaction, visibility-version, rowset lifecycle, delete-bitmap mutation, EditLog or replay change.
  • Writes/atomicity/crashes: no data-write path or persistence mutation added; no new crash-atomicity obligation.
  • FE/BE variables: none added. Existing explicit analyzer bindings were traced through FE translation into BE selection for the findings.
  • Performance and memory: a raw keyword term avoids analyzer work and uses the existing deduplicated term/statistics path. Scored requests correctly bypass bitmap-result cache/single-flight so scores are populated; unscored requests retain existing behavior. No additional substantiated performance, memory-ownership or error-propagation issue found.
  • BE module conventions: no new headers/includes, template instantiations, src .cpp test inclusions or build-gate table edits. Index lifetime and three-valued bitmap handling are unchanged.
  • Other issues and user focus: no additional user focus was supplied. The full nine-file diff and related control flow were reviewed; no other verified new issue remains.

Review converged after 2 rounds. All candidates and initial risk focuses have evidence-backed dispositions; no unresolved static coverage gap remains. This was a static-only review: no builds, Doris tests, benchmarks or runtime validation were run, and generation provenance of the .out files was not independently verified.

Review converged after 2 round(s).

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

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.48% (36298/46847)
Line Coverage 62.74% (410159/653733)
Region Coverage 59.20% (347219/586543)
Branch Coverage 60.10% (160790/267556)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.48% (36298/46847)
Line Coverage 62.74% (410146/653733)
Region Coverage 59.20% (347253/586543)
Branch Coverage 60.10% (160793/267556)

…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 <[email protected]>
@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.

Review converged after three bounded rounds. I reviewed all nine changed files and the related MATCH, SEARCH, collection-statistics, CLucene, SNII, variant, and regression paths. Two new findings are included inline: an untyped VARIANT cast can make statistics open a physical BKD index through the text-reader path, and malformed raw keyword bytes can collide after conversion to wide-string statistics keys. Existing P1 comment 4409110669 remains applicable because same-column mixed physical indexes are still rejected; it is carried by ID only. The earlier numeric-BKD and phrase-slop findings are fixed in this head. The added V2/V3/SNII tests and filter cases are coherent; the SNII keyword score difference matches unchanged SEARCH behavior and was treated as pre-existing. There was no user-provided focus beyond the complete review. This was a static-only review; no builds or tests were executed.

Review converged after 3 round(s).

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

@@ -285,6 +286,32 @@

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] Do not score untyped VARIANT paths as text

When an extracted VARIANT path has no materialized type, resolve_text_scoring_index_candidates() forces it to STRING and clones the parent index. A dynamic numeric path can inherit metadata with parser/analyzer removed and be written as BKD, while CAST(v['n'] AS STRING) MATCH '1' is excluded from index execution because the source type is VARIANT. This gate nevertheless admits the statistics collector, which can open that BKD file through the CLucene/SNII text-statistics path and fail before row-level filtering. Keep unproven VARIANT paths out of scoring or prove the physical reader type, and add a dynamic numeric VARIANT cast regression.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 27cdbb5, at the point where the failure happens rather than in the collector.

Reproduced on 7881be5 with INDEX idx_v(v) USING INVERTED and SELECT score() ... WHERE CAST(v['n'] AS STRING) MATCH '1': V2/V3 fail with "No segments* file found", SNII with "logical index is not a text inverted index". The same query also failed before this PR when the VARIANT index is analyzed (parser=english), so the collector is not the only way in; this PR made the plain VARIANT index reach it too.

Keeping untyped VARIANT paths out of scoring in the collector would also drop text paths, which score correctly today (CAST(v['s'] AS STRING) MATCH 'abc' returns BM25), and the collector cannot tell the two apart: the path type is only known per segment. So statistics collection now treats a segment whose index for the field is BKD like a segment without the path: it adds no statistics (V2/V3: BKD data file in the compound directory, checked before opening a CLucene reader; SNII: the logical index kind). Those rows still match without the index and score 0; the text path keeps its score.

test_match_keyword_score adds a VARIANT case with a numeric path that is BKD in one segment and text in another, plus a text path, on V2/V3/SNII; it failed on all three before the change. 26 score-related suites pass.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17580	5190	5043	5043
q2	2144	302	248	248
q3	10249	1364	829	829
q4	4677	461	335	335
q5	7445	783	531	531
q6	182	184	151	151
q7	734	786	596	596
q8	9270	1324	1082	1082
q9	5428	4175	4168	4168
q10	6809	1298	1010	1010
q11	433	243	231	231
q12	638	400	285	285
q13	18105	3556	3023	3023
q14	285	273	252	252
q15	q16	732	717	660	660
q17	1195	743	591	591
q18	7005	6171	6093	6093
q19	1124	955	598	598
q20	382	335	222	222
q21	5179	2152	2257	2152
q22	345	290	251	251
Total cold run time: 99941 ms
Total hot run time: 28351 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5576	5568	5577	5568
q2	668	576	522	522
q3	5207	5400	4906	4906
q4	1992	2132	1555	1555
q5	5217	5155	5453	5155
q6	266	211	167	167
q7	2331	2106	1942	1942
q8	3023	2651	2681	2651
q9	7961	7945	7908	7908
q10	3731	3761	3256	3256
q11	603	423	425	423
q12	715	723	520	520
q13	3587	3908	3346	3346
q14	328	318	285	285
q15	q16	698	723	646	646
q17	8320	6984	6869	6869
q18	13000	12237	12918	12237
q19	902	795	796	795
q20	2182	2161	1924	1924
q21	5529	4591	4672	4591
q22	513	440	414	414
Total cold run time: 72349 ms
Total hot run time: 65680 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150987 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 db7d9a86dc1f966c2b4d006889445040be9fd612, data reload: false

query5	4319	604	469	469
query6	424	209	191	191
query7	4802	455	230	230
query8	319	166	149	149
query9	8786	3200	3221	3200
query10	484	304	254	254
query11	5970	3825	3576	3576
query12	159	95	89	89
query13	1267	453	350	350
query14	6154	3905	3663	3663
query14_1	3463	3399	3430	3399
query15	204	195	181	181
query16	947	440	407	407
query17	864	652	535	535
query18	2419	417	314	314
query19	204	165	142	142
query20	89	85	87	85
query21	218	128	110	110
query22	12977	13018	12804	12804
query23	13353	12873	12497	12497
query23_1	12623	12546	12623	12546
query24	7402	826	476	476
query24_1	484	500	507	500
query25	542	421	362	362
query26	1274	265	157	157
query27	2743	442	277	277
query28	4617	2063	2054	2054
query29	1543	604	457	457
query30	309	223	179	179
query31	1000	934	829	829
query32	143	93	94	93
query33	528	306	244	244
query34	883	859	506	506
query35	731	759	659	659
query36	845	845	732	732
query37	137	96	85	85
query38	1794	1769	1683	1683
query39	790	799	748	748
query39_1	743	738	762	738
query40	223	127	100	100
query41	73	71	72	71
query42	97	90	85	85
query43	352	368	320	320
query44	1371	740	739	739
query45	198	183	172	172
query46	855	966	564	564
query47	2962	2814	2682	2682
query48	336	313	218	218
query49	577	387	289	289
query50	683	269	214	214
query51	10324	10415	10162	10162
query52	77	78	69	69
query53	183	208	151	151
query54	247	189	180	180
query55	71	65	62	62
query56	222	206	213	206
query57	1620	1605	1567	1567
query58	284	265	259	259
query59	2135	2247	1986	1986
query60	288	254	217	217
query61	150	148	139	139
query62	385	339	285	285
query63	188	159	161	159
query64	2751	969	749	749
query65	3309	3743	3292	3292
query66	1768	425	300	300
query67	20162	20203	19846	19846
query68	3083	1035	627	627
query69	386	298	244	244
query70	844	782	773	773
query71	288	227	209	209
query72	2692	2740	2261	2261
query73	512	517	309	309
query74	4612	4550	4340	4340
query75	2315	2301	1913	1913
query76	2335	1004	577	577
query77	328	371	258	258
query78	9787	9699	9123	9123
query79	979	845	530	530
query80	1173	443	343	343
query81	571	312	270	270
query82	571	136	103	103
query83	315	199	178	178
query84	316	131	106	106
query85	849	424	372	372
query86	379	250	228	228
query87	1988	1937	1793	1793
query88	3757	2795	2765	2765
query89	334	282	243	243
query90	1860	165	164	164
query91	163	149	127	127
query92	94	91	93	91
query93	1006	1001	574	574
query94	586	311	294	294
query95	571	342	363	342
query96	677	479	248	248
query97	2438	2441	2326	2326
query98	174	153	147	147
query99	775	776	654	654
Total cold run time: 235059 ms
Total hot run time: 150987 ms

@hello-stephen

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

query1	0.01	0.01	0.01
query2	0.09	0.05	0.05
query3	0.26	0.09	0.08
query4	1.61	0.09	0.09
query5	0.26	0.25	0.24
query6	1.17	0.67	0.67
query7	0.04	0.00	0.00
query8	0.06	0.04	0.04
query9	0.41	0.33	0.36
query10	0.58	0.59	0.57
query11	0.20	0.14	0.14
query12	0.18	0.15	0.15
query13	0.49	0.50	0.48
query14	1.03	1.01	1.00
query15	0.64	0.61	0.61
query16	0.35	0.34	0.33
query17	1.16	1.11	1.14
query18	0.20	0.20	0.19
query19	2.13	1.94	1.95
query20	0.02	0.01	0.01
query21	15.49	0.19	0.13
query22	4.84	0.05	0.06
query23	16.27	0.32	0.13
query24	3.05	0.54	0.39
query25	0.10	0.06	0.03
query26	1.34	0.21	0.16
query27	0.07	0.04	0.05
query28	3.60	0.75	0.39
query29	12.49	4.15	3.24
query30	0.29	0.13	0.11
query31	2.77	0.55	0.33
query32	3.22	0.59	0.50
query33	3.16	3.18	3.22
query34	15.28	4.00	3.38
query35	3.33	3.32	3.38
query36	0.53	0.47	0.43
query37	0.09	0.07	0.06
query38	0.05	0.04	0.03
query39	0.03	0.03	0.04
query40	0.16	0.14	0.14
query41	0.08	0.03	0.03
query42	0.04	0.02	0.03
query43	0.04	0.04	0.04
Total cold run time: 97.21 s
Total hot run time: 24.2 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 67.22% (32555/48427)
Line Coverage 52.11% (344936/661970)
Region Coverage 47.52% (280295/589849)
Branch Coverage 49.24% (132466/269029)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100% (0/0) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.55% (36392/46929)
Line Coverage 62.83% (411483/654937)
Region Coverage 59.43% (349303/587710)
Branch Coverage 60.21% (161595/268371)

@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 the complete 10-file diff at 0487c2c, including the collector, both text-reader formats, upstream expression/index selection, and all changed tests and results.

One new P2 performance finding: the SNII presence/kind precheck deep-copies the complete logical-index directory for every collected field in every segment. This also adds avoidable allocation/copying to existing analyzed MATCH and SEARCH scoring queries on wide VARIANT tables. See the inline finding.

Existing review context: independently confirmed the previous P1 issues are fixed at this head: phrase-slop term identity, same-column physical-index separation, ordinary numeric BKD exclusion, dynamic numeric VARIANT handling, and absent SNII paths. Cached BKD searchers are also type-checked. No existing P0/P1 comment is carried as blocking. The known malformed-direct-UTF8 concern was not substantiated for supported SQL literal inputs; it is not reposted.

Critical checkpoint conclusions:

  • Goal: keyword MATCH now uses the existing SEARCH/BM25 machinery; ordinary scalar cases and mixed analyzed/keyword sums agree with the supplied unit/regression evidence.
  • Scope and reuse: the change is focused on eligibility, raw keyword term extraction, shared phrase-slop parsing, and optional per-segment index handling.
  • Concurrency and locks: no new threads or shared mutable query state. Existing tablet-statistics collection is mutex-serialized, scanner statistics are separate, and cache/reader locking follows existing order. The inline finding identifies unnecessary directory-copy work under the reader's shared lock.
  • Lifecycle and static initialization: cache handles/shared ownership retain readers through statistics access; optional-index early exits release local ownership. No new static initialization dependency or ownership cycle found.
  • Configuration: no settings added; regression setup explicitly enables the required index execution.
  • Compatibility: no storage encoding or externally visible function-symbol change; V2/V3 and SNII reader paths were traced.
  • Thrift/protobuf IDs: no definitions changed.
  • Parallel paths: checked both eligibility overloads, analyzed/keyword MATCH, MATCH/SEARCH coexistence, numeric and array leaf types, dynamic VARIANT paths, cached/uncached BKD, and prefix/regexp scoring.
  • Conditional branches and errors: new checks describe concrete failure cases; missing whole-column SNII indexes and ordinary corruption/I/O failures still propagate errors.
  • Test coverage: inspected both unit-test files and all changed regression suites/results, including 54 new format-specific cases, NULLs, phrase slop, mixed numeric/text and sparse VARIANT segments, cache warmup, and WAND toggles.
  • Expected results: all changed score families match the source/formula. SNII's nullable-keyword average-length difference and positional keyword-array frequencies are existing SEARCH behavior; new MATCH preserves that behavior.
  • Observability: existing statistics logging and propagated statuses remain available; no additional observability requirement identified.
  • Transactions/persistence: no transaction, EditLog, replay or failover logic changed; current read-version/hidden-row filtering is preserved.
  • Data writes and atomicity: production write paths are unchanged; no new write/crash-atomicity mechanism.
  • FE/BE variables: none added; existing analyzer/query metadata is reused.
  • Performance and memory: the inline P2 is a verified repeated owning-container copy; its runtime magnitude is unmeasured. No other new substantial allocation or hot-path regression was substantiated.
  • Other concerns/user focus: no additional user focus was supplied. All candidates and initial risk foci have explicit dispositions; no unresolved source-review coverage gap remains.

Static review only: builds, unit/regression tests, benchmarks, and binary artifact checks were not run. Generated-output provenance and runtime performance cannot be established by this review.

The review converged after 2 rounds: every assigned agent in the final round returned NO_NEW_VALUABLE_FINDINGS, and the final coverage/candidate/risk sweep is complete.

Review converged after 2 round(s).

…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 <[email protected]>
@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@airborne12

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Selected model is at capacity. Please try a different model.
Workflow run: https://github.com/apache/doris/actions/runs/41398340871

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@airborne12

Copy link
Copy Markdown
Member Author

/review

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17580	5139	5041	5041
q2	2142	356	305	305
q3	10130	1418	839	839
q4	4685	468	343	343
q5	7446	792	528	528
q6	181	180	155	155
q7	748	808	613	613
q8	9247	1372	1102	1102
q9	5465	4218	4191	4191
q10	6821	1288	1029	1029
q11	432	261	236	236
q12	636	397	273	273
q13	18143	3568	3073	3073
q14	283	268	245	245
q15	q16	741	745	641	641
q17	1219	757	584	584
q18	7005	6217	6120	6120
q19	1261	957	611	611
q20	376	350	232	232
q21	5980	2383	2480	2383
q22	388	326	281	281
Total cold run time: 100909 ms
Total hot run time: 28825 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5975	5840	6117	5840
q2	704	604	562	562
q3	5677	5824	5485	5485
q4	2007	2202	1600	1600
q5	5851	5729	5578	5578
q6	238	185	145	145
q7	2304	1961	1807	1807
q8	3030	2652	2666	2652
q9	7885	7489	7547	7489
q10	3623	3566	3181	3181
q11	596	439	392	392
q12	677	682	509	509
q13	3274	3594	3064	3064
q14	279	288	263	263
q15	q16	658	680	617	617
q17	7829	7023	6922	6922
q18	12978	12133	12865	12133
q19	928	781	767	767
q20	2177	2157	1920	1920
q21	5543	4554	4655	4554
q22	494	484	399	399
Total cold run time: 72727 ms
Total hot run time: 65879 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151172 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 a66e73d8fa6823dd59ecb7a53329c9188ac6391e, data reload: false

query5	4312	600	440	440
query6	431	207	200	200
query7	4805	447	229	229
query8	322	165	145	145
query9	8777	3169	3195	3169
query10	465	343	254	254
query11	6055	3874	3588	3588
query12	150	90	88	88
query13	1264	461	336	336
query14	6130	3912	3689	3689
query14_1	3464	3377	3354	3354
query15	205	198	179	179
query16	951	444	408	408
query17	885	687	518	518
query18	2415	409	303	303
query19	187	157	129	129
query20	81	82	81	81
query21	212	122	106	106
query22	13022	13134	12832	12832
query23	13586	12818	12420	12420
query23_1	12579	12440	12612	12440
query24	7366	816	461	461
query24_1	477	473	474	473
query25	509	395	343	343
query26	1253	261	138	138
query27	2779	439	276	276
query28	4610	2058	2056	2056
query29	1533	574	439	439
query30	295	225	188	188
query31	1019	925	812	812
query32	167	90	89	89
query33	515	293	227	227
query34	890	851	498	498
query35	751	737	658	658
query36	821	819	742	742
query37	119	94	80	80
query38	1819	1728	1675	1675
query39	796	788	744	744
query39_1	742	731	740	731
query40	221	116	97	97
query41	71	70	64	64
query42	90	86	80	80
query43	350	366	317	317
query44	1324	696	720	696
query45	177	170	171	170
query46	833	980	568	568
query47	3068	2978	2693	2693
query48	306	317	231	231
query49	587	404	301	301
query50	694	281	221	221
query51	10734	10724	10343	10343
query52	81	77	78	77
query53	182	214	153	153
query54	232	191	191	191
query55	72	69	62	62
query56	222	218	214	214
query57	1665	1642	1554	1554
query58	287	267	265	265
query59	2147	2211	1940	1940
query60	277	260	224	224
query61	149	137	145	137
query62	397	335	279	279
query63	185	160	164	160
query64	2731	937	744	744
query65	3344	3274	3279	3274
query66	1778	431	297	297
query67	20399	20121	20173	20121
query68	2948	1013	616	616
query69	377	294	250	250
query70	852	772	727	727
query71	304	236	195	195
query72	2699	2693	2424	2424
query73	525	533	305	305
query74	4638	4543	4353	4353
query75	2341	2308	1903	1903
query76	2326	1022	602	602
query77	358	382	264	264
query78	9798	9823	9130	9130
query79	994	859	548	548
query80	817	455	410	410
query81	574	320	274	274
query82	768	130	120	120
query83	284	195	185	185
query84	319	121	106	106
query85	968	445	374	374
query86	351	237	233	233
query87	2016	1959	1801	1801
query88	3777	2786	2750	2750
query89	353	273	244	244
query90	1712	165	170	165
query91	168	151	130	130
query92	102	87	87	87
query93	1035	1001	578	578
query94	535	325	286	286
query95	579	336	383	336
query96	701	488	238	238
query97	2456	2471	2374	2374
query98	157	154	152	152
query99	779	761	650	650
Total cold run time: 235993 ms
Total hot run time: 151172 ms

@hello-stephen

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

query1	0.01	0.01	0.00
query2	0.10	0.06	0.05
query3	0.26	0.09	0.08
query4	1.60	0.10	0.10
query5	0.28	0.23	0.23
query6	1.17	0.67	0.68
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.39	0.35	0.34
query10	0.57	0.59	0.56
query11	0.20	0.15	0.14
query12	0.19	0.15	0.15
query13	0.50	0.50	0.50
query14	1.01	1.01	1.01
query15	0.63	0.61	0.62
query16	0.35	0.35	0.36
query17	1.11	1.14	1.10
query18	0.21	0.19	0.20
query19	2.09	2.01	1.96
query20	0.02	0.02	0.01
query21	15.50	0.19	0.14
query22	4.91	0.05	0.05
query23	16.27	0.32	0.13
query24	3.09	0.56	0.38
query25	0.13	0.05	0.04
query26	1.15	0.21	0.15
query27	0.04	0.03	0.06
query28	3.11	0.69	0.36
query29	12.46	4.19	3.23
query30	0.28	0.13	0.10
query31	2.78	0.56	0.33
query32	3.22	0.59	0.51
query33	3.24	3.30	3.20
query34	15.47	4.07	3.37
query35	3.32	3.33	3.32
query36	0.56	0.45	0.43
query37	0.09	0.07	0.06
query38	0.05	0.04	0.04
query39	0.03	0.03	0.03
query40	0.18	0.15	0.15
query41	0.09	0.03	0.02
query42	0.04	0.03	0.03
query43	0.04	0.03	0.04
Total cold run time: 96.84 s
Total hot run time: 24.22 s

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Selected model is at capacity. Please try a different model.
Workflow run: https://github.com/apache/doris/actions/runs/41402906130

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@airborne12

Copy link
Copy Markdown
Member Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 100.00% (3/3) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 67.16% (32018/47676)
Line Coverage 51.99% (336592/647357)
Region Coverage 47.52% (273478/575530)
Branch Coverage 49.37% (128962/261222)

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Selected model is at capacity. Please try a different model.
Workflow run: https://github.com/apache/doris/actions/runs/41407490276

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (3/3) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.69% (35891/46195)
Line Coverage 63.00% (403419/640351)
Region Coverage 59.52% (340974/572848)
Branch Coverage 60.71% (158009/260281)

@airborne12

Copy link
Copy Markdown
Member Author

/review

@eldenmoon eldenmoon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: Error running remote compact task: Selected model is at capacity. Please try a different model.
Workflow run: https://github.com/apache/doris/actions/runs/41451293357

Please inspect the workflow logs and rerun the review after the underlying issue is resolved.

@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.

Static review of PR 69790 at exact head a66e73d found no new P0-P2 defect. The keyword MATCH collector now uses the same whole-value and phrase-slop lowering as the reader; numeric/BKD leaves are excluded before text statistics; V2/V3 and SNII skip only legitimate BKD or absent suffixed VARIANT paths; cached searcher variants are type-checked; and the new SNII kind lookup preserves lock and lifetime behavior without copying the directory. Existing prior findings for phrase slop, numeric BKD, untyped VARIANT, sparse paths, and the SNII directory copy were revalidated as fixed. The mixed-index finding is fixed in the base commit c4b11b5, so no existing blocking comment ID is carried.

Critical checkpoints: the scope is focused and reuses existing lowering, analyzer, cache, and statistics paths; no new concurrency, lifecycle, configuration, FE/BE protocol, thrift/protobuf, transaction, persistence, or data-write behavior was introduced; status and corruption/error propagation remain intact; hidden-row, NULL, prefix, WAND, array, and normalizer behavior follows existing consumers; and no additional performance or observability defect was substantiated. Unit and regression cases were inspected for deterministic ordering and V2/V3/SNII coverage, including keyword, analyzed, numeric-cast, NULL, sparse VARIANT, BKD-cache, phrase-slop, and WAND cases.

This was a static-only review: builds, tests, benchmarks, and runtime validation were not run. The review converged after 3 bounded rounds; the final gpt-6-sol source and test assignments returned no new valuable findings after the earlier temporary model-capacity failures. No inline comments are proposed.

Review converged after 3 round(s).

@airborne12
airborne12 merged commit 7cca01b into apache:master Nov 3, 2026
37 checks passed
@airborne12
airborne12 deleted the fix-keyword-match-score branch November 3, 2026 01:05
csun5285 added a commit to csun5285/doris that referenced this pull request Nov 3, 2026
Brings in test_match_keyword_score (apache#69790), which the next commit pins to a positive
subcolumn count. No conflicts; ColumnPB 36 and TColumn 31 stay unique after the merge.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
csun5285 added a commit to csun5285/doris that referenced this pull request Nov 3, 2026
…est_match_keyword_score

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69812, apache#69790

Problem Summary:

test_match_keyword_score (apache#69790) creates `v VARIANT NULL`, so its subcolumn count comes from
the session default, which the fuzzy session variables can set to 0. With this PR a new column with
count 0 keeps every dynamic path in the sparse column, so `s` has no subcolumn index, MATCH still
finds the rows, and score() returns 0 instead of 0.693147. Cloud P0 build 1089519 failed this way on
`V2_variant_text_path`. The column now states a count of 2048, like the other suites that need
indexed subcolumns. The expected output is unchanged.

### Release note

None

### Check List (For Author)

- Test: Regression test. With `SET GLOBAL default_variant_max_subcolumns_count = 0` the suite fails
  on `V2_variant_text_path` without this change and passes with it; it also passes with the default
  count. FE UT on the merged code: 485 tests, 0 failures.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
yiguolei pushed a commit that referenced this pull request Nov 5, 2026
…ARCH (#69791)

### What problem does this PR solve?

Issue Number: None

Related PR: #69790

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 on branch-4.1, three places keep keyword MATCH out of
scoring:
StringTypeInvertedIndexReader::query never sets is_similarity_score (and
answers score() queries from the query cache), MatchPredicateCollector
skips indexes without an analyzer, and is_need_similarity_score only
accepts phrase-capable or lowercase-normalizer indexes.

Fix: a keyword (non-analyzed) index is eligible for MATCH scoring
regardless of support_phrase; the keyword reader requests scoring for
the MATCH query types and bypasses the result cache like the full-text
reader does; MatchPredicateCollector collects the whole value as one
term (case and spaces kept), the term the reader looks up. The legacy
scorer already uses norm 1 when a field has none. Equality runs as
MATCH_ANY inside the reader but keeps EQUAL_QUERY for the decision, so
it stays a pure filter. MATCH_REGEXP and MATCH_PHRASE_PREFIX keep their
current scores.

### 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 and Elasticsearch, instead of 0.

### Check List (For Author)

- Test: Regression test / Unit Test / Manual test
    - Regression test: new test_match_keyword_score (V2/V3, implicit and
      explicit parser=none); its output equals the master PR's V2/V3
      blocks
    - Unit Test: IndexReaderHelperTest, CollectionStatisticsTest
    - Manual test: the reported table on a branch-4.1 ASAN build; MATCH
      equals SEARCH and Elasticsearch 9.5.5 to 7 digits, also for
      ARRAY<STRING>, a column with both a keyword and an analyzed index,
      and OR/AND mixes
- 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 <[email protected]>
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 <[email protected]>
feiniaofeiafei added a commit to feiniaofeiafei/doris that referenced this pull request Nov 6, 2026
### Background

SEARCH fields are bound during analysis before score binding runs in rewrite. Bound SEARCH predicates use SearchExpression instead of the scalar Search function.

### What problem does this PR solve?

Issue Number: None
Related PR: apache#69344 (early SEARCH binding), apache#69292 (score binding)
PR Type: Bugfix

Problem Summary: Valid SEARCH queries that call score() are rejected because score binding only recognizes the unbound Search type. Recognize SearchExpression in the WHERE predicate check, scoring alias propagation and HAVING/QUALIFY validation.

### Details

- **Frequency:** SEARCH scoring queries after early SEARCH binding.
- **Root cause:** The downstream score binder checks the expression type from the previous rule ordering.
- **Why existing tests missed it:** The targeted SEARCH suites did not cover score(); existing score regression suites expose the failure.
- **Fix:** Replace all three Search type checks with SearchExpression and update the import.
- **Other paths and branches:** MATCH recognition and score usage restrictions remain unchanged.
- **Behavior change:** Valid SEARCH scoring queries can pass score binding.

### Testing

FE Checkstyle and git diff --check passed. Built FE with DISABLE_BUILD_UI=ON ./build.sh --fe and deployed it to the development cluster.

The five targeted FE test classes executed 147 tests with zero failures, errors or skips, including 82 BindScoreToOlapScanTest cases.

Ran 21 targeted regression suites: 20 passed, one failed because the existing BE lacks the keyword MATCH scoring change from apache#69790. Its actual 0.290716 score matches the BE-version expected output, while the current expected output is 0.983863. Re-running that suite against the existing generated expected output from the running BE commit passed. Repository expected outputs were not changed. The previous SEARCH score binding error was not reproduced.

The initial local FE unit-test attempt stopped during dependency resolution due to Maven-cache sandbox permissions; remote tests provide the validation above.

### Release note

Fix planning failures when using score() with SEARCH predicates.

### Check List (For Author)

- Behavior changed: Yes (see Details)
- Does this need documentation: No
- Pre-submission status: Partially verified: current keyword MATCH expected scores require a BE containing apache#69790; validation used the existing BE

### AI Disclosure

- Tool: OpenAI Codex
- Generated: Code and commit description
- Human review: Pending
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants