Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,14 @@ class AllWeight : public Weight {

~AllWeight() override = default;

ScorerPtr scorer(const QueryExecutionContext& context) override {
ScorerPtr scorer(const QueryExecutionContext& context) override { return scorer(context, {}); }

ScorerPtr scorer(const QueryExecutionContext& context,
const std::string& binding_key) override {
auto inner = std::make_shared<AllScorer>(context.segment_num_rows, _enable_scoring);
if (_nullable && context.null_resolver != nullptr) {
std::string logical = logical_field_or_fallback(context, "", _field);
// NULL bitmaps use SQL field names ("body"), not physical index field IDs ("1").
std::string logical = logical_field_or_fallback(context, binding_key, _field);
auto nulls = FieldNullBitmapFetcher::fetch(context, logical, inner.get());
if (nulls != nullptr && !nulls->isEmpty()) {
auto truths = std::make_shared<roaring::Roaring>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,17 +325,19 @@ struct NullableAllCase {
std::array<float, 9> scores {};
};

NullableAllCase nullable_all_case(int mode, const roaring::Roaring& field_nulls) {
NullableAllCase nullable_all_case(int mode, const roaring::Roaring& field_nulls,
const std::wstring& stored_field = L"body",
const std::string& binding_key = {}) {
const auto right_true =
std::make_shared<roaring::Roaring>(roaring::Roaring::bitmapOf(3, 3, 4, 8));
const auto right_null = std::make_shared<roaring::Roaring>(roaring::Roaring::bitmapOf(2, 1, 6));
NullableAllCase result;
result.query = std::make_shared<AllQuery>(L"body", true);
result.query = std::make_shared<AllQuery>(stored_field, true);
constexpr std::array ops {OperatorType::OP_OR, OperatorType::OP_NOT, OperatorType::OP_AND,
OperatorType::OP_OR};
if (mode != 0) {
OperatorBooleanQueryBuilder builder(ops[mode]);
builder.add(result.query);
builder.add(result.query, binding_key);
if (mode != 1) {
builder.add(std::make_shared<BitSetQuery>(right_true, right_null));
}
Expand All @@ -362,8 +364,8 @@ NullableAllCase nullable_all_case(int mode, const roaring::Roaring& field_nulls)
}

void verify_nullable_all_case(const NullableAllCase& query, const QueryExecutionContext& context,
bool scoring, uint32_t target) {
auto scorer = query.query->weight(scoring)->scorer(context);
bool scoring, uint32_t target, const std::string& binding_key = {}) {
auto scorer = query.query->weight(scoring)->scorer(context, binding_key);
const auto observed_nulls = [&]() {
const auto* nulls = scorer->get_null_bitmap();
return nulls == nullptr ? roaring::Roaring() : *nulls;
Expand Down Expand Up @@ -404,6 +406,27 @@ TEST(BooleanTruthContractTest, NullableMatchAllPreservesUnknownThroughNestedOper
}
}

TEST(BooleanTruthContractTest, NullableMatchAllResolvesLogicalFieldFromBinding) {
const auto field_nulls = roaring::Roaring::bitmapOf(3, 1, 4, 7);
FieldNullIterator iterator(field_nulls);
FieldNullResolver resolver(&iterator);
QueryExecutionContext context;
context.segment_num_rows = 9;
context.null_resolver = &resolver;
context.binding_fields["body-key"] = {
.logical_field_name = "body", .stored_field_name = "1", .stored_field_wstr = L"1"};
for (int mode = 0; mode < 4; ++mode) {
const auto query = nullable_all_case(mode, field_nulls, L"1", "body-key");
for (bool scoring : {false, true}) {
for (uint32_t target : {0U, 3U, 8U}) {
SCOPED_TRACE(testing::Message()
<< "scoring=" << scoring << " mode=" << mode << " seek=" << target);
verify_nullable_all_case(query, context, scoring, target, "body-key");
}
}
}
}

struct BinaryTruthFixture {
std::array<TruthRows, 2> leaves;
TruthRows expected;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,12 @@ public QsNode visitBareQuery(SearchParser.BareQueryContext ctx) {
if (ctx.searchValue() == null) {
throw new RuntimeException("Invalid bare query: missing search value");
}
// An unscoped '*' matches every document, even with a default field.
// Inside an explicit field group, '*' is a field-existence leaf.
if ("*".equals(ctx.searchValue().getText())
&& (previousFieldName == null || previousFieldName.isEmpty())) {
return new QsNode(QsClauseType.MATCH_ALL_DOCS, (List<QsNode>) null);
}
QsNode result = visit(ctx.searchValue());
if (result == null) {
throw new RuntimeException("Invalid search value");
Expand Down Expand Up @@ -940,9 +946,9 @@ private QsNode createTermNode(String fieldName, String value) {
}

private QsNode createPrefixNode(String fieldName, String value) {
// Standalone * → MATCH_ALL_DOCS (matches ES behavior: field:* becomes ExistsQuery)
// Field-scoped '*' is an existence predicate, not a fieldless match-all.
if ("*".equals(value)) {
return new QsNode(QsClauseType.MATCH_ALL_DOCS, (List<QsNode>) null);
return new QsNode(QsClauseType.WILDCARD, fieldName, value);
}
return new QsNode(QsClauseType.PREFIX, fieldName, unescapeTermValue(value));
}
Expand Down Expand Up @@ -2400,6 +2406,12 @@ public QsNode visitBareQuery(SearchParser.BareQueryContext ctx) {
if (ctx.searchValue() == null) {
throw new RuntimeException("Invalid bare query: missing search value");
}
// An unscoped '*' matches every document, even with a default field.
// Inside an explicit field group, '*' is a field-existence leaf.
if ("*".equals(ctx.searchValue().getText())
&& (previousFieldName == null || previousFieldName.isEmpty())) {
return new QsNode(QsClauseType.MATCH_ALL_DOCS, (List<QsNode>) null);
}
QsNode result = visit(ctx.searchValue());
if (result == null) {
throw new RuntimeException("Invalid search value");
Expand Down Expand Up @@ -2505,9 +2517,9 @@ public QsNode visitSearchValue(SearchParser.SearchValueContext ctx) {
}
if (ctx.PREFIX() != null) {
String prefixText = ctx.PREFIX().getText();
// Standalone * → MATCH_ALL_DOCS (matches ES behavior: field:* becomes ExistsQuery)
// Field-scoped '*' retains its field for existence evaluation in BE.
if ("*".equals(prefixText)) {
return new QsNode(QsClauseType.MATCH_ALL_DOCS, (List<QsNode>) null);
return new QsNode(QsClauseType.WILDCARD, fieldName, prefixText);
}
return new QsNode(QsClauseType.PREFIX, fieldName, unescapeTermValue(prefixText));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Arrays;

/**
* Unit tests for SearchDslParser
*/
Expand Down Expand Up @@ -2254,6 +2256,22 @@ public void testStandaloneWildcardWithOr() {
Assertions.assertTrue(hasMatchAll, "Should contain MATCH_ALL_DOCS node for '*'");
}

@Test
public void testFieldScopedWildcardRetainsExistenceLeaf() {
for (String mode : Arrays.asList("standard", "lucene")) {
String options = "{\"mode\":\"" + mode + "\",\"default_field\":\"body\"}";
for (String dsl : Arrays.asList("title:*", "title:(*)")) {
QsNode root = SearchDslParser.parseDsl(dsl, options).getRoot();
Assertions.assertEquals(QsClauseType.WILDCARD, root.getType());
Assertions.assertEquals("title", root.getField());
Assertions.assertEquals("*", root.getValue());
Assertions.assertTrue(root.isExplicitField());
}
Assertions.assertEquals(QsClauseType.MATCH_ALL_DOCS,
SearchDslParser.parseDsl("*", options).getRoot().getType());
}
}

// ===== Field-Grouped Query Tests =====
@Test
public void testFieldGroupQuerySimpleOr() {
Expand Down
83 changes: 83 additions & 0 deletions regression-test/data/search/test_search_field_scoped_wildcard.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
-- This file is automatically generated. You should know what you did if you want to edit this
-- !source --
1 alpha \N
2 \N beta
3 gamma delta
4 \N \N

-- !standard_a --
1
3

-- !standard_a_group --
1
3

-- !standard_b --
2
3

-- !standard_b_group --
2
3

-- !standard_both --
3

-- !standard_either --
1
2
3

-- !standard_scoped_or --
1
3

-- !standard_bare_star --
1
2
3
4

-- !standard_scored --
1
3

-- !lucene_a --
1
3

-- !lucene_a_group --
1
3

-- !lucene_b --
2
3

-- !lucene_b_group --
2
3

-- !lucene_both --
3

-- !lucene_either --
1
2
3

-- !lucene_scoped_or --
1
3

-- !lucene_bare_star --
1
2
3
4

-- !lucene_scored --
1
3

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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.

// Preserve explicit wildcard fields and resolve their NULL bitmap through the logical binding.
suite("test_search_field_scoped_wildcard", "p0") {
sql "DROP TABLE IF EXISTS test_search_field_scoped_wildcard"
sql """
CREATE TABLE test_search_field_scoped_wildcard (
id INT,
a STRING,
b STRING,
INDEX idx_a(a) USING INVERTED PROPERTIES("parser"="unicode"),
INDEX idx_b(b) USING INVERTED PROPERTIES("parser"="unicode")
) DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES("replication_num"="1", "inverted_index_storage_format"="V2")
"""
sql """INSERT INTO test_search_field_scoped_wildcard VALUES
(1, 'alpha', NULL), (2, NULL, 'beta'),
(3, 'gamma', 'delta'), (4, NULL, NULL)"""
sql "SYNC"
qt_source "SELECT id, a, b FROM test_search_field_scoped_wildcard ORDER BY id"
["standard", "lucene"].each { mode ->
def options = '{"mode":"' + mode + '","default_field":"a"}'
[a: "a:*", a_group: "a:(*)", b: "b:*", b_group: "b:(*)",
both: "a:* AND b:*", either: "a:* OR b:*",
scoped_or: "a:(* OR nevermatchingtoken)",
bare_star: "* OR b:nevermatchingtoken"].each { label, dsl ->
quickTest("${mode}_${label}", """
SELECT id FROM test_search_field_scoped_wildcard
WHERE SEARCH('${dsl}', '${options}') ORDER BY id
""")
}
quickTest("${mode}_scored", """
SELECT id FROM (
SELECT id, score() AS s FROM test_search_field_scoped_wildcard
WHERE SEARCH('a:*', '${options}') ORDER BY s DESC LIMIT 20
) scored ORDER BY id
""")
}
}
Loading