From fa34177a13edde6c8f929abf1c483aec7abae80e Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 31 Oct 2025 13:49:03 +0200 Subject: [PATCH 01/17] POC - Automatic prefiltering for semantic_text queries This is a POC that implements automatic prefiltering for `semantic_text` queries in Query DSL. Semantic text queries for `text_embedding` tasks get rewritten as kNN vector queries. The latter support filters that are applied before search. In this PR we introduce a `Prefiltering` interface that gets implemented by query builders that need prefiltering (match/semantic) or need to propagate prefilters (all compound query builders). When compound queries get rewritten, we propagate filter queries to direct child queries that support prefiltering. When match or semantic queries get rewritten to kNN queries, we set their prefilters to the kNN query so that they are applied before search. --- .../index/query/BoolQueryBuilder.java | 26 ++++++- .../index/query/BoostingQueryBuilder.java | 26 ++++++- .../query/ConstantScoreQueryBuilder.java | 29 +++++++- .../index/query/DisMaxQueryBuilder.java | 22 +++++- .../index/query/MatchQueryBuilder.java | 23 ++++++- .../index/query/Prefiltering.java | 34 ++++++++++ .../FunctionScoreQueryBuilder.java | 27 +++++++- .../referable/query_prefiltering.csv | 1 + .../resources/transport/upper_bounds/9.3.csv | 2 +- .../index/query/PrefilteringTestUtils.java | 68 +++++++++++++++++++ .../mapper/SemanticTextFieldMapper.java | 12 +++- ...InterceptedInferenceMatchQueryBuilder.java | 3 +- .../queries/SemanticQueryBuilder.java | 32 ++++++++- .../queries/SemanticQueryBuilderTests.java | 30 ++++++-- 14 files changed, 316 insertions(+), 19 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/query/Prefiltering.java create mode 100644 server/src/main/resources/transport/definitions/referable/query_prefiltering.csv create mode 100644 server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 5944fc3d8df7a..8597fe2cbc8f3 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -30,13 +30,14 @@ import java.util.Map; import java.util.Objects; import java.util.function.Consumer; +import java.util.stream.Stream; import static org.elasticsearch.common.lucene.search.Queries.fixNegativeQueryIfNeeded; /** * A Query that matches documents matching boolean combinations of other queries. */ -public class BoolQueryBuilder extends AbstractQueryBuilder { +public class BoolQueryBuilder extends AbstractQueryBuilder implements Prefiltering { public static final String NAME = "bool"; public static final boolean ADJUST_PURE_NEGATIVE_DEFAULT = true; @@ -60,6 +61,8 @@ public class BoolQueryBuilder extends AbstractQueryBuilder { private String minimumShouldMatch; + private final List prefilters = new ArrayList<>(); + /** * Build an empty bool query. */ @@ -76,6 +79,9 @@ public BoolQueryBuilder(StreamInput in) throws IOException { filterClauses.addAll(readQueries(in)); adjustPureNegative = in.readBoolean(); minimumShouldMatch = in.readOptionalString(); + if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + } } @Override @@ -86,6 +92,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { writeQueries(out, filterClauses); out.writeBoolean(adjustPureNegative); out.writeOptionalString(minimumShouldMatch); + if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + out.writeNamedWriteableCollection(prefilters); + } } /** @@ -353,6 +362,9 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws if (clauses == 0) { return new MatchAllQueryBuilder().boost(boost()).queryName(queryName()); } + + propagatePrefilters(Stream.concat(mustClauses.stream(), shouldClauses.stream()).toList()); + changed |= rewriteClauses(queryRewriteContext, mustClauses, newBuilder::must); try { @@ -415,6 +427,7 @@ private static boolean rewriteClauses( ) throws IOException { boolean changed = false; for (QueryBuilder builder : builders) { + QueryBuilder result = builder.rewrite(queryRewriteContext); if (result != builder) { changed = true; @@ -452,4 +465,15 @@ public BoolQueryBuilder shallowCopy() { } return copy; } + + @Override + public BoolQueryBuilder setPrefilters(List prefilters) { + this.prefilters.addAll(prefilters); + return this; + } + + @Override + public List getPrefilters() { + return Stream.concat(prefilters.stream(), filterClauses.stream()).toList(); + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java index 9e439efd71dc9..96ff654841909 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java @@ -20,8 +20,11 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Stream; /** * The BoostingQuery class can be used to effectively demote results that match a given query. @@ -35,7 +38,7 @@ * multiplied by the supplied "boost" parameter, so this should be less than 1 to achieve a * demoting effect */ -public class BoostingQueryBuilder extends AbstractQueryBuilder { +public class BoostingQueryBuilder extends AbstractQueryBuilder implements Prefiltering { public static final String NAME = "boosting"; private static final ParseField POSITIVE_FIELD = new ParseField("positive"); @@ -48,6 +51,8 @@ public class BoostingQueryBuilder extends AbstractQueryBuilder prefilters = new ArrayList<>(); + /** * Create a new {@link BoostingQueryBuilder} * @@ -73,6 +78,9 @@ public BoostingQueryBuilder(StreamInput in) throws IOException { positiveQuery = in.readNamedWriteable(QueryBuilder.class); negativeQuery = in.readNamedWriteable(QueryBuilder.class); negativeBoost = in.readFloat(); + if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + } } @Override @@ -80,6 +88,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeNamedWriteable(positiveQuery); out.writeNamedWriteable(negativeQuery); out.writeFloat(negativeBoost); + if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + out.writeNamedWriteableCollection(prefilters); + } } /** @@ -209,6 +220,8 @@ protected boolean doEquals(BoostingQueryBuilder other) { @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { + propagatePrefilters(List.of(positiveQuery)); + QueryBuilder positiveQuery = this.positiveQuery.rewrite(queryRewriteContext); if (positiveQuery instanceof MatchNoneQueryBuilder) { return positiveQuery; @@ -233,4 +246,15 @@ protected void extractInnerHitBuilders(Map inner public TransportVersion getMinimalSupportedVersion() { return TransportVersion.zero(); } + + @Override + public BoostingQueryBuilder setPrefilters(List prefilters) { + prefilters.addAll(prefilters); + return this; + } + + @Override + public List getPrefilters() { + return Stream.concat(prefilters.stream(), List.of(positiveQuery).stream()).toList(); + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java index f70f095ecd5ea..7f853554fe3d4 100644 --- a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java @@ -20,6 +20,8 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -27,13 +29,18 @@ * A query that wraps a filter and simply returns a constant score equal to the * query boost for every document in the filter. */ -public class ConstantScoreQueryBuilder extends AbstractQueryBuilder { +public class ConstantScoreQueryBuilder extends AbstractQueryBuilder + implements + Prefiltering { + public static final String NAME = "constant_score"; private static final ParseField INNER_QUERY_FIELD = new ParseField("filter"); private final QueryBuilder filterBuilder; + private final List prefilters = new ArrayList<>(); + /** * A query that wraps another query and simply returns a constant score equal to the * query boost for every document in the query. @@ -53,11 +60,17 @@ public ConstantScoreQueryBuilder(QueryBuilder filterBuilder) { public ConstantScoreQueryBuilder(StreamInput in) throws IOException { super(in); filterBuilder = in.readNamedWriteable(QueryBuilder.class); + if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + } } @Override protected void doWriteTo(StreamOutput out) throws IOException { out.writeNamedWriteable(filterBuilder); + if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + out.writeNamedWriteableCollection(prefilters); + } } /** @@ -145,10 +158,13 @@ protected boolean doEquals(ConstantScoreQueryBuilder other) { @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { + propagatePrefilters(List.of(filterBuilder)); + QueryBuilder rewrite = filterBuilder.rewrite(queryRewriteContext); if (rewrite instanceof MatchNoneQueryBuilder) { return rewrite; // we won't match anyway } + if (rewrite != filterBuilder) { return new ConstantScoreQueryBuilder(rewrite); } @@ -164,4 +180,15 @@ protected void extractInnerHitBuilders(Map inner public TransportVersion getMinimalSupportedVersion() { return TransportVersion.zero(); } + + @Override + public ConstantScoreQueryBuilder setPrefilters(List prefilters) { + this.prefilters.addAll(prefilters); + return this; + } + + @Override + public List getPrefilters() { + return prefilters; + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java index 4ddb28b76ee6c..e034bd04a695d 100644 --- a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java @@ -32,7 +32,7 @@ * with the maximum score for that document as produced by any sub-query, plus a tie breaking increment for any * additional matching sub-queries. */ -public class DisMaxQueryBuilder extends AbstractQueryBuilder { +public class DisMaxQueryBuilder extends AbstractQueryBuilder implements Prefiltering { public static final String NAME = "dis_max"; /** Default multiplication factor for breaking ties in document scores.*/ @@ -42,6 +42,7 @@ public class DisMaxQueryBuilder extends AbstractQueryBuilder private static final ParseField QUERIES_FIELD = new ParseField("queries"); private final List queries = new ArrayList<>(); + private final List prefilters = new ArrayList<>(); private float tieBreaker = DEFAULT_TIE_BREAKER; @@ -54,12 +55,18 @@ public DisMaxQueryBuilder(StreamInput in) throws IOException { super(in); queries.addAll(readQueries(in)); tieBreaker = in.readFloat(); + if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + prefilters.addAll(readQueries(in)); + } } @Override protected void doWriteTo(StreamOutput out) throws IOException { writeQueries(out, queries); out.writeFloat(tieBreaker); + if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + out.writeNamedWriteableCollection(prefilters); + } } /** @@ -182,6 +189,8 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { + propagatePrefilters(queries); + DisMaxQueryBuilder newBuilder = new DisMaxQueryBuilder(); boolean changed = false; for (QueryBuilder query : queries) { @@ -227,4 +236,15 @@ protected void extractInnerHitBuilders(Map inner public TransportVersion getMinimalSupportedVersion() { return TransportVersion.zero(); } + + @Override + public DisMaxQueryBuilder setPrefilters(List prefilters) { + prefilters.addAll(prefilters); + return this; + } + + @Override + public List getPrefilters() { + return prefilters; + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java index 56e002287e1e3..9c55ef14df1a8 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java @@ -29,13 +29,15 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** * Match query is a query that analyzes the text and constructs a query as the * result of the analysis. */ -public class MatchQueryBuilder extends AbstractQueryBuilder { +public class MatchQueryBuilder extends AbstractQueryBuilder implements Prefiltering { public static final ParseField ZERO_TERMS_QUERY_FIELD = new ParseField("zero_terms_query"); public static final ParseField LENIENT_FIELD = new ParseField("lenient"); @@ -81,6 +83,8 @@ public class MatchQueryBuilder extends AbstractQueryBuilder { private boolean autoGenerateSynonymsPhraseQuery = true; + private final List prefilters = new ArrayList<>(); + /** * Constructs a new match query. */ @@ -118,6 +122,9 @@ public MatchQueryBuilder(StreamInput in) throws IOException { in.readOptionalFloat(); } autoGenerateSynonymsPhraseQuery = in.readBoolean(); + if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + } } @Override @@ -140,6 +147,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeOptionalFloat(null); } out.writeBoolean(autoGenerateSynonymsPhraseQuery); + if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + out.writeNamedWriteableCollection(prefilters); + } } /** Returns the field name used in this query. */ @@ -570,4 +580,15 @@ public static MatchQueryBuilder fromXContent(XContentParser parser) throws IOExc public TransportVersion getMinimalSupportedVersion() { return TransportVersion.zero(); } + + @Override + public MatchQueryBuilder setPrefilters(List prefilters) { + this.prefilters.addAll(prefilters); + return this; + } + + @Override + public List getPrefilters() { + return prefilters; + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/Prefiltering.java b/server/src/main/java/org/elasticsearch/index/query/Prefiltering.java new file mode 100644 index 0000000000000..40d2150ad3bb5 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/Prefiltering.java @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.TransportVersion; + +import java.util.List; + +public interface Prefiltering { + + TransportVersion QUERY_PREFILTERING = TransportVersion.fromName("query_prefiltering"); + + T setPrefilters(List prefilters); + + List getPrefilters(); + + default void propagatePrefilters(List targetQueries) { + List prefilters = getPrefilters(); + if (prefilters.isEmpty() == false) { + for (QueryBuilder targetQuery : targetQueries) { + if (targetQuery instanceof Prefiltering prefilteredQuery) { + prefilteredQuery.setPrefilters(prefilters.stream().filter(q -> q != targetQuery).toList()); + } + } + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java index 00553ab535fd3..a92a87e1a87c7 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java @@ -23,6 +23,7 @@ import org.elasticsearch.index.query.InnerHitContextBuilder; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.MatchNoneQueryBuilder; +import org.elasticsearch.index.query.Prefiltering; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.SearchExecutionContext; @@ -44,7 +45,10 @@ * A query that uses a filters with a script associated with them to compute the * score. */ -public class FunctionScoreQueryBuilder extends AbstractQueryBuilder { +public class FunctionScoreQueryBuilder extends AbstractQueryBuilder + implements + Prefiltering { + public static final String NAME = "function_score"; // For better readability of error message @@ -74,6 +78,8 @@ public class FunctionScoreQueryBuilder extends AbstractQueryBuilder prefilters = new ArrayList<>(); + /** * Creates a function_score query without functions * @@ -144,6 +150,9 @@ public FunctionScoreQueryBuilder(StreamInput in) throws IOException { minScore = in.readOptionalFloat(); boostMode = in.readOptionalWriteable(CombineFunction::readFromStream); scoreMode = FunctionScoreQuery.ScoreMode.readFromStream(in); + if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + } } @Override @@ -154,6 +163,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeOptionalFloat(minScore); out.writeOptionalWriteable(boostMode); scoreMode.writeTo(out); + if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + out.writeNamedWriteableCollection(prefilters); + } } /** @@ -322,6 +334,17 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { return new FunctionScoreQuery(query, scoreMode, filterFunctions, boostMode, minScore, maxBoost); } + @Override + public FunctionScoreQueryBuilder setPrefilters(List prefilters) { + this.prefilters.addAll(prefilters); + return this; + } + + @Override + public List getPrefilters() { + return prefilters; + } + /** * Function to be associated with an optional filter, meaning it will be executed only for the documents * that match the given filter. @@ -405,6 +428,8 @@ public FilterFunctionBuilder rewrite(QueryRewriteContext context) throws IOExcep @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { + propagatePrefilters(List.of(query)); + QueryBuilder queryBuilder = this.query.rewrite(queryRewriteContext); if (queryBuilder instanceof MatchNoneQueryBuilder) { return queryBuilder; diff --git a/server/src/main/resources/transport/definitions/referable/query_prefiltering.csv b/server/src/main/resources/transport/definitions/referable/query_prefiltering.csv new file mode 100644 index 0000000000000..9b32a5cf8b643 --- /dev/null +++ b/server/src/main/resources/transport/definitions/referable/query_prefiltering.csv @@ -0,0 +1 @@ +9211000 diff --git a/server/src/main/resources/transport/upper_bounds/9.3.csv b/server/src/main/resources/transport/upper_bounds/9.3.csv index 88b7f13fd8f9f..2d9e67d8be12f 100644 --- a/server/src/main/resources/transport/upper_bounds/9.3.csv +++ b/server/src/main/resources/transport/upper_bounds/9.3.csv @@ -1 +1 @@ -add_sample_method_downsample_ilm,9210000 +query_prefiltering,9211000 diff --git a/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java b/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java new file mode 100644 index 0000000000000..2f3023d95483a --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.query; + +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.KnnByteVectorQuery; +import org.apache.lucene.search.KnnFloatVectorQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.elasticsearch.test.ESTestCase.randomAlphaOfLength; +import static org.elasticsearch.test.ESTestCase.randomFrom; +import static org.elasticsearch.test.ESTestCase.randomIntBetween; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +public class PrefilteringTestUtils { + + public static void setRandomTermQueryPrefilters(Prefiltering queryBuilder, String... termFieldNames) { + List filters = new ArrayList<>(); + int numFilters = randomIntBetween(1, 5); + for (int i = 0; i < numFilters; i++) { + String filterFieldName = randomFrom(termFieldNames); + filters.add(QueryBuilders.termQuery(filterFieldName, randomAlphaOfLength(10))); + } + queryBuilder.setPrefilters(filters); + } + + public static void assertQueryHasPrefilters(Prefiltering queryBuilder, Query query, SearchExecutionContext context) + throws IOException { + assertThat(query, anyOf(instanceOf(KnnFloatVectorQuery.class), instanceOf(KnnByteVectorQuery.class))); + Query queryFilter; + if (query instanceof KnnFloatVectorQuery q) { + queryFilter = q.getFilter(); + } else if (query instanceof KnnByteVectorQuery q) { + queryFilter = q.getFilter(); + } else { + throw new IllegalStateException("Unexpected query type " + query.getClass()); + } + + if (queryBuilder.getPrefilters().isEmpty()) { + assertThat(queryFilter, is(nullValue())); + } else { + for (QueryBuilder qb : queryBuilder.getPrefilters()) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(qb.toQuery(context), BooleanClause.Occur.MUST); + BooleanQuery booleanQuery = builder.build(); + assertThat(queryFilter.toString(), containsString(booleanQuery.toString())); + } + } + } +} diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java index 109646cf0e827..82f55876f6e84 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java @@ -1004,7 +1004,13 @@ public boolean fieldHasValue(FieldInfos fieldInfos) { return fieldInfos.fieldInfo(getEmbeddingsFieldName(name())) != null; } - public QueryBuilder semanticQuery(InferenceResults inferenceResults, Integer requestSize, float boost, String queryName) { + public QueryBuilder semanticQuery( + InferenceResults inferenceResults, + Integer requestSize, + float boost, + String queryName, + List filters + ) { String nestedFieldPath = getChunksFieldName(name()); String inferenceResultsFieldName = getEmbeddingsFieldName(name()); QueryBuilder childQueryBuilder; @@ -1055,7 +1061,9 @@ yield new SparseVectorQueryBuilder( k = Math.max(k, DEFAULT_SIZE); } - yield new KnnVectorQueryBuilder(inferenceResultsFieldName, inference, k, null, null, null, null); + yield new KnnVectorQueryBuilder(inferenceResultsFieldName, inference, k, null, null, null, null).addFilterQueries( + filters + ); } default -> throw new IllegalStateException( "Field [" diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/InterceptedInferenceMatchQueryBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/InterceptedInferenceMatchQueryBuilder.java index 018fdca7fabdb..80004eb78e63d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/InterceptedInferenceMatchQueryBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/InterceptedInferenceMatchQueryBuilder.java @@ -95,7 +95,8 @@ protected QueryBuilder queryFields( rewritten = new MatchNoneQueryBuilder(); } else if (fieldType instanceof SemanticTextFieldMapper.SemanticTextFieldType) { rewritten = new SemanticQueryBuilder(getField(), getQuery(), null, inferenceResultsMap).boost(originalQuery.boost()) - .queryName(originalQuery.queryName()); + .queryName(originalQuery.queryName()) + .setPrefilters(originalQuery.getPrefilters()); } else { rewritten = originalQuery; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java index 4060d1c6bc4a9..9427759da4c19 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java @@ -24,6 +24,7 @@ import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.MatchNoneQueryBuilder; +import org.elasticsearch.index.query.Prefiltering; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.SearchExecutionContext; @@ -62,7 +63,7 @@ import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; -public class SemanticQueryBuilder extends AbstractQueryBuilder { +public class SemanticQueryBuilder extends AbstractQueryBuilder implements Prefiltering { public static final String NAME = "semantic"; public static final NodeFeature SEMANTIC_QUERY_MULTIPLE_INFERENCE_IDS = new NodeFeature("semantic_query.multiple_inference_ids"); @@ -101,6 +102,7 @@ public class SemanticQueryBuilder extends AbstractQueryBuilder inferenceResultsMap; private final SetOnce> inferenceResultsMapSupplier; private final Boolean lenient; + private final List prefilters = new ArrayList<>(); // ccsRequest is only used on the local cluster coordinator node to detect when: // - The request references a remote index @@ -178,6 +180,10 @@ public SemanticQueryBuilder(StreamInput in) throws IOException { this.ccsRequest = false; } + if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + this.prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + } + this.inferenceResultsMapSupplier = null; } @@ -230,6 +236,10 @@ protected void doWriteTo(StreamOutput out) throws IOException { + "." ); } + + if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + out.writeNamedWriteableCollection(prefilters); + } } private SemanticQueryBuilder( @@ -247,6 +257,7 @@ private SemanticQueryBuilder( this.inferenceResultsMapSupplier = inferenceResultsMapSupplier; this.lenient = other.lenient; this.ccsRequest = ccsRequest; + this.prefilters.addAll(other.prefilters); } @Override @@ -493,7 +504,13 @@ private QueryBuilder doRewriteBuildSemanticQuery(SearchExecutionContext searchEx ); } - return semanticTextFieldType.semanticQuery(inferenceResults, searchExecutionContext.requestSize(), boost(), queryName()); + return semanticTextFieldType.semanticQuery( + inferenceResults, + searchExecutionContext.requestSize(), + boost(), + queryName(), + prefilters + ); } else if (lenient != null && lenient) { return new MatchNoneQueryBuilder(); } else { @@ -653,4 +670,15 @@ protected boolean doEquals(SemanticQueryBuilder other) { protected int doHashCode() { return Objects.hash(fieldName, query, inferenceResultsMap, inferenceResultsMapSupplier, ccsRequest); } + + @Override + public SemanticQueryBuilder setPrefilters(List prefilters) { + this.prefilters.addAll(prefilters); + return this; + } + + @Override + public List getPrefilters() { + return prefilters; + } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java index cb5d1d40e2c2a..01469556eaa80 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java @@ -44,8 +44,10 @@ import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapperTestUtils; import org.elasticsearch.index.query.MatchNoneQueryBuilder; +import org.elasticsearch.index.query.PrefilteringTestUtils; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.RandomQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.search.ESToParentBlockJoinQuery; import org.elasticsearch.inference.InferenceResults; @@ -242,6 +244,9 @@ protected SemanticQueryBuilder doCreateTestQueryBuilder() { if (randomBoolean()) { builder.queryName(randomAlphaOfLength(4)); } + if (randomBoolean()) { + PrefilteringTestUtils.setRandomTermQueryPrefilters(builder, KEYWORD_FIELD_NAME, TEXT_FIELD_NAME); + } return builder; } @@ -258,7 +263,7 @@ protected void doAssertLuceneQuery(SemanticQueryBuilder queryBuilder, Query quer switch (inferenceResultType) { case NONE -> assertThat(nestedQuery.getChildQuery(), instanceOf(MatchNoDocsQuery.class)); case SPARSE_EMBEDDING -> assertSparseEmbeddingLuceneQuery(nestedQuery.getChildQuery()); - case TEXT_EMBEDDING -> assertTextEmbeddingLuceneQuery(nestedQuery.getChildQuery()); + case TEXT_EMBEDDING -> assertTextEmbeddingLuceneQuery(queryBuilder, nestedQuery.getChildQuery(), context); } } @@ -273,14 +278,22 @@ private void assertSparseEmbeddingLuceneQuery(Query query) { assertThat(innerBooleanQuery.clauses().size(), equalTo(0)); } - private void assertTextEmbeddingLuceneQuery(Query query) { + private void assertTextEmbeddingLuceneQuery(SemanticQueryBuilder queryBuilder, Query query, SearchExecutionContext context) + throws IOException { Query innerQuery = assertOuterBooleanQuery(query); - Class expectedKnnQueryClass = switch (denseVectorElementType) { - case FLOAT -> KnnFloatVectorQuery.class; - case BYTE, BIT -> KnnByteVectorQuery.class; - }; - assertThat(innerQuery, instanceOf(expectedKnnQueryClass)); + switch (denseVectorElementType) { + case FLOAT: { + assertThat(innerQuery, instanceOf(KnnFloatVectorQuery.class)); + PrefilteringTestUtils.assertQueryHasPrefilters(queryBuilder, innerQuery, context); + break; + } + case BYTE, BIT: { + assertThat(innerQuery, instanceOf(KnnByteVectorQuery.class)); + PrefilteringTestUtils.assertQueryHasPrefilters(queryBuilder, innerQuery, context); + break; + } + } } private Query assertOuterBooleanQuery(Query query) { @@ -424,6 +437,9 @@ public void testSerializationBwc() throws IOException { null, Map.of(new FullyQualifiedInferenceId(LOCAL_CLUSTER_GROUP_KEY, randomAlphaOfLength(5)), inferenceResults) ); + if (randomBoolean()) { + originalQuery.setPrefilters(randomList(1, 5, () -> RandomQueryBuilder.createQuery(random()))); + } SemanticQueryBuilder bwcQuery = new SemanticQueryBuilder( fieldName, query, From cf68a9fa79e4610eb10ef98c0b9401e364690cbf Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 31 Oct 2025 16:12:59 +0000 Subject: [PATCH 02/17] [CI] Auto commit changes from spotless --- .../org/elasticsearch/index/query/PrefilteringTestUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java b/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java index 2f3023d95483a..38a93a7dff22a 100644 --- a/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java +++ b/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java @@ -14,7 +14,6 @@ import org.apache.lucene.search.KnnByteVectorQuery; import org.apache.lucene.search.KnnFloatVectorQuery; import org.apache.lucene.search.Query; -import org.elasticsearch.test.ESTestCase; import java.io.IOException; import java.util.ArrayList; From 2f936251d97e3eed6c54edc86f93460ac2ef3c38 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Mon, 3 Nov 2025 12:26:24 +0200 Subject: [PATCH 03/17] Set prefilters rather than add to existing ones --- .../org/elasticsearch/index/query/BoolQueryBuilder.java | 6 +++--- .../org/elasticsearch/index/query/BoostingQueryBuilder.java | 6 +++--- .../index/query/ConstantScoreQueryBuilder.java | 6 +++--- .../org/elasticsearch/index/query/DisMaxQueryBuilder.java | 6 +++--- .../org/elasticsearch/index/query/MatchQueryBuilder.java | 6 +++--- .../query/functionscore/FunctionScoreQueryBuilder.java | 6 +++--- .../xpack/inference/queries/SemanticQueryBuilder.java | 6 +++--- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 8597fe2cbc8f3..3dda1d66a9397 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -61,7 +61,7 @@ public class BoolQueryBuilder extends AbstractQueryBuilder imp private String minimumShouldMatch; - private final List prefilters = new ArrayList<>(); + private List prefilters = new ArrayList<>(); /** * Build an empty bool query. @@ -80,7 +80,7 @@ public BoolQueryBuilder(StreamInput in) throws IOException { adjustPureNegative = in.readBoolean(); minimumShouldMatch = in.readOptionalString(); if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { - prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -468,7 +468,7 @@ public BoolQueryBuilder shallowCopy() { @Override public BoolQueryBuilder setPrefilters(List prefilters) { - this.prefilters.addAll(prefilters); + this.prefilters = prefilters; return this; } diff --git a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java index 96ff654841909..fd92a4467fb8a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java @@ -51,7 +51,7 @@ public class BoostingQueryBuilder extends AbstractQueryBuilder prefilters = new ArrayList<>(); + private List prefilters = new ArrayList<>(); /** * Create a new {@link BoostingQueryBuilder} @@ -79,7 +79,7 @@ public BoostingQueryBuilder(StreamInput in) throws IOException { negativeQuery = in.readNamedWriteable(QueryBuilder.class); negativeBoost = in.readFloat(); if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { - prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -249,7 +249,7 @@ public TransportVersion getMinimalSupportedVersion() { @Override public BoostingQueryBuilder setPrefilters(List prefilters) { - prefilters.addAll(prefilters); + this.prefilters = prefilters; return this; } diff --git a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java index 7f853554fe3d4..f27adc6ca4b5c 100644 --- a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java @@ -39,7 +39,7 @@ public class ConstantScoreQueryBuilder extends AbstractQueryBuilder prefilters = new ArrayList<>(); + private List prefilters = new ArrayList<>(); /** * A query that wraps another query and simply returns a constant score equal to the @@ -61,7 +61,7 @@ public ConstantScoreQueryBuilder(StreamInput in) throws IOException { super(in); filterBuilder = in.readNamedWriteable(QueryBuilder.class); if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { - prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -183,7 +183,7 @@ public TransportVersion getMinimalSupportedVersion() { @Override public ConstantScoreQueryBuilder setPrefilters(List prefilters) { - this.prefilters.addAll(prefilters); + this.prefilters = prefilters; return this; } diff --git a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java index e034bd04a695d..9f07dd8af1cc8 100644 --- a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java @@ -42,7 +42,7 @@ public class DisMaxQueryBuilder extends AbstractQueryBuilder private static final ParseField QUERIES_FIELD = new ParseField("queries"); private final List queries = new ArrayList<>(); - private final List prefilters = new ArrayList<>(); + private List prefilters = new ArrayList<>(); private float tieBreaker = DEFAULT_TIE_BREAKER; @@ -56,7 +56,7 @@ public DisMaxQueryBuilder(StreamInput in) throws IOException { queries.addAll(readQueries(in)); tieBreaker = in.readFloat(); if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { - prefilters.addAll(readQueries(in)); + prefilters = readQueries(in); } } @@ -239,7 +239,7 @@ public TransportVersion getMinimalSupportedVersion() { @Override public DisMaxQueryBuilder setPrefilters(List prefilters) { - prefilters.addAll(prefilters); + this.prefilters = prefilters; return this; } diff --git a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java index 9c55ef14df1a8..13cb8ddcf9269 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java @@ -83,7 +83,7 @@ public class MatchQueryBuilder extends AbstractQueryBuilder i private boolean autoGenerateSynonymsPhraseQuery = true; - private final List prefilters = new ArrayList<>(); + private List prefilters = new ArrayList<>(); /** * Constructs a new match query. @@ -123,7 +123,7 @@ public MatchQueryBuilder(StreamInput in) throws IOException { } autoGenerateSynonymsPhraseQuery = in.readBoolean(); if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { - prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -583,7 +583,7 @@ public TransportVersion getMinimalSupportedVersion() { @Override public MatchQueryBuilder setPrefilters(List prefilters) { - this.prefilters.addAll(prefilters); + this.prefilters = prefilters; return this; } diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java index a92a87e1a87c7..da9f928b351ff 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java @@ -78,7 +78,7 @@ public class FunctionScoreQueryBuilder extends AbstractQueryBuilder prefilters = new ArrayList<>(); + private List prefilters = new ArrayList<>(); /** * Creates a function_score query without functions @@ -151,7 +151,7 @@ public FunctionScoreQueryBuilder(StreamInput in) throws IOException { boostMode = in.readOptionalWriteable(CombineFunction::readFromStream); scoreMode = FunctionScoreQuery.ScoreMode.readFromStream(in); if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { - prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -336,7 +336,7 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { @Override public FunctionScoreQueryBuilder setPrefilters(List prefilters) { - this.prefilters.addAll(prefilters); + this.prefilters = prefilters; return this; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java index 9427759da4c19..ad2d049d43fda 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java @@ -102,7 +102,7 @@ public class SemanticQueryBuilder extends AbstractQueryBuilder inferenceResultsMap; private final SetOnce> inferenceResultsMapSupplier; private final Boolean lenient; - private final List prefilters = new ArrayList<>(); + private List prefilters = new ArrayList<>(); // ccsRequest is only used on the local cluster coordinator node to detect when: // - The request references a remote index @@ -181,7 +181,7 @@ public SemanticQueryBuilder(StreamInput in) throws IOException { } if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { - this.prefilters.addAll(in.readNamedWriteableCollectionAsList(QueryBuilder.class)); + this.prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } this.inferenceResultsMapSupplier = null; @@ -673,7 +673,7 @@ protected int doHashCode() { @Override public SemanticQueryBuilder setPrefilters(List prefilters) { - this.prefilters.addAll(prefilters); + this.prefilters = prefilters; return this; } From d26b648caf13a6291fdaee6054c8b388d3b624da Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Mon, 3 Nov 2025 12:26:40 +0200 Subject: [PATCH 04/17] boosting query needs not propagate its positive query --- .../org/elasticsearch/index/query/BoostingQueryBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java index fd92a4467fb8a..340ed3d1f0804 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java @@ -255,6 +255,6 @@ public BoostingQueryBuilder setPrefilters(List prefilters) { @Override public List getPrefilters() { - return Stream.concat(prefilters.stream(), List.of(positiveQuery).stream()).toList(); + return prefilters; } } From c7c62db8cece9f08185198dbfd31591f8ba0653b Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Mon, 3 Nov 2025 12:29:18 +0200 Subject: [PATCH 05/17] prefiltering for nested query --- .../index/query/NestedQueryBuilder.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java index 2007f378ed1bd..c6ac76fdc4900 100644 --- a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java @@ -45,7 +45,9 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -53,7 +55,7 @@ import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES; import static org.elasticsearch.search.fetch.subphase.InnerHitsContext.intersect; -public class NestedQueryBuilder extends AbstractQueryBuilder { +public class NestedQueryBuilder extends AbstractQueryBuilder implements Prefiltering { public static final String NAME = "nested"; /** * The default value for ignore_unmapped. @@ -71,6 +73,7 @@ public class NestedQueryBuilder extends AbstractQueryBuilder private final QueryBuilder query; private InnerHitBuilder innerHitBuilder; private boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED; + private List prefilters = new ArrayList<>(); public NestedQueryBuilder(String path, QueryBuilder query, ScoreMode scoreMode) { this(path, query, scoreMode, null); @@ -93,6 +96,9 @@ public NestedQueryBuilder(StreamInput in) throws IOException { query = in.readNamedWriteable(QueryBuilder.class); innerHitBuilder = in.readOptionalWriteable(InnerHitBuilder::new); ignoreUnmapped = in.readBoolean(); + if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); + } } @Override @@ -102,6 +108,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeNamedWriteable(query); out.writeOptionalWriteable(innerHitBuilder); out.writeBoolean(ignoreUnmapped); + if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + out.writeNamedWriteableCollection(prefilters); + } } /** @@ -353,6 +362,17 @@ public void extractInnerHitBuilders(Map innerHit } } + @Override + public NestedQueryBuilder setPrefilters(List prefilters) { + this.prefilters = prefilters; + return this; + } + + @Override + public List getPrefilters() { + return prefilters; + } + static class NestedInnerHitContextBuilder extends InnerHitContextBuilder { private final String path; From 4a4c0600e12e04cfaa46a61ceda1a04de1e312aa Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 3 Nov 2025 14:02:44 +0000 Subject: [PATCH 06/17] [CI] Auto commit changes from spotless --- .../java/org/elasticsearch/index/query/BoostingQueryBuilder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java index 340ed3d1f0804..52db60e472a58 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Stream; /** * The BoostingQuery class can be used to effectively demote results that match a given query. From 3a4c3e4d65087b995e02a7fdfd72ae9fcb15b005 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Tue, 4 Nov 2025 13:30:04 +0200 Subject: [PATCH 07/17] Add interface method for declaring prefiltering targets --- .../elasticsearch/index/query/BoolQueryBuilder.java | 9 +++++++-- .../index/query/BoostingQueryBuilder.java | 10 +++++++--- .../index/query/ConstantScoreQueryBuilder.java | 10 +++++++--- .../elasticsearch/index/query/DisMaxQueryBuilder.java | 9 +++++++-- .../elasticsearch/index/query/MatchQueryBuilder.java | 8 ++++++-- .../elasticsearch/index/query/NestedQueryBuilder.java | 9 +++++++-- .../org/elasticsearch/index/query/Prefiltering.java | 6 ++++-- .../query/functionscore/FunctionScoreQueryBuilder.java | 9 +++++++-- .../xpack/inference/queries/SemanticQueryBuilder.java | 7 ++++++- 9 files changed, 58 insertions(+), 19 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 3dda1d66a9397..f40ea259fc53c 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -61,7 +61,7 @@ public class BoolQueryBuilder extends AbstractQueryBuilder imp private String minimumShouldMatch; - private List prefilters = new ArrayList<>(); + private List prefilters = List.of(); /** * Build an empty bool query. @@ -363,7 +363,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws return new MatchAllQueryBuilder().boost(boost()).queryName(queryName()); } - propagatePrefilters(Stream.concat(mustClauses.stream(), shouldClauses.stream()).toList()); + propagatePrefilters(); changed |= rewriteClauses(queryRewriteContext, mustClauses, newBuilder::must); @@ -476,4 +476,9 @@ public BoolQueryBuilder setPrefilters(List prefilters) { public List getPrefilters() { return Stream.concat(prefilters.stream(), filterClauses.stream()).toList(); } + + @Override + public List getPrefilteringTargetQueries() { + return Stream.concat(mustClauses.stream(), shouldClauses.stream()).toList(); + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java index 52db60e472a58..15fdc45222534 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java @@ -20,7 +20,6 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -50,7 +49,7 @@ public class BoostingQueryBuilder extends AbstractQueryBuilder prefilters = new ArrayList<>(); + private List prefilters = List.of(); /** * Create a new {@link BoostingQueryBuilder} @@ -219,7 +218,7 @@ protected boolean doEquals(BoostingQueryBuilder other) { @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { - propagatePrefilters(List.of(positiveQuery)); + propagatePrefilters(); QueryBuilder positiveQuery = this.positiveQuery.rewrite(queryRewriteContext); if (positiveQuery instanceof MatchNoneQueryBuilder) { @@ -256,4 +255,9 @@ public BoostingQueryBuilder setPrefilters(List prefilters) { public List getPrefilters() { return prefilters; } + + @Override + public List getPrefilteringTargetQueries() { + return List.of(positiveQuery); + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java index f27adc6ca4b5c..ec2680d699bb6 100644 --- a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java @@ -20,7 +20,6 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -39,7 +38,7 @@ public class ConstantScoreQueryBuilder extends AbstractQueryBuilder prefilters = new ArrayList<>(); + private List prefilters = List.of(); /** * A query that wraps another query and simply returns a constant score equal to the @@ -158,7 +157,7 @@ protected boolean doEquals(ConstantScoreQueryBuilder other) { @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { - propagatePrefilters(List.of(filterBuilder)); + propagatePrefilters(); QueryBuilder rewrite = filterBuilder.rewrite(queryRewriteContext); if (rewrite instanceof MatchNoneQueryBuilder) { @@ -191,4 +190,9 @@ public ConstantScoreQueryBuilder setPrefilters(List prefilters) { public List getPrefilters() { return prefilters; } + + @Override + public List getPrefilteringTargetQueries() { + return List.of(filterBuilder); + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java index 9f07dd8af1cc8..8990109a3ac8a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java @@ -42,7 +42,7 @@ public class DisMaxQueryBuilder extends AbstractQueryBuilder private static final ParseField QUERIES_FIELD = new ParseField("queries"); private final List queries = new ArrayList<>(); - private List prefilters = new ArrayList<>(); + private List prefilters = List.of(); private float tieBreaker = DEFAULT_TIE_BREAKER; @@ -189,7 +189,7 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { - propagatePrefilters(queries); + propagatePrefilters(); DisMaxQueryBuilder newBuilder = new DisMaxQueryBuilder(); boolean changed = false; @@ -247,4 +247,9 @@ public DisMaxQueryBuilder setPrefilters(List prefilters) { public List getPrefilters() { return prefilters; } + + @Override + public List getPrefilteringTargetQueries() { + return queries; + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java index 13cb8ddcf9269..9209f36ce6657 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java @@ -29,7 +29,6 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -83,7 +82,7 @@ public class MatchQueryBuilder extends AbstractQueryBuilder i private boolean autoGenerateSynonymsPhraseQuery = true; - private List prefilters = new ArrayList<>(); + private List prefilters = List.of(); /** * Constructs a new match query. @@ -591,4 +590,9 @@ public MatchQueryBuilder setPrefilters(List prefilters) { public List getPrefilters() { return prefilters; } + + @Override + public List getPrefilteringTargetQueries() { + return List.of(); + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java index c6ac76fdc4900..147f643fbadbd 100644 --- a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java @@ -45,7 +45,6 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -73,7 +72,7 @@ public class NestedQueryBuilder extends AbstractQueryBuilder private final QueryBuilder query; private InnerHitBuilder innerHitBuilder; private boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED; - private List prefilters = new ArrayList<>(); + private List prefilters = List.of(); public NestedQueryBuilder(String path, QueryBuilder query, ScoreMode scoreMode) { this(path, query, scoreMode, null); @@ -338,6 +337,7 @@ public static Query toQuery( @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { + propagatePrefilters(); QueryBuilder rewrittenQuery = query.rewrite(queryRewriteContext); if (rewrittenQuery != query) { NestedQueryBuilder nestedQuery = new NestedQueryBuilder(path, rewrittenQuery, scoreMode, innerHitBuilder); @@ -373,6 +373,11 @@ public List getPrefilters() { return prefilters; } + @Override + public List getPrefilteringTargetQueries() { + return List.of(query); + } + static class NestedInnerHitContextBuilder extends InnerHitContextBuilder { private final String path; diff --git a/server/src/main/java/org/elasticsearch/index/query/Prefiltering.java b/server/src/main/java/org/elasticsearch/index/query/Prefiltering.java index 40d2150ad3bb5..0c97a91cac16b 100644 --- a/server/src/main/java/org/elasticsearch/index/query/Prefiltering.java +++ b/server/src/main/java/org/elasticsearch/index/query/Prefiltering.java @@ -21,10 +21,12 @@ public interface Prefiltering { List getPrefilters(); - default void propagatePrefilters(List targetQueries) { + List getPrefilteringTargetQueries(); + + default void propagatePrefilters() { List prefilters = getPrefilters(); if (prefilters.isEmpty() == false) { - for (QueryBuilder targetQuery : targetQueries) { + for (QueryBuilder targetQuery : getPrefilteringTargetQueries()) { if (targetQuery instanceof Prefiltering prefilteredQuery) { prefilteredQuery.setPrefilters(prefilters.stream().filter(q -> q != targetQuery).toList()); } diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java index da9f928b351ff..e0829525d7574 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java @@ -78,7 +78,7 @@ public class FunctionScoreQueryBuilder extends AbstractQueryBuilder prefilters = new ArrayList<>(); + private List prefilters = List.of(); /** * Creates a function_score query without functions @@ -345,6 +345,11 @@ public List getPrefilters() { return prefilters; } + @Override + public List getPrefilteringTargetQueries() { + return List.of(query); + } + /** * Function to be associated with an optional filter, meaning it will be executed only for the documents * that match the given filter. @@ -428,7 +433,7 @@ public FilterFunctionBuilder rewrite(QueryRewriteContext context) throws IOExcep @Override protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { - propagatePrefilters(List.of(query)); + propagatePrefilters(); QueryBuilder queryBuilder = this.query.rewrite(queryRewriteContext); if (queryBuilder instanceof MatchNoneQueryBuilder) { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java index ad2d049d43fda..74f850a95134b 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java @@ -102,7 +102,7 @@ public class SemanticQueryBuilder extends AbstractQueryBuilder inferenceResultsMap; private final SetOnce> inferenceResultsMapSupplier; private final Boolean lenient; - private List prefilters = new ArrayList<>(); + private List prefilters = List.of(); // ccsRequest is only used on the local cluster coordinator node to detect when: // - The request references a remote index @@ -681,4 +681,9 @@ public SemanticQueryBuilder setPrefilters(List prefilters) { public List getPrefilters() { return prefilters; } + + @Override + public List getPrefilteringTargetQueries() { + return List.of(); + } } From 2dd0b4374b8c0a932329481a7e0cb5ea0db90eb8 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Tue, 4 Nov 2025 13:45:05 +0200 Subject: [PATCH 08/17] Add bool query `must_not` clauses as prefilters --- .../org/elasticsearch/index/query/BoolQueryBuilder.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index f40ea259fc53c..634aa253069d2 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -26,10 +26,12 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.elasticsearch.common.lucene.search.Queries.fixNegativeQueryIfNeeded; @@ -474,7 +476,9 @@ public BoolQueryBuilder setPrefilters(List prefilters) { @Override public List getPrefilters() { - return Stream.concat(prefilters.stream(), filterClauses.stream()).toList(); + return Stream.of(prefilters, filterClauses, mustNotClauses.stream().map(c -> QueryBuilders.boolQuery().mustNot(c)).toList()) + .flatMap(Collection::stream) + .collect(Collectors.toList()); } @Override From 7291d4e7735c7de87411e6ec190ab99b99963470 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 5 Nov 2025 11:48:58 +0200 Subject: [PATCH 09/17] Rename Prefiltering -> PrefilteredQuery --- .../org/elasticsearch/index/query/BoolQueryBuilder.java | 6 +++--- .../elasticsearch/index/query/BoostingQueryBuilder.java | 6 +++--- .../index/query/ConstantScoreQueryBuilder.java | 6 +++--- .../org/elasticsearch/index/query/DisMaxQueryBuilder.java | 6 +++--- .../org/elasticsearch/index/query/MatchQueryBuilder.java | 6 +++--- .../org/elasticsearch/index/query/NestedQueryBuilder.java | 6 +++--- .../query/{Prefiltering.java => PrefilteredQuery.java} | 4 ++-- .../query/functionscore/FunctionScoreQueryBuilder.java | 8 ++++---- .../elasticsearch/index/query/PrefilteringTestUtils.java | 4 ++-- .../xpack/inference/queries/SemanticQueryBuilder.java | 8 ++++---- 10 files changed, 30 insertions(+), 30 deletions(-) rename server/src/main/java/org/elasticsearch/index/query/{Prefiltering.java => PrefilteredQuery.java} (89%) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 634aa253069d2..519eac6f49e71 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -39,7 +39,7 @@ /** * A Query that matches documents matching boolean combinations of other queries. */ -public class BoolQueryBuilder extends AbstractQueryBuilder implements Prefiltering { +public class BoolQueryBuilder extends AbstractQueryBuilder implements PrefilteredQuery { public static final String NAME = "bool"; public static final boolean ADJUST_PURE_NEGATIVE_DEFAULT = true; @@ -81,7 +81,7 @@ public BoolQueryBuilder(StreamInput in) throws IOException { filterClauses.addAll(readQueries(in)); adjustPureNegative = in.readBoolean(); minimumShouldMatch = in.readOptionalString(); - if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (in.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -94,7 +94,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { writeQueries(out, filterClauses); out.writeBoolean(adjustPureNegative); out.writeOptionalString(minimumShouldMatch); - if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (out.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { out.writeNamedWriteableCollection(prefilters); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java index 15fdc45222534..9fb4cba3c7ba0 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java @@ -36,7 +36,7 @@ * multiplied by the supplied "boost" parameter, so this should be less than 1 to achieve a * demoting effect */ -public class BoostingQueryBuilder extends AbstractQueryBuilder implements Prefiltering { +public class BoostingQueryBuilder extends AbstractQueryBuilder implements PrefilteredQuery { public static final String NAME = "boosting"; private static final ParseField POSITIVE_FIELD = new ParseField("positive"); @@ -76,7 +76,7 @@ public BoostingQueryBuilder(StreamInput in) throws IOException { positiveQuery = in.readNamedWriteable(QueryBuilder.class); negativeQuery = in.readNamedWriteable(QueryBuilder.class); negativeBoost = in.readFloat(); - if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (in.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -86,7 +86,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeNamedWriteable(positiveQuery); out.writeNamedWriteable(negativeQuery); out.writeFloat(negativeBoost); - if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (out.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { out.writeNamedWriteableCollection(prefilters); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java index ec2680d699bb6..2827a7e9ff9ab 100644 --- a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java @@ -30,7 +30,7 @@ */ public class ConstantScoreQueryBuilder extends AbstractQueryBuilder implements - Prefiltering { + PrefilteredQuery { public static final String NAME = "constant_score"; @@ -59,7 +59,7 @@ public ConstantScoreQueryBuilder(QueryBuilder filterBuilder) { public ConstantScoreQueryBuilder(StreamInput in) throws IOException { super(in); filterBuilder = in.readNamedWriteable(QueryBuilder.class); - if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (in.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -67,7 +67,7 @@ public ConstantScoreQueryBuilder(StreamInput in) throws IOException { @Override protected void doWriteTo(StreamOutput out) throws IOException { out.writeNamedWriteable(filterBuilder); - if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (out.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { out.writeNamedWriteableCollection(prefilters); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java index 8990109a3ac8a..b88ff95e1da2a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java @@ -32,7 +32,7 @@ * with the maximum score for that document as produced by any sub-query, plus a tie breaking increment for any * additional matching sub-queries. */ -public class DisMaxQueryBuilder extends AbstractQueryBuilder implements Prefiltering { +public class DisMaxQueryBuilder extends AbstractQueryBuilder implements PrefilteredQuery { public static final String NAME = "dis_max"; /** Default multiplication factor for breaking ties in document scores.*/ @@ -55,7 +55,7 @@ public DisMaxQueryBuilder(StreamInput in) throws IOException { super(in); queries.addAll(readQueries(in)); tieBreaker = in.readFloat(); - if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (in.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { prefilters = readQueries(in); } } @@ -64,7 +64,7 @@ public DisMaxQueryBuilder(StreamInput in) throws IOException { protected void doWriteTo(StreamOutput out) throws IOException { writeQueries(out, queries); out.writeFloat(tieBreaker); - if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (out.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { out.writeNamedWriteableCollection(prefilters); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java index 9209f36ce6657..87f64f700f997 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java @@ -36,7 +36,7 @@ * Match query is a query that analyzes the text and constructs a query as the * result of the analysis. */ -public class MatchQueryBuilder extends AbstractQueryBuilder implements Prefiltering { +public class MatchQueryBuilder extends AbstractQueryBuilder implements PrefilteredQuery { public static final ParseField ZERO_TERMS_QUERY_FIELD = new ParseField("zero_terms_query"); public static final ParseField LENIENT_FIELD = new ParseField("lenient"); @@ -121,7 +121,7 @@ public MatchQueryBuilder(StreamInput in) throws IOException { in.readOptionalFloat(); } autoGenerateSynonymsPhraseQuery = in.readBoolean(); - if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (in.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -146,7 +146,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeOptionalFloat(null); } out.writeBoolean(autoGenerateSynonymsPhraseQuery); - if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (out.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { out.writeNamedWriteableCollection(prefilters); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java index 147f643fbadbd..27ae3e36fc6bf 100644 --- a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java @@ -54,7 +54,7 @@ import static org.elasticsearch.search.SearchService.ALLOW_EXPENSIVE_QUERIES; import static org.elasticsearch.search.fetch.subphase.InnerHitsContext.intersect; -public class NestedQueryBuilder extends AbstractQueryBuilder implements Prefiltering { +public class NestedQueryBuilder extends AbstractQueryBuilder implements PrefilteredQuery { public static final String NAME = "nested"; /** * The default value for ignore_unmapped. @@ -95,7 +95,7 @@ public NestedQueryBuilder(StreamInput in) throws IOException { query = in.readNamedWriteable(QueryBuilder.class); innerHitBuilder = in.readOptionalWriteable(InnerHitBuilder::new); ignoreUnmapped = in.readBoolean(); - if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (in.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -107,7 +107,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeNamedWriteable(query); out.writeOptionalWriteable(innerHitBuilder); out.writeBoolean(ignoreUnmapped); - if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (out.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { out.writeNamedWriteableCollection(prefilters); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/Prefiltering.java b/server/src/main/java/org/elasticsearch/index/query/PrefilteredQuery.java similarity index 89% rename from server/src/main/java/org/elasticsearch/index/query/Prefiltering.java rename to server/src/main/java/org/elasticsearch/index/query/PrefilteredQuery.java index 0c97a91cac16b..e3255a6273776 100644 --- a/server/src/main/java/org/elasticsearch/index/query/Prefiltering.java +++ b/server/src/main/java/org/elasticsearch/index/query/PrefilteredQuery.java @@ -13,7 +13,7 @@ import java.util.List; -public interface Prefiltering { +public interface PrefilteredQuery { TransportVersion QUERY_PREFILTERING = TransportVersion.fromName("query_prefiltering"); @@ -27,7 +27,7 @@ default void propagatePrefilters() { List prefilters = getPrefilters(); if (prefilters.isEmpty() == false) { for (QueryBuilder targetQuery : getPrefilteringTargetQueries()) { - if (targetQuery instanceof Prefiltering prefilteredQuery) { + if (targetQuery instanceof PrefilteredQuery prefilteredQuery) { prefilteredQuery.setPrefilters(prefilters.stream().filter(q -> q != targetQuery).toList()); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java index e0829525d7574..6a34f82334f3a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java @@ -23,7 +23,7 @@ import org.elasticsearch.index.query.InnerHitContextBuilder; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.MatchNoneQueryBuilder; -import org.elasticsearch.index.query.Prefiltering; +import org.elasticsearch.index.query.PrefilteredQuery; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.SearchExecutionContext; @@ -47,7 +47,7 @@ */ public class FunctionScoreQueryBuilder extends AbstractQueryBuilder implements - Prefiltering { + PrefilteredQuery { public static final String NAME = "function_score"; @@ -150,7 +150,7 @@ public FunctionScoreQueryBuilder(StreamInput in) throws IOException { minScore = in.readOptionalFloat(); boostMode = in.readOptionalWriteable(CombineFunction::readFromStream); scoreMode = FunctionScoreQuery.ScoreMode.readFromStream(in); - if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (in.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } } @@ -163,7 +163,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeOptionalFloat(minScore); out.writeOptionalWriteable(boostMode); scoreMode.writeTo(out); - if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (out.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { out.writeNamedWriteableCollection(prefilters); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java b/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java index 38a93a7dff22a..49c7bb0270aa7 100644 --- a/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java +++ b/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java @@ -31,7 +31,7 @@ public class PrefilteringTestUtils { - public static void setRandomTermQueryPrefilters(Prefiltering queryBuilder, String... termFieldNames) { + public static void setRandomTermQueryPrefilters(PrefilteredQuery queryBuilder, String... termFieldNames) { List filters = new ArrayList<>(); int numFilters = randomIntBetween(1, 5); for (int i = 0; i < numFilters; i++) { @@ -41,7 +41,7 @@ public static void setRandomTermQueryPrefilters(Prefiltering queryBuilder, St queryBuilder.setPrefilters(filters); } - public static void assertQueryHasPrefilters(Prefiltering queryBuilder, Query query, SearchExecutionContext context) + public static void assertQueryHasPrefilters(PrefilteredQuery queryBuilder, Query query, SearchExecutionContext context) throws IOException { assertThat(query, anyOf(instanceOf(KnnFloatVectorQuery.class), instanceOf(KnnByteVectorQuery.class))); Query queryFilter; diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java index 74f850a95134b..8ecf575148760 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java @@ -24,7 +24,7 @@ import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.MatchNoneQueryBuilder; -import org.elasticsearch.index.query.Prefiltering; +import org.elasticsearch.index.query.PrefilteredQuery; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.SearchExecutionContext; @@ -63,7 +63,7 @@ import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; -public class SemanticQueryBuilder extends AbstractQueryBuilder implements Prefiltering { +public class SemanticQueryBuilder extends AbstractQueryBuilder implements PrefilteredQuery { public static final String NAME = "semantic"; public static final NodeFeature SEMANTIC_QUERY_MULTIPLE_INFERENCE_IDS = new NodeFeature("semantic_query.multiple_inference_ids"); @@ -180,7 +180,7 @@ public SemanticQueryBuilder(StreamInput in) throws IOException { this.ccsRequest = false; } - if (in.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (in.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { this.prefilters = in.readNamedWriteableCollectionAsList(QueryBuilder.class); } @@ -237,7 +237,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { ); } - if (out.getTransportVersion().supports(Prefiltering.QUERY_PREFILTERING)) { + if (out.getTransportVersion().supports(PrefilteredQuery.QUERY_PREFILTERING)) { out.writeNamedWriteableCollection(prefilters); } } From 2d9c7e66a128c374a93622703f2df396f47f965e Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 5 Nov 2025 12:09:42 +0200 Subject: [PATCH 10/17] Adds a comment --- .../java/org/elasticsearch/index/query/BoolQueryBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 519eac6f49e71..0fee73c7f8303 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -476,6 +476,7 @@ public BoolQueryBuilder setPrefilters(List prefilters) { @Override public List getPrefilters() { + // We declare as prefilters clauses run in the filter context, namely filter and must_not return Stream.of(prefilters, filterClauses, mustNotClauses.stream().map(c -> QueryBuilders.boolQuery().mustNot(c)).toList()) .flatMap(Collection::stream) .collect(Collectors.toList()); From a53c9f2b5035a12d74483993ef6357407821beb2 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 5 Nov 2025 12:35:41 +0200 Subject: [PATCH 11/17] Fix prefilters assignment on SemanticQueryBuilder copy --- .../xpack/inference/queries/SemanticQueryBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java index 8ecf575148760..1332c50a163ea 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java @@ -257,7 +257,7 @@ private SemanticQueryBuilder( this.inferenceResultsMapSupplier = inferenceResultsMapSupplier; this.lenient = other.lenient; this.ccsRequest = ccsRequest; - this.prefilters.addAll(other.prefilters); + this.prefilters = other.prefilters; } @Override From f41f0f1480f8c40f2bc1d8a4d89cd7e463dbb823 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 5 Nov 2025 15:23:04 +0200 Subject: [PATCH 12/17] SemanticQueryBuilderTests --- .../index/query/PrefilteringTestUtils.java | 36 -------- .../test/AbstractQueryTestCase.java | 5 ++ .../queries/SemanticQueryBuilder.java | 5 +- .../queries/SemanticQueryBuilderTests.java | 84 ++++++++++++++----- 4 files changed, 71 insertions(+), 59 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java b/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java index 49c7bb0270aa7..b0cd0e575a0f9 100644 --- a/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java +++ b/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java @@ -9,25 +9,12 @@ package org.elasticsearch.index.query; -import org.apache.lucene.search.BooleanClause; -import org.apache.lucene.search.BooleanQuery; -import org.apache.lucene.search.KnnByteVectorQuery; -import org.apache.lucene.search.KnnFloatVectorQuery; -import org.apache.lucene.search.Query; - -import java.io.IOException; import java.util.ArrayList; import java.util.List; import static org.elasticsearch.test.ESTestCase.randomAlphaOfLength; import static org.elasticsearch.test.ESTestCase.randomFrom; import static org.elasticsearch.test.ESTestCase.randomIntBetween; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; public class PrefilteringTestUtils { @@ -41,27 +28,4 @@ public static void setRandomTermQueryPrefilters(PrefilteredQuery queryBuilder queryBuilder.setPrefilters(filters); } - public static void assertQueryHasPrefilters(PrefilteredQuery queryBuilder, Query query, SearchExecutionContext context) - throws IOException { - assertThat(query, anyOf(instanceOf(KnnFloatVectorQuery.class), instanceOf(KnnByteVectorQuery.class))); - Query queryFilter; - if (query instanceof KnnFloatVectorQuery q) { - queryFilter = q.getFilter(); - } else if (query instanceof KnnByteVectorQuery q) { - queryFilter = q.getFilter(); - } else { - throw new IllegalStateException("Unexpected query type " + query.getClass()); - } - - if (queryBuilder.getPrefilters().isEmpty()) { - assertThat(queryFilter, is(nullValue())); - } else { - for (QueryBuilder qb : queryBuilder.getPrefilters()) { - BooleanQuery.Builder builder = new BooleanQuery.Builder(); - builder.add(qb.toQuery(context), BooleanClause.Occur.MUST); - BooleanQuery booleanQuery = builder.build(); - assertThat(queryFilter.toString(), containsString(booleanQuery.toString())); - } - } - } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java index be4443c84eb02..45dc794fa860a 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java @@ -735,6 +735,11 @@ protected QB changeNameOrBoost(QB original) throws IOException { return secondQuery; } + @Override + protected Settings createTestIndexSettings() { + return super.createTestIndexSettings(); + } + // we use the streaming infra to create a copy of the query provided as argument @SuppressWarnings("unchecked") protected QB copyQuery(QB query) throws IOException { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java index 1332c50a163ea..68b455a7e70ef 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java @@ -663,12 +663,13 @@ protected boolean doEquals(SemanticQueryBuilder other) { && Objects.equals(query, other.query) && Objects.equals(inferenceResultsMap, other.inferenceResultsMap) && Objects.equals(inferenceResultsMapSupplier, other.inferenceResultsMapSupplier) - && Objects.equals(ccsRequest, other.ccsRequest); + && Objects.equals(ccsRequest, other.ccsRequest) + && Objects.equals(prefilters, other.prefilters); } @Override protected int doHashCode() { - return Objects.hash(fieldName, query, inferenceResultsMap, inferenceResultsMapSupplier, ccsRequest); + return Objects.hash(fieldName, query, inferenceResultsMap, inferenceResultsMapSupplier, ccsRequest, prefilters); } @Override diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java index 01469556eaa80..d5e9dd821726b 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java @@ -44,6 +44,8 @@ import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapperTestUtils; import org.elasticsearch.index.query.MatchNoneQueryBuilder; +import org.elasticsearch.index.query.NestedQueryBuilder; +import org.elasticsearch.index.query.PrefilteredQuery; import org.elasticsearch.index.query.PrefilteringTestUtils; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; @@ -57,6 +59,7 @@ import org.elasticsearch.inference.TaskType; import org.elasticsearch.inference.WeightedToken; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.vectors.KnnVectorQueryBuilder; import org.elasticsearch.search.vectors.SparseVectorQueryWrapper; import org.elasticsearch.test.AbstractQueryTestCase; import org.elasticsearch.test.ClusterServiceUtils; @@ -72,6 +75,7 @@ import org.elasticsearch.xpack.core.inference.results.SparseEmbeddingResults; import org.elasticsearch.xpack.core.ml.inference.results.MlDenseEmbeddingResults; import org.elasticsearch.xpack.core.ml.inference.results.TextExpansionResults; +import org.elasticsearch.xpack.core.ml.search.SparseVectorQueryBuilder; import org.elasticsearch.xpack.inference.FakeMlPlugin; import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.inference.mapper.SemanticTextField; @@ -244,9 +248,6 @@ protected SemanticQueryBuilder doCreateTestQueryBuilder() { if (randomBoolean()) { builder.queryName(randomAlphaOfLength(4)); } - if (randomBoolean()) { - PrefilteringTestUtils.setRandomTermQueryPrefilters(builder, KEYWORD_FIELD_NAME, TEXT_FIELD_NAME); - } return builder; } @@ -263,7 +264,7 @@ protected void doAssertLuceneQuery(SemanticQueryBuilder queryBuilder, Query quer switch (inferenceResultType) { case NONE -> assertThat(nestedQuery.getChildQuery(), instanceOf(MatchNoDocsQuery.class)); case SPARSE_EMBEDDING -> assertSparseEmbeddingLuceneQuery(nestedQuery.getChildQuery()); - case TEXT_EMBEDDING -> assertTextEmbeddingLuceneQuery(queryBuilder, nestedQuery.getChildQuery(), context); + case TEXT_EMBEDDING -> assertTextEmbeddingLuceneQuery(nestedQuery.getChildQuery()); } } @@ -278,22 +279,14 @@ private void assertSparseEmbeddingLuceneQuery(Query query) { assertThat(innerBooleanQuery.clauses().size(), equalTo(0)); } - private void assertTextEmbeddingLuceneQuery(SemanticQueryBuilder queryBuilder, Query query, SearchExecutionContext context) - throws IOException { + private void assertTextEmbeddingLuceneQuery(Query query) { Query innerQuery = assertOuterBooleanQuery(query); - switch (denseVectorElementType) { - case FLOAT: { - assertThat(innerQuery, instanceOf(KnnFloatVectorQuery.class)); - PrefilteringTestUtils.assertQueryHasPrefilters(queryBuilder, innerQuery, context); - break; - } - case BYTE, BIT: { - assertThat(innerQuery, instanceOf(KnnByteVectorQuery.class)); - PrefilteringTestUtils.assertQueryHasPrefilters(queryBuilder, innerQuery, context); - break; - } - } + Class expectedKnnQueryClass = switch (denseVectorElementType) { + case FLOAT -> KnnFloatVectorQuery.class; + case BYTE, BIT -> KnnByteVectorQuery.class; + }; + assertThat(innerQuery, instanceOf(expectedKnnQueryClass)); } private Query assertOuterBooleanQuery(Query query) { @@ -437,9 +430,6 @@ public void testSerializationBwc() throws IOException { null, Map.of(new FullyQualifiedInferenceId(LOCAL_CLUSTER_GROUP_KEY, randomAlphaOfLength(5)), inferenceResults) ); - if (randomBoolean()) { - originalQuery.setPrefilters(randomList(1, 5, () -> RandomQueryBuilder.createQuery(random()))); - } SemanticQueryBuilder bwcQuery = new SemanticQueryBuilder( fieldName, query, @@ -572,6 +562,58 @@ public void testSerializingQueryWhenNoInferenceId() throws IOException { assertThat(rewritten, instanceOf(MatchNoneQueryBuilder.class)); } + public void testRewriteWithPrefilters() throws IOException { + QueryRewriteContext queryRewriteContext = createQueryRewriteContext(); + SearchExecutionContext searchExecutionContext = createSearchExecutionContext(); + SemanticQueryBuilder queryBuilder = doCreateTestQueryBuilder(); + PrefilteringTestUtils.setRandomTermQueryPrefilters(queryBuilder, KEYWORD_FIELD_NAME, TEXT_FIELD_NAME); + + QueryBuilder rewritten = rewriteQuery(queryBuilder, queryRewriteContext, searchExecutionContext); + + assertThat(rewritten, instanceOf(NestedQueryBuilder.class)); + NestedQueryBuilder nestedQueryBuilder = (NestedQueryBuilder) rewritten; + switch (inferenceResultType) { + case NONE -> assertThat(nestedQueryBuilder.query(), instanceOf(MatchNoneQueryBuilder.class)); + case SPARSE_EMBEDDING -> assertThat(nestedQueryBuilder.query(), instanceOf(SparseVectorQueryBuilder.class)); + case TEXT_EMBEDDING -> assertVectorQueryBuilderWithPrefilters(nestedQueryBuilder.query(), queryBuilder.getPrefilters()); + default -> fail("Unexpected inference result type [" + inferenceResultType + "]"); + } + } + + public void testSerializationPrefiltersBwc() throws Exception { + SemanticQueryBuilder originalQuery = new SemanticQueryBuilder(randomAlphaOfLength(5), randomAlphaOfLength(5)); + if (randomBoolean()) { + originalQuery.setPrefilters(randomList(1, 5, () -> RandomQueryBuilder.createQuery(random()))); + } + + for (int i = 0; i < 100; i++) { + TransportVersion transportVersion = TransportVersionUtils.randomVersionBetween( + random(), + originalQuery.getMinimalSupportedVersion(), + TransportVersionUtils.getPreviousVersion(TransportVersion.current()) + ); + + QueryBuilder deserializedQuery = copyNamedWriteable( + originalQuery, + namedWriteableRegistry(), + QueryBuilder.class, + transportVersion + ); + + if (transportVersion.supports(PrefilteredQuery.QUERY_PREFILTERING) == false) { + ((SemanticQueryBuilder) deserializedQuery).setPrefilters(List.of()); + } + + assertThat(deserializedQuery, instanceOf(SemanticQueryBuilder.class)); + } + } + + private static void assertVectorQueryBuilderWithPrefilters(QueryBuilder queryBuilder, List prefilters) { + assertThat(queryBuilder, instanceOf(KnnVectorQueryBuilder.class)); + KnnVectorQueryBuilder knnVectorQueryBuilder = (KnnVectorQueryBuilder) queryBuilder; + assertThat(knnVectorQueryBuilder.filterQueries(), equalTo(prefilters)); + } + private static SourceToParse buildSemanticTextFieldWithInferenceResults( InferenceResultType inferenceResultType, DenseVectorFieldMapper.ElementType denseVectorElementType, From 72a486126bc30aaacee2cbbe0b89a82d148aab56 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 5 Nov 2025 15:24:32 +0200 Subject: [PATCH 13/17] clean up --- .../java/org/elasticsearch/test/AbstractQueryTestCase.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java index 45dc794fa860a..be4443c84eb02 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java @@ -735,11 +735,6 @@ protected QB changeNameOrBoost(QB original) throws IOException { return secondQuery; } - @Override - protected Settings createTestIndexSettings() { - return super.createTestIndexSettings(); - } - // we use the streaming infra to create a copy of the query provided as argument @SuppressWarnings("unchecked") protected QB copyQuery(QB query) throws IOException { From a672a52ca4f403ec973a1c6f34b7439ceccc9b12 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 5 Nov 2025 15:34:55 +0200 Subject: [PATCH 14/17] Boosting query should also prefilter negative query --- .../org/elasticsearch/index/query/BoostingQueryBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java index 9fb4cba3c7ba0..d22667b731399 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java @@ -258,6 +258,6 @@ public List getPrefilters() { @Override public List getPrefilteringTargetQueries() { - return List.of(positiveQuery); + return List.of(positiveQuery, negativeQuery); } } From 45c39eb4d3edac0059309d1d04e90c885b9310bb Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 5 Nov 2025 17:54:25 +0200 Subject: [PATCH 15/17] Serialization and equality tests --- .../index/query/BoolQueryBuilder.java | 5 +- .../index/query/BoostingQueryBuilder.java | 5 +- .../query/ConstantScoreQueryBuilder.java | 4 +- .../index/query/DisMaxQueryBuilder.java | 6 +- .../index/query/MatchQueryBuilder.java | 6 +- .../index/query/NestedQueryBuilder.java | 5 +- .../FunctionScoreQueryBuilder.java | 6 +- .../AbstractPrefilteredQueryTestCase.java | 66 +++++++++++++++++++ .../index/query/BoolQueryBuilderTests.java | 3 +- .../query/BoostingQueryBuilderTests.java | 3 +- .../query/ConstantScoreQueryBuilderTests.java | 3 +- .../index/query/DisMaxQueryBuilderTests.java | 3 +- .../index/query/MatchQueryBuilderTests.java | 3 +- .../index/query/NestedQueryBuilderTests.java | 3 +- .../FunctionScoreQueryBuilderTests.java | 4 +- .../queries/SemanticQueryBuilderTests.java | 34 +--------- 16 files changed, 99 insertions(+), 60 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 0fee73c7f8303..92fa0bb3cceed 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -343,7 +343,7 @@ private static void addBooleanClauses( @Override protected int doHashCode() { - return Objects.hash(adjustPureNegative, minimumShouldMatch, mustClauses, shouldClauses, mustNotClauses, filterClauses); + return Objects.hash(adjustPureNegative, minimumShouldMatch, mustClauses, shouldClauses, mustNotClauses, filterClauses, prefilters); } @Override @@ -353,7 +353,8 @@ protected boolean doEquals(BoolQueryBuilder other) { && Objects.equals(mustClauses, other.mustClauses) && Objects.equals(shouldClauses, other.shouldClauses) && Objects.equals(mustNotClauses, other.mustNotClauses) - && Objects.equals(filterClauses, other.filterClauses); + && Objects.equals(filterClauses, other.filterClauses) + && Objects.equals(prefilters, other.prefilters); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java index d22667b731399..0ad2e55e8502b 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoostingQueryBuilder.java @@ -206,14 +206,15 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { @Override protected int doHashCode() { - return Objects.hash(negativeBoost, positiveQuery, negativeQuery); + return Objects.hash(negativeBoost, positiveQuery, negativeQuery, prefilters); } @Override protected boolean doEquals(BoostingQueryBuilder other) { return Objects.equals(negativeBoost, other.negativeBoost) && Objects.equals(positiveQuery, other.positiveQuery) - && Objects.equals(negativeQuery, other.negativeQuery); + && Objects.equals(negativeQuery, other.negativeQuery) + && Objects.equals(prefilters, other.prefilters); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java index 2827a7e9ff9ab..ecd223b131fe1 100644 --- a/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/ConstantScoreQueryBuilder.java @@ -147,12 +147,12 @@ public String getWriteableName() { @Override protected int doHashCode() { - return Objects.hash(filterBuilder); + return Objects.hash(filterBuilder, prefilters); } @Override protected boolean doEquals(ConstantScoreQueryBuilder other) { - return Objects.equals(filterBuilder, other.filterBuilder); + return Objects.equals(filterBuilder, other.filterBuilder) && Objects.equals(prefilters, other.prefilters); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java index b88ff95e1da2a..e2ad52a142eaf 100644 --- a/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/DisMaxQueryBuilder.java @@ -212,12 +212,14 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws @Override protected int doHashCode() { - return Objects.hash(queries, tieBreaker); + return Objects.hash(queries, tieBreaker, prefilters); } @Override protected boolean doEquals(DisMaxQueryBuilder other) { - return Objects.equals(queries, other.queries) && Objects.equals(tieBreaker, other.tieBreaker); + return Objects.equals(queries, other.queries) + && Objects.equals(tieBreaker, other.tieBreaker) + && Objects.equals(prefilters, other.prefilters); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java index 87f64f700f997..623ab559193bb 100644 --- a/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/MatchQueryBuilder.java @@ -439,7 +439,8 @@ protected boolean doEquals(MatchQueryBuilder other) { && Objects.equals(lenient, other.lenient) && Objects.equals(fuzzyTranspositions, other.fuzzyTranspositions) && Objects.equals(zeroTermsQuery, other.zeroTermsQuery) - && Objects.equals(autoGenerateSynonymsPhraseQuery, other.autoGenerateSynonymsPhraseQuery); + && Objects.equals(autoGenerateSynonymsPhraseQuery, other.autoGenerateSynonymsPhraseQuery) + && Objects.equals(prefilters, other.prefilters); } @Override @@ -457,7 +458,8 @@ protected int doHashCode() { lenient, fuzzyTranspositions, zeroTermsQuery, - autoGenerateSynonymsPhraseQuery + autoGenerateSynonymsPhraseQuery, + prefilters ); } diff --git a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java index 27ae3e36fc6bf..260af2af09c6e 100644 --- a/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java @@ -266,12 +266,13 @@ protected boolean doEquals(NestedQueryBuilder that) { && Objects.equals(path, that.path) && Objects.equals(scoreMode, that.scoreMode) && Objects.equals(innerHitBuilder, that.innerHitBuilder) - && Objects.equals(ignoreUnmapped, that.ignoreUnmapped); + && Objects.equals(ignoreUnmapped, that.ignoreUnmapped) + && Objects.equals(prefilters, that.prefilters); } @Override protected int doHashCode() { - return Objects.hash(query, path, scoreMode, innerHitBuilder, ignoreUnmapped); + return Objects.hash(query, path, scoreMode, innerHitBuilder, ignoreUnmapped, prefilters); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java index 6a34f82334f3a..657e65cac855b 100644 --- a/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilder.java @@ -289,7 +289,8 @@ protected boolean doEquals(FunctionScoreQueryBuilder other) { && Objects.equals(this.boostMode, other.boostMode) && Objects.equals(this.scoreMode, other.scoreMode) && Objects.equals(this.minScore, other.minScore) - && Objects.equals(this.maxBoost, other.maxBoost); + && Objects.equals(this.maxBoost, other.maxBoost) + && Objects.equals(this.prefilters, other.prefilters); } @Override @@ -300,7 +301,8 @@ protected int doHashCode() { this.boostMode, this.scoreMode, this.minScore, - this.maxBoost + this.maxBoost, + this.prefilters ); } diff --git a/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java b/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java new file mode 100644 index 0000000000000..c7f2685ebd449 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; +import org.elasticsearch.test.AbstractQueryTestCase; +import org.elasticsearch.test.TransportVersionUtils; + +import java.io.IOException; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; + +public abstract class AbstractPrefilteredQueryTestCase & PrefilteredQuery> extends + AbstractQueryTestCase { + + public void testSerializationPrefiltersBwc() throws Exception { + QB originalQuery = createTestQueryBuilder(); + originalQuery.setPrefilters(randomList(1, 5, () -> RandomQueryBuilder.createQuery(random()))); + + for (int i = 0; i < 100; i++) { + TransportVersion transportVersion = TransportVersionUtils.randomVersionBetween( + random(), + originalQuery.getMinimalSupportedVersion().id() == 0 + ? TransportVersions.V_8_0_0 // The first major before introducing prefiltering + : originalQuery.getMinimalSupportedVersion(), + TransportVersionUtils.getPreviousVersion(TransportVersion.current()) + ); + + @SuppressWarnings("unchecked") + QB deserializedQuery = (QB) copyNamedWriteable(originalQuery, namedWriteableRegistry(), QueryBuilder.class, transportVersion); + + if (transportVersion.supports(PrefilteredQuery.QUERY_PREFILTERING)) { + assertThat(deserializedQuery, equalTo(originalQuery)); + } else { + QB originalQueryWithoutPrefilters = copyQuery(originalQuery).setPrefilters(List.of()); + assertThat(deserializedQuery, equalTo(originalQueryWithoutPrefilters)); + } + } + } + + public void testEqualsAndHashcodeForPrefilters() throws IOException { + QB originalQuery = createTestQueryBuilder(); + originalQuery.setPrefilters(randomList(1, 5, () -> RandomQueryBuilder.createQuery(random()))); + + @SuppressWarnings("unchecked") + QB deserializedQuery = (QB) copyNamedWriteable(originalQuery, namedWriteableRegistry(), QueryBuilder.class); + + assertThat(deserializedQuery, equalTo(originalQuery)); + assertThat(deserializedQuery.hashCode(), equalTo(originalQuery.hashCode())); + + deserializedQuery.setPrefilters(List.of()); + assertThat(deserializedQuery, not(equalTo(originalQuery))); + assertThat(deserializedQuery.hashCode(), not(equalTo(originalQuery.hashCode()))); + } + +} diff --git a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java index e9ef3ac8ad748..baa36e9c98fc9 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java @@ -14,7 +14,6 @@ import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; -import org.elasticsearch.test.AbstractQueryTestCase; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParseException; @@ -37,7 +36,7 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.not; -public class BoolQueryBuilderTests extends AbstractQueryTestCase { +public class BoolQueryBuilderTests extends AbstractPrefilteredQueryTestCase { @Override protected BoolQueryBuilder doCreateTestQueryBuilder() { BoolQueryBuilder query = new BoolQueryBuilder(); diff --git a/server/src/test/java/org/elasticsearch/index/query/BoostingQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoostingQueryBuilderTests.java index 763c9b585256e..4c8fbcd05154f 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoostingQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoostingQueryBuilderTests.java @@ -12,14 +12,13 @@ import org.apache.lucene.queries.function.FunctionScoreQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; -import org.elasticsearch.test.AbstractQueryTestCase; import java.io.IOException; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.nullValue; -public class BoostingQueryBuilderTests extends AbstractQueryTestCase { +public class BoostingQueryBuilderTests extends AbstractPrefilteredQueryTestCase { @Override protected BoostingQueryBuilder doCreateTestQueryBuilder() { diff --git a/server/src/test/java/org/elasticsearch/index/query/ConstantScoreQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/ConstantScoreQueryBuilderTests.java index ce7480f643c08..63149c9e8adfc 100644 --- a/server/src/test/java/org/elasticsearch/index/query/ConstantScoreQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/ConstantScoreQueryBuilderTests.java @@ -14,7 +14,6 @@ import org.apache.lucene.search.Query; import org.elasticsearch.common.ParsingException; import org.elasticsearch.core.Strings; -import org.elasticsearch.test.AbstractQueryTestCase; import java.io.IOException; @@ -22,7 +21,7 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.Matchers.containsString; -public class ConstantScoreQueryBuilderTests extends AbstractQueryTestCase { +public class ConstantScoreQueryBuilderTests extends AbstractPrefilteredQueryTestCase { /** * @return a {@link ConstantScoreQueryBuilder} with random boost between 0.1f and 2.0f */ diff --git a/server/src/test/java/org/elasticsearch/index/query/DisMaxQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/DisMaxQueryBuilderTests.java index 0b7893312b997..827837da42c5f 100644 --- a/server/src/test/java/org/elasticsearch/index/query/DisMaxQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/DisMaxQueryBuilderTests.java @@ -15,7 +15,6 @@ import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.elasticsearch.core.Strings; -import org.elasticsearch.test.AbstractQueryTestCase; import java.io.IOException; import java.util.Collection; @@ -23,7 +22,7 @@ import java.util.List; import java.util.Map; -public class DisMaxQueryBuilderTests extends AbstractQueryTestCase { +public class DisMaxQueryBuilderTests extends AbstractPrefilteredQueryTestCase { /** * @return a {@link DisMaxQueryBuilder} with random inner queries */ diff --git a/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java index ba46bf76efbfe..a04c152082788 100644 --- a/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java @@ -40,7 +40,6 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.search.MatchQueryParser; import org.elasticsearch.index.search.MatchQueryParser.Type; -import org.elasticsearch.test.AbstractQueryTestCase; import org.hamcrest.Matcher; import org.hamcrest.Matchers; @@ -57,7 +56,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; -public class MatchQueryBuilderTests extends AbstractQueryTestCase { +public class MatchQueryBuilderTests extends AbstractPrefilteredQueryTestCase { @Override protected MatchQueryBuilder doCreateTestQueryBuilder() { diff --git a/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java index 39520db299f65..be294c565e5b4 100644 --- a/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java @@ -31,7 +31,6 @@ import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.search.vectors.ExactKnnQueryBuilder; import org.elasticsearch.search.vectors.KnnVectorQueryBuilder; -import org.elasticsearch.test.AbstractQueryTestCase; import org.elasticsearch.test.TransportVersionUtils; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -51,7 +50,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class NestedQueryBuilderTests extends AbstractQueryTestCase { +public class NestedQueryBuilderTests extends AbstractPrefilteredQueryTestCase { private static final String VECTOR_FIELD = "vector"; private static final int VECTOR_DIMENSION = 3; diff --git a/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java index 108ac8101122b..2c91bb3fcc34d 100644 --- a/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java @@ -33,6 +33,7 @@ import org.elasticsearch.common.unit.DistanceUnit; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.index.mapper.SeqNoFieldMapper; +import org.elasticsearch.index.query.AbstractPrefilteredQueryTestCase; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.MatchNoneQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; @@ -47,7 +48,6 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.MultiValueMode; -import org.elasticsearch.test.AbstractQueryTestCase; import org.elasticsearch.xcontent.XContentParseException; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentType; @@ -80,7 +80,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.nullValue; -public class FunctionScoreQueryBuilderTests extends AbstractQueryTestCase { +public class FunctionScoreQueryBuilderTests extends AbstractPrefilteredQueryTestCase { private static final String[] SHUFFLE_PROTECTED_FIELDS = new String[] { Script.PARAMS_PARSE_FIELD.getPreferredName(), diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java index d5e9dd821726b..5a9c84e1d0740 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java @@ -43,13 +43,12 @@ import org.elasticsearch.index.mapper.SourceToParse; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapperTestUtils; +import org.elasticsearch.index.query.AbstractPrefilteredQueryTestCase; import org.elasticsearch.index.query.MatchNoneQueryBuilder; import org.elasticsearch.index.query.NestedQueryBuilder; -import org.elasticsearch.index.query.PrefilteredQuery; import org.elasticsearch.index.query.PrefilteringTestUtils; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; -import org.elasticsearch.index.query.RandomQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.search.ESToParentBlockJoinQuery; import org.elasticsearch.inference.InferenceResults; @@ -61,7 +60,6 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.vectors.KnnVectorQueryBuilder; import org.elasticsearch.search.vectors.SparseVectorQueryWrapper; -import org.elasticsearch.test.AbstractQueryTestCase; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.TransportVersionUtils; import org.elasticsearch.test.client.NoOpClient; @@ -106,7 +104,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.notNullValue; -public class SemanticQueryBuilderTests extends AbstractQueryTestCase { +public class SemanticQueryBuilderTests extends AbstractPrefilteredQueryTestCase { private static final String SEMANTIC_TEXT_FIELD = "semantic"; private static final float TOKEN_WEIGHT = 0.5f; private static final int QUERY_TOKEN_LENGTH = 4; @@ -580,34 +578,6 @@ public void testRewriteWithPrefilters() throws IOException { } } - public void testSerializationPrefiltersBwc() throws Exception { - SemanticQueryBuilder originalQuery = new SemanticQueryBuilder(randomAlphaOfLength(5), randomAlphaOfLength(5)); - if (randomBoolean()) { - originalQuery.setPrefilters(randomList(1, 5, () -> RandomQueryBuilder.createQuery(random()))); - } - - for (int i = 0; i < 100; i++) { - TransportVersion transportVersion = TransportVersionUtils.randomVersionBetween( - random(), - originalQuery.getMinimalSupportedVersion(), - TransportVersionUtils.getPreviousVersion(TransportVersion.current()) - ); - - QueryBuilder deserializedQuery = copyNamedWriteable( - originalQuery, - namedWriteableRegistry(), - QueryBuilder.class, - transportVersion - ); - - if (transportVersion.supports(PrefilteredQuery.QUERY_PREFILTERING) == false) { - ((SemanticQueryBuilder) deserializedQuery).setPrefilters(List.of()); - } - - assertThat(deserializedQuery, instanceOf(SemanticQueryBuilder.class)); - } - } - private static void assertVectorQueryBuilderWithPrefilters(QueryBuilder queryBuilder, List prefilters) { assertThat(queryBuilder, instanceOf(KnnVectorQueryBuilder.class)); KnnVectorQueryBuilder knnVectorQueryBuilder = (KnnVectorQueryBuilder) queryBuilder; From e9a72186ca40ec8ffaa383d651f797cdbad9ef64 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Wed, 5 Nov 2025 17:57:57 +0200 Subject: [PATCH 16/17] Move prefiltering test util in abstract class --- .../AbstractPrefilteredQueryTestCase.java | 10 ++++++ .../index/query/PrefilteringTestUtils.java | 31 ------------------- .../queries/SemanticQueryBuilderTests.java | 3 +- 3 files changed, 11 insertions(+), 33 deletions(-) delete mode 100644 server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java diff --git a/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java b/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java index c7f2685ebd449..2a25249025541 100644 --- a/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java +++ b/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java @@ -15,6 +15,7 @@ import org.elasticsearch.test.TransportVersionUtils; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import static org.hamcrest.Matchers.equalTo; @@ -63,4 +64,13 @@ public void testEqualsAndHashcodeForPrefilters() throws IOException { assertThat(deserializedQuery.hashCode(), not(equalTo(originalQuery.hashCode()))); } + public static void setRandomTermQueryPrefilters(PrefilteredQuery queryBuilder, String... termFieldNames) { + List filters = new ArrayList<>(); + int numFilters = randomIntBetween(1, 5); + for (int i = 0; i < numFilters; i++) { + String filterFieldName = randomFrom(termFieldNames); + filters.add(QueryBuilders.termQuery(filterFieldName, randomAlphaOfLength(10))); + } + queryBuilder.setPrefilters(filters); + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java b/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java deleted file mode 100644 index b0cd0e575a0f9..0000000000000 --- a/server/src/test/java/org/elasticsearch/index/query/PrefilteringTestUtils.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.index.query; - -import java.util.ArrayList; -import java.util.List; - -import static org.elasticsearch.test.ESTestCase.randomAlphaOfLength; -import static org.elasticsearch.test.ESTestCase.randomFrom; -import static org.elasticsearch.test.ESTestCase.randomIntBetween; - -public class PrefilteringTestUtils { - - public static void setRandomTermQueryPrefilters(PrefilteredQuery queryBuilder, String... termFieldNames) { - List filters = new ArrayList<>(); - int numFilters = randomIntBetween(1, 5); - for (int i = 0; i < numFilters; i++) { - String filterFieldName = randomFrom(termFieldNames); - filters.add(QueryBuilders.termQuery(filterFieldName, randomAlphaOfLength(10))); - } - queryBuilder.setPrefilters(filters); - } - -} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java index 5a9c84e1d0740..1d814adcc2307 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java @@ -46,7 +46,6 @@ import org.elasticsearch.index.query.AbstractPrefilteredQueryTestCase; import org.elasticsearch.index.query.MatchNoneQueryBuilder; import org.elasticsearch.index.query.NestedQueryBuilder; -import org.elasticsearch.index.query.PrefilteringTestUtils; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.SearchExecutionContext; @@ -564,7 +563,7 @@ public void testRewriteWithPrefilters() throws IOException { QueryRewriteContext queryRewriteContext = createQueryRewriteContext(); SearchExecutionContext searchExecutionContext = createSearchExecutionContext(); SemanticQueryBuilder queryBuilder = doCreateTestQueryBuilder(); - PrefilteringTestUtils.setRandomTermQueryPrefilters(queryBuilder, KEYWORD_FIELD_NAME, TEXT_FIELD_NAME); + setRandomTermQueryPrefilters(queryBuilder, KEYWORD_FIELD_NAME, TEXT_FIELD_NAME); QueryBuilder rewritten = rewriteQuery(queryBuilder, queryRewriteContext, searchExecutionContext); From 84bebea32ca9293b1e5f2b00e593870772b75506 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Thu, 6 Nov 2025 12:52:23 +0200 Subject: [PATCH 17/17] Adds tests --- .../AbstractPrefilteredQueryTestCase.java | 54 +++++++++++++++++-- .../index/query/BoolQueryBuilderTests.java | 47 ++++++++++++++++ .../query/BoostingQueryBuilderTests.java | 20 +++++++ .../query/ConstantScoreQueryBuilderTests.java | 16 ++++++ .../index/query/DisMaxQueryBuilderTests.java | 19 +++++++ .../index/query/MatchQueryBuilderTests.java | 11 ++++ .../index/query/NestedQueryBuilderTests.java | 15 ++++++ .../FunctionScoreQueryBuilderTests.java | 16 ++++++ ...ceptedInferenceMatchQueryBuilderTests.java | 6 ++- .../queries/SemanticQueryBuilderTests.java | 15 +++--- 10 files changed, 207 insertions(+), 12 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java b/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java index 2a25249025541..368309e48bedd 100644 --- a/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java +++ b/server/src/test/java/org/elasticsearch/index/query/AbstractPrefilteredQueryTestCase.java @@ -9,6 +9,7 @@ package org.elasticsearch.index.query; +import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; import org.elasticsearch.test.AbstractQueryTestCase; @@ -17,6 +18,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; @@ -24,6 +26,10 @@ public abstract class AbstractPrefilteredQueryTestCase & PrefilteredQuery> extends AbstractQueryTestCase { + protected abstract QB createQueryBuilderForPrefilteredRewriteTest(Supplier prefilteredQuerySupplier); + + protected abstract void assertRewrittenHasPropagatedPrefilters(QueryBuilder rewritten, List prefilters); + public void testSerializationPrefiltersBwc() throws Exception { QB originalQuery = createTestQueryBuilder(); originalQuery.setPrefilters(randomList(1, 5, () -> RandomQueryBuilder.createQuery(random()))); @@ -64,13 +70,55 @@ public void testEqualsAndHashcodeForPrefilters() throws IOException { assertThat(deserializedQuery.hashCode(), not(equalTo(originalQuery.hashCode()))); } - public static void setRandomTermQueryPrefilters(PrefilteredQuery queryBuilder, String... termFieldNames) { + public void testRewriteWithPrefilters() throws IOException { + QueryRewriteContext queryRewriteContext = createQueryRewriteContext(); + SearchExecutionContext searchExecutionContext = createSearchExecutionContext(); + + for (int i = 0; i < 100; i++) { + QB queryBuilder = createQueryBuilderForPrefilteredRewriteTest(() -> createRandomPrefilteredQuery()); + if (queryBuilder == null) { + return; + } + setRandomPrefilters(queryBuilder); + + QueryBuilder rewritten = rewriteQuery(queryBuilder, queryRewriteContext, searchExecutionContext); + + assertRewrittenHasPropagatedPrefilters(rewritten, queryBuilder.getPrefilters()); + } + } + + private static void setRandomPrefilters(PrefilteredQuery queryBuilder) { List filters = new ArrayList<>(); int numFilters = randomIntBetween(1, 5); for (int i = 0; i < numFilters; i++) { - String filterFieldName = randomFrom(termFieldNames); - filters.add(QueryBuilders.termQuery(filterFieldName, randomAlphaOfLength(10))); + filters.add(randomFrom(randomTermQuery(), createRandomPrefilteredQuery())); } queryBuilder.setPrefilters(filters); } + + private static QueryBuilder randomTermQuery() { + String filterFieldName = randomFrom(KEYWORD_FIELD_NAME, TEXT_FIELD_NAME); + return QueryBuilders.termQuery(filterFieldName, randomAlphaOfLength(10)); + } + + private static QueryBuilder createRandomPrefilteredQuery() { + return switch (randomFrom(PrefilteredQueryType.values())) { + case BOOL -> QueryBuilders.boolQuery().must(randomTermQuery()); + case BOOSTING -> QueryBuilders.boostingQuery(randomTermQuery(), randomTermQuery()); + case CONSTANT_SCORE -> QueryBuilders.constantScoreQuery(randomTermQuery()); + case DIS_MAX -> QueryBuilders.disMaxQuery().add(randomTermQuery()).add(randomTermQuery()); + case FUNCTION_SCORE -> QueryBuilders.functionScoreQuery(randomTermQuery()); + case NESTED -> QueryBuilders.nestedQuery(OBJECT_FIELD_NAME, randomTermQuery(), randomFrom(ScoreMode.values())); + }; + } + + private enum PrefilteredQueryType { + // We only include query types that have child queries. + BOOL, + BOOSTING, + CONSTANT_SCORE, + DIS_MAX, + FUNCTION_SCORE, + NESTED + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java index baa36e9c98fc9..2257e3a137c57 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java @@ -24,9 +24,13 @@ import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; import static org.elasticsearch.index.query.QueryBuilders.boolQuery; import static org.elasticsearch.index.query.QueryBuilders.termQuery; @@ -34,6 +38,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; public class BoolQueryBuilderTests extends AbstractPrefilteredQueryTestCase { @@ -506,4 +511,46 @@ public void testShallowCopy() { } } } + + public void testGetPrefilters() { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + randomList(5, () -> RandomQueryBuilder.createQuery(random())).forEach(boolQueryBuilder::must); + randomList(5, () -> RandomQueryBuilder.createQuery(random())).forEach(boolQueryBuilder::should); + randomList(5, () -> RandomQueryBuilder.createQuery(random())).forEach(boolQueryBuilder::filter); + randomList(5, () -> RandomQueryBuilder.createQuery(random())).forEach(boolQueryBuilder::mustNot); + List topLevelPrefilters = randomList(5, () -> RandomQueryBuilder.createQuery(random())); + boolQueryBuilder.setPrefilters(topLevelPrefilters); + + Set expectedPrefilters = new HashSet<>(); + expectedPrefilters.addAll(boolQueryBuilder.filter()); + expectedPrefilters.addAll(boolQueryBuilder.mustNot().stream().map(q -> QueryBuilders.boolQuery().mustNot(q)).toList()); + expectedPrefilters.addAll(topLevelPrefilters); + + Set actualPrefilters = new HashSet<>(boolQueryBuilder.getPrefilters()); + assertThat(actualPrefilters, equalTo(expectedPrefilters)); + } + + @Override + protected BoolQueryBuilder createQueryBuilderForPrefilteredRewriteTest(Supplier prefilteredQuerySupplier) { + BoolQueryBuilder boolQueryBuilder = boolQuery(); + randomList(5, () -> prefilteredQuerySupplier.get()).forEach(boolQueryBuilder::must); + randomList(5, () -> prefilteredQuerySupplier.get()).forEach(boolQueryBuilder::should); + randomList(5, () -> prefilteredQuerySupplier.get()).forEach(boolQueryBuilder::filter); + randomList(5, () -> prefilteredQuerySupplier.get()).forEach(boolQueryBuilder::mustNot); + return boolQueryBuilder; + } + + @Override + protected void assertRewrittenHasPropagatedPrefilters(QueryBuilder rewritten, List prefilters) { + assertThat(rewritten, instanceOf(BoolQueryBuilder.class)); + BoolQueryBuilder boolQueryBuilder = (BoolQueryBuilder) rewritten; + for (QueryBuilder query : Stream.concat(boolQueryBuilder.must().stream(), boolQueryBuilder.should().stream()).toList()) { + assertThat(query, instanceOf(PrefilteredQuery.class)); + assertThat(((PrefilteredQuery) query).getPrefilters(), equalTo(prefilters)); + } + for (QueryBuilder query : Stream.concat(boolQueryBuilder.filter().stream(), boolQueryBuilder.mustNot().stream()).toList()) { + assertThat(query, instanceOf(PrefilteredQuery.class)); + assertThat(((PrefilteredQuery) query).getPrefilters().isEmpty(), is(true)); + } + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/BoostingQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoostingQueryBuilderTests.java index 4c8fbcd05154f..2ce49e393ac0d 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoostingQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoostingQueryBuilderTests.java @@ -14,9 +14,12 @@ import org.apache.lucene.search.Query; import java.io.IOException; +import java.util.List; +import java.util.function.Supplier; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.equalTo; public class BoostingQueryBuilderTests extends AbstractPrefilteredQueryTestCase { @@ -147,4 +150,21 @@ public void testMustRewrite() throws IOException { e = expectThrows(IllegalStateException.class, () -> queryBuilder2.toQuery(context)); assertEquals("Rewrite first", e.getMessage()); } + + @Override + protected BoostingQueryBuilder createQueryBuilderForPrefilteredRewriteTest(Supplier prefilteredQuerySupplier) { + return QueryBuilders.boostingQuery(prefilteredQuerySupplier.get(), prefilteredQuerySupplier.get()); + } + + @Override + protected void assertRewrittenHasPropagatedPrefilters(QueryBuilder rewritten, List prefilters) { + assertThat(rewritten, instanceOf(BoostingQueryBuilder.class)); + BoostingQueryBuilder boostingQueryBuilder = (BoostingQueryBuilder) rewritten; + QueryBuilder positiveQuery = boostingQueryBuilder.positiveQuery(); + assertThat(positiveQuery, instanceOf(PrefilteredQuery.class)); + assertThat(((PrefilteredQuery) positiveQuery).getPrefilters(), equalTo(prefilters)); + QueryBuilder negativeQuery = boostingQueryBuilder.negativeQuery(); + assertThat(negativeQuery, instanceOf(PrefilteredQuery.class)); + assertThat(((PrefilteredQuery) negativeQuery).getPrefilters(), equalTo(prefilters)); + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/ConstantScoreQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/ConstantScoreQueryBuilderTests.java index 63149c9e8adfc..e99b464642bf8 100644 --- a/server/src/test/java/org/elasticsearch/index/query/ConstantScoreQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/ConstantScoreQueryBuilderTests.java @@ -16,6 +16,8 @@ import org.elasticsearch.core.Strings; import java.io.IOException; +import java.util.List; +import java.util.function.Supplier; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.nullValue; @@ -117,4 +119,18 @@ public void testMustRewrite() throws IOException { IllegalStateException e = expectThrows(IllegalStateException.class, () -> queryBuilder.toQuery(context)); assertEquals("Rewrite first", e.getMessage()); } + + @Override + protected ConstantScoreQueryBuilder createQueryBuilderForPrefilteredRewriteTest(Supplier prefilteredQuerySupplier) { + return QueryBuilders.constantScoreQuery(prefilteredQuerySupplier.get()); + } + + @Override + protected void assertRewrittenHasPropagatedPrefilters(QueryBuilder rewritten, List prefilters) { + assertThat(rewritten, instanceOf(ConstantScoreQueryBuilder.class)); + QueryBuilder innerQuery = ((ConstantScoreQueryBuilder) rewritten).innerQuery(); + assertThat(innerQuery, instanceOf(PrefilteredQuery.class)); + assertEquals(prefilters, ((PrefilteredQuery) innerQuery).getPrefilters()); + } + } diff --git a/server/src/test/java/org/elasticsearch/index/query/DisMaxQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/DisMaxQueryBuilderTests.java index 827837da42c5f..24c7507240e3b 100644 --- a/server/src/test/java/org/elasticsearch/index/query/DisMaxQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/DisMaxQueryBuilderTests.java @@ -21,6 +21,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; public class DisMaxQueryBuilderTests extends AbstractPrefilteredQueryTestCase { /** @@ -143,4 +147,19 @@ public void testRewriteMultipleTimes() throws IOException { assertEquals(rewrittenAgain, expected); assertEquals(Rewriteable.rewrite(dismax, createSearchExecutionContext()), expected); } + + @Override + protected DisMaxQueryBuilder createQueryBuilderForPrefilteredRewriteTest(Supplier prefilteredQuerySupplier) { + return QueryBuilders.disMaxQuery().add(prefilteredQuerySupplier.get()).add(prefilteredQuerySupplier.get()); + } + + @Override + protected void assertRewrittenHasPropagatedPrefilters(QueryBuilder rewritten, List prefilters) { + assertThat(rewritten, instanceOf(DisMaxQueryBuilder.class)); + DisMaxQueryBuilder innerQueries = (DisMaxQueryBuilder) rewritten; + for (QueryBuilder prefilter : innerQueries.innerQueries()) { + assertThat(prefilter, instanceOf(PrefilteredQuery.class)); + assertThat(((PrefilteredQuery) prefilter).getPrefilters(), equalTo(prefilters)); + } + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java index a04c152082788..e95692709c34d 100644 --- a/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/MatchQueryBuilderTests.java @@ -49,6 +49,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Supplier; import static org.hamcrest.CoreMatchers.either; import static org.hamcrest.CoreMatchers.instanceOf; @@ -532,6 +533,16 @@ public void testMaxBooleanClause() { expectThrows(IndexSearcher.TooManyClauses.class, () -> query.parse(Type.PHRASE, TEXT_FIELD_NAME, "")); } + @Override + protected MatchQueryBuilder createQueryBuilderForPrefilteredRewriteTest(Supplier prefilteredQuerySupplier) { + return null; + } + + @Override + protected void assertRewrittenHasPropagatedPrefilters(QueryBuilder rewritten, List prefilters) { + // Do nothing, match prefiltering is tested via the inference interceptor + } + private static class MockGraphAnalyzer extends Analyzer { CannedBinaryTokenStream tokenStream; diff --git a/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java index be294c565e5b4..f6b75bd48a9a1 100644 --- a/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java @@ -39,7 +39,9 @@ import java.io.IOException; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.function.Supplier; import static org.elasticsearch.index.IndexSettingsTests.newIndexMeta; import static org.elasticsearch.index.query.InnerHitBuilderTests.randomNestedInnerHits; @@ -468,4 +470,17 @@ public void testDisallowExpensiveQueries() { ElasticsearchException e = expectThrows(ElasticsearchException.class, () -> queryBuilder.toQuery(searchExecutionContext)); assertEquals("[joining] queries cannot be executed when 'search.allow_expensive_queries' is set to false.", e.getMessage()); } + + @Override + protected NestedQueryBuilder createQueryBuilderForPrefilteredRewriteTest(Supplier prefilteredQuerySupplier) { + return QueryBuilders.nestedQuery(OBJECT_FIELD_NAME, prefilteredQuerySupplier.get(), ScoreMode.None); + } + + @Override + protected void assertRewrittenHasPropagatedPrefilters(QueryBuilder rewritten, List prefilters) { + assertThat(rewritten, instanceOf(NestedQueryBuilder.class)); + NestedQueryBuilder nestedQueryBuilder = (NestedQueryBuilder) rewritten; + assertThat(nestedQueryBuilder.query(), instanceOf(PrefilteredQuery.class)); + assertThat(((PrefilteredQuery) nestedQueryBuilder.query()).getPrefilters(), equalTo(prefilters)); + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java index 2c91bb3fcc34d..81ae05c58c543 100644 --- a/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/functionscore/FunctionScoreQueryBuilderTests.java @@ -36,7 +36,9 @@ import org.elasticsearch.index.query.AbstractPrefilteredQueryTestCase; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.MatchNoneQueryBuilder; +import org.elasticsearch.index.query.PrefilteredQuery; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.RandomQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.TermQueryBuilder; @@ -64,6 +66,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import static java.util.Collections.singletonList; import static org.elasticsearch.index.query.QueryBuilders.functionScoreQuery; @@ -844,6 +847,19 @@ private void expectParsingException(String json, String message) { expectParsingException(json, equalTo("failed to parse [function_score] query. " + message)); } + @Override + protected FunctionScoreQueryBuilder createQueryBuilderForPrefilteredRewriteTest(Supplier prefilteredQuerySupplier) { + return QueryBuilders.functionScoreQuery(prefilteredQuerySupplier.get()); + } + + @Override + protected void assertRewrittenHasPropagatedPrefilters(QueryBuilder rewritten, List prefilters) { + assertThat(rewritten, instanceOf(FunctionScoreQueryBuilder.class)); + FunctionScoreQueryBuilder functionScoreQueryBuilder = (FunctionScoreQueryBuilder) rewritten; + assertThat(functionScoreQueryBuilder.query(), instanceOf(PrefilteredQuery.class)); + assertThat(((PrefilteredQuery) functionScoreQueryBuilder.query()).getPrefilters(), equalTo(prefilters)); + } + /** * A hack on top of the normal random score function that fixed toQuery to work properly in this unit testing environment. */ diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/InterceptedInferenceMatchQueryBuilderTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/InterceptedInferenceMatchQueryBuilderTests.java index ed87d5adda0b6..9eca85c7d9b8e 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/InterceptedInferenceMatchQueryBuilderTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/InterceptedInferenceMatchQueryBuilderTests.java @@ -11,9 +11,11 @@ import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.RandomQueryBuilder; import org.elasticsearch.inference.InferenceResults; import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; +import java.util.List; import java.util.Map; import static org.elasticsearch.transport.RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY; @@ -97,6 +99,8 @@ public void testInterceptAndRewrite() throws Exception { final TestIndex testIndex2 = new TestIndex("test-index-2", Map.of(field, SPARSE_INFERENCE_ID), Map.of()); final TestIndex testIndex3 = new TestIndex("test-index-3", Map.of(), Map.of(field, Map.of("type", "text"))); final MatchQueryBuilder matchQuery = new MatchQueryBuilder(field, queryText).boost(3.0f).queryName("bar"); + List prefilters = randomList(5, () -> RandomQueryBuilder.createQuery(random())); + matchQuery.setPrefilters(prefilters); // Perform coordinator node rewrite final QueryRewriteContext queryRewriteContext = createQueryRewriteContext( @@ -137,7 +141,7 @@ public void testInterceptAndRewrite() throws Exception { queryText, null, coordinatorIntercepted.inferenceResultsMap - ).boost(matchQuery.boost()).queryName(matchQuery.queryName()); + ).boost(matchQuery.boost()).queryName(matchQuery.queryName()).setPrefilters(prefilters); // Perform data node rewrite on test index 1 final QueryRewriteContext indexMetadataContextTestIndex1 = createIndexMetadataContext( diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java index 1d814adcc2307..44602f3e6520b 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilderTests.java @@ -559,20 +559,19 @@ public void testSerializingQueryWhenNoInferenceId() throws IOException { assertThat(rewritten, instanceOf(MatchNoneQueryBuilder.class)); } - public void testRewriteWithPrefilters() throws IOException { - QueryRewriteContext queryRewriteContext = createQueryRewriteContext(); - SearchExecutionContext searchExecutionContext = createSearchExecutionContext(); - SemanticQueryBuilder queryBuilder = doCreateTestQueryBuilder(); - setRandomTermQueryPrefilters(queryBuilder, KEYWORD_FIELD_NAME, TEXT_FIELD_NAME); - - QueryBuilder rewritten = rewriteQuery(queryBuilder, queryRewriteContext, searchExecutionContext); + @Override + protected SemanticQueryBuilder createQueryBuilderForPrefilteredRewriteTest(Supplier prefilteredQuerySupplier) { + return doCreateTestQueryBuilder(); + } + @Override + protected void assertRewrittenHasPropagatedPrefilters(QueryBuilder rewritten, List prefilters) { assertThat(rewritten, instanceOf(NestedQueryBuilder.class)); NestedQueryBuilder nestedQueryBuilder = (NestedQueryBuilder) rewritten; switch (inferenceResultType) { case NONE -> assertThat(nestedQueryBuilder.query(), instanceOf(MatchNoneQueryBuilder.class)); case SPARSE_EMBEDDING -> assertThat(nestedQueryBuilder.query(), instanceOf(SparseVectorQueryBuilder.class)); - case TEXT_EMBEDDING -> assertVectorQueryBuilderWithPrefilters(nestedQueryBuilder.query(), queryBuilder.getPrefilters()); + case TEXT_EMBEDDING -> assertVectorQueryBuilderWithPrefilters(nestedQueryBuilder.query(), prefilters); default -> fail("Unexpected inference result type [" + inferenceResultType + "]"); } }