Skip to content

[fix](inverted index) Handle deleted documents in score TopN - #66979

Merged
airborne12 merged 1 commit into
apache:masterfrom
hoshinojyunn:fix/topN_delete_predicate
Aug 21, 2026
Merged

airborne12 merged 1 commit into
apache:masterfrom
hoshinojyunn:fix/topN_delete_predicate

Conversation

@hoshinojyunn

@hoshinojyunn hoshinojyunn commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary:

A score-sorted inverted-index SEARCH query can return an empty result when its highest-scoring hit has already been deleted. The scan applies deletion visibility after the score TopN collector has consumed the query LIMIT; if the collector retains only a deleted document, the lower-scoring visible document has already been discarded.

Reproduction:

CREATE TABLE test_search_score_topn_delete_predicate (
    id INT,
    title TEXT,
    INDEX idx_title (title) USING INVERTED PROPERTIES("parser" = "english")
) ENGINE=OLAP
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES ("replication_allocation" = "tag.location.default: 1");

INSERT INTO test_search_score_topn_delete_predicate VALUES
    (1, "alpha alpha alpha alpha alpha alpha"),
    (2, "alpha");
SYNC;
DELETE FROM test_search_score_topn_delete_predicate WHERE id = 1;

SELECT id
FROM test_search_score_topn_delete_predicate
WHERE SEARCH("title:alpha")
ORDER BY score() DESC
LIMIT 1;

Before this change, the score collector keeps only deleted id = 1; the later visibility filtering removes it, so the actual result is:

Empty set

The expected result is:

+----+
| id |
+----+
|  2 |
+----+

There are two deletion mechanisms with different handling requirements:

  1. Delete predicate

    DUP tables and Unique MOW tables with enable_mow_light_delete = true use delete predicates. A delete predicate is evaluated during scan after the inverted-index score collector and BM25 score materialization. Therefore, for a segment with delete predicates, this change disables both early score TopN truncation points: the index query receives no score limit, and score materialization does not keep only the query TopN. The scan evaluates the delete predicate first and applies the final query limit only to visible rows. Segments without delete predicates retain the existing fast path.

  2. Delete bitmap

    Unique MOW tables with enable_mow_light_delete = false mark obsolete physical rows in a delete bitmap. These rows are also filtered later in scan, so they can occupy a score TopN slot for the same reason. Unlike delete predicates, delete bitmaps are common for key-overwrite imports as well as deletes, so disabling TopN whenever one exists would unnecessarily lose the optimization. This change passes the segment delete bitmap to collect_multi_segment_top_k and excludes deleted document IDs while collecting TopK candidates. The collector can therefore retain the highest-scoring visible document.

MATCH_ANY does not call collect_multi_segment_top_k; it reads the inverted-index bitmap through FunctionMatchBase::evaluate_inverted_index(). It is not part of the score TopN path fixed here.

The regression covers the delete-predicate path for a DUP table and MOW light-delete table, and the delete-bitmap path for a Unique MOW table. The BE unit test verifies that multi-segment TopK collection skips entries present in the delete bitmap.

Release note

Fixes incorrect empty results for score-sorted inverted-index SEARCH queries when deleted documents would otherwise occupy the TopN candidates.

Check List (For Author)

  • Test

    • Regression test
      • ./run-regression-test.sh --conf /tmp/doris-topn-regression.hNbTTJ/regression-conf.groovy --run -g p0 -d search -s test_search_score_topn_delete_predicate
    • Unit test
      • ./run-be-ut.sh --run --filter=MultiSegmentCollectorTest.CollectTopKExcludesDeletedDocs -j 192
  • Behavior changed:

    • No.
    • Yes. Deleted documents no longer consume score TopN candidates. Delete-predicate segments defer score TopN truncation until after visibility filtering; delete bitmap entries are excluded during TopK collection.
  • Does this need documentation?

    • No.
    • Yes.

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?

@hoshinojyunn

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17555	3063	3046	3046
q2	1916	233	159	159
q3	10435	881	510	510
q4	4677	249	206	206
q5	7678	573	388	388
q6	144	118	94	94
q7	529	504	401	401
q8	9244	928	929	928
q9	3466	2433	2421	2421
q10	6508	858	696	696
q11	453	261	240	240
q12	694	405	339	339
q13	17852	1556	1176	1176
q14	161	151	143	143
q15	q16	446	394	364	364
q17	721	784	817	784
q18	3186	2261	2243	2243
q19	1104	930	791	791
q20	642	545	472	472
q21	4910	1764	1954	1764
q22	342	270	240	240
Total cold run time: 92663 ms
Total hot run time: 17405 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3402	3348	3329	3329
q2	217	219	162	162
q3	2207	2345	2296	2296
q4	1217	1193	892	892
q5	2193	2119	2097	2097
q6	179	126	89	89
q7	1035	917	840	840
q8	1646	1439	1438	1438
q9	3149	3136	3150	3136
q10	1857	1806	1654	1654
q11	363	276	260	260
q12	457	432	350	350
q13	1497	1546	1179	1179
q14	174	163	166	163
q15	q16	401	402	358	358
q17	1054	1046	1033	1033
q18	5086	4476	4794	4476
q19	919	889	867	867
q20	994	936	807	807
q21	3794	3226	3337	3226
q22	402	365	340	340
Total cold run time: 32243 ms
Total hot run time: 28992 ms

@hello-stephen

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

query5	4276	424	363	363
query6	427	178	160	160
query7	4800	436	270	270
query8	291	128	136	128
query9	8678	2927	2937	2927
query10	433	262	236	236
query11	5376	1061	911	911
query12	122	73	81	73
query13	1197	470	343	343
query14	6154	2283	2150	2150
query14_1	2046	2039	2031	2031
query15	173	122	116	116
query16	944	384	307	307
query17	825	464	351	351
query18	2319	313	239	239
query19	168	139	113	113
query20	71	70	72	70
query21	215	119	104	104
query22	5428	5400	5392	5392
query23	6550	6186	5917	5917
query23_1	6263	5970	6051	5970
query24	7242	1088	763	763
query24_1	774	790	766	766
query25	410	282	247	247
query26	1256	259	157	157
query27	2732	475	288	288
query28	4586	1499	1487	1487
query29	930	434	347	347
query30	281	175	152	152
query31	864	431	361	361
query32	100	50	47	47
query33	474	209	170	170
query34	976	837	475	475
query35	399	415	354	354
query36	580	565	526	526
query37	122	84	72	72
query38	1008	848	821	821
query39	525	482	465	465
query39_1	464	460	478	460
query40	223	136	118	118
query41	60	57	60	57
query42	84	81	82	81
query43	243	251	217	217
query44	1045	564	575	564
query45	116	108	104	104
query46	818	839	545	545
query47	764	753	705	705
query48	319	322	218	218
query49	548	230	184	184
query50	818	337	268	268
query51	8217	8320	8176	8176
query52	75	76	69	69
query53	207	210	159	159
query54	223	298	167	167
query55	87	60	55	55
query56	223	231	209	209
query57	690	673	619	619
query58	239	198	196	196
query59	1245	1225	1129	1129
query60	267	205	223	205
query61	140	133	136	133
query62	381	219	196	196
query63	192	159	160	159
query64	2816	812	685	685
query65	1614	1587	1667	1587
query66	1788	304	251	251
query67	9881	9733	9580	9580
query68	3030	1213	804	804
query69	354	227	190	190
query70	675	633	594	594
query71	305	271	247	247
query72	2346	1775	1438	1438
query73	665	587	341	341
query74	2019	1227	1186	1186
query75	1240	1149	996	996
query76	2382	740	585	585
query77	261	249	211	211
query78	4045	3674	3240	3240
query79	2860	829	587	587
query80	1621	397	353	353
query81	507	200	175	175
query82	651	137	103	103
query83	330	258	231	231
query84	305	124	101	101
query85	886	442	376	376
query86	484	178	165	165
query87	1004	971	912	912
query88	3150	2149	2149	2149
query89	324	232	204	204
query90	2169	149	140	140
query91	155	142	125	125
query92	69	47	42	42
query93	2383	1243	802	802
query94	670	245	228	228
query95	653	421	345	345
query96	787	565	291	291
query97	1052	1049	999	999
query98	182	141	140	140
query99	424	350	313	313
Total cold run time: 181195 ms
Total hot run time: 83599 ms

@hello-stephen

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

query1	0.01	0.00	0.00
query2	0.08	0.04	0.03
query3	0.25	0.12	0.11
query4	1.60	0.10	0.10
query5	0.18	0.17	0.15
query6	1.23	0.68	0.68
query7	0.03	0.01	0.00
query8	0.04	0.03	0.03
query9	0.29	0.22	0.22
query10	0.35	0.34	0.36
query11	0.16	0.11	0.12
query12	0.15	0.12	0.12
query13	0.30	0.30	0.31
query14	0.46	0.44	0.45
query15	0.35	0.34	0.35
query16	0.22	0.23	0.22
query17	0.67	0.70	0.67
query18	0.16	0.16	0.17
query19	1.21	1.19	1.18
query20	0.01	0.00	0.00
query21	15.44	0.17	0.12
query22	5.06	0.04	0.04
query23	16.20	0.24	0.10
query24	3.03	0.29	0.28
query25	0.12	0.03	0.04
query26	0.85	0.16	0.13
query27	0.04	0.03	0.03
query28	3.70	0.53	0.27
query29	12.46	3.11	2.54
query30	0.27	0.12	0.11
query31	2.75	0.37	0.18
query32	3.51	0.32	0.24
query33	1.41	1.38	1.61
query34	15.37	2.18	1.76
query35	1.77	1.78	1.73
query36	0.46	0.30	0.30
query37	0.06	0.04	0.04
query38	0.05	0.03	0.03
query39	0.03	0.02	0.03
query40	0.12	0.08	0.07
query41	0.08	0.03	0.02
query42	0.03	0.02	0.02
query43	0.04	0.03	0.03
Total cold run time: 90.6 s
Total hot run time: 14.67 s

@hoshinojyunn
hoshinojyunn force-pushed the fix/topN_delete_predicate branch from 871b036 to 6aad708 Compare August 20, 2026 09:10
@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 62.18% (28909/46494)
Line Coverage 47.26% (302653/640451)
Region Coverage 42.91% (244109/568871)
Branch Coverage 44.49% (113664/255462)

@hoshinojyunn
hoshinojyunn force-pushed the fix/topN_delete_predicate branch from 6aad708 to 75f1e55 Compare August 20, 2026 10:49
@hoshinojyunn hoshinojyunn changed the title [fix](be) Prevent score TopN truncation before delete predicates [fix](be) Handle deleted documents in score TopN Aug 20, 2026
@hoshinojyunn hoshinojyunn changed the title [fix](be) Handle deleted documents in score TopN [fix](inverted index) Handle deleted documents in score TopN Aug 20, 2026
@hoshinojyunn

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17637	3058	3058	3058
q2	1879	237	153	153
q3	10479	864	537	537
q4	4676	258	210	210
q5	7660	568	388	388
q6	139	117	96	96
q7	529	515	385	385
q8	9243	903	901	901
q9	3507	2458	2430	2430
q10	6530	855	722	722
q11	450	257	237	237
q12	693	399	321	321
q13	17868	1571	1194	1194
q14	165	156	139	139
q15	q16	436	407	375	375
q17	792	803	773	773
q18	3164	2375	2355	2355
q19	1263	957	771	771
q20	700	547	477	477
q21	5681	1848	2014	1848
q22	339	266	240	240
Total cold run time: 93830 ms
Total hot run time: 17610 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3474	3385	3387	3385
q2	215	214	160	160
q3	2281	2396	2222	2222
q4	1234	1199	923	923
q5	2252	2213	2208	2208
q6	174	119	90	90
q7	1048	947	892	892
q8	1640	1444	1458	1444
q9	3265	3267	3251	3251
q10	1924	1872	1703	1703
q11	363	283	261	261
q12	470	442	346	346
q13	1509	1563	1190	1190
q14	175	181	157	157
q15	q16	390	404	373	373
q17	1055	1052	1047	1047
q18	5120	4593	4966	4593
q19	883	842	875	842
q20	990	980	970	970
q21	3871	3233	3483	3233
q22	402	348	346	346
Total cold run time: 32735 ms
Total hot run time: 29636 ms

@hello-stephen

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

query5	4264	415	361	361
query6	408	170	160	160
query7	4843	468	267	267
query8	287	126	117	117
query9	8693	2904	2947	2904
query10	444	285	227	227
query11	5381	1101	966	966
query12	123	74	73	73
query13	1210	467	331	331
query14	6184	2343	2190	2190
query14_1	2091	2122	2060	2060
query15	177	123	113	113
query16	943	405	342	342
query17	868	472	375	375
query18	2359	328	251	251
query19	168	155	117	117
query20	73	69	72	69
query21	217	119	102	102
query22	5561	5484	5457	5457
query23	6721	6382	6226	6226
query23_1	6251	6347	6276	6276
query24	7319	1109	792	792
query24_1	791	812	816	812
query25	447	315	262	262
query26	1262	281	173	173
query27	2687	455	292	292
query28	4652	1543	1520	1520
query29	968	470	388	388
query30	287	179	156	156
query31	869	437	373	373
query32	103	53	49	49
query33	484	231	188	188
query34	1023	846	490	490
query35	423	431	354	354
query36	573	560	527	527
query37	124	87	72	72
query38	1037	876	842	842
query39	487	515	484	484
query39_1	477	481	465	465
query40	227	127	119	119
query41	58	57	57	57
query42	95	82	81	81
query43	249	247	220	220
query44	1015	564	575	564
query45	111	140	104	104
query46	775	898	535	535
query47	777	774	720	720
query48	318	326	242	242
query49	554	238	187	187
query50	861	345	264	264
query51	8101	8013	7947	7947
query52	74	76	61	61
query53	203	215	161	161
query54	218	179	170	170
query55	71	59	56	56
query56	249	254	232	232
query57	688	704	668	668
query58	234	218	202	202
query59	1276	1292	1119	1119
query60	250	203	193	193
query61	112	134	118	118
query62	359	205	176	176
query63	184	161	157	157
query64	2703	729	570	570
query65	1710	1653	1578	1578
query66	1794	298	243	243
query67	10048	9926	9939	9926
query68	3026	1193	791	791
query69	347	229	207	207
query70	676	638	611	611
query71	304	258	237	237
query72	2418	1788	1441	1441
query73	699	579	375	375
query74	1984	1266	1175	1175
query75	1257	1176	1041	1041
query76	2369	778	571	571
query77	261	264	221	221
query78	4148	3925	3397	3397
query79	1852	807	576	576
query80	1177	386	341	341
query81	495	202	179	179
query82	634	135	105	105
query83	320	265	234	234
query84	313	127	103	103
query85	854	428	390	390
query86	386	176	174	174
query87	1019	1007	947	947
query88	2832	2151	2162	2151
query89	331	230	206	206
query90	1977	161	144	144
query91	160	153	131	131
query92	56	48	44	44
query93	1444	1163	793	793
query94	655	262	221	221
query95	630	372	364	364
query96	797	595	283	283
query97	1115	1065	1069	1065
query98	148	141	140	140
query99	417	350	315	315
Total cold run time: 179095 ms
Total hot run time: 85093 ms

@hello-stephen

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

query1	0.00	0.01	0.01
query2	0.07	0.04	0.03
query3	0.25	0.11	0.11
query4	1.60	0.09	0.10
query5	0.17	0.15	0.16
query6	1.27	0.70	0.67
query7	0.04	0.01	0.00
query8	0.05	0.03	0.03
query9	0.30	0.22	0.21
query10	0.35	0.34	0.35
query11	0.17	0.12	0.11
query12	0.16	0.12	0.12
query13	0.31	0.30	0.30
query14	0.45	0.46	0.46
query15	0.36	0.35	0.37
query16	0.23	0.23	0.23
query17	0.68	0.67	0.68
query18	0.18	0.17	0.16
query19	1.17	1.16	1.20
query20	0.01	0.01	0.00
query21	15.47	0.14	0.13
query22	5.08	0.04	0.04
query23	16.24	0.24	0.09
query24	2.99	0.30	0.28
query25	0.12	0.05	0.03
query26	0.87	0.17	0.13
query27	0.02	0.03	0.02
query28	3.66	0.52	0.26
query29	12.50	3.16	2.56
query30	0.25	0.12	0.12
query31	2.76	0.37	0.18
query32	3.51	0.31	0.23
query33	1.39	1.42	1.55
query34	15.40	2.22	1.78
query35	1.77	1.75	1.73
query36	0.44	0.30	0.30
query37	0.06	0.04	0.03
query38	0.05	0.03	0.03
query39	0.03	0.02	0.02
query40	0.12	0.08	0.08
query41	0.07	0.03	0.02
query42	0.03	0.03	0.02
query43	0.04	0.03	0.03
Total cold run time: 90.69 s
Total hot run time: 14.73 s

@hoshinojyunn
hoshinojyunn force-pushed the fix/topN_delete_predicate branch from 75f1e55 to ad8158a Compare August 20, 2026 11:55
Issue Number: None

Related PR: None

Problem Summary: A score-sorted query could truncate inverted-index hits or BM25 score materialization to its LIMIT before evaluating a delete predicate. A deleted high-score document then occupied the only TopN slot, so a lower-score visible document was never returned. When a rowset has delete predicates, directly disable both score TopN early-truncation points while retaining the existing fast path when no delete predicate is present. The regression verifies score order before deletion, visible results after deletion, and that a later data rowset is not affected by an earlier delete predicate.

Fixes incorrect results for score-sorted searches with delete predicates.

- Test: Regression test / Build
    - ./build.sh --be -j 192
    - ./run-regression-test.sh --conf /tmp/score-topn-delete-predicate.mz4MAv.groovy --run -g p0 -d search -s test_search_score_topn_delete_predicate -forceGenOut
- Behavior changed: Yes (score TopN is not truncated before delete predicates are evaluated)
- Does this need documentation: No
@hoshinojyunn
hoshinojyunn force-pushed the fix/topN_delete_predicate branch from ad8158a to 5b4c4e1 Compare August 20, 2026 12:05
@hoshinojyunn

Copy link
Copy Markdown
Contributor Author

run buildall

@airborne12

Copy link
Copy Markdown
Member

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

Requesting changes for two substantive issues in the score-TopN eligibility/resource behavior and two test gaps.

Critical checkpoint conclusions:

  • Goal and proof: the delete-predicate and MOW delete-bitmap mechanisms fix the named deleted-winner cases, but the early TopK candidate domain is still incomplete when ordinary filters or UNIQUE-MOR merge/delete-sign visibility reject winners later (MAIN-001). The added tests cover the three advertised deletion modes, but MAIN-003 and MAIN-004 leave the offset/block-WAND behavior and normal golden output unproved.
  • Scope and clarity: the production change is localized and generally clear; the incomplete eligibility condition and unbounded fallback are the two material design issues.
  • Concurrency and lifecycle: no new concurrent mutation or locking path is introduced. The delete bitmap is an immutable visible-version snapshot whose shared cache handle remains pinned through the iterator/query context; no ownership cycle or cross-TU static-initialization issue was found.
  • Configuration and compatibility: no configuration, wire protocol, storage format, public symbol, or rolling-upgrade behavior changes. The collector API addition is internal and has a trailing default argument.
  • Parallel paths and conditions: I traced SEARCH versus legacy MATCH, CLucene/SNII reader shapes, WAND/non-WAND, delete predicates, MOW bitmaps, ordinary/key/residual filters, and UNIQUE-MOR merge visibility. Runtime bitmap offsets and deleted-hit threshold feedback are sound, but the new delete-predicate-only eligibility fence does not cover every later visibility/filtering path.
  • Tests and results: the author reports the targeted regression suite and BE unit test; I did not rerun them because this automated-review environment forbids builds. Header-hygiene checks and git diff --check pass. Deterministic regression results need the repository golden-output mechanism, and the unit fixture needs nonzero-base plus genuine block-WAND coverage.
  • Error, memory, nullability, and observability: status/error and nullable-column handling are unchanged. Existing index/row-filter statistics are adequate for correctness investigation, but MAIN-002 introduces full-match score-map/vector materialization without a concrete resource bound.
  • Transactions, persistence, and writes: no transaction, EditLog, persistence, or data-write semantics are changed.
  • FE/BE variables: no new cross-layer variable propagation is introduced.
  • Performance: the delete-predicate fallback changes bounded TopK work into O(matches) score storage/materialization (MAIN-002).
  • Additional review focus: none was supplied.

Review completion: two full rounds completed; both normal passes and the separate risk-focused pass returned NO_NEW_VALUABLE_FINDINGS in Round 2. All accepted and dismissed candidates have concrete conclusions.

_index_query_context->collection_statistics = _opts.collection_statistics;
_index_query_context->collection_similarity = std::make_shared<CollectionSimilarity>();
_index_query_context->query_limit = _score_runtime->get_limit();
_index_query_context->query_limit =

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] Gate early TopK on the complete visible candidate domain

Absence of a rowset delete predicate is not enough to make segment-local TopK safe. For example, with a higher-scoring id=1 and lower-scoring id=2, WHERE SEARCH('title:alpha') AND id=2 ORDER BY score() DESC LIMIT 1 lets FunctionSearch keep only id=1 from the full index, after which the existing id/key predicate intersects it away and id=2 cannot be recovered. UNIQUE-MOR has the same issue when an explicit __DORIS_DELETE_SIGN__=1 row or version merge removes a high-scoring key above SegmentIterator; neither source appears in this predicate count or the MOW bitmap. Please enable early TopK only when no later filter/merge can reject a hit, or feed the complete admissible-row domain into the collector, and cover both triggers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is actually another bug in topN, unrelated to the issue of deleting predicates/bitmaps addressed in this PR. It should be fixed in a separate PR later.

IColumn::MutablePtr result_column;
auto result_row_ids = std::make_unique<std::vector<uint64_t>>();
if (_score_runtime->get_limit() > 0 && _col_predicates.empty() &&
if (_opts.delete_condition_predicates->num_of_column_predicate() == 0 &&

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] Avoid materializing every score for a small TopK query

Together with query_limit = 0, this branch makes any applicable delete predicate turn ORDER BY score() ... LIMIT 1 into full-match collection: collect_multi_segment_doc_set inserts every score into _bm25_scores, then get_bm25_scores allocates row-id and score arrays for the entire bitmap before the delete predicate runs. A broad term on an old rowset with even one delete predicate can therefore consume O(matches) memory/CPU instead of bounded TopK resources. Please make the candidate collection visibility-aware/refillable while remaining bounded, or add a concrete resource guard with scale evidence that prevents this fallback from exhausting a query.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In fact, only rowsets with a version less than the delete predicate will follow the full materialization path, while rowsets with a version greater than the delete predicate will follow the topN path normally. The performance loss of the former is paid for correctness and is within expectations.


auto index_query_context = std::make_shared<IndexQueryContext>();
auto field = StringHelper::to_wstring("title");
TermQuery query(index_query_context, field, StringHelper::to_wstring("fleabag"));

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.

[P2] Exercise the offset and actual block-WAND paths

This case does not fail if + seg_base is removed: bitmap {0} still deletes first-reader local doc 0, while the expected second-reader hit is local doc 1/global doc 3 and remains admitted. Also, TermQuery::weight(false) is held as WeightPtr; TermWeight has only a templated same-name overload, not a virtual PruningCallback override, so the use_wand=true leg takes Weight's generic scorer loop just like false, and these two hits have equal frequency/length rather than a deleted high scorer plus a lower replacement. Please add a nonzero-base deletion that fails on local-id lookup, plus a genuinely scoring Boolean query whose OccurBooleanWeight enters block WAND with a deleted high scorer and a visible replacement.

}

def assertScoreTopN = { tableName, predicate, expectedId ->
assertEquals(expectedId, sql("""

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.

[P2] Use the regression golden-output mechanism

These are deterministic query results, but the suite wraps them in raw assertEquals(sql(...)) calls and adds no generated .out. The repository testing rules require qt_/order_qt_ (or explicit ORDER BY) with runner-generated expected output rather than Groovy assertions for determined results. Please convert the cases to named query tests and commit the generated output so the full rows/types are reviewed by the normal regression harness.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17632	3110	3092	3092
q2	1876	229	150	150
q3	10487	894	508	508
q4	4676	253	202	202
q5	7677	567	387	387
q6	140	122	96	96
q7	540	515	390	390
q8	9244	872	990	872
q9	3452	2441	2425	2425
q10	6517	859	715	715
q11	441	258	248	248
q12	709	392	333	333
q13	17879	1541	1165	1165
q14	168	152	141	141
q15	q16	436	402	369	369
q17	834	778	691	691
q18	3204	2269	2250	2250
q19	1104	976	803	803
q20	653	514	432	432
q21	5323	1899	2000	1899
q22	322	267	231	231
Total cold run time: 93314 ms
Total hot run time: 17399 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3509	3464	3411	3411
q2	210	216	155	155
q3	2220	2331	2126	2126
q4	1199	1174	909	909
q5	2202	2138	2111	2111
q6	173	119	87	87
q7	1066	907	871	871
q8	1644	1436	1430	1430
q9	3177	3159	3168	3159
q10	1857	1780	1648	1648
q11	357	272	257	257
q12	462	432	342	342
q13	1504	1539	1174	1174
q14	181	176	168	168
q15	q16	397	397	355	355
q17	1060	1057	1045	1045
q18	4978	4366	4776	4366
q19	889	838	871	838
q20	966	949	805	805
q21	3803	3083	3317	3083
q22	407	357	323	323
Total cold run time: 32261 ms
Total hot run time: 28663 ms

@hello-stephen

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

query5	4267	437	337	337
query6	405	161	152	152
query7	4853	446	265	265
query8	306	127	117	117
query9	8690	2928	2912	2912
query10	383	248	229	229
query11	5389	1040	905	905
query12	117	71	71	71
query13	1197	425	336	336
query14	6171	2281	2120	2120
query14_1	2019	2034	1987	1987
query15	176	117	108	108
query16	916	376	387	376
query17	819	483	366	366
query18	2339	334	233	233
query19	170	146	127	127
query20	73	71	71	71
query21	215	118	106	106
query22	5329	5540	5291	5291
query23	6682	6174	6074	6074
query23_1	6091	5935	5986	5935
query24	7315	1082	769	769
query24_1	759	788	797	788
query25	434	307	264	264
query26	1261	273	163	163
query27	2715	470	286	286
query28	4618	1515	1499	1499
query29	955	458	377	377
query30	286	170	153	153
query31	869	435	357	357
query32	99	52	51	51
query33	481	227	192	192
query34	995	826	492	492
query35	409	417	360	360
query36	555	564	544	544
query37	123	85	75	75
query38	1007	866	836	836
query39	513	489	461	461
query39_1	476	475	492	475
query40	217	125	140	125
query41	52	50	50	50
query42	74	81	77	77
query43	247	245	219	219
query44	1007	545	552	545
query45	114	106	104	104
query46	782	875	553	553
query47	755	759	714	714
query48	315	319	223	223
query49	544	236	177	177
query50	846	336	252	252
query51	8033	8069	8064	8064
query52	75	77	78	77
query53	214	253	162	162
query54	227	180	168	168
query55	68	61	57	57
query56	225	215	223	215
query57	695	666	631	631
query58	216	222	192	192
query59	1231	1283	1112	1112
query60	257	223	213	213
query61	139	145	112	112
query62	378	206	176	176
query63	183	158	154	154
query64	2747	725	581	581
query65	1607	1573	1591	1573
query66	1871	320	248	248
query67	9826	9763	9652	9652
query68	3033	1173	747	747
query69	328	225	197	197
query70	677	603	614	603
query71	308	268	238	238
query72	2322	1723	1532	1532
query73	664	639	343	343
query74	1983	1231	1132	1132
query75	1240	1145	1003	1003
query76	2371	740	539	539
query77	255	258	219	219
query78	4122	3659	3160	3160
query79	2312	855	585	585
query80	1551	390	340	340
query81	485	198	178	178
query82	617	137	110	110
query83	321	251	226	226
query84	300	120	103	103
query85	846	446	396	396
query86	385	177	163	163
query87	1002	981	898	898
query88	2813	2156	2138	2138
query89	320	228	207	207
query90	2023	149	151	149
query91	157	146	121	121
query92	52	47	48	47
query93	1495	1159	807	807
query94	631	224	221	221
query95	616	413	349	349
query96	809	594	285	285
query97	1058	1071	1003	1003
query98	158	144	134	134
query99	407	344	304	304
Total cold run time: 178476 ms
Total hot run time: 83413 ms

@hello-stephen

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

query1	0.01	0.00	0.00
query2	0.08	0.04	0.04
query3	0.24	0.12	0.11
query4	1.60	0.09	0.09
query5	0.16	0.15	0.16
query6	1.27	0.69	0.69
query7	0.03	0.00	0.01
query8	0.04	0.02	0.02
query9	0.30	0.21	0.21
query10	0.36	0.35	0.34
query11	0.16	0.11	0.12
query12	0.15	0.12	0.12
query13	0.32	0.31	0.31
query14	0.45	0.45	0.45
query15	0.35	0.34	0.34
query16	0.21	0.21	0.25
query17	0.70	0.72	0.67
query18	0.16	0.16	0.17
query19	1.27	1.16	1.16
query20	0.02	0.02	0.01
query21	15.44	0.15	0.10
query22	5.10	0.05	0.04
query23	16.28	0.25	0.10
query24	3.01	0.32	0.23
query25	0.10	0.04	0.03
query26	0.75	0.16	0.12
query27	0.03	0.03	0.03
query28	3.56	0.57	0.28
query29	12.41	3.16	2.58
query30	0.25	0.12	0.14
query31	2.79	0.36	0.17
query32	3.54	0.32	0.23
query33	1.37	1.45	1.40
query34	15.34	2.18	1.76
query35	1.72	1.72	1.70
query36	0.46	0.28	0.29
query37	0.06	0.04	0.04
query38	0.05	0.03	0.02
query39	0.04	0.02	0.02
query40	0.12	0.08	0.07
query41	0.07	0.02	0.03
query42	0.04	0.02	0.02
query43	0.03	0.03	0.03
Total cold run time: 90.44 s
Total hot run time: 14.58 s

@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 75.94% (34287/45151)
Line Coverage 60.92% (386522/634512)
Region Coverage 57.21% (325117/568255)
Branch Coverage 57.96% (148065/255457)

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

@airborne12
airborne12 merged commit 7121a6c into apache:master Aug 21, 2026
33 checks passed
airborne12 pushed a commit that referenced this pull request Oct 22, 2026
…of a tablet (#69352)

### What problem does this PR solve?

Issue Number: None

Related PR: #69313 (BM25 statistics after compaction), #66979 (deleted
documents in score TopN)

Problem Summary:

A parallel scan (`ParallelScannerBuilder`) splits one tablet into
several scanners. Each `OlapScanner` then calls
`CollectionStatistics::collect()` with only the `rs_splits` of its own
split, so the BM25 doc count, avgdl and term doc frequencies come from a
subset of the tablet's rowsets. Rows with identical text get different
scores depending on which scanner reads them.

The split depends on the tablet's total row count,
`parallel_scan_max_scanners_count` (defaulting to the core count) and
`parallel_scan_min_rows_per_scanner`. A row's score and its rank in
`ORDER BY score()` therefore change when unrelated rows are loaded into
the tablet, and differ between BEs with different core counts.
Pagination over a score-ordered search returns repeated and missing
rows.

Reproduction (one bucket, one replica, no concurrent writes):

```sql
CREATE TABLE t (
    id BIGINT,
    content TEXT,
    INDEX idx_content (content) USING INVERTED PROPERTIES("parser" = "english")
) UNIQUE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES ("replication_num" = "1", "enable_unique_key_merge_on_write" = "true",
            "disable_auto_compaction" = "true");

-- 8 loads (8 rowsets), each with a different share of matching rows
INSERT INTO t SELECT 0 * 2000 + number, IF(number % 2  = 0, 'we should try these jobs today', 'call the candidate about salary') FROM numbers("number" = "2000");
INSERT INTO t SELECT 1 * 2000 + number, IF(number % 5  = 0, 'we should try these jobs today', 'call the candidate about salary') FROM numbers("number" = "2000");
-- ... batches 2..7 with modulus 8, 11, 14, 17, 20, 23

SELECT /*+ SET_VAR(enable_parallel_scan=true, parallel_scan_min_rows_per_scanner=1024, parallel_scan_max_scanners_count=16) */
       score() AS s, count(*)
FROM (SELECT score() FROM t WHERE SEARCH('content:jobs OR content:try') ORDER BY score() DESC LIMIT 100000) x
GROUP BY s;
```

On 4.1.4, the 2,280 matching rows all have the same text but get **15
distinct scores (1.34 to 5.81)**. With `enable_parallel_scan=false` they
all get 3.647686. On a production-sized table, the default thresholds
give the same behavior.

Fix:

- `TabletCollectionStatistics` holds the rowsets of a tablet's entire
read source. The first scanner of the tablet to prepare collects the
statistics once with `collect_full_collection()`, and every scanner of
that tablet copies them into its own `CollectionStatistics`. Later
scanners wait for the first collection and get its result or error.
- `CollectionStatistics::copy_collected_from()` copies the collected
counters and resets the derived idf/avgdl caches, so no cache is shared
between scanner threads.
- `ParallelScannerBuilder` creates one holder per tablet, only when the
scan has a `score()` runtime. Both the by-rowid and per-segment build
paths pass it to their scanners.
- The non-parallel path is unchanged. Each of its scanners already
receives the tablet's full read source.

Statistics remain per tablet. Making them consistent across tablets and
BEs (as with Elasticsearch's `dfs_query_then_fetch`) is out of scope.
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