Skip to content

Commit bea6607

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 5b0373c commit bea6607

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
@@ -203,6 +203,21 @@ public void testMissingRequiredOptionRejects() throws IOException {
203203
assertThat(ex.getMessage(), containsString("Missing required option"));
204204
}
205205

206+
@Test
207+
public void testRadialWithoutLimitRejects() throws IOException {
208+
ResponseException ex =
209+
expectThrows(
210+
ResponseException.class,
211+
() ->
212+
executeQuery(
213+
"SELECT v._id FROM vectorSearch(table='"
214+
+ TEST_INDEX
215+
+ "', field='embedding', "
216+
+ "vector='[1.0, 2.0]', option='max_distance=10.5') AS v"));
217+
218+
assertThat(ex.getMessage(), containsString("LIMIT is required for radial vector search"));
219+
}
220+
206221
// ── Sort restriction validation ─────────────────────────────────────────
207222

208223
@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(
@@ -95,6 +96,7 @@ public boolean pushDownLimit(LogicalLimit limit) {
9596
String.format("LIMIT %d exceeds k=%d in top-k vector search", limit.getLimit(), k));
9697
}
9798
}
99+
limitPushed = true;
98100
return super.pushDownLimit(limit);
99101
}
100102

@@ -131,6 +133,12 @@ public OpenSearchRequestBuilder build() {
131133
if (filterTypeExplicit && !filterPushed) {
132134
throw new ExpressionEvaluationException("filter_type requires a pushdownable WHERE clause");
133135
}
136+
boolean isRadial = !options.containsKey("k");
137+
if (isRadial && !limitPushed) {
138+
throw new ExpressionEvaluationException(
139+
"LIMIT is required for radial vector search (max_distance or min_score)."
140+
+ " Without LIMIT, the result set size is unbounded.");
141+
}
134142
return super.build();
135143
}
136144
}

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
@@ -382,6 +382,56 @@ void buildSucceedsWithFilterTypeAndPushedWhere() {
382382
assertNotNull(result);
383383
}
384384

385+
// ── Radial without LIMIT rejection ─────────────────────────────────
386+
387+
@Test
388+
void buildRejectsRadialMaxDistanceWithoutLimit() {
389+
var requestBuilder = createRequestBuilder();
390+
var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}");
391+
var builder =
392+
new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("max_distance", "10.0"));
393+
394+
ExpressionEvaluationException ex =
395+
assertThrows(ExpressionEvaluationException.class, builder::build);
396+
assertTrue(ex.getMessage().contains("LIMIT is required for radial vector search"));
397+
}
398+
399+
@Test
400+
void buildRejectsRadialMinScoreWithoutLimit() {
401+
var requestBuilder = createRequestBuilder();
402+
var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}");
403+
var builder =
404+
new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("min_score", "0.5"));
405+
406+
ExpressionEvaluationException ex =
407+
assertThrows(ExpressionEvaluationException.class, builder::build);
408+
assertTrue(ex.getMessage().contains("LIMIT is required for radial vector search"));
409+
}
410+
411+
@Test
412+
void buildSucceedsRadialWithLimit() {
413+
var requestBuilder = createRequestBuilder();
414+
var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}");
415+
var builder =
416+
new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("max_distance", "10.0"));
417+
418+
var dummyChild = new LogicalValues(Collections.emptyList());
419+
builder.pushDownLimit(new LogicalLimit(dummyChild, 50, 0));
420+
421+
OpenSearchRequestBuilder result = builder.build();
422+
assertNotNull(result);
423+
}
424+
425+
@Test
426+
void buildSucceedsTopKWithoutLimit() {
427+
var requestBuilder = createRequestBuilder();
428+
var knnQuery = new WrapperQueryBuilder("{\"knn\":{}}");
429+
var builder = new VectorSearchQueryBuilder(requestBuilder, knnQuery, Map.of("k", "5"));
430+
431+
OpenSearchRequestBuilder result = builder.build();
432+
assertNotNull(result);
433+
}
434+
385435
// ── Regression: LIMIT and sort invariants under efficient mode ──────
386436

387437
@Test

0 commit comments

Comments
 (0)