Skip to content

Commit 50d6e19

Browse files
committed
Fix generating QueryBuilder prefilters for knn when they cannot be pushed down
1 parent 3ae0e9b commit 50d6e19

File tree

3 files changed

+65
-21
lines changed

3 files changed

+65
-21
lines changed

x-pack/plugin/esql/qa/testFixtures/src/main/resources/knn-function.csv-spec

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,20 @@ c: long | primary: boolean
266266
41 | false
267267
9 | true
268268
;
269+
270+
testKnnUsesPrefiltering
271+
required_capability: knn_function_v3
272+
273+
from colors metadata _score
274+
| where knn(rgb_vector, [255, 0, 0], 5) and primary == true
275+
| sort _score desc, color asc
276+
| keep color
277+
;
278+
279+
color:text
280+
red
281+
gray
282+
black
283+
magenta
284+
yellow
285+
;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/vector/Knn.java

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,22 @@ private Map<String, Object> knnQueryOptions() throws InvalidArgumentException {
253253
return matchOptions;
254254
}
255255

256+
@Override
257+
public Expression replaceQueryBuilder(QueryBuilder queryBuilder) {
258+
return new Knn(source(), field(), query(), k(), options(), queryBuilder, filterExpressions());
259+
}
260+
261+
@Override
262+
public Translatable translatable(LucenePushdownPredicates pushdownPredicates) {
263+
Translatable translatable = super.translatable(pushdownPredicates);
264+
// We need to check whether filter expressions are translatable as well
265+
for (Expression filterExpression : filterExpressions()) {
266+
translatable = translatable.merge(TranslationAware.translatable(filterExpression, pushdownPredicates));
267+
}
268+
269+
return translatable;
270+
}
271+
256272
@Override
257273
protected Query translate(LucenePushdownPredicates pushdownPredicates, TranslatorHandler handler) {
258274
var fieldAttribute = Match.fieldAsFieldAttribute(field());
@@ -272,28 +288,19 @@ protected Query translate(LucenePushdownPredicates pushdownPredicates, Translato
272288

273289
List<QueryBuilder> filterQueries = new ArrayList<>();
274290
for (Expression filterExpression : filterExpressions()) {
275-
filterQueries.add(handler.asQuery(pushdownPredicates, filterExpression).toQueryBuilder());
291+
if (filterExpression instanceof TranslationAware translationAware) {
292+
// We can only translate filter expressions that are translatable. In case any is not translatable,
293+
// Knn won't be pushed down as it will not be translatable so it's safe not to translate all filters and check them
294+
// when creating an evaluator for the non-pushed down query
295+
if (translationAware.translatable(pushdownPredicates) == Translatable.YES) {
296+
filterQueries.add(handler.asQuery(pushdownPredicates, filterExpression).toQueryBuilder());
297+
}
298+
}
276299
}
277300

278301
return new KnnQuery(source(), fieldName, queryAsFloats, opts, filterQueries);
279302
}
280303

281-
@Override
282-
public Expression replaceQueryBuilder(QueryBuilder queryBuilder) {
283-
return new Knn(source(), field(), query(), k(), options(), queryBuilder, filterExpressions());
284-
}
285-
286-
@Override
287-
public Translatable translatable(LucenePushdownPredicates pushdownPredicates) {
288-
Translatable translatable = super.translatable(pushdownPredicates);
289-
// We need to check whether filter expressions are translatable as well
290-
for (Expression filterExpression : filterExpressions()) {
291-
translatable = translatable.merge(TranslationAware.translatable(filterExpression, pushdownPredicates));
292-
}
293-
294-
return translatable;
295-
}
296-
297304
public Expression withFilters(List<Expression> filterExpressions) {
298305
return new Knn(source(), field(), query(), k(), options(), queryBuilder(), filterExpressions);
299306
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,30 +1979,50 @@ public void testMultipleKnnQueriesInPrefilters() {
19791979
var queryExec = as(field.child(), EsQueryExec.class);
19801980

19811981
KnnVectorQueryBuilder firstKnnQuery = new KnnVectorQueryBuilder("dense_vector", new float[] { 0, 1, 2 }, 10, null, null, null);
1982+
KnnVectorQueryBuilder firstKnnQueryAsFilter = new KnnVectorQueryBuilder(
1983+
"dense_vector",
1984+
new float[] { 0, 1, 2 },
1985+
10,
1986+
null,
1987+
null,
1988+
null
1989+
);
19821990
// Integer range query (right side of first OR)
19831991
QueryBuilder integerRangeQuery = wrapWithSingleQuery(
19841992
query,
19851993
unscore(rangeQuery("integer").gt(10)),
19861994
"integer",
1987-
new Source(2, 45, "integer > 10")
1995+
new Source(2, 46, "integer > 10")
19881996
);
19891997

19901998
// Second KNN query (right side of second OR)
19911999
KnnVectorQueryBuilder secondKnnQuery = new KnnVectorQueryBuilder("dense_vector", new float[] { 4, 5, 6 }, 10, null, null, null);
2000+
KnnVectorQueryBuilder secondKnnQueryAsFilter = new KnnVectorQueryBuilder(
2001+
"dense_vector",
2002+
new float[] { 4, 5, 6 },
2003+
10,
2004+
null,
2005+
null,
2006+
null
2007+
);
19922008

19932009
// Keyword term query (left side of second OR)
19942010
QueryBuilder keywordQuery = wrapWithSingleQuery(
19952011
query,
19962012
unscore(termQuery("keyword", "test")),
19972013
"keyword",
1998-
new Source(2, 87, "keyword == \"test\"")
2014+
new Source(2, 66, "keyword == \"test\"")
19992015
);
20002016

20012017
// First OR (knn1 OR integer > 10)
20022018
var firstOr = boolQuery().should(firstKnnQuery).should(integerRangeQuery);
2019+
var firstOrAsFilter = boolQuery().should(firstKnnQueryAsFilter).should(integerRangeQuery);
20032020
// Second OR (keyword == "test" OR knn2)
2004-
var secondOr = boolQuery().should(keywordQuery).should(secondKnnQuery.addFilterQuery(firstOr));
2005-
firstKnnQuery.addFilterQuery(secondOr);
2021+
var secondOr = boolQuery().should(keywordQuery).should(secondKnnQuery);
2022+
var secondOrAsFilter = boolQuery().should(keywordQuery).should(secondKnnQueryAsFilter);
2023+
// Add prefilters to the knn queries. knn queries in prefilters don't have prefilters so we use copies of the queries
2024+
firstKnnQuery.addFilterQuery(secondOrAsFilter);
2025+
secondKnnQuery.addFilterQuery(firstOrAsFilter);
20062026

20072027
// Top-level AND combining both ORs
20082028
var expectedQuery = boolQuery().must(firstOr).must(secondOr);

0 commit comments

Comments
 (0)