Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ public SearchAfterBuilder setSortValues(Object[] values) {
throw new IllegalArgumentException("Values must contains at least one value.");
}
for (int i = 0; i < values.length; i++) {
if (values[i] == null) continue;
if (values[i] == null) {
throw new IllegalArgumentException("Values cannot contain null at position " + i + ".");
}
if (values[i] instanceof String) continue;
if (values[i] instanceof Text) continue;
if (values[i] instanceof Long) continue;
Expand Down Expand Up @@ -117,7 +119,7 @@ public static FieldDoc buildFieldDoc(SortAndFormats sort, Object[] values, @Null
if (values[i] != null) {
fieldValues[i] = convertValueFromSortField(values[i], sortField, format);
} else {
fieldValues[i] = null;
throw new IllegalArgumentException("Values cannot contain null at position " + i + ".");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you know what scenario this covers for in practice?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the constructor SearchAfterBuilder(StreamInput in), readGenericValue() can return null (case -1), and these null values are directly assigned to the sortValues array, bypassing the validation in setSortValues().

Copy link
Member

@javanna javanna Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I see, but these you can only get from a serialized SearchAfterBuilder, so perhaps null values can only come in from previous versions (before your change is made)? At the same time version based logic complicates things so maybe it is ok to leave it as-is.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your point about backward compatibility. Keeping the validation as it is seems like the simplest and safest approach. Appreciate the feedback!

}
}
/*
Expand Down Expand Up @@ -250,7 +252,7 @@ public static SearchAfterBuilder fromXContent(XContentParser parser) throws IOEx
} else if (token == XContentParser.Token.VALUE_BOOLEAN) {
values.add(parser.booleanValue());
} else if (token == XContentParser.Token.VALUE_NULL) {
values.add(null);
throw new ParsingException(parser.getTokenLocation(), "[search_after] values cannot contain null.");
} else {
throw new ParsingException(
parser.getTokenLocation(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.apache.lucene.search.SortedNumericSortField;
import org.apache.lucene.search.SortedSetSortField;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
Expand Down Expand Up @@ -59,7 +60,7 @@ public static SearchAfterBuilder randomSearchAfterBuilder() throws IOException {
SearchAfterBuilder searchAfterBuilder = new SearchAfterBuilder();
Object[] values = new Object[numSearchFrom];
for (int i = 0; i < numSearchFrom; i++) {
int branch = randomInt(10);
int branch = randomInt(9);
switch (branch) {
case 0 -> values[i] = randomInt();
case 1 -> values[i] = randomFloat();
Expand All @@ -70,8 +71,7 @@ public static SearchAfterBuilder randomSearchAfterBuilder() throws IOException {
case 6 -> values[i] = randomByte();
case 7 -> values[i] = randomShort();
case 8 -> values[i] = new Text(randomAlphaOfLengthBetween(5, 20));
case 9 -> values[i] = null;
case 10 -> values[i] = randomBigInteger();
case 9 -> values[i] = randomBigInteger();
}
}
searchAfterBuilder.setSortValues(values);
Expand All @@ -91,7 +91,7 @@ public static SearchAfterBuilder randomJsonSearchFromBuilder(BiFunction<XContent
jsonBuilder.startObject();
jsonBuilder.startArray("search_after");
for (int i = 0; i < numSearchAfter; i++) {
int branch = randomInt(9);
int branch = randomInt(8);
switch (branch) {
case 0 -> jsonBuilder.value(randomInt());
case 1 -> jsonBuilder.value(randomFloat());
Expand All @@ -102,7 +102,6 @@ public static SearchAfterBuilder randomJsonSearchFromBuilder(BiFunction<XContent
case 6 -> jsonBuilder.value(randomByte());
case 7 -> jsonBuilder.value(randomShort());
case 8 -> jsonBuilder.value(new Text(randomAlphaOfLengthBetween(5, 20)));
case 9 -> jsonBuilder.nullValue();
}
}
jsonBuilder.endArray();
Expand Down Expand Up @@ -280,4 +279,48 @@ public void testBuildFieldDocWithCollapse() {
);
assertEquals(fieldDoc.toString(), new FieldDoc(Integer.MAX_VALUE, 0, new Object[] { new BytesRef("foo") }).toString());
}

/**
* Test that null values are properly rejected in setSortValues
*/
public void testSetSortValuesRejectsNull() {
SearchAfterBuilder builder = new SearchAfterBuilder();

IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, () -> { builder.setSortValues(new Object[] { null }); });
assertThat(e1.getMessage(), equalTo("Values cannot contain null at position 0."));

IllegalArgumentException e2 = expectThrows(IllegalArgumentException.class, () -> {
builder.setSortValues(new Object[] { 123L, null, "test" });
});
assertThat(e2.getMessage(), equalTo("Values cannot contain null at position 1."));
}

/**
* Test that null values are properly rejected in JSON parsing
*/
public void testFromXContentRejectsNull() throws Exception {
String jsonWithNull = "{\"search_after\": [123, null, \"test\"]}";
try (XContentParser parser = createParser(JsonXContent.jsonXContent, jsonWithNull)) {
parser.nextToken();
parser.nextToken();
parser.nextToken();
ParsingException e = expectThrows(ParsingException.class, () -> { SearchAfterBuilder.fromXContent(parser); });
assertThat(e.getMessage(), equalTo("[search_after] values cannot contain null."));
}
}

/**
* Test that buildFieldDoc rejects null values
*/
public void testBuildFieldDocRejectsNull() {
SortField longSortField = new SortField("timestamp", SortField.Type.LONG, true);
Sort sort = new Sort(longSortField);
DocValueFormat[] formats = { DocValueFormat.RAW };
SortAndFormats sortAndFormats = new SortAndFormats(sort, formats);

IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
SearchAfterBuilder.buildFieldDoc(sortAndFormats, new Object[] { null }, null);
});
assertThat(e.getMessage(), equalTo("Values cannot contain null at position 0."));
}
}