Skip to content
Closed
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 @@ -32,6 +32,7 @@

package org.opensearch.search.fetch.subphase;

import org.opensearch.Version;
import org.opensearch.common.Booleans;
import org.opensearch.common.annotation.PublicApi;
import org.opensearch.common.xcontent.support.XContentMapValues;
Expand All @@ -49,6 +50,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
Expand All @@ -69,12 +71,21 @@ public class FetchSourceContext implements Writeable, ToXContentObject {
private final boolean fetchSource;
private final String[] includes;
private final String[] excludes;
private final boolean includesExplicit;
private Function<Map<String, ?>, Map<String, Object>> filter;

public FetchSourceContext(boolean fetchSource, String[] includes, String[] excludes) {
this.fetchSource = fetchSource;
this.includes = includes == null ? Strings.EMPTY_ARRAY : includes;
this.excludes = excludes == null ? Strings.EMPTY_ARRAY : excludes;
this.includesExplicit = false;
}

private FetchSourceContext(boolean fetchSource, String[] includes, String[] excludes, boolean includesExplicit) {
this.fetchSource = fetchSource;
this.includes = includes == null ? Strings.EMPTY_ARRAY : includes;
this.excludes = excludes == null ? Strings.EMPTY_ARRAY : excludes;
this.includesExplicit = includesExplicit;
}

public FetchSourceContext(boolean fetchSource) {
Expand All @@ -85,13 +96,21 @@ public FetchSourceContext(StreamInput in) throws IOException {
fetchSource = in.readBoolean();
includes = in.readStringArray();
excludes = in.readStringArray();
if (in.getVersion().onOrAfter(Version.V_3_6_0)) {
includesExplicit = in.readBoolean();
} else {
includesExplicit = false;
}
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeBoolean(fetchSource);
out.writeStringArray(includes);
out.writeStringArray(excludes);
if (out.getVersion().onOrAfter(Version.V_3_6_0)) {
out.writeBoolean(includesExplicit);
}
}

public boolean fetchSource() {
Expand All @@ -106,10 +125,20 @@ public String[] excludes() {
return this.excludes;
}

/**
* Returns true if the includes filter was explicitly set, even if the array is empty.
* This is used to distinguish between "no includes specified" (include everything)
* and "empty includes specified" (include nothing).
*/
public boolean includesExplicit() {
return this.includesExplicit;
}

public static FetchSourceContext parseFromRestRequest(RestRequest request) {
Boolean fetchSource = null;
String[] sourceExcludes = null;
String[] sourceIncludes = null;
boolean includesExplicit = false;

String source = request.param("_source");
if (source != null) {
Expand All @@ -119,12 +148,14 @@ public static FetchSourceContext parseFromRestRequest(RestRequest request) {
fetchSource = false;
} else {
sourceIncludes = Strings.splitStringByCommaToArray(source);
includesExplicit = true;
}
}

String sIncludes = request.param("_source_includes");
if (sIncludes != null) {
sourceIncludes = Strings.splitStringByCommaToArray(sIncludes);
includesExplicit = true;
}

String sExcludes = request.param("_source_excludes");
Expand All @@ -133,7 +164,7 @@ public static FetchSourceContext parseFromRestRequest(RestRequest request) {
}

if (fetchSource != null || sourceIncludes != null || sourceExcludes != null) {
return new FetchSourceContext(fetchSource == null ? true : fetchSource, sourceIncludes, sourceExcludes);
return new FetchSourceContext(fetchSource == null ? true : fetchSource, sourceIncludes, sourceExcludes, includesExplicit);
}
return null;
}
Expand All @@ -143,16 +174,19 @@ public static FetchSourceContext fromXContent(XContentParser parser) throws IOEx
boolean fetchSource = true;
String[] includes = Strings.EMPTY_ARRAY;
String[] excludes = Strings.EMPTY_ARRAY;
boolean includesExplicit = false;
if (token == XContentParser.Token.VALUE_BOOLEAN) {
fetchSource = parser.booleanValue();
} else if (token == XContentParser.Token.VALUE_STRING) {
includes = new String[] { parser.text() };
includesExplicit = true;
} else if (token == XContentParser.Token.START_ARRAY) {
ArrayList<String> list = new ArrayList<>();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
list.add(parser.text());
}
includes = list.toArray(new String[0]);
includesExplicit = true;
} else if (token == XContentParser.Token.START_OBJECT) {
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
Expand All @@ -173,6 +207,7 @@ public static FetchSourceContext fromXContent(XContentParser parser) throws IOEx
}
}
includes = includesList.toArray(new String[0]);
includesExplicit = true;
} else if (EXCLUDES_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
List<String> excludesList = new ArrayList<>();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
Expand All @@ -197,6 +232,7 @@ public static FetchSourceContext fromXContent(XContentParser parser) throws IOEx
} else if (token == XContentParser.Token.VALUE_STRING) {
if (INCLUDES_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
includes = new String[] { parser.text() };
includesExplicit = true;
} else if (EXCLUDES_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
excludes = new String[] { parser.text() };
} else {
Expand Down Expand Up @@ -227,7 +263,7 @@ public static FetchSourceContext fromXContent(XContentParser parser) throws IOEx
parser.getTokenLocation()
);
}
return new FetchSourceContext(fetchSource, includes, excludes);
return new FetchSourceContext(fetchSource, includes, excludes, includesExplicit);
}

@Override
Expand All @@ -251,6 +287,7 @@ public boolean equals(Object o) {
FetchSourceContext that = (FetchSourceContext) o;

if (fetchSource != that.fetchSource) return false;
if (includesExplicit != that.includesExplicit) return false;
if (!Arrays.equals(excludes, that.excludes)) return false;
if (!Arrays.equals(includes, that.includes)) return false;

Expand All @@ -260,6 +297,7 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
int result = (fetchSource ? 1 : 0);
result = 31 * result + (includesExplicit ? 1 : 0);
result = 31 * result + (includes != null ? Arrays.hashCode(includes) : 0);
result = 31 * result + (excludes != null ? Arrays.hashCode(excludes) : 0);
return result;
Expand All @@ -271,7 +309,13 @@ public int hashCode() {
*/
public Function<Map<String, ?>, Map<String, Object>> getFilter() {
if (filter == null) {
filter = XContentMapValues.filter(includes, excludes, true);
// If includes was explicitly set to an empty array, return an empty map
// rather than treating it as "include everything"
if (includesExplicit && includes.length == 0) {
filter = (sourceAsMap) -> Collections.emptyMap();
} else {
filter = XContentMapValues.filter(includes, excludes, true);
}
}
return filter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ private void hitExecute(String index, FetchSourceContext fetchSourceContext, Hit
}

private static boolean containsFilters(FetchSourceContext context) {
return context.includes().length != 0 || context.excludes().length != 0;
return context.includes().length != 0 || context.excludes().length != 0 || context.includesExplicit();
}

private Map<String, Object> getNestedSource(Map<String, Object> sourceAsMap, HitContext hitContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.memory.MemoryIndex;
import org.opensearch.common.xcontent.XContentFactory;
import org.opensearch.common.xcontent.json.JsonXContent;
import org.opensearch.core.common.Strings;
import org.opensearch.core.common.bytes.BytesReference;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.core.xcontent.XContentParser;
import org.opensearch.search.SearchHit;
import org.opensearch.search.fetch.FetchContext;
import org.opensearch.search.fetch.FetchSubPhase.HitContext;
Expand Down Expand Up @@ -75,6 +77,52 @@ public void testBasicFiltering() throws IOException {
assertEquals(Collections.singletonMap("field1", "value"), hitContext.hit().getSourceAsMap());
}

public void testEmptyIncludesArray() throws IOException {
XContentBuilder source = XContentFactory.jsonBuilder().startObject().field("field1", "value").field("field2", "value2").endObject();

// Test with _source: [] (empty array parsed via fromXContent)
FetchSourceContext fetchSourceContext;
try (XContentParser parser = createParser(JsonXContent.jsonXContent, "[]")) {
parser.nextToken();
fetchSourceContext = FetchSourceContext.fromXContent(parser);
}
assertTrue(fetchSourceContext.fetchSource());
assertTrue(fetchSourceContext.includesExplicit());
assertEquals(0, fetchSourceContext.includes().length);

HitContext hitContext = hitExecuteWithContext(source, fetchSourceContext);
assertEquals(Collections.emptyMap(), hitContext.hit().getSourceAsMap());

// Test with _source: {"includes": []} (empty includes in object)
try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"includes\": []}")) {
parser.nextToken();
fetchSourceContext = FetchSourceContext.fromXContent(parser);
}
assertTrue(fetchSourceContext.fetchSource());
assertTrue(fetchSourceContext.includesExplicit());
assertEquals(0, fetchSourceContext.includes().length);

hitContext = hitExecuteWithContext(source, fetchSourceContext);
assertEquals(Collections.emptyMap(), hitContext.hit().getSourceAsMap());
}

public void testEmptyIncludesWithExcludes() throws IOException {
XContentBuilder source = XContentFactory.jsonBuilder().startObject().field("field1", "value").field("field2", "value2").endObject();

// Test with _source: {"includes": [], "excludes": ["field1"]}
// Empty includes with explicit excludes - should still return empty since includes is empty
FetchSourceContext fetchSourceContext;
try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{\"includes\": [], \"excludes\": [\"field1\"]}")) {
parser.nextToken();
fetchSourceContext = FetchSourceContext.fromXContent(parser);
}
assertTrue(fetchSourceContext.fetchSource());
assertTrue(fetchSourceContext.includesExplicit());

HitContext hitContext = hitExecuteWithContext(source, fetchSourceContext);
assertEquals(Collections.emptyMap(), hitContext.hit().getSourceAsMap());
}

public void testMultipleFiltering() throws IOException {
XContentBuilder source = XContentFactory.jsonBuilder().startObject().field("field", "value").field("field2", "value2").endObject();
HitContext hitContext = hitExecuteMultiple(source, true, new String[] { "*.notexisting", "field" }, null);
Expand Down Expand Up @@ -209,4 +257,27 @@ private HitContext hitExecuteMultiple(
return hitContext;
}

private HitContext hitExecuteWithContext(XContentBuilder source, FetchSourceContext fetchSourceContext) throws IOException {
FetchContext fetchContext = mock(FetchContext.class);
when(fetchContext.fetchSourceContext()).thenReturn(fetchSourceContext);
when(fetchContext.getIndexName()).thenReturn("index");

final SearchHit searchHit = new SearchHit(1, null, null, null, null);

MemoryIndex index = new MemoryIndex();
LeafReaderContext leafReaderContext = index.createSearcher().getIndexReader().leaves().get(0);
HitContext hitContext = new HitContext(searchHit, leafReaderContext, 1, new SourceLookup());
hitContext.sourceLookup().setSource(source == null ? null : BytesReference.bytes(source));

FetchSourcePhase phase = new FetchSourcePhase();
FetchSubPhaseProcessor processor = phase.getProcessor(fetchContext);
if (fetchSourceContext.fetchSource()) {
assertNotNull(processor);
processor.process(hitContext);
} else {
assertNull(processor);
}
return hitContext;
}

}
Loading