diff --git a/modules/mapper-extras/src/main/java/org/opensearch/index/mapper/ScaledFloatFieldMapper.java b/modules/mapper-extras/src/main/java/org/opensearch/index/mapper/ScaledFloatFieldMapper.java index 9329d08e03765..87b34eca320e4 100644 --- a/modules/mapper-extras/src/main/java/org/opensearch/index/mapper/ScaledFloatFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/opensearch/index/mapper/ScaledFloatFieldMapper.java @@ -262,7 +262,7 @@ protected FieldTypeCapabilities.Capability searchCapability() { public Query termQuery(Object value, QueryShardContext context) { failIfNotIndexedAndNoDocValues(); long scaledValue = Math.round(scale(value)); - Query query = NumberFieldMapper.NumberType.LONG.termQuery(name(), scaledValue, hasDocValues(), isSearchable()); + Query query = NumberFieldMapper.NumberType.LONG.termQuery(name(), scaledValue, hasDocValues(), isEffectiveSearchable(context)); if (boost() != 1f) { query = new BoostQuery(query, boost()); } @@ -281,7 +281,7 @@ public Query termsQuery(List values, QueryShardContext context) { name(), Collections.unmodifiableList(scaledValues), hasDocValues(), - isSearchable() + isEffectiveSearchable(context) ); if (boost() != 1f) { query = new BoostQuery(query, boost()); @@ -307,7 +307,7 @@ public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower includeLower, includeUpper, hasDocValues(), - isSearchable(), + isEffectiveSearchable(context), context ); if (boost() != 1f) { diff --git a/modules/mapper-extras/src/test/java/org/opensearch/index/mapper/ScaledFloatFieldTypeTests.java b/modules/mapper-extras/src/test/java/org/opensearch/index/mapper/ScaledFloatFieldTypeTests.java index 10ccdc02a0690..91627d73a50fb 100644 --- a/modules/mapper-extras/src/test/java/org/opensearch/index/mapper/ScaledFloatFieldTypeTests.java +++ b/modules/mapper-extras/src/test/java/org/opensearch/index/mapper/ScaledFloatFieldTypeTests.java @@ -51,12 +51,16 @@ import org.opensearch.index.fielddata.IndexNumericFieldData; import org.opensearch.index.fielddata.LeafNumericFieldData; import org.opensearch.index.fielddata.SortedNumericDoubleValues; +import org.opensearch.index.query.QueryShardContext; import org.opensearch.search.approximate.ApproximateScoreQuery; import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class ScaledFloatFieldTypeTests extends FieldTypeTestCase { public void testTermQuery() { @@ -68,7 +72,7 @@ public void testTermQuery() { long scaledValue = Math.round(value * ft.getScalingFactor()); Query dvQuery = SortedNumericDocValuesField.newSlowExactQuery("scaled_float", scaledValue); Query query = new IndexOrDocValuesQuery(LongPoint.newExactQuery("scaled_float", scaledValue), dvQuery); - assertEquals(query, ft.termQuery(value, null)); + assertEquals(query, ft.termQuery(value, MOCK_QSC)); } public void testTermsQuery() { @@ -80,7 +84,10 @@ public void testTermsQuery() { long scaledValue1 = Math.round(value1 * ft.getScalingFactor()); double value2 = (randomDouble() * 2 - 1) * 10000; long scaledValue2 = Math.round(value2 * ft.getScalingFactor()); - assertEquals(LongField.newSetQuery("scaled_float", scaledValue1, scaledValue2), ft.termsQuery(Arrays.asList(value1, value2), null)); + assertEquals( + LongField.newSetQuery("scaled_float", scaledValue1, scaledValue2), + ft.termsQuery(Arrays.asList(value1, value2), MOCK_QSC) + ); } public void testRangeQuery() throws IOException { @@ -343,4 +350,41 @@ public void testLargeNumberIndexingAndQuerying() throws IOException { assertEquals("Terms query should find both documents", 2, searcher.count(termsQuery)); IOUtils.close(reader, dir); } + + /** + * On a pluggable-dataformat index the scaled_float mapper delegates to + * {@code NumberType.LONG} with {@code isEffectiveSearchable(context)} so the point-side of + * {@link IndexOrDocValuesQuery} is dropped. The Lucene secondary writes no BKD on such + * indices; keeping the point side would let the cost-based dispatch inside + * {@link IndexOrDocValuesQuery} pick an empty {@code PointValues} and return zero hits. + */ + public void testTermQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + ScaledFloatFieldMapper.ScaledFloatFieldType ft = new ScaledFloatFieldMapper.ScaledFloatFieldType("scaled_float", 100.0); + Query query = ft.termQuery(13.5, mockPluggableDataFormatContext()); + Query expected = SortedNumericDocValuesField.newSlowRangeQuery("scaled_float", 1350L, 1350L); + assertEquals(expected, query); + } + + /** Terms queries route to the DV-only branch on pluggable-dataformat indices. */ + public void testTermsQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + ScaledFloatFieldMapper.ScaledFloatFieldType ft = new ScaledFloatFieldMapper.ScaledFloatFieldType("scaled_float", 100.0); + Query query = ft.termsQuery(Arrays.asList(1.0, 2.0), mockPluggableDataFormatContext()); + Query expected = SortedNumericDocValuesField.newSlowSetQuery("scaled_float", 100L, 200L); + assertEquals(expected, query); + } + + /** Range queries route to the DV-only branch on pluggable-dataformat indices. */ + public void testRangeQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + ScaledFloatFieldMapper.ScaledFloatFieldType ft = new ScaledFloatFieldMapper.ScaledFloatFieldType("scaled_float", 100.0); + Query query = ft.rangeQuery(1.0, 10.0, true, true, mockPluggableDataFormatContext()); + Query expected = SortedNumericDocValuesField.newSlowRangeQuery("scaled_float", 100L, 1000L); + assertEquals(expected, query); + } + + /** A {@link QueryShardContext} mock that reports the pluggable-dataformat gate as enabled. */ + private static QueryShardContext mockPluggableDataFormatContext() { + QueryShardContext ctx = mock(QueryShardContext.class); + when(ctx.isPluggableDataFormatEnabled()).thenReturn(true); + return ctx; + } } diff --git a/server/src/main/java/org/opensearch/index/mapper/BooleanFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/BooleanFieldMapper.java index 93c940ab4bbb5..472ab432c6104 100644 --- a/server/src/main/java/org/opensearch/index/mapper/BooleanFieldMapper.java +++ b/server/src/main/java/org/opensearch/index/mapper/BooleanFieldMapper.java @@ -270,9 +270,10 @@ public DocValueFormat docValueFormat(@Nullable String format, ZoneId timeZone) { @Override public Query termQuery(Object value, QueryShardContext context) { failIfNotIndexedAndNoDocValues(); - if (!isSearchable()) { + if (!isEffectiveSearchable(context)) { return SortedNumericDocValuesField.newSlowExactQuery(name(), Values.TRUE.bytesEquals(indexedValueForSearch(value)) ? 1 : 0); } + Query query = new TermQuery(new Term(name(), indexedValueForSearch(value))); if (boost() != 1f) { query = new BoostQuery(query, boost()); diff --git a/server/src/main/java/org/opensearch/index/mapper/DateFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/DateFieldMapper.java index 1448a362d92bf..9e591699807e0 100644 --- a/server/src/main/java/org/opensearch/index/mapper/DateFieldMapper.java +++ b/server/src/main/java/org/opensearch/index/mapper/DateFieldMapper.java @@ -538,8 +538,7 @@ public Query rangeQuery( (l, u, nowUsed) -> { Query dvQuery = hasDocValues() ? SortedNumericDocValuesField.newSlowRangeQuery(name(), l, u) : null; - // Not searchable. Must have doc values. - if (!isSearchable()) { + if (!isEffectiveSearchable(context)) { if (context.indexSortedOnField(name())) { dvQuery = new IndexSortSortedNumericDocValuesRangeQuery(name(), l, u, dvQuery); } diff --git a/server/src/main/java/org/opensearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/IpFieldMapper.java index f51a2c8ea238c..54b5d8f0ba52b 100644 --- a/server/src/main/java/org/opensearch/index/mapper/IpFieldMapper.java +++ b/server/src/main/java/org/opensearch/index/mapper/IpFieldMapper.java @@ -302,10 +302,11 @@ public Query termQuery(Object value, @Nullable QueryShardContext context) { true ); } - if (isSearchable() && hasDocValues()) { + boolean effectiveSearchable = isEffectiveSearchable(context); + if (effectiveSearchable && hasDocValues()) { return new IndexOrDocValuesQuery(pointQuery, dvQuery); } else { - return isSearchable() ? pointQuery : dvQuery; + return effectiveSearchable ? pointQuery : dvQuery; } } @@ -317,7 +318,7 @@ public Query termsQuery(List values, QueryShardContext context) { List masks = new ArrayList<>(); parseIps(values, concreteIPs, masks); - if (!isSearchable()) { + if (!isEffectiveSearchable(context)) { return hasDocValues() ? docValuesTermsQuery(concreteIPs, masks) : new MatchNoDocsQuery("never happened"); } @@ -440,10 +441,11 @@ public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower true ); } - if (isSearchable() && hasDocValues()) { + boolean effectiveSearchable = isEffectiveSearchable(context); + if (effectiveSearchable && hasDocValues()) { return new IndexOrDocValuesQuery(pointQuery, dvQuery); } else { - return isSearchable() ? pointQuery : dvQuery; + return effectiveSearchable ? pointQuery : dvQuery; } }); } diff --git a/server/src/main/java/org/opensearch/index/mapper/MappedFieldType.java b/server/src/main/java/org/opensearch/index/mapper/MappedFieldType.java index 6310a7f270fb5..14041d136732d 100644 --- a/server/src/main/java/org/opensearch/index/mapper/MappedFieldType.java +++ b/server/src/main/java/org/opensearch/index/mapper/MappedFieldType.java @@ -190,6 +190,22 @@ public boolean isSearchable() { return isIndexed; } + /** + * Returns whether this numeric field should be advertised as searchable to the + * per-type query factory. On indices backed by a pluggable dataformat (composite + * primary + Lucene secondary), the Lucene secondary writes no BKD for numeric + * fields, so the point-side of {@link org.apache.lucene.search.IndexOrDocValuesQuery} + * reports {@code cost=0} and wins the cost race — returning zero hits. Routing + * through the pure doc-values branch (by treating the field as not-searchable) + * avoids that trap and executes correctly against the codec-served DV column. + * + *

Non-pluggable-dataformat indices retain their normal {@link #isSearchable()} + * behavior so the BKD fast path is preserved. + */ + public boolean isEffectiveSearchable(QueryShardContext context) { + return isSearchable() && !context.isPluggableDataFormatEnabled(); + } + /** * Returns true if the field is stored separately. */ diff --git a/server/src/main/java/org/opensearch/index/mapper/NumberFieldMapper.java b/server/src/main/java/org/opensearch/index/mapper/NumberFieldMapper.java index 5dff164b9532f..cbdf162d64ebd 100644 --- a/server/src/main/java/org/opensearch/index/mapper/NumberFieldMapper.java +++ b/server/src/main/java/org/opensearch/index/mapper/NumberFieldMapper.java @@ -1989,7 +1989,7 @@ public NumericType numericType() { @Override public Query termQuery(Object value, QueryShardContext context) { failIfNotIndexedAndNoDocValues(); - Query query = type.termQuery(name(), value, hasDocValues(), isSearchable()); + Query query = type.termQuery(name(), value, hasDocValues(), isEffectiveSearchable(context)); if (boost() != 1f) { query = new BoostQuery(query, boost()); } @@ -1999,16 +1999,16 @@ public Query termQuery(Object value, QueryShardContext context) { @Override public Query termsQuery(List values, QueryShardContext context) { failIfNotIndexedAndNoDocValues(); - Query query = type.termsQuery(name(), values, hasDocValues(), isSearchable()); + Query query = type.termsQuery(name(), values, hasDocValues(), isEffectiveSearchable(context)); if (boost() != 1f) { query = new BoostQuery(query, boost()); } return query; } - public Query bitmapQuery(BytesArray bitmap) { + public Query bitmapQuery(BytesArray bitmap, QueryShardContext context) { failIfNotIndexedAndNoDocValues(); - return type.bitmapQuery(name(), bitmap, isSearchable(), hasDocValues()); + return type.bitmapQuery(name(), bitmap, isEffectiveSearchable(context), hasDocValues()); } @Override @@ -2021,7 +2021,7 @@ public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower includeLower, includeUpper, hasDocValues(), - isSearchable(), + isEffectiveSearchable(context), context ); if (boost() != 1f) { diff --git a/server/src/main/java/org/opensearch/index/query/QueryShardContext.java b/server/src/main/java/org/opensearch/index/query/QueryShardContext.java index f2c278f04b021..e9a152c9bb36a 100644 --- a/server/src/main/java/org/opensearch/index/query/QueryShardContext.java +++ b/server/src/main/java/org/opensearch/index/query/QueryShardContext.java @@ -691,6 +691,17 @@ public IndexSettings getIndexSettings() { return indexSettings; } + /** + * Null-safe check for the pluggable-dataformat gate. Returns {@code true} only when this + * context has index-scoped settings and the setting is enabled. Callers use this to route + * query construction (e.g. numeric range/term/bitmap) through the pure doc-values branch + * on pluggable-dataformat indices, where the Lucene secondary writes no BKD for numeric + * fields. + */ + public boolean isPluggableDataFormatEnabled() { + return indexSettings != null && indexSettings.isPluggableDataFormatEnabled(); + } + /** * Return the MapperService. */ diff --git a/server/src/main/java/org/opensearch/index/query/TermsQueryBuilder.java b/server/src/main/java/org/opensearch/index/query/TermsQueryBuilder.java index 41e436f2ed59d..9cd06c280bcce 100644 --- a/server/src/main/java/org/opensearch/index/query/TermsQueryBuilder.java +++ b/server/src/main/java/org/opensearch/index/query/TermsQueryBuilder.java @@ -565,7 +565,7 @@ protected Query doToQuery(QueryShardContext context) throws IOException { && values.size() == 1 && values.get(0) instanceof BytesArray bytesArray && fieldType.unwrap() instanceof NumberFieldMapper.NumberFieldType numberFieldType) { - return numberFieldType.bitmapQuery(bytesArray); + return numberFieldType.bitmapQuery(bytesArray, context); } return fieldType.termsQuery(values, context); } diff --git a/server/src/test/java/org/opensearch/index/mapper/BooleanFieldTypeTests.java b/server/src/test/java/org/opensearch/index/mapper/BooleanFieldTypeTests.java index 2ddec0c628bac..94a8860a675a7 100644 --- a/server/src/test/java/org/opensearch/index/mapper/BooleanFieldTypeTests.java +++ b/server/src/test/java/org/opensearch/index/mapper/BooleanFieldTypeTests.java @@ -38,14 +38,25 @@ import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.TermQuery; import org.apache.lucene.util.BytesRef; +import org.opensearch.index.query.QueryShardContext; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class BooleanFieldTypeTests extends FieldTypeTestCase { + /** Pluggable-dataformat context — for tests that exercise the isEffectiveSearchable gate. */ + private static QueryShardContext pluggableContext() { + QueryShardContext ctx = mock(QueryShardContext.class); + when(ctx.isPluggableDataFormatEnabled()).thenReturn(true); + return ctx; + } + public void testValueFormat() { MappedFieldType ft = new BooleanFieldMapper.BooleanFieldType("field"); assertEquals(false, ft.docValueFormat(null, null).format(0)); @@ -63,68 +74,83 @@ public void testValueForSearch() { public void testTermQuery() { MappedFieldType ft = new BooleanFieldMapper.BooleanFieldType("field"); - assertEquals(new TermQuery(new Term("field", "T")), ft.termQuery("true", null)); - assertEquals(new TermQuery(new Term("field", "F")), ft.termQuery("false", null)); + assertEquals(new TermQuery(new Term("field", "T")), ft.termQuery("true", MOCK_QSC)); + assertEquals(new TermQuery(new Term("field", "F")), ft.termQuery("false", MOCK_QSC)); MappedFieldType doc_ft = new BooleanFieldMapper.BooleanFieldType("field", false, true); - assertEquals(SortedNumericDocValuesField.newSlowExactQuery("field", 1), doc_ft.termQuery("true", null)); - assertEquals(SortedNumericDocValuesField.newSlowExactQuery("field", 0), doc_ft.termQuery("false", null)); + assertEquals(SortedNumericDocValuesField.newSlowExactQuery("field", 1), doc_ft.termQuery("true", MOCK_QSC)); + assertEquals(SortedNumericDocValuesField.newSlowExactQuery("field", 0), doc_ft.termQuery("false", MOCK_QSC)); MappedFieldType boost_ft = new BooleanFieldMapper.BooleanFieldType("field"); boost_ft.setBoost(2f); - assertEquals(new BoostQuery(new TermQuery(new Term("field", "T")), 2f), boost_ft.termQuery("true", null)); - assertEquals(new BoostQuery(new TermQuery(new Term("field", "F")), 2f), boost_ft.termQuery("false", null)); + assertEquals(new BoostQuery(new TermQuery(new Term("field", "T")), 2f), boost_ft.termQuery("true", MOCK_QSC)); + assertEquals(new BoostQuery(new TermQuery(new Term("field", "F")), 2f), boost_ft.termQuery("false", MOCK_QSC)); MappedFieldType unsearchable = new BooleanFieldMapper.BooleanFieldType("field", false, false); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> unsearchable.termQuery("true", null)); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> unsearchable.termQuery("true", MOCK_QSC)); assertEquals("Cannot search on field [field] since it is both not indexed, and does not have doc_values enabled.", e.getMessage()); } + /** + * On a pluggable-dataformat index the boolean mapper must route to the doc-values exact + * query rather than a postings {@code TermQuery}. The Lucene secondary writes no postings + * for boolean fields on such indices, so a {@code TermQuery} would return zero hits. + */ + public void testTermQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + MappedFieldType ft = new BooleanFieldMapper.BooleanFieldType("field"); + QueryShardContext pluggable = pluggableContext(); + assertEquals(SortedNumericDocValuesField.newSlowExactQuery("field", 1), ft.termQuery("true", pluggable)); + assertEquals(SortedNumericDocValuesField.newSlowExactQuery("field", 0), ft.termQuery("false", pluggable)); + } + public void testTermsQuery() { MappedFieldType ft = new BooleanFieldMapper.BooleanFieldType("field"); List terms = new ArrayList<>(); terms.add(new BytesRef("true")); terms.add(new BytesRef("false")); - assertEquals(new FieldExistsQuery("field"), ft.termsQuery(terms, null)); + assertEquals(new FieldExistsQuery("field"), ft.termsQuery(terms, MOCK_QSC)); List newTerms = new ArrayList<>(); newTerms.add(new BytesRef("true")); - assertEquals(new TermQuery(new Term("field", "T")), ft.termsQuery(newTerms, null)); + assertEquals(new TermQuery(new Term("field", "T")), ft.termsQuery(newTerms, MOCK_QSC)); List incorrectTerms = new ArrayList<>(); incorrectTerms.add(new BytesRef("true")); incorrectTerms.add(new BytesRef("random")); - IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> ft.termsQuery(incorrectTerms, null)); + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> ft.termsQuery(incorrectTerms, MOCK_QSC)); assertEquals("Can't parse boolean value [random], expected [true] or [false]", ex.getMessage()); MappedFieldType doc_only_ft = new BooleanFieldMapper.BooleanFieldType("field", false, true); - assertEquals(SortedNumericDocValuesField.newSlowExactQuery("field", 1), doc_only_ft.termsQuery(newTerms, null)); + assertEquals(SortedNumericDocValuesField.newSlowExactQuery("field", 1), doc_only_ft.termsQuery(newTerms, MOCK_QSC)); MappedFieldType unsearchable = new BooleanFieldMapper.BooleanFieldType("field", false, false); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> unsearchable.termsQuery(terms, null)); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> unsearchable.termsQuery(terms, MOCK_QSC)); assertEquals("Cannot search on field [field] since it is both not indexed, and does not have doc_values enabled.", e.getMessage()); } public void testRangeQuery() { BooleanFieldMapper.BooleanFieldType ft = new BooleanFieldMapper.BooleanFieldType("field"); - assertEquals(new FieldExistsQuery("field"), ft.rangeQuery(false, true, true, true, null)); + assertEquals(new FieldExistsQuery("field"), ft.rangeQuery(false, true, true, true, MOCK_QSC)); - assertEquals(new TermQuery(new Term("field", "T")), ft.rangeQuery(false, true, false, true, null)); + assertEquals(new TermQuery(new Term("field", "T")), ft.rangeQuery(false, true, false, true, MOCK_QSC)); - assertEquals(new TermQuery(new Term("field", "F")), ft.rangeQuery(false, true, true, false, null)); + assertEquals(new TermQuery(new Term("field", "F")), ft.rangeQuery(false, true, true, false, MOCK_QSC)); - assertEquals(new MatchNoDocsQuery(), ft.rangeQuery(false, true, false, false, null)); + assertEquals(new MatchNoDocsQuery(), ft.rangeQuery(false, true, false, false, MOCK_QSC)); - assertEquals(new MatchNoDocsQuery(), ft.rangeQuery(false, true, false, false, null)); + assertEquals(new MatchNoDocsQuery(), ft.rangeQuery(false, true, false, false, MOCK_QSC)); - assertEquals(new TermQuery(new Term("field", "F")), ft.rangeQuery(false, false, true, true, null)); + assertEquals(new TermQuery(new Term("field", "F")), ft.rangeQuery(false, false, true, true, MOCK_QSC)); - assertEquals(new TermQuery(new Term("field", "F")), ft.rangeQuery(null, false, true, true, null)); + assertEquals(new TermQuery(new Term("field", "F")), ft.rangeQuery(null, false, true, true, MOCK_QSC)); - assertEquals(new FieldExistsQuery("field"), ft.rangeQuery(false, null, true, true, null)); + assertEquals(new FieldExistsQuery("field"), ft.rangeQuery(false, null, true, true, MOCK_QSC)); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> ft.rangeQuery("random", null, true, true, null)); + IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> ft.rangeQuery("random", null, true, true, MOCK_QSC) + ); assertEquals("Can't parse boolean value [random], expected [true] or [false]", e.getMessage()); } diff --git a/server/src/test/java/org/opensearch/index/mapper/DateFieldTypeTests.java b/server/src/test/java/org/opensearch/index/mapper/DateFieldTypeTests.java index b436f8a8a8ecd..fa8995633b297 100644 --- a/server/src/test/java/org/opensearch/index/mapper/DateFieldTypeTests.java +++ b/server/src/test/java/org/opensearch/index/mapper/DateFieldTypeTests.java @@ -92,6 +92,9 @@ import java.util.Locale; import static org.apache.lucene.document.LongPoint.pack; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class DateFieldTypeTests extends FieldTypeTestCase { @@ -654,4 +657,48 @@ public void testDateFieldTypeWithNulls() throws IOException { } IOUtils.close(reader, w, dir); } + + /** + * On a pluggable-dataformat index the mapper must skip the point-based query construction + * for date ranges and emit only the doc-values range. The Lucene secondary writes no BKD + * on such indices, so keeping the point side would let the cost-based dispatch inside + * {@link IndexOrDocValuesQuery} pick an empty {@code PointValues} and return zero hits. + */ + public void testRangeQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + DateFieldType ft = new DateFieldType("field"); + String date1 = "2015-10-12T14:10:55"; + String date2 = "2016-04-28T11:33:52"; + long instant1 = DateFormatters.from(DateFieldMapper.getDefaultDateTimeFormatter().parse(date1)).toInstant().toEpochMilli(); + long instant2 = DateFormatters.from(DateFieldMapper.getDefaultDateTimeFormatter().parse(date2)).toInstant().toEpochMilli() + 999; + Query expected = SortedNumericDocValuesField.newSlowRangeQuery("field", instant1, instant2); + Query actual = ft.rangeQuery(date1, date2, true, true, null, null, null, mockPluggableDataFormatContext()); + assertEquals(expected, actual); + } + + /** + * Term queries on a date field route through the same lambda as range queries, so the same + * gate applies: on a pluggable-dataformat index the point path must not be built. + */ + public void testTermQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + DateFieldType ft = new DateFieldType("field"); + String date = "2015-10-12T14:10:55"; + long instant = DateFormatters.from(DateFieldMapper.getDefaultDateTimeFormatter().parse(date)).toInstant().toEpochMilli(); + long lower = instant; + long upper = DateFormatters.from(DateFieldMapper.getDefaultDateTimeFormatter().parse(date)).toInstant().toEpochMilli() + 999; + Query expected = SortedNumericDocValuesField.newSlowRangeQuery("field", lower, upper); + Query actual = ft.termQuery(date, mockPluggableDataFormatContext()); + assertEquals(expected, actual); + } + + /** + * A {@link QueryShardContext} mock that reports the pluggable-dataformat gate as enabled. + * Simulates a Mustang-backed index without needing the real feature-flag / IndexSettings + * plumbing — the mapper code only cares about the boolean returned here. + */ + private static QueryShardContext mockPluggableDataFormatContext() { + QueryShardContext ctx = mock(QueryShardContext.class); + when(ctx.isPluggableDataFormatEnabled()).thenReturn(true); + when(ctx.indexSortedOnField(anyString())).thenReturn(false); + return ctx; + } } diff --git a/server/src/test/java/org/opensearch/index/mapper/IpFieldTypeTests.java b/server/src/test/java/org/opensearch/index/mapper/IpFieldTypeTests.java index d11d32cf65bc1..45f3c6250e3de 100644 --- a/server/src/test/java/org/opensearch/index/mapper/IpFieldTypeTests.java +++ b/server/src/test/java/org/opensearch/index/mapper/IpFieldTypeTests.java @@ -46,6 +46,7 @@ import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.common.network.InetAddresses; import org.opensearch.common.settings.Settings; +import org.opensearch.index.query.QueryShardContext; import java.io.IOException; import java.net.InetAddress; @@ -54,6 +55,9 @@ import java.util.List; import java.util.Objects; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class IpFieldTypeTests extends FieldTypeTestCase { public void testValueFormat() throws Exception { @@ -90,7 +94,7 @@ public void testTermQuery() { query, SortedSetDocValuesField.newSlowExactQuery("field", new BytesRef(((PointRangeQuery) query).getLowerPoint())) ), - ft.termQuery(ip, null) + ft.termQuery(ip, MOCK_QSC) ); ip = "192.168.1.7"; @@ -100,7 +104,7 @@ public void testTermQuery() { query, SortedSetDocValuesField.newSlowExactQuery("field", new BytesRef(((PointRangeQuery) query).getLowerPoint())) ), - ft.termQuery(ip, null) + ft.termQuery(ip, MOCK_QSC) ); ip = "2001:db8::2:1"; @@ -118,7 +122,7 @@ public void testTermQuery() { true ) ), - ft.termQuery(prefix, null) + ft.termQuery(prefix, MOCK_QSC) ); ip = "192.168.1.7"; @@ -135,11 +139,11 @@ public void testTermQuery() { true ) ), - ft.termQuery(prefix, null) + ft.termQuery(prefix, MOCK_QSC) ); MappedFieldType unsearchable = new IpFieldMapper.IpFieldType("field", false, false, false, null, Collections.emptyMap()); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> unsearchable.termQuery("::1", null)); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> unsearchable.termQuery("::1", MOCK_QSC)); assertEquals("Cannot search on field [field] since it is both not indexed, and does not have doc_values enabled.", e.getMessage()); } @@ -151,14 +155,14 @@ public void testDvOnlyTermQuery() { assertEquals( SortedSetDocValuesField.newSlowExactQuery("field", new BytesRef(((PointRangeQuery) query).getLowerPoint())), - dvOnly.termQuery(ip, null) + dvOnly.termQuery(ip, MOCK_QSC) ); ip = "192.168.1.7"; query = InetAddressPoint.newExactQuery("field", InetAddresses.forString(ip)); assertEquals( SortedSetDocValuesField.newSlowExactQuery("field", new BytesRef(((PointRangeQuery) query).getLowerPoint())), - dvOnly.termQuery(ip, null) + dvOnly.termQuery(ip, MOCK_QSC) ); ip = "2001:db8::2:1"; @@ -172,7 +176,7 @@ public void testDvOnlyTermQuery() { true, true ), - dvOnly.termQuery(prefix, null) + dvOnly.termQuery(prefix, MOCK_QSC) ); ip = "192.168.1.7"; @@ -185,7 +189,7 @@ public void testDvOnlyTermQuery() { true, true ), - dvOnly.termQuery(prefix, null) + dvOnly.termQuery(prefix, MOCK_QSC) ); } @@ -198,15 +202,15 @@ public void testTermsQuery() { assertEquals( InetAddressPoint.newSetQuery("field", InetAddresses.forString("::2"), InetAddresses.forString("::5")), - ft.termsQuery(Arrays.asList(InetAddresses.forString("::2"), InetAddresses.forString("::5")), null) + ft.termsQuery(Arrays.asList(InetAddresses.forString("::2"), InetAddresses.forString("::5")), MOCK_QSC) ); assertEquals( InetAddressPoint.newSetQuery("field", InetAddresses.forString("::2"), InetAddresses.forString("::5")), - ft.termsQuery(Arrays.asList("::2", "::5"), null) + ft.termsQuery(Arrays.asList("::2", "::5"), MOCK_QSC) ); // if the list includes a prefix query we fallback to a bool query - Query actual = ft.termsQuery(Arrays.asList("::42", "::2/16"), null); + Query actual = ft.termsQuery(Arrays.asList("::42", "::2/16"), MOCK_QSC); assertTrue(actual instanceof ConstantScoreQuery); assertTrue(((ConstantScoreQuery) actual).getQuery() instanceof BooleanQuery); BooleanQuery bq = (BooleanQuery) ((ConstantScoreQuery) actual).getQuery(); @@ -219,13 +223,13 @@ public void testDvOnlyTermsQuery() { assertEquals( SortedSetDocValuesField.newSlowSetQuery("field", List.of(ipToByteRef("::2"), ipToByteRef("::5"))), - dvOnly.termsQuery(Arrays.asList(InetAddresses.forString("::2"), InetAddresses.forString("::5")), null) + dvOnly.termsQuery(Arrays.asList(InetAddresses.forString("::2"), InetAddresses.forString("::5")), MOCK_QSC) ); assertEquals( SortedSetDocValuesField.newSlowSetQuery("field", List.of(ipToByteRef("::2"), ipToByteRef("::5"))), - dvOnly.termsQuery(Arrays.asList("::2", "::5"), null) + dvOnly.termsQuery(Arrays.asList("::2", "::5"), MOCK_QSC) ); - assertEquals(SortedSetDocValuesField.newSlowExactQuery("field", ipToByteRef("::2")), dvOnly.termsQuery(List.of("::2"), null)); + assertEquals(SortedSetDocValuesField.newSlowExactQuery("field", ipToByteRef("::2")), dvOnly.termsQuery(List.of("::2"), MOCK_QSC)); assertEquals( SortedSetDocValuesField.newSlowRangeQuery( "field", @@ -234,24 +238,28 @@ public void testDvOnlyTermsQuery() { true, true ), - dvOnly.termsQuery(List.of("::2/16"), null) + dvOnly.termsQuery(List.of("::2/16"), MOCK_QSC) ); // multirange handles both DocValuesMultiRangeQuery.SortedSetStabbingBuilder expect = new DocValuesMultiRangeQuery.SortedSetStabbingBuilder("field"); expect.add(ipToByteRef("::42")); expect.add(ipToByteRef("::"), ipToByteRef("::ffff:ffff:ffff:ffff:ffff:ffff:ffff")); - assertEquals(expect.build(), dvOnly.termsQuery(Arrays.asList("::42", "::2/16"), null)); + assertEquals(expect.build(), dvOnly.termsQuery(Arrays.asList("::42", "::2/16"), MOCK_QSC)); } public void testDvVsPoint() { MappedFieldType indexOnly = new IpFieldMapper.IpFieldType("field", true, false, false, null, Collections.emptyMap()); MappedFieldType dvOnly = new IpFieldMapper.IpFieldType("field", false, false, true, null, Collections.emptyMap()); MappedFieldType indexDv = new IpFieldMapper.IpFieldType("field", true, false, true, null, Collections.emptyMap()); - assertNotEquals("obey DocValues", indexOnly.termsQuery(List.of("::2/16"), null), indexDv.termsQuery(List.of("::2/16"), null)); - assertEquals(dvOnly.termQuery("::2/16", null), dvOnly.termsQuery(List.of("::2/16"), null)); + assertNotEquals( + "obey DocValues", + indexOnly.termsQuery(List.of("::2/16"), MOCK_QSC), + indexDv.termsQuery(List.of("::2/16"), MOCK_QSC) + ); + assertEquals(dvOnly.termQuery("::2/16", MOCK_QSC), dvOnly.termsQuery(List.of("::2/16"), MOCK_QSC)); assertEquals( - new IndexOrDocValuesQuery(indexOnly.termsQuery(List.of("::2/16"), null), dvOnly.termsQuery(List.of("::2/16"), null)), - indexDv.termsQuery(List.of("::2/16"), null) + new IndexOrDocValuesQuery(indexOnly.termsQuery(List.of("::2/16"), MOCK_QSC), dvOnly.termsQuery(List.of("::2/16"), MOCK_QSC)), + indexDv.termsQuery(List.of("::2/16"), MOCK_QSC) ); } @@ -269,7 +277,7 @@ public void testRangeQuery() { true ) ), - ft.rangeQuery(null, null, randomBoolean(), randomBoolean(), null, null, null, null) + ft.rangeQuery(null, null, randomBoolean(), randomBoolean(), null, null, null, MOCK_QSC) ); query = InetAddressPoint.newRangeQuery("field", InetAddresses.forString("::"), InetAddresses.forString("192.168.2.0")); @@ -284,7 +292,7 @@ public void testRangeQuery() { true ) ), - ft.rangeQuery(null, "192.168.2.0", randomBoolean(), true, null, null, null, null) + ft.rangeQuery(null, "192.168.2.0", randomBoolean(), true, null, null, null, MOCK_QSC) ); query = InetAddressPoint.newRangeQuery("field", InetAddresses.forString("::"), InetAddresses.forString("192.168.1.255")); @@ -299,7 +307,7 @@ public void testRangeQuery() { true ) ), - ft.rangeQuery(null, "192.168.2.0", randomBoolean(), false, null, null, null, null) + ft.rangeQuery(null, "192.168.2.0", randomBoolean(), false, null, null, null, MOCK_QSC) ); query = InetAddressPoint.newRangeQuery("field", InetAddresses.forString("2001:db8::"), InetAddressPoint.MAX_VALUE); @@ -314,7 +322,7 @@ public void testRangeQuery() { true ) ), - ft.rangeQuery("2001:db8::", null, true, randomBoolean(), null, null, null, null) + ft.rangeQuery("2001:db8::", null, true, randomBoolean(), null, null, null, MOCK_QSC) ); query = InetAddressPoint.newRangeQuery("field", InetAddresses.forString("2001:db8::1"), InetAddressPoint.MAX_VALUE); @@ -329,7 +337,7 @@ public void testRangeQuery() { true ) ), - ft.rangeQuery("2001:db8::", null, false, randomBoolean(), null, null, null, null) + ft.rangeQuery("2001:db8::", null, false, randomBoolean(), null, null, null, MOCK_QSC) ); query = InetAddressPoint.newRangeQuery("field", InetAddresses.forString("2001:db8::"), InetAddresses.forString("2001:db8::ffff")); @@ -344,7 +352,7 @@ public void testRangeQuery() { true ) ), - ft.rangeQuery("2001:db8::", "2001:db8::ffff", true, true, null, null, null, null) + ft.rangeQuery("2001:db8::", "2001:db8::ffff", true, true, null, null, null, MOCK_QSC) ); query = InetAddressPoint.newRangeQuery("field", InetAddresses.forString("2001:db8::1"), InetAddresses.forString("2001:db8::fffe")); @@ -359,7 +367,7 @@ public void testRangeQuery() { true ) ), - ft.rangeQuery("2001:db8::", "2001:db8::ffff", false, false, null, null, null, null) + ft.rangeQuery("2001:db8::", "2001:db8::ffff", false, false, null, null, null, MOCK_QSC) ); query = InetAddressPoint.newRangeQuery("field", InetAddresses.forString("2001:db8::2"), InetAddresses.forString("2001:db8::")); @@ -375,11 +383,11 @@ public void testRangeQuery() { ) ), // same lo/hi values but inclusive=false so this won't match anything - ft.rangeQuery("2001:db8::1", "2001:db8::1", false, false, null, null, null, null) + ft.rangeQuery("2001:db8::1", "2001:db8::1", false, false, null, null, null, MOCK_QSC) ); // Upper bound is the min IP and is not inclusive - assertEquals(new MatchNoDocsQuery(), ft.rangeQuery("::", "::", true, false, null, null, null, null)); + assertEquals(new MatchNoDocsQuery(), ft.rangeQuery("::", "::", true, false, null, null, null, MOCK_QSC)); // Lower bound is the max IP and is not inclusive assertEquals( @@ -392,7 +400,7 @@ public void testRangeQuery() { null, null, null, - null + MOCK_QSC ) ); @@ -409,7 +417,7 @@ public void testRangeQuery() { ) ), // same lo/hi values but inclusive=false so this won't match anything - ft.rangeQuery("::", "0.0.0.0", true, false, null, null, null, null) + ft.rangeQuery("::", "0.0.0.0", true, false, null, null, null, MOCK_QSC) ); query = InetAddressPoint.newRangeQuery("field", InetAddresses.forString("::1:0:0:0"), InetAddressPoint.MAX_VALUE); @@ -425,7 +433,7 @@ public void testRangeQuery() { ) ), // same lo/hi values but inclusive=false so this won't match anything - ft.rangeQuery("255.255.255.255", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false, true, null, null, null, null) + ft.rangeQuery("255.255.255.255", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false, true, null, null, null, MOCK_QSC) ); query = InetAddressPoint.newRangeQuery("field", InetAddresses.forString("192.168.1.7"), InetAddresses.forString("2001:db8::")); @@ -441,13 +449,13 @@ public void testRangeQuery() { true ) ), - ft.rangeQuery("::ffff:c0a8:107", "2001:db8::", true, true, null, null, null, null) + ft.rangeQuery("::ffff:c0a8:107", "2001:db8::", true, true, null, null, null, MOCK_QSC) ); MappedFieldType unsearchable = new IpFieldMapper.IpFieldType("field", false, false, false, null, Collections.emptyMap()); IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> unsearchable.rangeQuery("::1", "2001::", true, true, null, null, null, null) + () -> unsearchable.rangeQuery("::1", "2001::", true, true, null, null, null, MOCK_QSC) ); assertEquals("Cannot search on field [field] since it is both not indexed, and does not have doc_values enabled.", e.getMessage()); } @@ -466,4 +474,37 @@ public void testFetchSourceValue() throws IOException { .fieldType(); assertEquals(Collections.singletonList("2001:db8::2:7"), fetchSourceValue(nullValueMapper, null)); } + + /** + * On a pluggable-dataformat index the mapper must skip the point-based query construction + * and emit only the doc-values term query. The Lucene secondary writes no BKD on such + * indices, so keeping the point side would let the cost-based dispatch inside + * {@link IndexOrDocValuesQuery} pick an empty {@code PointValues} and return zero hits. + */ + public void testTermQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + MappedFieldType ft = new IpFieldMapper.IpFieldType("field"); + Query query = ft.termQuery("192.168.1.1", mockPluggableDataFormatContext()); + assertFalse("term path must not wrap points in IndexOrDocValuesQuery on pluggable", query instanceof IndexOrDocValuesQuery); + } + + /** Terms queries route to the DV-only branch on pluggable-dataformat indices. */ + public void testTermsQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + MappedFieldType ft = new IpFieldMapper.IpFieldType("field"); + Query query = ft.termsQuery(List.of("192.168.1.1"), mockPluggableDataFormatContext()); + assertFalse("terms path must not wrap points in IndexOrDocValuesQuery on pluggable", query instanceof IndexOrDocValuesQuery); + } + + /** Range queries route to the DV-only branch on pluggable-dataformat indices. */ + public void testRangeQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + MappedFieldType ft = new IpFieldMapper.IpFieldType("field"); + Query query = ft.rangeQuery("10.0.0.0", "10.255.255.255", true, true, null, null, null, mockPluggableDataFormatContext()); + assertFalse("range path must not wrap points in IndexOrDocValuesQuery on pluggable", query instanceof IndexOrDocValuesQuery); + } + + /** Pluggable-dataformat context — for tests that exercise the isEffectiveSearchable gate. */ + private static QueryShardContext mockPluggableDataFormatContext() { + QueryShardContext ctx = mock(QueryShardContext.class); + when(ctx.isPluggableDataFormatEnabled()).thenReturn(true); + return ctx; + } } diff --git a/server/src/test/java/org/opensearch/index/mapper/NumberFieldTypeTests.java b/server/src/test/java/org/opensearch/index/mapper/NumberFieldTypeTests.java index ced21855038bf..fadb8a251bc8b 100644 --- a/server/src/test/java/org/opensearch/index/mapper/NumberFieldTypeTests.java +++ b/server/src/test/java/org/opensearch/index/mapper/NumberFieldTypeTests.java @@ -104,6 +104,8 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.apache.lucene.document.LongPoint.pack; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class NumberFieldTypeTests extends FieldTypeTestCase { @@ -137,46 +139,46 @@ public void testIntegerTermsQueryWithDecimalPart() { MappedFieldType ft = new NumberFieldMapper.NumberFieldType("field", NumberType.INTEGER); assertEquals( new IndexOrDocValuesQuery(IntPoint.newSetQuery("field", 1), SortedNumericDocValuesField.newSlowSetQuery("field", 1)), - ft.termsQuery(Arrays.asList(1, 2.1), null) + ft.termsQuery(Arrays.asList(1, 2.1), MOCK_QSC) ); assertEquals( new IndexOrDocValuesQuery(IntPoint.newSetQuery("field", 1), SortedNumericDocValuesField.newSlowSetQuery("field", 1)), - ft.termsQuery(Arrays.asList(1.0, 2.1), null) + ft.termsQuery(Arrays.asList(1.0, 2.1), MOCK_QSC) ); - assertTrue(ft.termsQuery(Arrays.asList(1.1, 2.1), null) instanceof MatchNoDocsQuery); + assertTrue(ft.termsQuery(Arrays.asList(1.1, 2.1), MOCK_QSC) instanceof MatchNoDocsQuery); } public void testLongTermsQueryWithDecimalPart() { MappedFieldType ft = new NumberFieldMapper.NumberFieldType("field", NumberType.LONG); assertEquals( new IndexOrDocValuesQuery(LongPoint.newSetQuery("field", 1), SortedNumericDocValuesField.newSlowSetQuery("field", 1)), - ft.termsQuery(Arrays.asList(1, 2.1), null) + ft.termsQuery(Arrays.asList(1, 2.1), MOCK_QSC) ); assertEquals( new IndexOrDocValuesQuery(LongPoint.newSetQuery("field", 1), SortedNumericDocValuesField.newSlowSetQuery("field", 1)), - ft.termsQuery(Arrays.asList(1.0, 2.1), null) + ft.termsQuery(Arrays.asList(1.0, 2.1), MOCK_QSC) ); - assertTrue(ft.termsQuery(Arrays.asList(1.1, 2.1), null) instanceof MatchNoDocsQuery); + assertTrue(ft.termsQuery(Arrays.asList(1.1, 2.1), MOCK_QSC) instanceof MatchNoDocsQuery); } public void testByteTermQueryWithDecimalPart() { MappedFieldType ft = new NumberFieldMapper.NumberFieldType("field", NumberType.BYTE); - assertTrue(ft.termQuery(42.1, null) instanceof MatchNoDocsQuery); + assertTrue(ft.termQuery(42.1, MOCK_QSC) instanceof MatchNoDocsQuery); } public void testShortTermQueryWithDecimalPart() { MappedFieldType ft = new NumberFieldMapper.NumberFieldType("field", NumberType.SHORT); - assertTrue(ft.termQuery(42.1, null) instanceof MatchNoDocsQuery); + assertTrue(ft.termQuery(42.1, MOCK_QSC) instanceof MatchNoDocsQuery); } public void testIntegerTermQueryWithDecimalPart() { MappedFieldType ft = new NumberFieldMapper.NumberFieldType("field", NumberType.INTEGER); - assertTrue(ft.termQuery(42.1, null) instanceof MatchNoDocsQuery); + assertTrue(ft.termQuery(42.1, MOCK_QSC) instanceof MatchNoDocsQuery); } public void testLongTermQueryWithDecimalPart() { MappedFieldType ft = new NumberFieldMapper.NumberFieldType("field", NumberFieldMapper.NumberType.LONG); - assertTrue(ft.termQuery(42.1, null) instanceof MatchNoDocsQuery); + assertTrue(ft.termQuery(42.1, MOCK_QSC) instanceof MatchNoDocsQuery); } private static MappedFieldType unsearchable() { @@ -187,10 +189,10 @@ public void testTermQuery() { MappedFieldType ft = new NumberFieldMapper.NumberFieldType("field", NumberFieldMapper.NumberType.LONG); Query dvQuery = SortedNumericDocValuesField.newSlowExactQuery("field", 42); Query query = new IndexOrDocValuesQuery(LongPoint.newExactQuery("field", 42), dvQuery); - assertEquals(query, ft.termQuery("42", null)); + assertEquals(query, ft.termQuery("42", MOCK_QSC)); MappedFieldType unsearchable = unsearchable(); - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> unsearchable.termQuery("42", null)); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> unsearchable.termQuery("42", MOCK_QSC)); assertEquals("Cannot search on field [field] since it is both not indexed, and does not have doc_values enabled.", e.getMessage()); } @@ -1006,19 +1008,19 @@ public void testBitmapQuery() throws IOException { NumberFieldType ft = new NumberFieldMapper.NumberFieldType("field", NumberType.INTEGER); assertEquals( new IndexOrDocValuesQuery(new BitmapIndexQuery("field", r), new BitmapDocValuesQuery("field", r)), - ft.bitmapQuery(bitmap) + ft.bitmapQuery(bitmap, MOCK_QSC) ); ft = new NumberFieldType("field", NumberType.INTEGER, false, false, true, true, true, null, Collections.emptyMap()); - assertEquals(new BitmapDocValuesQuery("field", r), ft.bitmapQuery(bitmap)); + assertEquals(new BitmapDocValuesQuery("field", r), ft.bitmapQuery(bitmap, MOCK_QSC)); ft = new NumberFieldType("field", NumberType.INTEGER, true, false, false, false, true, null, Collections.emptyMap()); - assertEquals(new BitmapIndexQuery("field", r), ft.bitmapQuery(bitmap)); + assertEquals(new BitmapIndexQuery("field", r), ft.bitmapQuery(bitmap, MOCK_QSC)); Directory dir = newDirectory(); IndexWriter w = new IndexWriter(dir, new IndexWriterConfig()); DirectoryReader reader = DirectoryReader.open(w); - assertEquals(new MatchNoDocsQuery(), ft.bitmapQuery(bitmap).rewrite(newSearcher(reader))); + assertEquals(new MatchNoDocsQuery(), ft.bitmapQuery(bitmap, MOCK_QSC).rewrite(newSearcher(reader))); reader.close(); w.close(); dir.close(); @@ -1026,7 +1028,7 @@ public void testBitmapQuery() throws IOException { NumberType type = randomValueOtherThan(NumberType.INTEGER, () -> randomFrom(NumberType.values())); ft = new NumberFieldMapper.NumberFieldType("field", type); NumberFieldType finalFt = ft; - assertThrows(IllegalArgumentException.class, () -> finalFt.bitmapQuery(bitmap)); + assertThrows(IllegalArgumentException.class, () -> finalFt.bitmapQuery(bitmap, MOCK_QSC)); } public void testBitmapQuery64() throws IOException { @@ -1045,20 +1047,20 @@ public void testBitmapQuery64() throws IOException { assertEquals( new IndexOrDocValuesQuery(new Bitmap64IndexQuery("field", r), new Bitmap64DocValuesQuery("field", r)), - ft.bitmapQuery(bitmap) + ft.bitmapQuery(bitmap, MOCK_QSC) ); ft = new NumberFieldType("field", NumberType.LONG, false, false, true, true, true, null, Collections.emptyMap()); - assertEquals(new Bitmap64DocValuesQuery("field", r), ft.bitmapQuery(bitmap)); + assertEquals(new Bitmap64DocValuesQuery("field", r), ft.bitmapQuery(bitmap, MOCK_QSC)); ft = new NumberFieldType("field", NumberType.LONG, true, false, false, false, true, null, Collections.emptyMap()); - assertEquals(new Bitmap64IndexQuery("field", r), ft.bitmapQuery(bitmap)); + assertEquals(new Bitmap64IndexQuery("field", r), ft.bitmapQuery(bitmap, MOCK_QSC)); Directory dir = newDirectory(); IndexWriter w = new IndexWriter(dir, new IndexWriterConfig()); DirectoryReader reader = DirectoryReader.open(w); - assertEquals(new MatchNoDocsQuery(), ft.bitmapQuery(bitmap).rewrite(newSearcher(reader))); + assertEquals(new MatchNoDocsQuery(), ft.bitmapQuery(bitmap, MOCK_QSC).rewrite(newSearcher(reader))); reader.close(); w.close(); @@ -1068,7 +1070,7 @@ public void testBitmapQuery64() throws IOException { ft = new NumberFieldMapper.NumberFieldType("field", type); NumberFieldType finalFt = ft; - assertThrows(IllegalArgumentException.class, () -> finalFt.bitmapQuery(bitmap)); + assertThrows(IllegalArgumentException.class, () -> finalFt.bitmapQuery(bitmap, MOCK_QSC)); } public void testFetchUnsignedLongDocValues() throws IOException { @@ -1094,4 +1096,67 @@ public void testFetchUnsignedLongDocValues() throws IOException { } IOUtils.close(w, dir); } + + /** + * On a non-pluggable-dataformat index (default) a numeric range query is built as + * {@link ApproximateScoreQuery} wrapping an {@link IndexOrDocValuesQuery} — the BKD + * fast path is preserved. + */ + public void testRangeQueryUsesPointsWhenPluggableDataFormatDisabled() { + NumberFieldType ft = new NumberFieldType("field", NumberType.LONG); + Query expected = new ApproximateScoreQuery( + new IndexOrDocValuesQuery( + LongPoint.newRangeQuery("field", 1L, 10L), + SortedNumericDocValuesField.newSlowRangeQuery("field", 1L, 10L) + ), + new ApproximatePointRangeQuery("field", pack(1L).bytes, pack(10L).bytes, 1, ApproximatePointRangeQuery.LONG_FORMAT) + ); + assertEquals(expected, ft.rangeQuery(1L, 10L, true, true, null, null, null, MOCK_QSC)); + } + + /** + * On a pluggable-dataformat index the mapper must skip the point-based query construction + * and emit only the doc-values range. The Lucene secondary writes no BKD on such indices, + * so keeping the point side would let the cost-based dispatch inside + * {@link IndexOrDocValuesQuery} pick an empty {@code PointValues} and return zero hits. + */ + public void testRangeQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + NumberFieldType ft = new NumberFieldType("field", NumberType.LONG); + Query query = ft.rangeQuery(1L, 10L, true, true, null, null, null, mockPluggableDataFormatContext()); + Query expected = SortedNumericDocValuesField.newSlowRangeQuery("field", 1L, 10L); + assertEquals(expected, query); + } + + /** + * Same routing rule applies to numeric term queries: on a pluggable-dataformat index the + * DV-only exact-range form must be emitted, not the {@code IndexOrDocValuesQuery} that + * pairs a {@code LongPoint.newExactQuery} with a DV range. + */ + public void testTermQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + NumberFieldType ft = new NumberFieldType("field", NumberType.LONG); + Query query = ft.termQuery(42L, mockPluggableDataFormatContext()); + Query expected = SortedNumericDocValuesField.newSlowRangeQuery("field", 42L, 42L); + assertEquals(expected, query); + } + + /** + * Terms queries also route to the DV set-query form on a pluggable-dataformat index. + */ + public void testTermsQueryUsesDocValuesWhenPluggableDataFormatEnabled() { + NumberFieldType ft = new NumberFieldType("field", NumberType.LONG); + Query query = ft.termsQuery(java.util.List.of(1L, 2L, 3L), mockPluggableDataFormatContext()); + Query expected = SortedNumericDocValuesField.newSlowSetQuery("field", 1L, 2L, 3L); + assertEquals(expected, query); + } + + /** + * A {@link QueryShardContext} mock that reports the pluggable-dataformat gate as enabled. + * Simulates a Mustang-backed index without needing the real feature-flag / IndexSettings + * plumbing — the mapper code only cares about the boolean returned here. + */ + private static QueryShardContext mockPluggableDataFormatContext() { + QueryShardContext ctx = mock(QueryShardContext.class); + when(ctx.isPluggableDataFormatEnabled()).thenReturn(true); + return ctx; + } } diff --git a/server/src/test/java/org/opensearch/index/query/QueryShardContextTests.java b/server/src/test/java/org/opensearch/index/query/QueryShardContextTests.java index 12677edc8efa7..a496f2d8a230d 100644 --- a/server/src/test/java/org/opensearch/index/query/QueryShardContextTests.java +++ b/server/src/test/java/org/opensearch/index/query/QueryShardContextTests.java @@ -51,6 +51,7 @@ import org.opensearch.common.TriFunction; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.BigArrays; +import org.opensearch.common.util.FeatureFlags; import org.opensearch.core.common.io.stream.NamedWriteableRegistry; import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.NamedXContentRegistry; @@ -328,6 +329,53 @@ public void testSearchLookupShardId() { assertEquals(SHARD_ID, searchLookup.shardId()); } + /** + * When the index-level setting is on AND the pluggable-dataformat feature flag is on, + * the gate is on. Mapper-layer callers use this to route numeric/date queries down the + * doc-values branch. + */ + @LockFeatureFlag(FeatureFlags.PLUGGABLE_DATAFORMAT_EXPERIMENTAL_FLAG) + public void testIsPluggableDataFormatEnabledWhenIndexSettingAndFeatureFlagOn() { + Settings settings = Settings.builder() + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1) + .put(IndexSettings.PLUGGABLE_DATAFORMAT_ENABLED_SETTING.getKey(), true) + .build(); + QueryShardContext context = queryShardContextWithIndexSettings(settings); + assertTrue(context.isPluggableDataFormatEnabled()); + } + + /** + * Default state: neither the index-level setting nor the feature flag is on. This is what + * every non-Mustang index sees — BKD fast path is preserved. + */ + public void testIsPluggableDataFormatEnabledWhenNeitherFlagIsOn() { + Settings settings = Settings.builder() + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1) + .build(); + QueryShardContext context = queryShardContextWithIndexSettings(settings); + assertFalse(context.isPluggableDataFormatEnabled()); + } + + /** + * The index-level setting alone is not enough — the feature flag must also be on for the + * gate to activate. Verifies the two conditions are combined (AND, not OR). + */ + public void testIsPluggableDataFormatEnabledWhenOnlyIndexSettingIsOn() { + Settings settings = Settings.builder() + .put(IndexMetadata.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1) + .put(IndexSettings.PLUGGABLE_DATAFORMAT_ENABLED_SETTING.getKey(), true) + .build(); + // Feature flag intentionally NOT enabled → gate must remain off. + QueryShardContext context = queryShardContextWithIndexSettings(settings); + assertFalse(context.isPluggableDataFormatEnabled()); + } + public static QueryShardContext createQueryShardContext(String indexUuid, String clusterAlias) { return createQueryShardContext(indexUuid, clusterAlias, null); } @@ -479,4 +527,33 @@ public void collect(int doc) throws IOException { } } + /** + * Builds a minimal {@link QueryShardContext} for a hypothetical index with the given + * {@link Settings}, wiring in real {@link IndexSettings}. Used by the pluggable-dataformat + * gate tests where the value of {@link QueryShardContext#isPluggableDataFormatEnabled()} + * depends on both the index-level setting and the node-level feature flag. + */ + private static QueryShardContext queryShardContextWithIndexSettings(Settings settings) { + IndexMetadata indexMetadata = new IndexMetadata.Builder("index").settings(settings).build(); + IndexSettings indexSettings = new IndexSettings(indexMetadata, settings); + return new QueryShardContext( + 0, + indexSettings, + BigArrays.NON_RECYCLING_INSTANCE, + null, + null, + null, + null, + null, + NamedXContentRegistry.EMPTY, + new NamedWriteableRegistry(Collections.emptyList()), + null, + null, + () -> 0L, + null, + null, + () -> true, + null + ); + } } diff --git a/server/src/test/java/org/opensearch/index/query/TermQueryWithDocIdAndQueryTests.java b/server/src/test/java/org/opensearch/index/query/TermQueryWithDocIdAndQueryTests.java index 09b7ddd5dd31e..71a2d76c0acb3 100644 --- a/server/src/test/java/org/opensearch/index/query/TermQueryWithDocIdAndQueryTests.java +++ b/server/src/test/java/org/opensearch/index/query/TermQueryWithDocIdAndQueryTests.java @@ -86,7 +86,7 @@ public void testTermsQueryWithBitmapValueType() throws Exception { when(context.fieldMapper("student_id")).thenReturn(numberFieldType); when(numberFieldType.unwrap()).thenReturn(numberFieldType); Query bitmapQuery = mock(Query.class); - when(numberFieldType.bitmapQuery(any(BytesArray.class))).thenReturn(bitmapQuery); + when(numberFieldType.bitmapQuery(any(BytesArray.class), any(QueryShardContext.class))).thenReturn(bitmapQuery); Query result = builder.doToQuery(context); assertNotNull(result);