Skip to content

Commit 1090b36

Browse files
committed
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 <mengwei.eric@gmail.com>
1 parent 3f0b90e commit 1090b36

3 files changed

Lines changed: 73 additions & 0 deletions

File tree

integ-test/src/test/java/org/opensearch/sql/sql/VectorSearchIT.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,21 @@ public void testMissingRequiredOptionRejects() throws IOException {
133133
assertThat(ex.getMessage(), containsString("Missing required option"));
134134
}
135135

136+
@Test
137+
public void testRadialWithoutLimitRejects() throws IOException {
138+
ResponseException ex =
139+
expectThrows(
140+
ResponseException.class,
141+
() ->
142+
executeQuery(
143+
"SELECT v._id FROM vectorSearch(table='"
144+
+ TEST_INDEX
145+
+ "', field='embedding', "
146+
+ "vector='[1.0, 2.0]', option='max_distance=10.5') AS v"));
147+
148+
assertThat(ex.getMessage(), containsString("LIMIT is required for radial vector search"));
149+
}
150+
136151
// ── Sort restriction validation ─────────────────────────────────────────
137152

138153
@Test

opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class VectorSearchQueryBuilder extends OpenSearchIndexScanQueryBuilder {
4444
private final boolean filterTypeExplicit;
4545
private final Function<QueryBuilder, QueryBuilder> rebuildKnnWithFilter;
4646
private boolean filterPushed = false;
47+
private boolean limitPushed = false;
4748

4849
/** Full constructor with filter type support. */
4950
public VectorSearchQueryBuilder(
@@ -89,6 +90,7 @@ public boolean pushDownFilter(LogicalFilter filter) {
8990
@Override
9091
public boolean pushDownLimit(LogicalLimit limit) {
9192
validateLimitWithinK(limit.getLimit());
93+
limitPushed = true;
9294
return super.pushDownLimit(limit);
9395
}
9496

@@ -137,6 +139,12 @@ public OpenSearchRequestBuilder build() {
137139
if (filterTypeExplicit && !filterPushed) {
138140
throw new ExpressionEvaluationException("filter_type requires a pushdownable WHERE clause");
139141
}
142+
boolean isRadial = !options.containsKey("k");
143+
if (isRadial && !limitPushed) {
144+
throw new ExpressionEvaluationException(
145+
"LIMIT is required for radial vector search (max_distance or min_score)."
146+
+ " Without LIMIT, the result set size is unbounded.");
147+
}
140148
return super.build();
141149
}
142150
}

opensearch/src/test/java/org/opensearch/sql/opensearch/storage/scan/VectorSearchQueryBuilderTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,56 @@ void buildSucceedsWithFilterTypeAndPushedWhere() {
404404
assertNotNull(result);
405405
}
406406

407+
// ── Radial without LIMIT rejection ─────────────────────────────────
408+
409+
@Test
410+
void buildRejectsRadialMaxDistanceWithoutLimit() {
411+
var requestBuilder = createRequestBuilder();
412+
var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}");
413+
var builder =
414+
new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("max_distance", "10.0"));
415+
416+
ExpressionEvaluationException ex =
417+
assertThrows(ExpressionEvaluationException.class, builder::build);
418+
assertTrue(ex.getMessage().contains("LIMIT is required for radial vector search"));
419+
}
420+
421+
@Test
422+
void buildRejectsRadialMinScoreWithoutLimit() {
423+
var requestBuilder = createRequestBuilder();
424+
var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}");
425+
var builder =
426+
new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("min_score", "0.5"));
427+
428+
ExpressionEvaluationException ex =
429+
assertThrows(ExpressionEvaluationException.class, builder::build);
430+
assertTrue(ex.getMessage().contains("LIMIT is required for radial vector search"));
431+
}
432+
433+
@Test
434+
void buildSucceedsRadialWithLimit() {
435+
var requestBuilder = createRequestBuilder();
436+
var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}");
437+
var builder =
438+
new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("max_distance", "10.0"));
439+
440+
var dummyChild = new LogicalValues(Collections.emptyList());
441+
builder.pushDownLimit(new LogicalLimit(dummyChild, 50, 0));
442+
443+
OpenSearchRequestBuilder result = builder.build();
444+
assertNotNull(result);
445+
}
446+
447+
@Test
448+
void buildSucceedsTopKWithoutLimit() {
449+
var requestBuilder = createRequestBuilder();
450+
var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}");
451+
var builder = new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("k", "5"));
452+
453+
OpenSearchRequestBuilder result = builder.build();
454+
assertNotNull(result);
455+
}
456+
407457
// ── Regression: LIMIT and sort invariants under efficient mode ──────
408458

409459
@Test

0 commit comments

Comments
 (0)