-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[fix](score) disable search topn with extra predicates #65821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -194,17 +194,22 @@ private Plan pushDown( | |
| } | ||
|
|
||
| // When limit + offset overflows the long range, the pushed scan limit would wrap to a | ||
| // negative value; skip the push-down and let the TopN above the scan apply limit/offset. | ||
| // negative value. Fail with the same error as ordinary TopN instead of leaving score() | ||
| // unmaterialized and reporting an unrelated score() usage error. | ||
| if (Utils.addOverflows(topN.getLimit(), topN.getOffset())) { | ||
| return null; | ||
| throw new AnalysisException("limit + offset overflows the long range"); | ||
| } | ||
|
|
||
| long scoreLimit = topN.getLimit() + topN.getOffset(); | ||
| long pushedScoreLimit = shouldDisableSearchTopN(filter.getConjuncts(), extractedScorePredicate) | ||
| ? 0L : scoreLimit; | ||
|
|
||
| // All conditions met, perform the push down. | ||
| // This is the core action: push score() as a virtual column and also push the | ||
| // topN info. | ||
| Plan newScan = scan.appendVirtualColumnsAndTopN(ImmutableList.of(scoreAlias), | ||
| ImmutableList.of(), Optional.empty(), | ||
| topN.getOrderKeys(), Optional.of(topN.getLimit() + topN.getOffset()), | ||
| topN.getOrderKeys(), Optional.of(pushedScoreLimit), | ||
| scoreRangeInfo); | ||
|
|
||
| // Rebuild the plan tree above the new scan. | ||
|
|
@@ -243,6 +248,19 @@ private Plan pushDown( | |
| return topN.withChildren(newProject); | ||
| } | ||
|
|
||
| private boolean shouldDisableSearchTopN(Set<Expression> conjuncts, Expression extractedScorePredicate) { | ||
| List<Expression> nonScoreConjuncts = conjuncts.stream() | ||
| .filter(conjunct -> extractedScorePredicate == null || !conjunct.equals(extractedScorePredicate)) | ||
| .collect(ImmutableList.toImmutableList()); | ||
|
|
||
| boolean hasSearchPredicate = nonScoreConjuncts.stream() | ||
| .anyMatch(conjunct -> !conjunct.collect(e -> e instanceof SearchExpression).isEmpty()); | ||
| if (!hasSearchPredicate) { | ||
| return false; | ||
| } | ||
| return nonScoreConjuncts.size() > 1 || !(nonScoreConjuncts.get(0) instanceof SearchExpression); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Account for storage visibility before keeping SEARCH Top-K A lone SEARCH is not sufficient to make early Top-K safe. For example: This branch keeps
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked the MOW case with EXPLAIN, and the actual plan does not keep a positive score_sort_limit for a SQL-level lone SEARCH on a unique-key MOW table. SELECT id, score() AS s produces: SCORE SORT LIMIT: 0 So although the SQL text contains only one SEARCH predicate, the scan predicate is not a lone SEARCH in a unique-key table. Doris injects the hidden delete-sign predicate, and this PR classifies it as SEARCH plus an extra predicate, so the early SEARCH Top-K limit is disabled. I agree that if a unique-key/MOW scan kept a positive score_sort_limit, it would be unsafe because SEARCH Top-K is evaluated before post-SEARCH visibility filters such as delete bitmap / delete-sign filtering. But that is not the actual plan produced by this PR for the MOW case. With SCORE SORT LIMIT: 0, BE takes the full doc-set collection path instead of collect_multi_segment_top_k(), and the upper TopN applies the final limit after visibility filtering. I also tried to reproduce the deleted-highest-score case on a unique-key MOW table, comparing the SQL-level lone SEARCH query with a control query that forces SCORE SORT LIMIT: 0, and both returned the same correct visible rows. Could you provide a concrete MOW SQL/EXPLAIN where a unique-key MOW lone SEARCH still produces a positive SCORE SORT LIMIT? Otherwise, this P1 seems to be based on a plan shape that this PR does not generate.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The MOW plan is protected by the injected delete-sign predicate, but that does not close the broader visibility concern. For example, insert two SEARCH-matching rows where id 1 scores above id 2, then execute so this helper keeps Please account for storage delete predicates (and any other late semantic mask) before retaining positive SEARCH Top-K, and add a deleted-highest-score DUP_KEYS regression. I am replying here instead of opening another inline thread because this is concrete evidence for the same invariant already raised above.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR is scoped to the case where SEARCH is combined with other planner-visible predicates in the same FE plan, and the Top-K limit may be applied before those remaining predicates are evaluated. The DUP_KEYS + DELETE case is a separate storage-side visibility problem: the delete predicate is applied later than the SEARCH Top-K limit, so the FE rewrite in this PR cannot safely reason about it in the same way. I’ve filed a separate JIRA to track that broader bug: http://jira.selectdb-in.cc/browse/DORIS-28025. |
||
| } | ||
|
|
||
| /** | ||
| * Extract score range info from a single score predicate. | ||
| * Only supports min_score semantics (similar to Elasticsearch): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| -- This file is automatically generated. You should know what you did if you want to edit this | ||
| -- !single_search -- | ||
| 1 | ||
|
|
||
| -- !search_with_equal_predicate -- | ||
| 3 | ||
|
|
||
| -- !search_with_plain_equal_predicate -- | ||
| 3 | ||
|
|
||
| -- !search_with_equal_limit_two -- | ||
| 3 | ||
| 4 | ||
|
|
||
| -- !search_with_equal_offset -- | ||
| 4 | ||
|
|
||
| -- !search_with_match_predicate -- | ||
| 3 | ||
|
|
||
| -- !search_with_range_predicate -- | ||
| 3 | ||
|
|
||
| -- !multiple_search_predicates -- | ||
| 4 | ||
|
|
||
| -- !search_with_score_range_only -- | ||
| 1 | ||
| 3 | ||
|
|
||
| -- !search_with_score_range_and_other_predicate -- | ||
| 3 | ||
|
|
||
| -- !nested_search_with_other_predicate -- | ||
| 7 | ||
|
|
||
| -- !not_search_with_other_search -- | ||
| 6 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| suite("test_search_score_topn_predicates", "p0") { | ||
| sql "DROP TABLE IF EXISTS test_search_score_topn_predicates" | ||
|
|
||
| sql """ | ||
| CREATE TABLE test_search_score_topn_predicates ( | ||
| id INT, | ||
| status VARCHAR(20), | ||
| plain_status VARCHAR(20), | ||
| category VARCHAR(20), | ||
| title TEXT, | ||
| body TEXT, | ||
| INDEX idx_status (status) USING INVERTED, | ||
| INDEX idx_category (category) USING INVERTED, | ||
| INDEX idx_title (title) USING INVERTED PROPERTIES("parser" = "english", "support_phrase" = "true"), | ||
| INDEX idx_body (body) USING INVERTED PROPERTIES("parser" = "english", "support_phrase" = "true") | ||
| ) ENGINE=OLAP | ||
| DUPLICATE KEY(id) | ||
| DISTRIBUTED BY HASH(id) BUCKETS 1 | ||
| PROPERTIES ( | ||
| "replication_allocation" = "tag.location.default: 1", | ||
| "disable_auto_compaction" = "true" | ||
| ) | ||
| """ | ||
|
|
||
| sql """ | ||
| INSERT INTO test_search_score_topn_predicates VALUES | ||
| (1, 'drop', 'drop', 'plain', 'apple apple apple apple apple apple apple apple apple apple apple apple', 'alpha'), | ||
| (2, 'keep', 'keep', 'plain', 'apple', 'alpha'), | ||
| (3, 'keep', 'keep', 'plain', 'apple apple apple apple apple', 'alpha'), | ||
| (4, 'keep', 'keep', 'plain', 'apple apple apple', 'beta beta beta'), | ||
| (5, 'drop', 'drop', 'plain', 'banana', 'beta beta beta beta beta beta beta beta beta beta beta beta'), | ||
| (6, 'keep', 'keep', 'plain', 'pear', 'beta beta beta beta beta'), | ||
| (7, 'keep', 'keep', 'special', 'cherry cherry cherry cherry', 'gamma'), | ||
| (8, 'drop', 'drop', 'special', 'cherry cherry cherry cherry cherry cherry cherry cherry cherry cherry', 'gamma') | ||
| """ | ||
|
|
||
| sql "sync" | ||
| sql "set enable_nereids_planner = true" | ||
| sql "set enable_fallback_to_original_planner = false" | ||
| sql "set enable_segment_limit_pushdown = true" | ||
| sql "set enable_inverted_index_query_cache = false" | ||
|
|
||
| qt_single_search """ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Cover the positive branch if it remains These result-only queries cannot distinguish
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The added regression cases are primarily correctness tests. They are constructed so that, for SEARCH plus residual predicates, keeping a positive SCORE SORT LIMIT would prune the high-score row before the residual predicate is applied and produce the wrong result. Therefore the expected results verify the correctness issue that this PR fixes. |
||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') | ||
| ORDER BY s DESC | ||
| LIMIT 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_search_with_equal_predicate """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') AND status = 'keep' | ||
| ORDER BY s DESC | ||
| LIMIT 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_search_with_plain_equal_predicate """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') AND plain_status = 'keep' | ||
| ORDER BY s DESC | ||
| LIMIT 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_search_with_equal_limit_two """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') AND status = 'keep' | ||
| ORDER BY s DESC | ||
| LIMIT 2 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_search_with_equal_offset """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') AND status = 'keep' | ||
| ORDER BY s DESC | ||
| LIMIT 1 OFFSET 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_search_with_match_predicate """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') AND status MATCH 'keep' | ||
| ORDER BY s DESC | ||
| LIMIT 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_search_with_range_predicate """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') AND id > 1 | ||
| ORDER BY s DESC | ||
| LIMIT 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_multiple_search_predicates """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') AND search('body:beta') | ||
| ORDER BY s DESC | ||
| LIMIT 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_search_with_score_range_only """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') AND score() > 0 | ||
| ORDER BY s DESC | ||
| LIMIT 2 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_search_with_score_range_and_other_predicate """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') AND score() > 0 AND status = 'keep' | ||
| ORDER BY s DESC | ||
| LIMIT 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_nested_search_with_other_predicate """ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Make the nested cases depend on recursive SEARCH detection Neither named case fails with a shallow top-level-only classifier. In the OR case, |
||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE (search('title:cherry') OR category MATCH 'special') AND status = 'keep' | ||
| ORDER BY s DESC | ||
| LIMIT 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| qt_not_search_with_other_search """ | ||
| SELECT id FROM ( | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE NOT search('title:apple') AND search('body:beta') AND status = 'keep' | ||
| ORDER BY s DESC | ||
| LIMIT 1 | ||
| ) t | ||
| ORDER BY id | ||
| """ | ||
|
|
||
| // limit + offset overflows the long range. score() must report the standard TopN | ||
| // overflow error instead of skipping score pushdown and reporting a score() usage error. | ||
| test { | ||
| sql """ | ||
| SELECT id, score() AS s | ||
| FROM test_search_score_topn_predicates | ||
| WHERE search('title:apple') | ||
| ORDER BY s DESC | ||
| LIMIT 9223372036854775807 OFFSET 9223372036854775807 | ||
| """ | ||
| exception "limit + offset overflows the long range" | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please check overflow and do not push down like #64633
add a test case for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before that, the overflow has been checked:
if (Utils.addOverflows(topN.getLimit(), topN.getOffset())) { return null; }If overflow occurs, an error will occur:“
score() function requires WHERE clause with MATCH function, ORDER BY and LIMIT for optimization”,instead of “limit + offset overflows the long range”.Do I need to change
return null;tothrow new AnalysisException("limit + offset overflows the long range");?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The key is: return null (skip optimization) if no wrong result, else throw exception