Skip to content

Commit dd51e81

Browse files
committed
Add capability to disable source recovery_source for an index
Signed-off-by: Navneet Verma <navneev@amazon.com>
1 parent 4eb33b0 commit dd51e81

File tree

3 files changed

+212
-13
lines changed

3 files changed

+212
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
## [Unreleased 2.x]
77
### Added
88
- Add useCompoundFile index setting ([#13478](https://github.com/opensearch-project/OpenSearch/pull/13478))
9+
- Add capability to disable source recovery_source for an index ([#13590](https://github.com/opensearch-project/OpenSearch/pull/13590))
910

1011
### Dependencies
1112
- Bump `com.github.spullara.mustache.java:compiler` from 0.9.10 to 0.9.13 ([#13329](https://github.com/opensearch-project/OpenSearch/pull/13329), [#13559](https://github.com/opensearch-project/OpenSearch/pull/13559))

server/src/main/java/org/opensearch/index/mapper/SourceFieldMapper.java

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public class SourceFieldMapper extends MetadataFieldMapper {
7474

7575
public static final String CONTENT_TYPE = "_source";
7676
private final Function<Map<String, ?>, Map<String, Object>> filter;
77+
private final Function<Map<String, ?>, Map<String, Object>> recoverySourceFilter;
7778

7879
/**
7980
* Default parameters for source fields
@@ -119,21 +120,45 @@ public static class Builder extends MetadataFieldMapper.Builder {
119120
Collections.emptyList()
120121
);
121122

123+
private final Parameter<Boolean> recoverySourceEnabled = Parameter.boolParam(
124+
"recovery_source_enabled",
125+
false,
126+
m -> toType(m).recoverySourceEnabled,
127+
Defaults.ENABLED
128+
);
129+
130+
private final Parameter<List<String>> recoverySourceIncludes = Parameter.stringArrayParam(
131+
"recovery_source_includes",
132+
false,
133+
m -> Arrays.asList(toType(m).recoverySourceIncludes),
134+
Collections.emptyList()
135+
);
136+
137+
private final Parameter<List<String>> recoverySourceExcludes = Parameter.stringArrayParam(
138+
"recovery_source_excludes",
139+
false,
140+
m -> Arrays.asList(toType(m).recoverySourceExcludes),
141+
Collections.emptyList()
142+
);
143+
122144
public Builder() {
123145
super(Defaults.NAME);
124146
}
125147

126148
@Override
127149
protected List<Parameter<?>> getParameters() {
128-
return Arrays.asList(enabled, includes, excludes);
150+
return Arrays.asList(enabled, includes, excludes, recoverySourceEnabled, recoverySourceIncludes, recoverySourceExcludes);
129151
}
130152

131153
@Override
132154
public SourceFieldMapper build(BuilderContext context) {
133155
return new SourceFieldMapper(
134156
enabled.getValue(),
135157
includes.getValue().toArray(new String[0]),
136-
excludes.getValue().toArray(new String[0])
158+
excludes.getValue().toArray(new String[0]),
159+
recoverySourceEnabled.getValue(),
160+
recoverySourceIncludes.getValue().toArray(new String[0]),
161+
recoverySourceExcludes.getValue().toArray(new String[0])
137162
);
138163
}
139164
}
@@ -173,24 +198,44 @@ public Query termQuery(Object value, QueryShardContext context) {
173198
}
174199

175200
private final boolean enabled;
201+
private final boolean recoverySourceEnabled;
176202
/** indicates whether the source will always exist and be complete, for use by features like the update API */
177203
private final boolean complete;
178204

179205
private final String[] includes;
180206
private final String[] excludes;
207+
private final String[] recoverySourceIncludes;
208+
private final String[] recoverySourceExcludes;
181209

182210
private SourceFieldMapper() {
183-
this(Defaults.ENABLED, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY);
211+
this(Defaults.ENABLED, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY, Defaults.ENABLED, Strings.EMPTY_ARRAY, Strings.EMPTY_ARRAY);
184212
}
185213

186-
private SourceFieldMapper(boolean enabled, String[] includes, String[] excludes) {
214+
private SourceFieldMapper(
215+
boolean enabled,
216+
String[] includes,
217+
String[] excludes,
218+
boolean recoverySourceEnabled,
219+
String[] recoverySourceIncludes,
220+
String[] recoverySourceExcludes
221+
) {
187222
super(new SourceFieldType(enabled));
188223
this.enabled = enabled;
189224
this.includes = includes;
190225
this.excludes = excludes;
191226
final boolean filtered = CollectionUtils.isEmpty(includes) == false || CollectionUtils.isEmpty(excludes) == false;
192227
this.filter = enabled && filtered ? XContentMapValues.filter(includes, excludes) : null;
193228
this.complete = enabled && CollectionUtils.isEmpty(includes) && CollectionUtils.isEmpty(excludes);
229+
230+
// Set parameters for recovery source
231+
this.recoverySourceEnabled = recoverySourceEnabled;
232+
this.recoverySourceIncludes = recoverySourceIncludes;
233+
this.recoverySourceExcludes = recoverySourceExcludes;
234+
final boolean recoverySourcefiltered = CollectionUtils.isEmpty(recoverySourceIncludes) == false
235+
|| CollectionUtils.isEmpty(recoverySourceExcludes) == false;
236+
this.recoverySourceFilter = this.recoverySourceEnabled && recoverySourcefiltered
237+
? XContentMapValues.filter(recoverySourceIncludes, recoverySourceExcludes)
238+
: null;
194239
}
195240

196241
public boolean enabled() {
@@ -212,22 +257,40 @@ public void preParse(ParseContext context) throws IOException {
212257
context.doc().add(new StoredField(fieldType().name(), ref.bytes, ref.offset, ref.length));
213258
}
214259

215-
if (originalSource != null && adaptedSource != originalSource) {
216-
// if we omitted source or modified it we add the _recovery_source to ensure we have it for ops based recovery
217-
BytesRef ref = originalSource.toBytesRef();
218-
context.doc().add(new StoredField(RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length));
219-
context.doc().add(new NumericDocValuesField(RECOVERY_SOURCE_NAME, 1));
260+
if (recoverySourceEnabled) {
261+
if (originalSource != null && adaptedSource != originalSource) {
262+
final BytesReference adaptedRecoverySource = applyFilters(
263+
originalSource,
264+
contentType,
265+
recoverySourceEnabled,
266+
recoverySourceFilter
267+
);
268+
// if we omitted source or modified it we add the _recovery_source to ensure we have it for ops based recovery
269+
BytesRef ref = adaptedRecoverySource.toBytesRef();
270+
context.doc().add(new StoredField(RECOVERY_SOURCE_NAME, ref.bytes, ref.offset, ref.length));
271+
context.doc().add(new NumericDocValuesField(RECOVERY_SOURCE_NAME, 1));
272+
}
220273
}
221274
}
222275

223276
@Nullable
224277
public BytesReference applyFilters(@Nullable BytesReference originalSource, @Nullable MediaType contentType) throws IOException {
225-
if (enabled && originalSource != null) {
278+
return applyFilters(originalSource, contentType, enabled, filter);
279+
}
280+
281+
@Nullable
282+
private BytesReference applyFilters(
283+
@Nullable BytesReference originalSource,
284+
@Nullable MediaType contentType,
285+
boolean isProvidedSourceEnabled,
286+
@Nullable final Function<Map<String, ?>, Map<String, Object>> filters
287+
) throws IOException {
288+
if (isProvidedSourceEnabled && originalSource != null) {
226289
// Percolate and tv APIs may not set the source and that is ok, because these APIs will not index any data
227-
if (filter != null) {
290+
if (filters != null) {
228291
// we don't update the context source if we filter, we want to keep it as is...
229292
Tuple<? extends MediaType, Map<String, Object>> mapTuple = XContentHelper.convertToMap(originalSource, true, contentType);
230-
Map<String, Object> filteredSource = filter.apply(mapTuple.v2());
293+
Map<String, Object> filteredSource = filters.apply(mapTuple.v2());
231294
BytesStreamOutput bStream = new BytesStreamOutput();
232295
MediaType actualContentType = mapTuple.v1();
233296
XContentBuilder builder = MediaTypeRegistry.contentBuilder(actualContentType, bStream).map(filteredSource);

server/src/test/java/org/opensearch/index/mapper/SourceFieldMapperTests.java

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ public void testNoFormat() throws Exception {
9090
XContentType.SMILE
9191
)
9292
);
93-
93+
final IndexableField recoverySourceIndexableField = doc.rootDoc().getField("_recovery_source");
94+
assertNull(recoverySourceIndexableField);
9495
assertThat(MediaTypeRegistry.xContentType(doc.source()), equalTo(XContentType.SMILE));
9596
}
9697

@@ -128,13 +129,92 @@ public void testIncludes() throws Exception {
128129
)
129130
);
130131

132+
IndexableField sourceField = doc.rootDoc().getField("_source");
133+
Map<String, Object> sourceAsMap;
134+
try (XContentParser parser = createParser(JsonXContent.jsonXContent, new BytesArray(sourceField.binaryValue()))) {
135+
sourceAsMap = parser.map();
136+
}
137+
final IndexableField recoverySourceIndexableField = doc.rootDoc().getField("_recovery_source");
138+
assertNotNull(recoverySourceIndexableField);
139+
assertThat(sourceAsMap.containsKey("path1"), equalTo(true));
140+
assertThat(sourceAsMap.containsKey("path2"), equalTo(false));
141+
}
142+
143+
public void testIncludesForRecoverySource() throws Exception {
144+
String mapping = XContentFactory.jsonBuilder()
145+
.startObject()
146+
.startObject("type")
147+
.startObject("_source")
148+
.array("includes", new String[] { "path1*" })
149+
.array("recovery_source_includes", new String[] { "path2*" })
150+
.endObject()
151+
.endObject()
152+
.endObject()
153+
.toString();
154+
155+
DocumentMapper documentMapper = createIndex("test").mapperService()
156+
.documentMapperParser()
157+
.parse("type", new CompressedXContent(mapping));
158+
159+
ParsedDocument doc = documentMapper.parse(
160+
new SourceToParse(
161+
"test",
162+
"1",
163+
BytesReference.bytes(
164+
XContentFactory.jsonBuilder()
165+
.startObject()
166+
.startObject("path1")
167+
.field("field1", "value1")
168+
.endObject()
169+
.startObject("path2")
170+
.field("field2", "value2")
171+
.endObject()
172+
.endObject()
173+
),
174+
MediaTypeRegistry.JSON
175+
)
176+
);
177+
131178
IndexableField sourceField = doc.rootDoc().getField("_source");
132179
Map<String, Object> sourceAsMap;
133180
try (XContentParser parser = createParser(JsonXContent.jsonXContent, new BytesArray(sourceField.binaryValue()))) {
134181
sourceAsMap = parser.map();
135182
}
136183
assertThat(sourceAsMap.containsKey("path1"), equalTo(true));
137184
assertThat(sourceAsMap.containsKey("path2"), equalTo(false));
185+
186+
final IndexableField recoverySourceIndexableField = doc.rootDoc().getField("_recovery_source");
187+
assertNotNull(recoverySourceIndexableField);
188+
Map<String, Object> recoverySourceAsMap;
189+
try (XContentParser parser = createParser(JsonXContent.jsonXContent, new BytesArray(recoverySourceIndexableField.binaryValue()))) {
190+
recoverySourceAsMap = parser.map();
191+
}
192+
193+
assertThat(recoverySourceAsMap.containsKey("path1"), equalTo(false));
194+
assertThat(recoverySourceAsMap.containsKey("path2"), equalTo(true));
195+
}
196+
197+
public void testNoRecoverySourceAndNoSource_whenBothAreDisabled() throws Exception {
198+
String mapping = XContentFactory.jsonBuilder()
199+
.startObject()
200+
.startObject("type")
201+
.startObject("_source")
202+
.field("enabled", "false")
203+
.field("recovery_source_enabled", "false")
204+
.endObject()
205+
.endObject()
206+
.endObject()
207+
.toString();
208+
209+
DocumentMapperParser parser = createIndex("test").mapperService().documentMapperParser();
210+
DocumentMapper documentMapper = parser.parse("type", new CompressedXContent(mapping));
211+
BytesReference source = BytesReference.bytes(XContentFactory.jsonBuilder().startObject().field("field", "value").endObject());
212+
ParsedDocument doc = documentMapper.parse(new SourceToParse("test", "1", source, MediaTypeRegistry.JSON));
213+
214+
final IndexableField sourceIndexableField = doc.rootDoc().getField("_source");
215+
final IndexableField recoverySourceIndexableField = doc.rootDoc().getField("_recovery_source");
216+
assertNull(recoverySourceIndexableField);
217+
assertNull(sourceIndexableField);
138218
}
139219

140220
public void testExcludes() throws Exception {
@@ -171,13 +251,68 @@ public void testExcludes() throws Exception {
171251
)
172252
);
173253

254+
IndexableField sourceField = doc.rootDoc().getField("_source");
255+
Map<String, Object> sourceAsMap;
256+
try (XContentParser parser = createParser(JsonXContent.jsonXContent, new BytesArray(sourceField.binaryValue()))) {
257+
sourceAsMap = parser.map();
258+
}
259+
final IndexableField recoverySourceIndexableField = doc.rootDoc().getField("_recovery_source");
260+
assertNotNull(recoverySourceIndexableField);
261+
assertThat(sourceAsMap.containsKey("path1"), equalTo(false));
262+
assertThat(sourceAsMap.containsKey("path2"), equalTo(true));
263+
}
264+
265+
public void testExcludesForRecoverySource() throws Exception {
266+
String mapping = XContentFactory.jsonBuilder()
267+
.startObject()
268+
.startObject("type")
269+
.startObject("_source")
270+
.array("excludes", "path1*")
271+
.array("recovery_source_excludes", "path2*")
272+
.endObject()
273+
.endObject()
274+
.endObject()
275+
.toString();
276+
277+
DocumentMapper documentMapper = createIndex("test").mapperService()
278+
.documentMapperParser()
279+
.parse("type", new CompressedXContent(mapping));
280+
281+
ParsedDocument doc = documentMapper.parse(
282+
new SourceToParse(
283+
"test",
284+
"1",
285+
BytesReference.bytes(
286+
XContentFactory.jsonBuilder()
287+
.startObject()
288+
.startObject("path1")
289+
.field("field1", "value1")
290+
.endObject()
291+
.startObject("path2")
292+
.field("field2", "value2")
293+
.endObject()
294+
.endObject()
295+
),
296+
MediaTypeRegistry.JSON
297+
)
298+
);
299+
174300
IndexableField sourceField = doc.rootDoc().getField("_source");
175301
Map<String, Object> sourceAsMap;
176302
try (XContentParser parser = createParser(JsonXContent.jsonXContent, new BytesArray(sourceField.binaryValue()))) {
177303
sourceAsMap = parser.map();
178304
}
179305
assertThat(sourceAsMap.containsKey("path1"), equalTo(false));
180306
assertThat(sourceAsMap.containsKey("path2"), equalTo(true));
307+
308+
final IndexableField recoverySourceIndexableField = doc.rootDoc().getField("_recovery_source");
309+
assertNotNull(recoverySourceIndexableField);
310+
Map<String, Object> recoverySourceAsMap;
311+
try (XContentParser parser = createParser(JsonXContent.jsonXContent, new BytesArray(recoverySourceIndexableField.binaryValue()))) {
312+
recoverySourceAsMap = parser.map();
313+
}
314+
assertThat(recoverySourceAsMap.containsKey("path1"), equalTo(true));
315+
assertThat(recoverySourceAsMap.containsKey("path2"), equalTo(false));
181316
}
182317

183318
public void testEnabledNotUpdateable() throws Exception {

0 commit comments

Comments
 (0)