diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CheckSearchUsage.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CheckSearchUsage.java index 0fd66ef061817b..40fc84e41f2d22 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CheckSearchUsage.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/CheckSearchUsage.java @@ -30,6 +30,8 @@ import org.apache.doris.nereids.trees.plans.logical.LogicalProject; import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.util.List; @@ -39,15 +41,16 @@ * Must run in analysis phase before search() gets optimized away. */ public class CheckSearchUsage implements AnalysisRuleFactory { + private static final Logger LOG = LogManager.getLogger(CheckSearchUsage.class); @Override public List buildRules() { return ImmutableList.of( - any().thenApply(ctx -> { - Plan plan = ctx.root; - checkPlanRecursively(plan); - return plan; - }).toRule(RuleType.CHECK_SEARCH_USAGE) + any().thenApply(ctx -> { + Plan plan = ctx.root; + checkPlanRecursively(plan); + return plan; + }).toRule(RuleType.CHECK_SEARCH_USAGE) ); } @@ -93,9 +96,10 @@ private void checkPlanRecursively(Plan plan) { } private void validateSearchUsage(Plan plan) { + LOG.debug("validateSearchUsage: {}", plan.treeString()); if (plan instanceof LogicalFilter) { Plan child = plan.child(0); - if (!(child instanceof LogicalOlapScan)) { + if (!isSingleTableScanPipeline(child)) { throw new AnalysisException("search() predicate only supports filtering directly on a single " + "table scan; remove joins, subqueries, or additional operators between search() " + "and the target table"); @@ -127,4 +131,17 @@ private boolean containsSearchExpression(Expression expression) { } return false; } + + private boolean isSingleTableScanPipeline(Plan plan) { + Plan current = plan; + while (true) { + if (current instanceof LogicalOlapScan) { + return true; + } + if (current.arity() != 1) { + return false; + } + current = current.child(0); + } + } } diff --git a/regression-test/suites/search/test_search_mow_support.groovy b/regression-test/suites/search/test_search_mow_support.groovy new file mode 100644 index 00000000000000..ce759ad99c9ba3 --- /dev/null +++ b/regression-test/suites/search/test_search_mow_support.groovy @@ -0,0 +1,78 @@ +// 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_mow_support") { + def tableName = "search_mow_support_tbl" + sql "DROP TABLE IF EXISTS ${tableName}" + + sql """ + CREATE TABLE ${tableName} ( + k1 BIGINT, + title VARCHAR(256), + desc_text TEXT, + INDEX idx_title (title) USING INVERTED PROPERTIES("parser" = "english") + ) ENGINE=OLAP + UNIQUE KEY(k1) + DISTRIBUTED BY HASH(k1) BUCKETS 1 + PROPERTIES ( + "replication_allocation" = "tag.location.default: 1", + "enable_unique_key_merge_on_write" = "true" + ) + """ + + // initial rows + sql """ + INSERT INTO ${tableName} VALUES + (1, 'Rainbowman', 'first version'), + (2, 'Other Title', 'unrelated row'), + (3, 'Rainbowman Story', 'should not match exact search') + """ + + // update existing key to exercise delete bitmap handling + sql """ + INSERT INTO ${tableName} VALUES + (1, 'Rainbowman', 'updated version'), + (4, 'rainbowman lowercase', 'case variants') + """ + + sql "SET enable_common_expr_pushdown = true" + sql "SET enable_common_expr_pushdown_for_inverted_index = true" + + def exactCount = sql """ + SELECT /*+SET_VAR(enable_inverted_index_query=true) */ count(*) + FROM ${tableName} + WHERE search('title:ALL("Rainbowman")') + """ + assert exactCount[0][0] == 3 + + def exactRows = sql """ + SELECT /*+SET_VAR(enable_inverted_index_query=true) */ k1, title + FROM ${tableName} + WHERE search('title:ALL("Rainbowman")') + ORDER BY k1 + """ + assert exactRows.size() == 3 + assert exactRows[0][0] == 1 + assert exactRows[0][1] == 'Rainbowman' + + def anyCount = sql """ + SELECT /*+SET_VAR(enable_inverted_index_query=true) */ count(*) + FROM ${tableName} + WHERE search('title:ANY("rainbowman")') + """ + assert anyCount[0][0] == 3 +}