From fcd5ac9e1f5ad8e143137397f22b0023ece3fed7 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 9 Apr 2026 15:10:09 -0700 Subject: [PATCH 01/14] Add FilterType enum for post|efficient filter placement Signed-off-by: Eric Wei --- .../sql/opensearch/storage/FilterType.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 opensearch/src/main/java/org/opensearch/sql/opensearch/storage/FilterType.java diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/FilterType.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/FilterType.java new file mode 100644 index 0000000000..cc42bb35f5 --- /dev/null +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/FilterType.java @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.sql.opensearch.storage; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import org.opensearch.sql.exception.ExpressionEvaluationException; + +/** Filter placement strategy for vectorSearch() WHERE clauses. */ +public enum FilterType { + /** WHERE placed in bool.filter outside the knn clause (post-filtering). */ + POST("post"), + + /** WHERE placed inside knn.filter for efficient pre-filtering. */ + EFFICIENT("efficient"); + + private final String value; + + FilterType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + private static final Set VALID_VALUES = + Arrays.stream(values()).map(FilterType::getValue).collect(Collectors.toSet()); + + public static FilterType fromString(String str) { + for (FilterType ft : values()) { + if (ft.value.equals(str)) { + return ft; + } + } + throw new ExpressionEvaluationException( + String.format("filter_type must be one of %s, got '%s'", VALID_VALUES, str)); + } +} From 6498aa39f287f891bf7dd6358f905419fc5202fc Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 9 Apr 2026 15:11:31 -0700 Subject: [PATCH 02/14] Add filter_type to allowed option keys with post|efficient validation Signed-off-by: Eric Wei --- ...ctorSearchTableFunctionImplementation.java | 7 +++- ...SearchTableFunctionImplementationTest.java | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementation.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementation.java index 7f01a97397..8b92544b97 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementation.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementation.java @@ -34,7 +34,8 @@ public class VectorSearchTableFunctionImplementation extends FunctionExpression implements TableFunctionImplementation { /** P0 allowed option keys. Rejects unknown/future keys to prevent unvalidated DSL injection. */ - static final Set ALLOWED_OPTION_KEYS = Set.of("k", "max_distance", "min_score"); + static final Set ALLOWED_OPTION_KEYS = + Set.of("k", "max_distance", "min_score", "filter_type"); /** * Field names must be safe for JSON interpolation: alphanumeric, dots (nested), underscores, @@ -190,6 +191,10 @@ private void validateOptions(Map options) { String.format("Unknown option key '%s'. Supported keys: %s", key, ALLOWED_OPTION_KEYS)); } } + if (options.containsKey("filter_type")) { + // Validate early — fromString throws if invalid + FilterType.fromString(options.get("filter_type")); + } boolean hasK = options.containsKey("k"); boolean hasMaxDistance = options.containsKey("max_distance"); boolean hasMinScore = options.containsKey("min_score"); diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementationTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementationTest.java index ec8e5161d8..76658ae94b 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementationTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementationTest.java @@ -388,6 +388,45 @@ void testCaseInsensitiveArgLookup() { assertTrue(table instanceof VectorSearchIndex); } + @Test + void testInvalidFilterTypeRejects() { + FunctionName functionName = FunctionName.of("vectorsearch"); + List args = + List.of( + DSL.namedArgument("table", DSL.literal("my-index")), + DSL.namedArgument("field", DSL.literal("embedding")), + DSL.namedArgument("vector", DSL.literal("[1.0, 2.0]")), + DSL.namedArgument("option", DSL.literal("k=5,filter_type=invalid"))); + VectorSearchTableFunctionImplementation impl = + new VectorSearchTableFunctionImplementation(functionName, args, client, settings); + ExpressionEvaluationException ex = + assertThrows(ExpressionEvaluationException.class, impl::applyArguments); + assertTrue(ex.getMessage().contains("filter_type must be one of")); + } + + @Test + void testFilterTypePostAccepted() { + VectorSearchTableFunctionImplementation impl = + createImplWithArgs("my-index", "embedding", "[1.0, 2.0]", "k=5,filter_type=post"); + Table table = impl.applyArguments(); + assertTrue(table instanceof VectorSearchIndex); + } + + @Test + void testFilterTypeEfficientAccepted() { + VectorSearchTableFunctionImplementation impl = + createImplWithArgs("my-index", "embedding", "[1.0, 2.0]", "k=5,filter_type=efficient"); + Table table = impl.applyArguments(); + assertTrue(table instanceof VectorSearchIndex); + } + + @Test + void testParseOptionsPreservesFilterTypeValue() { + Map options = + VectorSearchTableFunctionImplementation.parseOptions("k=5,filter_type=post"); + assertEquals("post", options.get("filter_type")); + } + private VectorSearchTableFunctionImplementation createImpl() { return createImplWithArgs("my-index", "embedding", "[1.0, 2.0, 3.0]", "k=5"); } From 3446fbb6dbb98a408830cc048d1f5e42970c03f7 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 9 Apr 2026 15:12:57 -0700 Subject: [PATCH 03/14] Strip filter_type from options and pass as typed FilterType to VectorSearchIndex Signed-off-by: Eric Wei --- .../opensearch/storage/VectorSearchIndex.java | 16 +++++++++++++++- .../VectorSearchTableFunctionImplementation.java | 9 ++++++++- .../storage/VectorSearchIndexTest.java | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java index fe79aea532..1d8efd918b 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java @@ -26,6 +26,7 @@ public class VectorSearchIndex extends OpenSearchIndex { private final String field; private final float[] vector; private final Map options; + private final FilterType filterType; // null means default (POST) public VectorSearchIndex( OpenSearchClient client, @@ -33,11 +34,24 @@ public VectorSearchIndex( String indexName, String field, float[] vector, - Map options) { + Map options, + FilterType filterType) { super(client, settings, indexName); this.field = field; this.vector = vector; this.options = options; + this.filterType = filterType; + } + + /** Backward-compatible constructor — defaults to no explicit filter type. */ + public VectorSearchIndex( + OpenSearchClient client, + Settings settings, + String indexName, + String field, + float[] vector, + Map options) { + this(client, settings, indexName, field, vector, options, null); } @Override diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementation.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementation.java index 8b92544b97..7eda27e7c5 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementation.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchTableFunctionImplementation.java @@ -100,7 +100,14 @@ public Table applyArguments() { Map options = parseOptions(optionStr); validateOptions(options); - return new VectorSearchIndex(client, settings, tableName, fieldName, vector, options); + // Strip filter_type — it's a SQL-layer directive, not a knn parameter + FilterType filterType = null; + if (options.containsKey("filter_type")) { + filterType = FilterType.fromString(options.remove("filter_type")); + } + + return new VectorSearchIndex( + client, settings, tableName, fieldName, vector, options, filterType); } private float[] parseVector(String vectorLiteral) { diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java index 2c90193847..d289ffde31 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java @@ -6,6 +6,7 @@ package org.opensearch.sql.opensearch.storage; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.LinkedHashMap; @@ -139,6 +140,21 @@ void buildKnnQueryJsonNonNumericOptionRenderedQuoted() { assertTrue(json.contains("\"k\":5"), "Numeric option should be unquoted"); } + @Test + void buildKnnQueryJsonExcludesFilterType() { + LinkedHashMap options = new LinkedHashMap<>(); + options.put("k", "5"); + + VectorSearchIndex index = + new VectorSearchIndex( + client, settings, "test-index", "embedding", + new float[] {1.0f}, options, FilterType.EFFICIENT); + + String json = index.buildKnnQueryJson(); + assertFalse(json.contains("filter_type"), "filter_type should not appear in knn JSON"); + assertTrue(json.contains("\"k\":5"), "k should still be present"); + } + @Test void isInstanceOfOpenSearchIndex() { VectorSearchIndex index = From 66b4ef43c813cfe5b44f4e4ae7d94917c62b0e24 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 9 Apr 2026 15:13:56 -0700 Subject: [PATCH 04/14] Collapse buildKnnQueryJson to accept optional filter clause Signed-off-by: Eric Wei --- .../opensearch/storage/VectorSearchIndex.java | 18 +++++++- .../storage/VectorSearchIndexTest.java | 44 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java index 1d8efd918b..5e4170835d 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java @@ -90,6 +90,15 @@ private QueryBuilder buildKnnQuery() { // Package-private for testing String buildKnnQueryJson() { + return buildKnnQueryJson(null); + } + + /** + * Builds knn query JSON, optionally embedding a filter clause for efficient filtering. + * + * @param filterJson serialized filter JSON to embed in knn.field.filter, or null for no filter + */ + String buildKnnQueryJson(String filterJson) { StringBuilder vectorJson = new StringBuilder("["); for (int i = 0; i < vector.length; i++) { if (i > 0) vectorJson.append(","); @@ -110,9 +119,14 @@ String buildKnnQueryJson() { } } + String filterClause = ""; + if (filterJson != null) { + filterClause = String.format(",\"filter\":%s", filterJson); + } + return String.format( - "{\"knn\":{\"%s\":{\"vector\":%s%s}}}", - field, vectorJson.toString(), optionsJson.toString()); + "{\"knn\":{\"%s\":{\"vector\":%s%s%s}}}", + field, vectorJson.toString(), optionsJson.toString(), filterClause); } private static boolean isNumeric(String str) { diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java index d289ffde31..25b93f5138 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java @@ -140,6 +140,50 @@ void buildKnnQueryJsonNonNumericOptionRenderedQuoted() { assertTrue(json.contains("\"k\":5"), "Numeric option should be unquoted"); } + @Test + void buildKnnQueryJsonWithFilterEmbeds() { + VectorSearchIndex index = + new VectorSearchIndex( + client, settings, "test-index", "embedding", + new float[] {1.0f, 2.0f}, Map.of("k", "5"), FilterType.EFFICIENT); + + String filterJson = "{\"term\":{\"city\":{\"value\":\"Miami\"}}}"; + String json = index.buildKnnQueryJson(filterJson); + + assertTrue(json.contains("\"filter\""), "Should contain filter field"); + assertTrue(json.contains("\"term\""), "Should contain the filter content"); + assertTrue(json.contains("\"k\":5"), "Should still contain k"); + assertTrue(json.contains("\"vector\":[1.0,2.0]"), "Should contain vector"); + } + + @Test + void buildKnnQueryJsonWithFilterRadial() { + VectorSearchIndex index = + new VectorSearchIndex( + client, settings, "test-index", "embedding", + new float[] {1.0f}, Map.of("max_distance", "10.5"), FilterType.EFFICIENT); + + String filterJson = "{\"range\":{\"rating\":{\"gte\":4.0}}}"; + String json = index.buildKnnQueryJson(filterJson); + + assertTrue(json.contains("\"max_distance\":10.5"), "Should contain max_distance"); + assertTrue(json.contains("\"filter\""), "Should contain filter"); + } + + @Test + void buildKnnQueryJsonNullFilterProducesBaseJson() { + VectorSearchIndex index = + new VectorSearchIndex( + client, settings, "test-index", "embedding", + new float[] {1.0f}, Map.of("k", "5"), null); + + String json = index.buildKnnQueryJson(null); + String baseJson = index.buildKnnQueryJson(); + + assertEquals(baseJson, json, "null filter should produce same JSON as no-arg version"); + assertFalse(json.contains("\"filter\""), "Should not contain filter field"); + } + @Test void buildKnnQueryJsonExcludesFilterType() { LinkedHashMap options = new LinkedHashMap<>(); From 039cab20ae186d230b6c5e97b1aa12adf24dcfb1 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 9 Apr 2026 15:15:43 -0700 Subject: [PATCH 05/14] Implement efficient filter pushdown branching and build-time validation in VectorSearchQueryBuilder Signed-off-by: Eric Wei --- .../scan/VectorSearchQueryBuilder.java | 53 ++++++++++++++++--- .../scan/VectorSearchQueryBuilderTest.java | 50 +++++++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java index ca4df9629a..2a53f2a8a2 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java @@ -6,6 +6,7 @@ package org.opensearch.sql.opensearch.storage.scan; import java.util.Map; +import java.util.function.Function; import org.apache.commons.lang3.tuple.Pair; import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.QueryBuilder; @@ -16,6 +17,7 @@ import org.opensearch.sql.expression.Expression; import org.opensearch.sql.expression.ReferenceExpression; import org.opensearch.sql.opensearch.request.OpenSearchRequestBuilder; +import org.opensearch.sql.opensearch.storage.FilterType; import org.opensearch.sql.opensearch.storage.script.filter.FilterQueryBuilder; import org.opensearch.sql.opensearch.storage.serde.DefaultExpressionSerializer; import org.opensearch.sql.planner.logical.LogicalFilter; @@ -27,20 +29,45 @@ * WHERE filters in a non-scoring (filter) context. This prevents the knn relevance scores from * being destroyed when a WHERE clause is pushed down. * - *

Without this, the default pushDownFilter wraps both queries into bool.filter, which is a - * non-scoring context. + *

Supports two filter placement strategies via {@link FilterType}: + * + *

    + *
  • {@code POST} — WHERE in {@code bool.filter} outside knn (post-filtering, default) + *
  • {@code EFFICIENT} — WHERE inside {@code knn.filter} for pre-filtering during ANN search + *
*/ public class VectorSearchQueryBuilder extends OpenSearchIndexScanQueryBuilder { private final QueryBuilder knnQuery; private final Map options; + private final FilterType filterType; + private final boolean filterTypeExplicit; + private final Function rebuildKnnWithFilter; + private boolean filterPushed = false; + /** Full constructor with filter type support. */ public VectorSearchQueryBuilder( - OpenSearchRequestBuilder requestBuilder, QueryBuilder knnQuery, Map options) { + OpenSearchRequestBuilder requestBuilder, + QueryBuilder knnQuery, + Map options, + FilterType filterType, + boolean filterTypeExplicit, + Function rebuildKnnWithFilter) { super(requestBuilder); requestBuilder.getSourceBuilder().query(knnQuery); this.knnQuery = knnQuery; this.options = options; + this.filterType = filterType != null ? filterType : FilterType.POST; + this.filterTypeExplicit = filterTypeExplicit; + this.rebuildKnnWithFilter = rebuildKnnWithFilter; + } + + /** Backward-compatible constructor — defaults to POST, not explicit. */ + public VectorSearchQueryBuilder( + OpenSearchRequestBuilder requestBuilder, + QueryBuilder knnQuery, + Map options) { + this(requestBuilder, knnQuery, options, FilterType.POST, false, null); } @Override @@ -48,10 +75,16 @@ public boolean pushDownFilter(LogicalFilter filter) { FilterQueryBuilder queryBuilder = new FilterQueryBuilder(new DefaultExpressionSerializer()); Expression queryCondition = filter.getCondition(); QueryBuilder whereQuery = queryBuilder.build(queryCondition); + filterPushed = true; - // Combine: knn in must (scores), WHERE in filter (no scoring impact) - BoolQueryBuilder combined = QueryBuilders.boolQuery().must(knnQuery).filter(whereQuery); - requestBuilder.getSourceBuilder().query(combined); + if (filterType == FilterType.EFFICIENT) { + QueryBuilder rebuiltKnn = rebuildKnnWithFilter.apply(whereQuery); + requestBuilder.getSourceBuilder().query(rebuiltKnn); + } else { + // POST mode: knn in must (scores), WHERE in filter (no scoring impact) + BoolQueryBuilder combined = QueryBuilders.boolQuery().must(knnQuery).filter(whereQuery); + requestBuilder.getSourceBuilder().query(combined); + } return true; } @@ -100,4 +133,12 @@ private void validateLimitWithinK(int limit) { } } } + + @Override + public OpenSearchRequestBuilder build() { + if (filterTypeExplicit && !filterPushed) { + throw new ExpressionEvaluationException("filter_type requires a pushdownable WHERE clause"); + } + return super.build(); + } } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java index bda5187244..059bc79ff8 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Function; import org.junit.jupiter.api.Test; import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.QueryBuilder; @@ -25,6 +26,7 @@ import org.opensearch.sql.expression.ReferenceExpression; import org.opensearch.sql.opensearch.data.value.OpenSearchExprValueFactory; import org.opensearch.sql.opensearch.request.OpenSearchRequestBuilder; +import org.opensearch.sql.opensearch.storage.FilterType; import org.opensearch.sql.planner.logical.LogicalFilter; import org.opensearch.sql.planner.logical.LogicalLimit; import org.opensearch.sql.planner.logical.LogicalValues; @@ -289,6 +291,54 @@ void pushDownFilterCompoundPredicateSurvives() { assertEquals(1, boolQuery.filter().size(), "compound WHERE should be in filter (non-scoring)"); } + @Test + void pushDownFilterEfficientPlacesInsideKnn() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + // Callback simulates VectorSearchIndex rebuilding knn with filter + Function rebuildWithFilter = + whereQuery -> new WrapperQueryBuilder("{\"knn\":{\"filter\":\"embedded\"}}"); + var builder = + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), + FilterType.EFFICIENT, true, rebuildWithFilter); + + var condition = DSL.equal(new ReferenceExpression("city", STRING), DSL.literal("Miami")); + var dummyChild = new LogicalValues(Collections.emptyList()); + var filter = new LogicalFilter(dummyChild, condition); + + boolean pushed = builder.pushDownFilter(filter); + + assertTrue(pushed, "pushDownFilter should succeed"); + QueryBuilder resultQuery = requestBuilder.getSourceBuilder().query(); + assertTrue( + resultQuery instanceof WrapperQueryBuilder, + "Efficient filter should produce a WrapperQueryBuilder (rebuilt knn), not BoolQuery"); + } + + @Test + void pushDownFilterExplicitPostProducesBool() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), + FilterType.POST, true, null); + + var condition = DSL.equal(new ReferenceExpression("name", STRING), DSL.literal("John")); + var dummyChild = new LogicalValues(Collections.emptyList()); + var filter = new LogicalFilter(dummyChild, condition); + + boolean pushed = builder.pushDownFilter(filter); + + assertTrue(pushed); + QueryBuilder resultQuery = requestBuilder.getSourceBuilder().query(); + assertTrue(resultQuery instanceof BoolQueryBuilder); + BoolQueryBuilder boolQuery = (BoolQueryBuilder) resultQuery; + assertEquals(1, boolQuery.must().size()); + assertEquals(1, boolQuery.filter().size()); + } + private OpenSearchRequestBuilder createRequestBuilder() { return new OpenSearchRequestBuilder( mock(OpenSearchExprValueFactory.class), 10000, mock(Settings.class)); From 8eaef06993a62ccb44c7789c6bfc065e8c09fab1 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 9 Apr 2026 15:17:09 -0700 Subject: [PATCH 06/14] Wire FilterType and rebuild callback through createScanBuilder Signed-off-by: Eric Wei --- .../sql/opensearch/storage/VectorSearchIndex.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java index 5e4170835d..cc3666c690 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java @@ -60,9 +60,18 @@ public TableScanBuilder createScanBuilder() { getSettings().getSettingValue(Settings.Key.SQL_CURSOR_KEEP_ALIVE); var requestBuilder = createRequestBuilder(); - // Use VectorSearchQueryBuilder to keep knn in must (scoring) context. - // WHERE filters will be placed in filter (non-scoring) context. - var queryBuilder = new VectorSearchQueryBuilder(requestBuilder, buildKnnQuery(), options); + // Callback for efficient filtering: serialize WHERE QueryBuilder to JSON, + // rebuild knn query with filter embedded. JSON handling stays in this class. + Function rebuildWithFilter = + whereQuery -> new WrapperQueryBuilder(buildKnnQueryJson(whereQuery.toString())); + + boolean filterTypeExplicit = filterType != null; + FilterType effectiveFilterType = filterType != null ? filterType : FilterType.POST; + + var queryBuilder = + new VectorSearchQueryBuilder( + requestBuilder, buildKnnQuery(), options, + effectiveFilterType, filterTypeExplicit, rebuildWithFilter); requestBuilder.pushDownTrackedScore(true); // Default size policy: LIMIT pushdown will further reduce if present. From 845557becc4241cb594d6ac7eee749b3afaeea9b Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 9 Apr 2026 15:18:35 -0700 Subject: [PATCH 07/14] Add build-time validation and regression tests for LIMIT/sort under efficient mode Signed-off-by: Eric Wei --- .../scan/VectorSearchQueryBuilderTest.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java index 059bc79ff8..1a16b8709f 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java @@ -6,6 +6,7 @@ package org.opensearch.sql.opensearch.storage.scan; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -339,6 +340,135 @@ void pushDownFilterExplicitPostProducesBool() { assertEquals(1, boolQuery.filter().size()); } + // ── Build-time validation ──────────────────────────────────────────── + + @Test + void buildRejectsExplicitFilterTypePostWithoutWhere() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), + FilterType.POST, true, null); + + ExpressionEvaluationException ex = + assertThrows(ExpressionEvaluationException.class, builder::build); + assertTrue(ex.getMessage().contains("filter_type requires a pushdownable WHERE clause")); + } + + @Test + void buildRejectsExplicitFilterTypeEfficientWithoutWhere() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + Function rebuildWithFilter = + whereQuery -> new WrapperQueryBuilder("{\"knn\":{\"filter\":\"embedded\"}}"); + var builder = + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), + FilterType.EFFICIENT, true, rebuildWithFilter); + + ExpressionEvaluationException ex = + assertThrows(ExpressionEvaluationException.class, builder::build); + assertTrue(ex.getMessage().contains("filter_type requires a pushdownable WHERE clause")); + } + + @Test + void buildSucceedsWithNoFilterTypeAndNoWhere() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("k", "5")); + + OpenSearchRequestBuilder result = builder.build(); + assertNotNull(result); + } + + @Test + void buildSucceedsWithFilterTypeAndPushedWhere() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), + FilterType.POST, true, null); + + var condition = DSL.equal(new ReferenceExpression("name", STRING), DSL.literal("John")); + var dummyChild = new LogicalValues(Collections.emptyList()); + builder.pushDownFilter(new LogicalFilter(dummyChild, condition)); + + OpenSearchRequestBuilder result = builder.build(); + assertNotNull(result); + } + + // ── Regression: LIMIT and sort invariants under efficient mode ────── + + @Test + void pushDownLimitExceedingKThrowsUnderEfficientMode() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + Function rebuildWithFilter = + whereQuery -> new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), + FilterType.EFFICIENT, true, rebuildWithFilter); + + var dummyChild = new LogicalValues(Collections.emptyList()); + var limit = new LogicalLimit(dummyChild, 10, 0); + + ExpressionEvaluationException ex = + assertThrows(ExpressionEvaluationException.class, () -> builder.pushDownLimit(limit)); + assertTrue(ex.getMessage().contains("LIMIT 10 exceeds k=5")); + } + + @Test + void pushDownSortScoreDescAcceptedUnderEfficientMode() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + Function rebuildWithFilter = + whereQuery -> new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), + FilterType.EFFICIENT, true, rebuildWithFilter); + + var dummyChild = new LogicalValues(Collections.emptyList()); + var sort = + new org.opensearch.sql.planner.logical.LogicalSort( + dummyChild, + List.of( + org.apache.commons.lang3.tuple.ImmutablePair.of( + org.opensearch.sql.ast.tree.Sort.SortOption.DEFAULT_DESC, + new ReferenceExpression("_score", ExprCoreType.FLOAT)))); + + boolean pushed = builder.pushDownSort(sort); + assertTrue(pushed, "ORDER BY _score DESC should be accepted under efficient mode"); + } + + @Test + void pushDownSortNonScoreRejectedUnderEfficientMode() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + Function rebuildWithFilter = + whereQuery -> new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), + FilterType.EFFICIENT, true, rebuildWithFilter); + + var dummyChild = new LogicalValues(Collections.emptyList()); + var sort = + new org.opensearch.sql.planner.logical.LogicalSort( + dummyChild, + List.of( + org.apache.commons.lang3.tuple.ImmutablePair.of( + org.opensearch.sql.ast.tree.Sort.SortOption.DEFAULT_ASC, + new ReferenceExpression("name", STRING)))); + + ExpressionEvaluationException ex = + assertThrows(ExpressionEvaluationException.class, () -> builder.pushDownSort(sort)); + assertTrue(ex.getMessage().contains("unsupported sort expression")); + } + private OpenSearchRequestBuilder createRequestBuilder() { return new OpenSearchRequestBuilder( mock(OpenSearchExprValueFactory.class), 10000, mock(Settings.class)); From 3f0b90e47b7583683701023f1e66ba365abbcb24 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 9 Apr 2026 15:20:45 -0700 Subject: [PATCH 08/14] Add integration tests for filter_type=post|efficient and spotless formatting Signed-off-by: Eric Wei --- .../sql/sql/VectorSearchExplainIT.java | 53 +++++++++++++++++++ .../opensearch/sql/sql/VectorSearchIT.java | 47 ++++++++++++++++ .../opensearch/storage/VectorSearchIndex.java | 8 ++- .../scan/VectorSearchQueryBuilder.java | 4 +- .../storage/VectorSearchIndexTest.java | 36 ++++++++++--- .../scan/VectorSearchQueryBuilderTest.java | 49 +++++++++++------ 6 files changed, 168 insertions(+), 29 deletions(-) diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchExplainIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchExplainIT.java index 136c7e3f3f..bd779d2f62 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchExplainIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchExplainIT.java @@ -170,4 +170,57 @@ public void testExplainLimitWithinKSucceeds() throws IOException { assertTrue("Explain should succeed with LIMIT <= k:\n" + explain, explain.contains("wrapper")); } + + // ── filter_type explain ───────────────────────────────────────────── + + @Test + public void testExplainFilterTypePostProducesBoolQuery() throws IOException { + String explain = + explainQuery( + "SELECT v._id, v._score " + + "FROM vectorSearch(table='" + + TEST_INDEX + + "', field='embedding', " + + "vector='[1.0, 2.0, 3.0]', option='k=10,filter_type=post') AS v " + + "WHERE v.state = 'TX' " + + "LIMIT 10"); + + assertTrue("Explain should contain bool query:\n" + explain, explain.contains("bool")); + assertTrue("Explain should contain must:\n" + explain, explain.contains("must")); + assertTrue("Explain should contain filter:\n" + explain, explain.contains("filter")); + } + + @Test + public void testExplainFilterTypeEfficientProducesKnnWithFilter() throws IOException { + String explain = + explainQuery( + "SELECT v._id, v._score " + + "FROM vectorSearch(table='" + + TEST_INDEX + + "', field='embedding', " + + "vector='[1.0, 2.0]', option='k=5,filter_type=efficient') AS v " + + "WHERE v.state = 'TX' " + + "LIMIT 5"); + + // Efficient mode: knn rebuilt with filter inside, wrapped in WrapperQueryBuilder + assertTrue("Explain should contain wrapper query:\n" + explain, explain.contains("wrapper")); + } + + @Test + public void testEfficientFilterWithOrderByScoreDescSucceeds() throws IOException { + String explain = + explainQuery( + "SELECT v._id, v._score " + + "FROM vectorSearch(table='" + + TEST_INDEX + + "', field='embedding', " + + "vector='[1.0, 2.0]', option='k=5,filter_type=efficient') AS v " + + "WHERE v.state = 'TX' " + + "ORDER BY v._score DESC " + + "LIMIT 5"); + + assertTrue( + "Explain should succeed with efficient + ORDER BY _score DESC:\n" + explain, + explain.contains("wrapper")); + } } diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchIT.java index 66c63c14e2..28f141d327 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchIT.java @@ -168,4 +168,51 @@ public void testOrderByScoreAscRejects() throws IOException { assertThat(ex.getMessage(), containsString("_score ASC is not supported")); } + + // ── filter_type validation ──────────────────────────────────────────── + + @Test + public void testFilterTypeEfficientWithoutWhereRejects() throws IOException { + ResponseException ex = + expectThrows( + ResponseException.class, + () -> + executeQuery( + "SELECT v._id FROM vectorSearch(table='" + + TEST_INDEX + + "', field='embedding', " + + "vector='[1.0, 2.0]', option='k=5,filter_type=efficient') AS v " + + "LIMIT 5")); + + assertThat(ex.getMessage(), containsString("filter_type requires a pushdownable WHERE clause")); + } + + @Test + public void testFilterTypePostWithoutWhereRejects() throws IOException { + ResponseException ex = + expectThrows( + ResponseException.class, + () -> + executeQuery( + "SELECT v._id FROM vectorSearch(table='" + + TEST_INDEX + + "', field='embedding', " + + "vector='[1.0, 2.0]', option='k=5,filter_type=post') AS v " + + "LIMIT 5")); + + assertThat(ex.getMessage(), containsString("filter_type requires a pushdownable WHERE clause")); + } + + @Test + public void testInvalidFilterTypeRejects() throws IOException { + ResponseException ex = + expectThrows( + ResponseException.class, + () -> + executeQuery( + "SELECT v._id FROM vectorSearch(table='t', field='f', " + + "vector='[1.0]', option='k=5,filter_type=bogus') AS v")); + + assertThat(ex.getMessage(), containsString("filter_type must be one of")); + } } diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java index cc3666c690..4ab0e2861c 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java @@ -70,8 +70,12 @@ public TableScanBuilder createScanBuilder() { var queryBuilder = new VectorSearchQueryBuilder( - requestBuilder, buildKnnQuery(), options, - effectiveFilterType, filterTypeExplicit, rebuildWithFilter); + requestBuilder, + buildKnnQuery(), + options, + effectiveFilterType, + filterTypeExplicit, + rebuildWithFilter); requestBuilder.pushDownTrackedScore(true); // Default size policy: LIMIT pushdown will further reduce if present. diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java index 2a53f2a8a2..f07cd4bd2b 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java @@ -64,9 +64,7 @@ public VectorSearchQueryBuilder( /** Backward-compatible constructor — defaults to POST, not explicit. */ public VectorSearchQueryBuilder( - OpenSearchRequestBuilder requestBuilder, - QueryBuilder knnQuery, - Map options) { + OpenSearchRequestBuilder requestBuilder, QueryBuilder knnQuery, Map options) { this(requestBuilder, knnQuery, options, FilterType.POST, false, null); } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java index 25b93f5138..43ebbb5eab 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/VectorSearchIndexTest.java @@ -144,8 +144,13 @@ void buildKnnQueryJsonNonNumericOptionRenderedQuoted() { void buildKnnQueryJsonWithFilterEmbeds() { VectorSearchIndex index = new VectorSearchIndex( - client, settings, "test-index", "embedding", - new float[] {1.0f, 2.0f}, Map.of("k", "5"), FilterType.EFFICIENT); + client, + settings, + "test-index", + "embedding", + new float[] {1.0f, 2.0f}, + Map.of("k", "5"), + FilterType.EFFICIENT); String filterJson = "{\"term\":{\"city\":{\"value\":\"Miami\"}}}"; String json = index.buildKnnQueryJson(filterJson); @@ -160,8 +165,13 @@ void buildKnnQueryJsonWithFilterEmbeds() { void buildKnnQueryJsonWithFilterRadial() { VectorSearchIndex index = new VectorSearchIndex( - client, settings, "test-index", "embedding", - new float[] {1.0f}, Map.of("max_distance", "10.5"), FilterType.EFFICIENT); + client, + settings, + "test-index", + "embedding", + new float[] {1.0f}, + Map.of("max_distance", "10.5"), + FilterType.EFFICIENT); String filterJson = "{\"range\":{\"rating\":{\"gte\":4.0}}}"; String json = index.buildKnnQueryJson(filterJson); @@ -174,8 +184,13 @@ void buildKnnQueryJsonWithFilterRadial() { void buildKnnQueryJsonNullFilterProducesBaseJson() { VectorSearchIndex index = new VectorSearchIndex( - client, settings, "test-index", "embedding", - new float[] {1.0f}, Map.of("k", "5"), null); + client, + settings, + "test-index", + "embedding", + new float[] {1.0f}, + Map.of("k", "5"), + null); String json = index.buildKnnQueryJson(null); String baseJson = index.buildKnnQueryJson(); @@ -191,8 +206,13 @@ void buildKnnQueryJsonExcludesFilterType() { VectorSearchIndex index = new VectorSearchIndex( - client, settings, "test-index", "embedding", - new float[] {1.0f}, options, FilterType.EFFICIENT); + client, + settings, + "test-index", + "embedding", + new float[] {1.0f}, + options, + FilterType.EFFICIENT); String json = index.buildKnnQueryJson(); assertFalse(json.contains("filter_type"), "filter_type should not appear in knn JSON"); diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java index 1a16b8709f..6595f4322b 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java @@ -301,8 +301,12 @@ void pushDownFilterEfficientPlacesInsideKnn() { whereQuery -> new WrapperQueryBuilder("{\"knn\":{\"filter\":\"embedded\"}}"); var builder = new VectorSearchQueryBuilder( - requestBuilder, knnQuery, Map.of("k", "5"), - FilterType.EFFICIENT, true, rebuildWithFilter); + requestBuilder, + knnQuery, + Map.of("k", "5"), + FilterType.EFFICIENT, + true, + rebuildWithFilter); var condition = DSL.equal(new ReferenceExpression("city", STRING), DSL.literal("Miami")); var dummyChild = new LogicalValues(Collections.emptyList()); @@ -323,8 +327,7 @@ void pushDownFilterExplicitPostProducesBool() { var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); var builder = new VectorSearchQueryBuilder( - requestBuilder, knnQuery, Map.of("k", "5"), - FilterType.POST, true, null); + requestBuilder, knnQuery, Map.of("k", "5"), FilterType.POST, true, null); var condition = DSL.equal(new ReferenceExpression("name", STRING), DSL.literal("John")); var dummyChild = new LogicalValues(Collections.emptyList()); @@ -348,8 +351,7 @@ void buildRejectsExplicitFilterTypePostWithoutWhere() { var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); var builder = new VectorSearchQueryBuilder( - requestBuilder, knnQuery, Map.of("k", "5"), - FilterType.POST, true, null); + requestBuilder, knnQuery, Map.of("k", "5"), FilterType.POST, true, null); ExpressionEvaluationException ex = assertThrows(ExpressionEvaluationException.class, builder::build); @@ -364,8 +366,12 @@ void buildRejectsExplicitFilterTypeEfficientWithoutWhere() { whereQuery -> new WrapperQueryBuilder("{\"knn\":{\"filter\":\"embedded\"}}"); var builder = new VectorSearchQueryBuilder( - requestBuilder, knnQuery, Map.of("k", "5"), - FilterType.EFFICIENT, true, rebuildWithFilter); + requestBuilder, + knnQuery, + Map.of("k", "5"), + FilterType.EFFICIENT, + true, + rebuildWithFilter); ExpressionEvaluationException ex = assertThrows(ExpressionEvaluationException.class, builder::build); @@ -388,8 +394,7 @@ void buildSucceedsWithFilterTypeAndPushedWhere() { var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); var builder = new VectorSearchQueryBuilder( - requestBuilder, knnQuery, Map.of("k", "5"), - FilterType.POST, true, null); + requestBuilder, knnQuery, Map.of("k", "5"), FilterType.POST, true, null); var condition = DSL.equal(new ReferenceExpression("name", STRING), DSL.literal("John")); var dummyChild = new LogicalValues(Collections.emptyList()); @@ -409,8 +414,12 @@ void pushDownLimitExceedingKThrowsUnderEfficientMode() { whereQuery -> new WrapperQueryBuilder("{\"knn\":{}}"); var builder = new VectorSearchQueryBuilder( - requestBuilder, knnQuery, Map.of("k", "5"), - FilterType.EFFICIENT, true, rebuildWithFilter); + requestBuilder, + knnQuery, + Map.of("k", "5"), + FilterType.EFFICIENT, + true, + rebuildWithFilter); var dummyChild = new LogicalValues(Collections.emptyList()); var limit = new LogicalLimit(dummyChild, 10, 0); @@ -428,8 +437,12 @@ void pushDownSortScoreDescAcceptedUnderEfficientMode() { whereQuery -> new WrapperQueryBuilder("{\"knn\":{}}"); var builder = new VectorSearchQueryBuilder( - requestBuilder, knnQuery, Map.of("k", "5"), - FilterType.EFFICIENT, true, rebuildWithFilter); + requestBuilder, + knnQuery, + Map.of("k", "5"), + FilterType.EFFICIENT, + true, + rebuildWithFilter); var dummyChild = new LogicalValues(Collections.emptyList()); var sort = @@ -452,8 +465,12 @@ void pushDownSortNonScoreRejectedUnderEfficientMode() { whereQuery -> new WrapperQueryBuilder("{\"knn\":{}}"); var builder = new VectorSearchQueryBuilder( - requestBuilder, knnQuery, Map.of("k", "5"), - FilterType.EFFICIENT, true, rebuildWithFilter); + requestBuilder, + knnQuery, + Map.of("k", "5"), + FilterType.EFFICIENT, + true, + rebuildWithFilter); var dummyChild = new LogicalValues(Collections.emptyList()); var sort = From 1090b365d7f8cd1694cade36aaa7bbe24da7f02f Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Fri, 10 Apr 2026 14:34:39 -0700 Subject: [PATCH 09/14] Reject radial vector search without LIMIT Radial search (max_distance or min_score) can return unbounded results. Add build-time validation that rejects radial queries without an explicit LIMIT clause, with a clear error message guiding the user. Signed-off-by: Eric Wei --- .../opensearch/sql/sql/VectorSearchIT.java | 15 ++++++ .../scan/VectorSearchQueryBuilder.java | 8 +++ .../scan/VectorSearchQueryBuilderTest.java | 50 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchIT.java index 28f141d327..2b072838c0 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchIT.java @@ -133,6 +133,21 @@ public void testMissingRequiredOptionRejects() throws IOException { assertThat(ex.getMessage(), containsString("Missing required option")); } + @Test + public void testRadialWithoutLimitRejects() throws IOException { + ResponseException ex = + expectThrows( + ResponseException.class, + () -> + executeQuery( + "SELECT v._id FROM vectorSearch(table='" + + TEST_INDEX + + "', field='embedding', " + + "vector='[1.0, 2.0]', option='max_distance=10.5') AS v")); + + assertThat(ex.getMessage(), containsString("LIMIT is required for radial vector search")); + } + // ── Sort restriction validation ───────────────────────────────────────── @Test diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java index f07cd4bd2b..b052905fec 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java @@ -44,6 +44,7 @@ public class VectorSearchQueryBuilder extends OpenSearchIndexScanQueryBuilder { private final boolean filterTypeExplicit; private final Function rebuildKnnWithFilter; private boolean filterPushed = false; + private boolean limitPushed = false; /** Full constructor with filter type support. */ public VectorSearchQueryBuilder( @@ -89,6 +90,7 @@ public boolean pushDownFilter(LogicalFilter filter) { @Override public boolean pushDownLimit(LogicalLimit limit) { validateLimitWithinK(limit.getLimit()); + limitPushed = true; return super.pushDownLimit(limit); } @@ -137,6 +139,12 @@ public OpenSearchRequestBuilder build() { if (filterTypeExplicit && !filterPushed) { throw new ExpressionEvaluationException("filter_type requires a pushdownable WHERE clause"); } + boolean isRadial = !options.containsKey("k"); + if (isRadial && !limitPushed) { + throw new ExpressionEvaluationException( + "LIMIT is required for radial vector search (max_distance or min_score)." + + " Without LIMIT, the result set size is unbounded."); + } return super.build(); } } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java index 6595f4322b..1012b7bf41 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java @@ -404,6 +404,56 @@ void buildSucceedsWithFilterTypeAndPushedWhere() { assertNotNull(result); } + // ── Radial without LIMIT rejection ───────────────────────────────── + + @Test + void buildRejectsRadialMaxDistanceWithoutLimit() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("max_distance", "10.0")); + + ExpressionEvaluationException ex = + assertThrows(ExpressionEvaluationException.class, builder::build); + assertTrue(ex.getMessage().contains("LIMIT is required for radial vector search")); + } + + @Test + void buildRejectsRadialMinScoreWithoutLimit() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("min_score", "0.5")); + + ExpressionEvaluationException ex = + assertThrows(ExpressionEvaluationException.class, builder::build); + assertTrue(ex.getMessage().contains("LIMIT is required for radial vector search")); + } + + @Test + void buildSucceedsRadialWithLimit() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("max_distance", "10.0")); + + var dummyChild = new LogicalValues(Collections.emptyList()); + builder.pushDownLimit(new LogicalLimit(dummyChild, 50, 0)); + + OpenSearchRequestBuilder result = builder.build(); + assertNotNull(result); + } + + @Test + void buildSucceedsTopKWithoutLimit() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("k", "5")); + + OpenSearchRequestBuilder result = builder.build(); + assertNotNull(result); + } + // ── Regression: LIMIT and sort invariants under efficient mode ────── @Test From fff5c02f0e4937f1f30c1c4987df1be915a395be Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Tue, 14 Apr 2026 16:15:11 -0700 Subject: [PATCH 10/14] Fix limitPushed not set when limit comes through pushDownSort count path pushDownSort with a non-zero sort.getCount() pushes a limit to requestBuilder directly, bypassing pushDownLimit() and leaving limitPushed=false. This causes build() to incorrectly reject radial vector search when the limit arrives via the sort-with-count path (e.g. PPL sort command). Set limitPushed=true in the sort.getCount() block alongside the existing requestBuilder.pushDownLimit() call. Signed-off-by: Eric Wei --- .../scan/VectorSearchQueryBuilder.java | 1 + .../scan/VectorSearchQueryBuilderTest.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java index b052905fec..74acc6db04 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java @@ -118,6 +118,7 @@ public boolean pushDownSort(LogicalSort sort) { // but PPL or future callers may set a non-zero count to combine sort+limit in one node. if (sort.getCount() != 0) { validateLimitWithinK(sort.getCount()); + limitPushed = true; requestBuilder.pushDownLimit(sort.getCount(), 0); } return true; diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java index 1012b7bf41..0be1aebb04 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java @@ -536,6 +536,31 @@ void pushDownSortNonScoreRejectedUnderEfficientMode() { assertTrue(ex.getMessage().contains("unsupported sort expression")); } + @Test + void buildSucceedsRadialWithSortEmbeddedLimit() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("max_distance", "10.0")); + + var dummyChild = new LogicalValues(Collections.emptyList()); + // LogicalSort with count=50 simulates PPL sort-with-limit path + var sort = + new org.opensearch.sql.planner.logical.LogicalSort( + dummyChild, + 50, + List.of( + org.apache.commons.lang3.tuple.ImmutablePair.of( + org.opensearch.sql.ast.tree.Sort.SortOption.DEFAULT_DESC, + new ReferenceExpression("_score", ExprCoreType.FLOAT)))); + + builder.pushDownSort(sort); + + // build() should not reject — limitPushed must be true via pushDownSort's count path + OpenSearchRequestBuilder result = builder.build(); + assertNotNull(result); + } + private OpenSearchRequestBuilder createRequestBuilder() { return new OpenSearchRequestBuilder( mock(OpenSearchExprValueFactory.class), 10000, mock(Settings.class)); From bf1f9e237dee8527c0e59fefa1c72f629cd8c9c5 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Tue, 14 Apr 2026 16:43:48 -0700 Subject: [PATCH 11/14] Handle non-pushdownable WHERE with explicit filter_type pushDownFilter() did not catch ScriptQueryUnSupportedException, so non-pushdownable filters (e.g. struct-type fields) would propagate a raw internal exception instead of a clean SQL-layer error. With explicit filter_type: throw a clear error explaining the WHERE clause cannot be pushed down for the requested filter placement. Without explicit filter_type: return false to fall back to in-memory filtering, matching the base class behavior. Signed-off-by: Eric Wei --- .../scan/VectorSearchQueryBuilder.java | 15 ++++++- .../scan/VectorSearchQueryBuilderTest.java | 43 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java index 74acc6db04..b9db0b6e22 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java @@ -19,6 +19,7 @@ import org.opensearch.sql.opensearch.request.OpenSearchRequestBuilder; import org.opensearch.sql.opensearch.storage.FilterType; import org.opensearch.sql.opensearch.storage.script.filter.FilterQueryBuilder; +import org.opensearch.sql.opensearch.storage.script.filter.FilterQueryBuilder.ScriptQueryUnSupportedException; import org.opensearch.sql.opensearch.storage.serde.DefaultExpressionSerializer; import org.opensearch.sql.planner.logical.LogicalFilter; import org.opensearch.sql.planner.logical.LogicalLimit; @@ -73,7 +74,19 @@ public VectorSearchQueryBuilder( public boolean pushDownFilter(LogicalFilter filter) { FilterQueryBuilder queryBuilder = new FilterQueryBuilder(new DefaultExpressionSerializer()); Expression queryCondition = filter.getCondition(); - QueryBuilder whereQuery = queryBuilder.build(queryCondition); + QueryBuilder whereQuery; + try { + whereQuery = queryBuilder.build(queryCondition); + } catch (ScriptQueryUnSupportedException e) { + if (filterTypeExplicit) { + throw new ExpressionEvaluationException( + "filter_type requires a pushdownable WHERE clause, but the given condition" + + " cannot be pushed down: " + + e.getMessage()); + } + // Default mode: fall back to in-memory filtering (matches base class behavior) + return false; + } filterPushed = true; if (filterType == FilterType.EFFICIENT) { diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java index 0be1aebb04..371b65a1b1 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java @@ -6,6 +6,7 @@ package org.opensearch.sql.opensearch.storage.scan; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -536,6 +537,48 @@ void pushDownSortNonScoreRejectedUnderEfficientMode() { assertTrue(ex.getMessage().contains("unsupported sort expression")); } + // ── Non-pushdownable filter handling ────────────────────────────────── + + @Test + void pushDownFilterNonPushdownableWithExplicitFilterTypeThrows() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), FilterType.POST, true, null); + + // STRUCT = STRUCT triggers ScriptQueryUnSupportedException in FilterQueryBuilder + var condition = + DSL.equal( + new ReferenceExpression("nested_field", ExprCoreType.STRUCT), + new ReferenceExpression("other_field", ExprCoreType.STRUCT)); + var dummyChild = new LogicalValues(Collections.emptyList()); + var filter = new LogicalFilter(dummyChild, condition); + + ExpressionEvaluationException ex = + assertThrows(ExpressionEvaluationException.class, () -> builder.pushDownFilter(filter)); + assertTrue(ex.getMessage().contains("filter_type requires a pushdownable WHERE clause")); + assertTrue(ex.getMessage().contains("cannot be pushed down")); + } + + @Test + void pushDownFilterNonPushdownableWithoutExplicitFilterTypeFallsBack() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + var builder = new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("k", "5")); + + // STRUCT = STRUCT triggers ScriptQueryUnSupportedException in FilterQueryBuilder + var condition = + DSL.equal( + new ReferenceExpression("nested_field", ExprCoreType.STRUCT), + new ReferenceExpression("other_field", ExprCoreType.STRUCT)); + var dummyChild = new LogicalValues(Collections.emptyList()); + var filter = new LogicalFilter(dummyChild, condition); + + boolean pushed = builder.pushDownFilter(filter); + assertFalse(pushed, "Non-pushdownable filter should return false for in-memory fallback"); + } + @Test void buildSucceedsRadialWithSortEmbeddedLimit() { var requestBuilder = createRequestBuilder(); From 1db11ce8c20a4df61f80c9f9b94f984d6f815037 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Wed, 15 Apr 2026 08:51:40 -0700 Subject: [PATCH 12/14] Add defensive null guard for EFFICIENT mode rebuild callback Reject construction of VectorSearchQueryBuilder with FilterType.EFFICIENT and a null rebuildKnnWithFilter callback at construction time instead of deferring to an NPE in pushDownFilter. Signed-off-by: Eric Wei --- .../storage/scan/VectorSearchQueryBuilder.java | 4 ++++ .../storage/scan/VectorSearchQueryBuilderTest.java | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java index b9db0b6e22..7ddf5fd461 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java @@ -61,6 +61,10 @@ public VectorSearchQueryBuilder( this.options = options; this.filterType = filterType != null ? filterType : FilterType.POST; this.filterTypeExplicit = filterTypeExplicit; + if (this.filterType == FilterType.EFFICIENT && rebuildKnnWithFilter == null) { + throw new IllegalArgumentException( + "EFFICIENT filter mode requires a non-null rebuildKnnWithFilter callback"); + } this.rebuildKnnWithFilter = rebuildKnnWithFilter; } diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java index 371b65a1b1..47e76e4778 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java @@ -344,6 +344,20 @@ void pushDownFilterExplicitPostProducesBool() { assertEquals(1, boolQuery.filter().size()); } + // ── Constructor validation ────────────────────────────────────────── + + @Test + void constructorRejectsEfficientModeWithNullCallback() { + var requestBuilder = createRequestBuilder(); + var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}"); + + assertThrows( + IllegalArgumentException.class, + () -> + new VectorSearchQueryBuilder( + requestBuilder, knnQuery, Map.of("k", "5"), FilterType.EFFICIENT, true, null)); + } + // ── Build-time validation ──────────────────────────────────────────── @Test From 76d905579f986d70e0434acaf17eb476b3effbde Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 16 Apr 2026 15:00:01 -0700 Subject: [PATCH 13/14] Address review feedback: reword user-facing error, strengthen explain test, rename constructor comment - Reword filter_type error message to be user-friendly and actionable (no longer leaks internal ScriptQueryUnSupportedException text) - Strengthen efficient-mode explain IT: assert no bool/must (proves not post-filter shape), decode base64 knn payload to verify filter and predicate field are embedded inside knn query - Rename "Backward-compatible constructor" to clarify intent Signed-off-by: Eric Wei --- .../sql/sql/VectorSearchExplainIT.java | 27 ++++++++++++++++++- .../opensearch/storage/VectorSearchIndex.java | 2 +- .../scan/VectorSearchQueryBuilder.java | 5 ++-- .../scan/VectorSearchQueryBuilderTest.java | 5 ++-- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchExplainIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchExplainIT.java index bd779d2f62..78c7034c74 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchExplainIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchExplainIT.java @@ -202,8 +202,33 @@ public void testExplainFilterTypeEfficientProducesKnnWithFilter() throws IOExcep + "WHERE v.state = 'TX' " + "LIMIT 5"); - // Efficient mode: knn rebuilt with filter inside, wrapped in WrapperQueryBuilder + // Efficient mode: knn rebuilt with filter inside, wrapped in WrapperQueryBuilder. + // The knn JSON (including the embedded filter) is base64-encoded inside the wrapper, + // so we verify structure by: (1) wrapper present, (2) no bool/must in plaintext + // (that would be post-filter shape), (3) decode the base64 payload to confirm + // the filter and predicate field are embedded inside the knn query. assertTrue("Explain should contain wrapper query:\n" + explain, explain.contains("wrapper")); + assertFalse( + "Efficient mode should not produce bool query (that is post-filter shape):\n" + explain, + explain.contains("\"bool\"")); + assertFalse( + "Efficient mode should not contain must clause:\n" + explain, explain.contains("\"must\"")); + + // Extract and decode the base64 knn payload to verify filter embedding. + // The explain output escapes quotes as \", so match both \" and " forms. + java.util.regex.Matcher m = + java.util.regex.Pattern.compile("\\\\?\"query\\\\?\":\\\\?\"([A-Za-z0-9+/=]+)\\\\?\"") + .matcher(explain); + assertTrue("Explain should contain base64-encoded knn query:\n" + explain, m.find()); + String knnJson = + new String( + java.util.Base64.getDecoder().decode(m.group(1)), + java.nio.charset.StandardCharsets.UTF_8); + assertTrue( + "Efficient mode knn JSON should contain filter:\n" + knnJson, knnJson.contains("filter")); + assertTrue( + "Efficient mode knn JSON should contain the WHERE predicate field:\n" + knnJson, + knnJson.contains("state")); } @Test diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java index 4ab0e2861c..0446b2dc00 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/VectorSearchIndex.java @@ -43,7 +43,7 @@ public VectorSearchIndex( this.filterType = filterType; } - /** Backward-compatible constructor — defaults to no explicit filter type. */ + /** Default constructor — preserves existing call sites; uses no explicit filter type. */ public VectorSearchIndex( OpenSearchClient client, Settings settings, diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java index 7ddf5fd461..300458b14f 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java @@ -84,9 +84,8 @@ public boolean pushDownFilter(LogicalFilter filter) { } catch (ScriptQueryUnSupportedException e) { if (filterTypeExplicit) { throw new ExpressionEvaluationException( - "filter_type requires a pushdownable WHERE clause, but the given condition" - + " cannot be pushed down: " - + e.getMessage()); + "filter_type only works when the WHERE clause can be translated to an" + + " OpenSearch filter. Rewrite the WHERE clause or omit filter_type."); } // Default mode: fall back to in-memory filtering (matches base class behavior) return false; diff --git a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java index 47e76e4778..6da4a6b9c5 100644 --- a/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java +++ b/opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java @@ -571,8 +571,9 @@ void pushDownFilterNonPushdownableWithExplicitFilterTypeThrows() { ExpressionEvaluationException ex = assertThrows(ExpressionEvaluationException.class, () -> builder.pushDownFilter(filter)); - assertTrue(ex.getMessage().contains("filter_type requires a pushdownable WHERE clause")); - assertTrue(ex.getMessage().contains("cannot be pushed down")); + assertTrue( + ex.getMessage().contains("filter_type only works when the WHERE clause can be translated")); + assertTrue(ex.getMessage().contains("Rewrite the WHERE clause or omit filter_type")); } @Test From 73dc7d4bec880c9a514e33c80d960aee7cdfcddd Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Thu, 16 Apr 2026 15:41:57 -0700 Subject: [PATCH 14/14] Rename remaining backward-compatible constructor comment for consistency Signed-off-by: Eric Wei --- .../sql/opensearch/storage/scan/VectorSearchQueryBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java index 300458b14f..4a59bb4fb5 100644 --- a/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java +++ b/opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java @@ -68,7 +68,7 @@ public VectorSearchQueryBuilder( this.rebuildKnnWithFilter = rebuildKnnWithFilter; } - /** Backward-compatible constructor — defaults to POST, not explicit. */ + /** Default constructor — preserves existing call sites; defaults to POST, not explicit. */ public VectorSearchQueryBuilder( OpenSearchRequestBuilder requestBuilder, QueryBuilder knnQuery, Map options) { this(requestBuilder, knnQuery, options, FilterType.POST, false, null);