Skip to content

[fix](index) Drop support_phrase from indexes that do not tokenize - #67219

Open
airborne12 wants to merge 1 commit into
apache:masterfrom
airborne12:snii-phrase-normalize
Open

[fix](index) Drop support_phrase from indexes that do not tokenize#67219
airborne12 wants to merge 1 commit into
apache:masterfrom
airborne12:snii-phrase-normalize

Conversation

@airborne12

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: N/A

Related PR: N/A

Problem Summary:

support_phrase=true asks the BE to persist term positions, and a position is only
observable when a query can supply a second term to match it against.
InvertedIndexAnalyzer::get_analyse_result() short-circuits on !should_analyzer() and
returns the entire search string as ONE term, for every analysis purpose. Every phrase
variant — MATCH_PHRASE, MATCH_PHRASE_PREFIX, MATCH_PHRASE_EDGE — sources its terms
from there, so a phrase against an index with no tokenizer is always a single-term phrase,
which is a term query. The option describes something no query can reach.

FE was manufacturing exactly that combination. Index's constructor unconditionally
defaulted support_phrase to true for every inverted index, including normalizer-only
indexes and parser=none ones, so a keyword index reached the BE carrying an option it can
never honour. Downstream, IndexReaderHelper::is_need_similarity_score() read that option
alone and reported such an index as scoreable, letting relevance work be planned against
positions and norms that were never written.

The fix aligns both sides on the same predicate:

  • FE keeps support_phrase only when the index actually tokenizes — an explicit
    analyzer, or a parser other than none — and removes the key otherwise, so new
    tablet metadata no longer carries it.
  • BE gains IndexReaderHelper::persists_scoring_inputs(), which requires both
    support_phrase=yes and InvertedIndexAnalyzer::should_analyzer(). Both
    is_need_similarity_score() overloads route through it. Tablet metadata already written
    to disk still holds the stale option, so the FE-side change alone is not sufficient and
    the guard has to be repeated on the read path.

ARRAY deserves an explicit note, because the obvious argument does not hold for it.
InvertedIndexUtil::checkInvertedIndexParser forces every ARRAY inverted index to
parser=none, yet an ARRAY row emits one term per element and
SniiIndexColumnWriter::_add_array_values does advance positions between them (CLucene
writes the same shape). Those positions are nevertheless unreachable, for the query-side
reason above — not because the document holds a single term.
UntokenizedQueriesCannotObserveAPosition pins that invariant so a future analyzer change
cannot silently invalidate it.

For reference, Elasticsearch rejects the equivalent shape at mapping time: a keyword
field's index_options accepts only docs and freqs, never positions.

Release note

None

Check List (For Author)

  • Test

    • Unit Test
      • FE: IndexTest.testSupportPhraseKeptForTokenizingIndexes,
        IndexTest.testSupportPhraseDroppedForNonTokenizingIndexes (covers the
        normalizer-only, parser=none and keyword shapes)
      • BE: IndexReaderHelperTest.IsNeedSimilarityScoreRequiresATokenizer,
        IndexReaderHelperTest.UntokenizedQueriesCannotObserveAPosition
      • Both sides verified RED first: reverting Index.java fails the FE normalizer
        assertion; reverting index_reader_helper.h makes the keyword and parser=none
        shapes report true.
      • Full BE inverted-index suite green (3205 tests / 285 suites); FE suite of the 14
        classes touching Index / support_phrase green (149 tests).
  • Behavior changed:

    • Yes. An inverted index with no tokenizer no longer stores support_phrase, and
      is no longer reported as carrying scoring inputs. No query result changes: such an
      index can only ever be queried with a single-term phrase, which is a term query.
      Existing tablets keep their stored property; the BE-side guard handles them.
  • Does this need documentation?

    • No.

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

Copy link
Copy Markdown
Member Author

run buildall

Problem Summary:

`support_phrase=true` asks the BE to persist term positions, and a position is
only observable when a query can supply a second term to match it against.
`InvertedIndexAnalyzer::get_analyse_result()` short-circuits on
`!should_analyzer()` and returns the entire search string as ONE term, for every
analysis purpose. Every phrase variant -- MATCH_PHRASE, MATCH_PHRASE_PREFIX,
MATCH_PHRASE_EDGE -- sources its terms from there, so a phrase against an index
with no analyzer is always a single-term phrase, which is a term query. The
option describes something no query can reach.

FE was manufacturing exactly that combination. `Index`'s constructor
unconditionally defaulted `support_phrase` to `true` for every inverted index,
including `parser=none` ones, so a keyword index reached the BE carrying an
option it can never honour. Downstream,
`IndexReaderHelper::is_need_similarity_score()` read that option alone and
reported such an index as scoreable, letting relevance work be planned against
positions and norms that were never written.

The fix aligns both sides on the same predicate:

- FE keeps `support_phrase` only where the BE would run an analyzer, and removes
  the key otherwise, so new tablet metadata no longer carries it.
- BE gains `IndexReaderHelper::persists_scoring_inputs()`, which requires both
  `support_phrase=yes` and `InvertedIndexAnalyzer::should_analyzer()`. Both
  `is_need_similarity_score()` overloads route through it. Tablet metadata
  already written to disk still holds the stale option, so the FE-side change
  alone is not sufficient and the guard has to be repeated on the read path.

`should_analyzer()` is the exact line, and it is subtler than "has a parser":
`get_analyzer_name_from_properties()` falls back to the **normalizer** key, so a
`normalizer`-only index IS analyzed on the BE and is served by a FULLTEXT reader.
`match.cpp` rejects `MATCH_PHRASE` outright on a FULLTEXT-served index whose
`support_phrase` is absent, so dropping the key there would turn a working
(degenerate, single-term) phrase query into a hard error. `isTokenizedInvertedIndex`
is therefore composed from `InvertedIndexProperties.getPreferredAnalyzer()` and
`getInvertedIndexParser()` -- the FE mirrors of the two resolvers `should_analyzer`
itself uses -- rather than re-reading the property keys by hand, so the two sides
cannot drift apart. `testSupportPhraseKeptForNormalizerIndexes` and the normalizer
case in `IsNeedSimilarityScoreRequiresATokenizer` pin both halves.

ARRAY deserves an explicit note, because the obvious argument does not hold for
it. `InvertedIndexUtil::checkInvertedIndexParser` forces every ARRAY inverted
index to `parser=none`, yet an ARRAY row emits one term per element and
`SniiIndexColumnWriter::_add_array_values` does advance positions between them
(CLucene writes the same shape). Those positions are nevertheless unreachable,
for the query-side reason above -- not because the document holds a single term.
`UntokenizedQueriesCannotObserveAPosition` pins that invariant so a future
analyzer change cannot silently invalidate it.

`test_index_meta.groovy` asserted the exact `SHOW INDEX` property string for a
`parser=none` index; `support_phrase` is no longer part of it. The `parser=standard`
assertions in the same file are unchanged, which is the intended split.

For reference, Elasticsearch rejects the equivalent shape at mapping time: a
`keyword` field's `index_options` accepts only `docs` and `freqs`, never
`positions`.

Release note: None
@airborne12
airborne12 force-pushed the snii-phrase-normalize branch from 1826997 to d9576ce Compare August 27, 2026 14:09
@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17580	3033	3013	3013
q2	2081	243	221	221
q3	10260	889	513	513
q4	4674	243	200	200
q5	7684	551	390	390
q6	135	113	92	92
q7	552	498	388	388
q8	9254	882	937	882
q9	3501	2430	2383	2383
q10	6513	825	704	704
q11	385	194	175	175
q12	613	256	201	201
q13	18127	1515	1153	1153
q14	157	151	142	142
q15	q16	440	399	365	365
q17	1442	937	804	804
q18	3017	2237	2256	2237
q19	1101	889	779	779
q20	374	293	203	203
q21	4870	1837	1888	1837
q22	329	270	227	227
Total cold run time: 93089 ms
Total hot run time: 16909 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3374	3330	3307	3307
q2	508	418	376	376
q3	2205	2268	2219	2219
q4	1174	1166	875	875
q5	2223	2127	2109	2109
q6	171	119	88	88
q7	1065	914	872	872
q8	1595	1428	1399	1399
q9	3142	3080	3092	3080
q10	1861	1813	1620	1620
q11	351	266	250	250
q12	449	430	346	346
q13	1470	1540	1156	1156
q14	172	170	164	164
q15	q16	388	393	357	357
q17	3593	3362	3263	3263
q18	4874	4435	4738	4435
q19	1206	822	953	822
q20	1002	985	828	828
q21	3845	3088	3294	3088
q22	402	336	317	317
Total cold run time: 35070 ms
Total hot run time: 30971 ms

@hello-stephen

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

query5	4296	425	346	346
query6	380	142	135	135
query7	4916	424	236	236
query8	296	125	120	120
query9	8697	2889	2890	2889
query10	394	217	183	183
query11	5374	1040	921	921
query12	119	72	74	72
query13	1195	455	319	319
query14	6029	2195	2066	2066
query14_1	1951	1943	1940	1940
query15	171	123	110	110
query16	920	374	355	355
query17	805	451	377	377
query18	2333	330	244	244
query19	165	137	114	114
query20	73	70	72	70
query21	204	99	92	92
query22	6116	5382	5896	5382
query23	6729	6083	5890	5890
query23_1	6087	6139	6185	6139
query24	7271	1071	755	755
query24_1	772	781	788	781
query25	435	310	263	263
query26	1239	232	131	131
query27	2781	415	263	263
query28	4690	1493	1502	1493
query29	946	448	361	361
query30	252	155	130	130
query31	830	402	324	324
query32	126	77	72	72
query33	468	219	180	180
query34	982	836	469	469
query35	402	409	352	352
query36	571	578	528	528
query37	123	89	72	72
query38	1014	852	801	801
query39	492	491	494	491
query39_1	439	463	446	446
query40	202	96	79	79
query41	60	59	58	58
query42	75	73	74	73
query43	239	242	215	215
query44	1050	547	554	547
query45	109	107	99	99
query46	791	828	528	528
query47	769	745	711	711
query48	296	296	230	230
query49	533	241	189	189
query50	722	256	195	195
query51	8243	7986	8020	7986
query52	66	67	58	58
query53	198	194	148	148
query54	214	164	158	158
query55	68	58	57	57
query56	199	245	157	157
query57	687	657	648	648
query58	195	162	171	162
query59	1181	1204	1085	1085
query60	237	184	170	170
query61	135	128	116	116
query62	340	202	188	188
query63	170	144	142	142
query64	2713	728	613	613
query65	1593	1561	1576	1561
query66	1867	274	200	200
query67	9989	9830	9595	9595
query68	2788	1154	710	710
query69	342	221	201	201
query70	676	613	616	613
query71	252	172	169	169
query72	2345	1700	1538	1538
query73	661	584	330	330
query74	1792	1237	1155	1155
query75	1188	1096	963	963
query76	2305	721	545	545
query77	250	263	217	217
query78	3987	3697	3293	3293
query79	2929	843	564	564
query80	1590	326	275	275
query81	517	156	137	137
query82	623	131	96	96
query83	281	207	195	195
query84	300	114	90	90
query85	834	355	298	298
query86	472	175	177	175
query87	1007	994	881	881
query88	3244	2110	2137	2110
query89	288	198	168	168
query90	2164	127	127	127
query91	133	122	101	101
query92	100	66	65	65
query93	2822	1133	697	697
query94	641	282	212	212
query95	516	329	223	223
query96	777	540	271	271
query97	1035	1051	1016	1016
query98	175	138	136	136
query99	411	341	303	303
Total cold run time: 180888 ms
Total hot run time: 81942 ms

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

@hello-stephen

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

query1	0.01	0.00	0.00
query2	0.07	0.03	0.04
query3	0.25	0.11	0.10
query4	1.60	0.10	0.09
query5	0.18	0.15	0.16
query6	1.28	0.68	0.67
query7	0.04	0.01	0.01
query8	0.05	0.03	0.03
query9	0.29	0.22	0.22
query10	0.35	0.33	0.36
query11	0.16	0.12	0.11
query12	0.16	0.12	0.12
query13	0.30	0.29	0.30
query14	0.44	0.45	0.44
query15	0.36	0.35	0.34
query16	0.23	0.23	0.22
query17	0.68	0.68	0.72
query18	0.18	0.17	0.18
query19	1.20	1.19	1.14
query20	0.01	0.01	0.01
query21	15.49	0.15	0.11
query22	5.09	0.05	0.04
query23	16.18	0.27	0.10
query24	2.96	0.30	0.25
query25	0.12	0.03	0.03
query26	0.85	0.17	0.11
query27	0.03	0.03	0.02
query28	3.66	0.53	0.25
query29	12.50	3.14	2.53
query30	0.26	0.11	0.12
query31	2.75	0.37	0.18
query32	3.52	0.32	0.23
query33	1.37	1.41	1.49
query34	15.36	2.18	1.75
query35	1.76	1.74	1.73
query36	0.44	0.29	0.28
query37	0.06	0.04	0.04
query38	0.04	0.03	0.02
query39	0.04	0.02	0.02
query40	0.12	0.07	0.07
query41	0.08	0.02	0.02
query42	0.03	0.02	0.02
query43	0.03	0.02	0.03
Total cold run time: 90.58 s
Total hot run time: 14.49 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (8/8) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 65.09% (29483/45294)
Line Coverage 49.96% (317725/635907)
Region Coverage 45.71% (260420/569667)
Branch Coverage 46.88% (120116/256229)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants