Skip to content

Commit 4c6bb4d

Browse files
committed
Add negations tests
1 parent a99e75f commit 4c6bb4d

File tree

2 files changed

+136
-3
lines changed

2 files changed

+136
-3
lines changed

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,29 @@ olive | [128.0, 128.0, 0.0]
8181
green | [0.0, 128.0, 0.0]
8282
;
8383

84+
knnWithNegatedPrefilter
85+
required_capability: knn_function_v3
86+
87+
from colors metadata _score
88+
| where knn(rgb_vector, [128,128,0], 10) and not (match(color, "olive") or match(color, "chocolate"))
89+
| sort _score desc, color asc
90+
| keep color, rgb_vector
91+
| LIMIT 10
92+
;
93+
94+
color:text | rgb_vector:dense_vector
95+
sienna | [160.0, 82.0, 45.0]
96+
peru | [205.0, 133.0, 63.0]
97+
golden rod | [218.0, 165.0, 32.0]
98+
brown | [165.0, 42.0, 42.0]
99+
firebrick | [178.0, 34.0, 34.0]
100+
chartreuse | [127.0, 255.0, 0.0]
101+
gray | [128.0, 128.0, 128.0]
102+
green | [0.0, 128.0, 0.0]
103+
maroon | [128.0, 0.0, 0.0]
104+
orange | [255.0, 165.0, 0.0]
105+
;
106+
84107
knnAfterKeep
85108
required_capability: knn_function_v3
86109

@@ -141,12 +164,10 @@ golden rod | true
141164
knnWithConjunction
142165
required_capability: knn_function_v3
143166

144-
# TODO We need kNN prefiltering here so we get more candidates that pass the filter
145167
from colors metadata _score
146-
| where knn(rgb_vector, [255,255,238], 140) and hex_code like "#FFF*"
168+
| where knn(rgb_vector, [255,255,238], 10) and hex_code like "#FFF*"
147169
| sort _score desc, color asc
148170
| keep color, hex_code, rgb_vector
149-
| limit 10
150171
;
151172

152173
color:text | hex_code:keyword | rgb_vector:dense_vector
@@ -182,6 +203,29 @@ red | [255.0, 0.0, 0.0]
182203
yellow | [255.0, 255.0, 0.0]
183204
;
184205

206+
knnWithNegationsAndFiltersConjunction
207+
required_capability: knn_function_v3
208+
209+
from colors metadata _score
210+
| where (knn(rgb_vector, [0,255,255], 140) and not(primary == true and match(color, "blue")))
211+
| sort _score desc, color asc
212+
| keep color, rgb_vector
213+
| limit 10
214+
;
215+
216+
color:text | rgb_vector:dense_vector
217+
cyan | [0.0, 255.0, 255.0]
218+
turquoise | [64.0, 224.0, 208.0]
219+
aqua marine | [127.0, 255.0, 212.0]
220+
teal | [0.0, 128.0, 128.0]
221+
silver | [192.0, 192.0, 192.0]
222+
gray | [128.0, 128.0, 128.0]
223+
gainsboro | [220.0, 220.0, 220.0]
224+
thistle | [216.0, 191.0, 216.0]
225+
lavender | [230.0, 230.0, 250.0]
226+
azure | [240.0, 255.0, 255.0]
227+
;
228+
185229
knnWithNonPushableConjunction
186230
required_capability: knn_function_v3
187231

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,6 +1944,44 @@ public void testPushDownConjunctionsToKnnPrefilter() {
19441944
assertEquals(expectedQuery.toString(), queryExec.query().toString());
19451945
}
19461946

1947+
1948+
public void testPushDownNegatedConjunctionsToKnnPrefilter() {
1949+
assumeTrue("knn must be enabled", EsqlCapabilities.Cap.KNN_FUNCTION_V3.isEnabled());
1950+
1951+
String query = """
1952+
from test
1953+
| where knn(dense_vector, [0, 1, 2], 10) and NOT integer > 10
1954+
""";
1955+
var plan = plannerOptimizer.plan(query, IS_SV_STATS, makeAnalyzer("mapping-all-types.json"));
1956+
1957+
var limit = as(plan, LimitExec.class);
1958+
var exchange = as(limit.child(), ExchangeExec.class);
1959+
var project = as(exchange.child(), ProjectExec.class);
1960+
var field = as(project.child(), FieldExtractExec.class);
1961+
var queryExec = as(field.child(), EsQueryExec.class);
1962+
1963+
// The filter condition should be pushed down to both the KNN query and the main query
1964+
QueryBuilder expectedFilterQueryBuilder = wrapWithSingleQuery(
1965+
query,
1966+
unscore(boolQuery().mustNot(unscore(rangeQuery("integer").gt(10)))),
1967+
"integer",
1968+
new Source(2, 45, "NOT integer > 10")
1969+
);
1970+
1971+
KnnVectorQueryBuilder expectedKnnQueryBuilder = new KnnVectorQueryBuilder(
1972+
"dense_vector",
1973+
new float[] { 0, 1, 2 },
1974+
10,
1975+
null,
1976+
null,
1977+
null
1978+
).addFilterQuery(expectedFilterQueryBuilder);
1979+
1980+
var expectedQuery = boolQuery().must(expectedKnnQueryBuilder).must(expectedFilterQueryBuilder);
1981+
1982+
assertEquals(expectedQuery.toString(), queryExec.query().toString());
1983+
}
1984+
19471985
public void testNotPushDownDisjunctionsToKnnPrefilter() {
19481986
assumeTrue("knn must be enabled", EsqlCapabilities.Cap.KNN_FUNCTION_V3.isEnabled());
19491987

@@ -2007,6 +2045,57 @@ public void testNotPushDownKnnWithNonPushablePrefilters() {
20072045
assertEquals(integerGtQuery.toString(), queryExec.query().toString());
20082046
}
20092047

2048+
public void testPushDownComplexNegationsToKnnPrefilter() {
2049+
assumeTrue("knn must be enabled", EsqlCapabilities.Cap.KNN_FUNCTION_V3.isEnabled());
2050+
2051+
String query = """
2052+
from test
2053+
| where ((knn(dense_vector, [0, 1, 2], 10) or NOT integer > 10) and NOT ((keyword == "test") or knn(dense_vector, [4, 5, 6], 10)))
2054+
""";
2055+
var plan = plannerOptimizer.plan(query, IS_SV_STATS, makeAnalyzer("mapping-all-types.json"));
2056+
2057+
var limit = as(plan, LimitExec.class);
2058+
var exchange = as(limit.child(), ExchangeExec.class);
2059+
var project = as(exchange.child(), ProjectExec.class);
2060+
var fieldExtract = as(project.child(), FieldExtractExec.class);
2061+
var queryExec = as(fieldExtract.child(), EsQueryExec.class);
2062+
2063+
QueryBuilder notKeywordFilter = wrapWithSingleQuery(
2064+
query,
2065+
unscore(boolQuery().mustNot(unscore(termQuery("keyword", "test")))),
2066+
"keyword",
2067+
new Source(2, 74, "keyword == \"test\"")
2068+
);
2069+
2070+
QueryBuilder notIntegerGt10 = wrapWithSingleQuery(
2071+
query,
2072+
unscore(boolQuery().mustNot(unscore(rangeQuery("integer").gt(10)))),
2073+
"integer",
2074+
new Source(2, 46, "NOT integer > 10")
2075+
);
2076+
2077+
KnnVectorQueryBuilder firstKnn = new KnnVectorQueryBuilder("dense_vector", new float[] { 0, 1, 2 }, 10, null, null, null);
2078+
KnnVectorQueryBuilder firstKnnFilter = new KnnVectorQueryBuilder("dense_vector", new float[] { 0, 1, 2 }, 10, null, null, null);
2079+
KnnVectorQueryBuilder secondKnn = new KnnVectorQueryBuilder("dense_vector", new float[] { 4, 5, 6 }, 10, null, null, null);
2080+
KnnVectorQueryBuilder secondKnnFilter = new KnnVectorQueryBuilder("dense_vector", new float[] { 4, 5, 6 }, 10, null, null, null);
2081+
2082+
firstKnn.addFilterQuery(boolQuery()
2083+
.must(notKeywordFilter)
2084+
.must(unscore(boolQuery().mustNot(secondKnnFilter))));
2085+
2086+
secondKnn.addFilterQuery(boolQuery()
2087+
.should(firstKnnFilter)
2088+
.should(notIntegerGt10));
2089+
2090+
// Build the main boolean query structure
2091+
BoolQueryBuilder expectedQuery = boolQuery()
2092+
.must(notKeywordFilter) // NOT (keyword == "test")
2093+
.must(unscore(boolQuery().mustNot(secondKnn)))
2094+
.must(boolQuery().should(firstKnn).should(notIntegerGt10));
2095+
2096+
assertEquals(expectedQuery.toString(), queryExec.query().toString());
2097+
}
2098+
20102099
public void testMultipleKnnQueriesInPrefilters() {
20112100
assumeTrue("knn must be enabled", EsqlCapabilities.Cap.KNN_FUNCTION_V3.isEnabled());
20122101

0 commit comments

Comments
 (0)