3939import org .elasticsearch .common .Strings ;
4040import org .elasticsearch .common .bytes .BytesArray ;
4141import org .elasticsearch .common .settings .Settings ;
42- import org .elasticsearch .common .util .Maps ;
43- import org .elasticsearch .common .util .iterable .Iterables ;
42+ import org .elasticsearch .core .CheckedFunction ;
4443import org .elasticsearch .index .mapper .MapperService ;
4544import org .elasticsearch .index .query .QueryBuilders ;
4645import org .elasticsearch .index .reindex .BulkByScrollResponse ;
4746import org .elasticsearch .index .reindex .ReindexRequest ;
4847import org .elasticsearch .index .reindex .ScrollableHitSource ;
4948import org .elasticsearch .search .builder .SearchSourceBuilder ;
5049import org .elasticsearch .tasks .TaskCancelledException ;
50+ import org .elasticsearch .xcontent .ObjectPath ;
5151import org .elasticsearch .xcontent .XContentBuilder ;
5252import org .elasticsearch .xcontent .XContentType ;
5353import org .elasticsearch .xcontent .json .JsonXContent ;
5757
5858import java .io .IOException ;
5959import java .io .UncheckedIOException ;
60- import java .util .HashMap ;
6160import java .util .HashSet ;
6261import java .util .List ;
6362import java .util .Map ;
@@ -181,9 +180,9 @@ static void validateMappings(
181180 }
182181 // Validate the key and values
183182 try {
184- validateAndGetMappingTypeAndFormat (mapping , policy .getMatchField (), true );
183+ validateField (mapping , policy .getMatchField (), true );
185184 for (String valueFieldName : policy .getEnrichFields ()) {
186- validateAndGetMappingTypeAndFormat (mapping , valueFieldName , false );
185+ validateField (mapping , valueFieldName , false );
187186 }
188187 } catch (ElasticsearchException e ) {
189188 throw new ElasticsearchException (
@@ -195,64 +194,11 @@ static void validateMappings(
195194 }
196195 }
197196
198- private record MappingTypeAndFormat (String type , String format ) {
199-
200- }
201-
202- private static MappingTypeAndFormat validateAndGetMappingTypeAndFormat (
203- String fieldName ,
204- EnrichPolicy policy ,
205- boolean strictlyRequired ,
206- List <Map <String , Object >> sourceMappings
207- ) {
208- var fieldMappings = sourceMappings .stream ()
209- .map (mapping -> validateAndGetMappingTypeAndFormat (mapping , fieldName , strictlyRequired ))
210- .filter (Objects ::nonNull )
211- .toList ();
212- Set <String > types = fieldMappings .stream ().map (tf -> tf .type ).collect (Collectors .toSet ());
213- if (types .size () > 1 ) {
214- if (strictlyRequired ) {
215- throw new ElasticsearchException (
216- "Multiple distinct mapping types for field '{}' - indices({}) types({})" ,
217- fieldName ,
218- Strings .collectionToCommaDelimitedString (policy .getIndices ()),
219- Strings .collectionToCommaDelimitedString (types )
220- );
221- }
222- return null ;
223- }
224- if (types .isEmpty ()) {
225- return null ;
226- }
227- Set <String > formats = fieldMappings .stream ().map (tf -> tf .format ).filter (Objects ::nonNull ).collect (Collectors .toSet ());
228- if (formats .size () > 1 ) {
229- if (strictlyRequired ) {
230- throw new ElasticsearchException (
231- "Multiple distinct formats specified for field '{}' - indices({}) format entries({})" ,
232- policy .getMatchField (),
233- Strings .collectionToCommaDelimitedString (policy .getIndices ()),
234- Strings .collectionToCommaDelimitedString (formats )
235- );
236- }
237- return null ;
238- }
239- return new MappingTypeAndFormat (Iterables .get (types , 0 ), formats .isEmpty () ? null : Iterables .get (formats , 0 ));
240- }
241-
242- @ SuppressWarnings ("unchecked" )
243- private static <T > T extractValues (Map <String , Object > properties , String path ) {
244- return (T ) properties .get (path );
245- }
246-
247- private static MappingTypeAndFormat validateAndGetMappingTypeAndFormat (
248- Map <String , Object > properties ,
249- String fieldName ,
250- boolean fieldRequired
251- ) {
197+ private static void validateField (Map <?, ?> properties , String fieldName , boolean fieldRequired ) {
252198 assert Strings .isEmpty (fieldName ) == false : "Field name cannot be null or empty" ;
253199 String [] fieldParts = fieldName .split ("\\ ." );
254200 StringBuilder parent = new StringBuilder ();
255- Map <String , Object > currentField = properties ;
201+ Map <?, ? > currentField = properties ;
256202 boolean onRoot = true ;
257203 for (String fieldPart : fieldParts ) {
258204 // Ensure that the current field is of object type only (not a nested type or a non compound field)
@@ -265,7 +211,7 @@ private static MappingTypeAndFormat validateAndGetMappingTypeAndFormat(
265211 type
266212 );
267213 }
268- Map <String , Object > currentProperties = extractValues ( currentField , "properties" );
214+ Map <?, ? > currentProperties = (( Map <?, ?>) currentField . get ( "properties" ) );
269215 if (currentProperties == null ) {
270216 if (fieldRequired ) {
271217 throw new ElasticsearchException (
@@ -274,10 +220,10 @@ private static MappingTypeAndFormat validateAndGetMappingTypeAndFormat(
274220 onRoot ? "root" : parent .toString ()
275221 );
276222 } else {
277- return null ;
223+ return ;
278224 }
279225 }
280- currentField = extractValues ( currentProperties , fieldPart );
226+ currentField = (( Map <?, ?>) currentProperties . get ( fieldPart ) );
281227 if (currentField == null ) {
282228 if (fieldRequired ) {
283229 throw new ElasticsearchException (
@@ -287,7 +233,7 @@ private static MappingTypeAndFormat validateAndGetMappingTypeAndFormat(
287233 onRoot ? "root" : parent .toString ()
288234 );
289235 } else {
290- return null ;
236+ return ;
291237 }
292238 }
293239 if (onRoot ) {
@@ -297,70 +243,95 @@ private static MappingTypeAndFormat validateAndGetMappingTypeAndFormat(
297243 }
298244 parent .append (fieldPart );
299245 }
300- if (currentField == null ) {
301- return null ;
246+ }
247+
248+ private XContentBuilder resolveEnrichMapping (final EnrichPolicy enrichPolicy , final List <Map <String , Object >> mappings ) {
249+ if (EnrichPolicy .MATCH_TYPE .equals (enrichPolicy .getType ())) {
250+ return createEnrichMappingBuilder ((builder ) -> builder .field ("type" , "keyword" ).field ("doc_values" , false ));
251+ } else if (EnrichPolicy .RANGE_TYPE .equals (enrichPolicy .getType ())) {
252+ return createRangeEnrichMappingBuilder (enrichPolicy , mappings );
253+ } else if (EnrichPolicy .GEO_MATCH_TYPE .equals (enrichPolicy .getType ())) {
254+ return createEnrichMappingBuilder ((builder ) -> builder .field ("type" , "geo_shape" ));
255+ } else {
256+ throw new ElasticsearchException ("Unrecognized enrich policy type [{}]" , enrichPolicy .getType ());
302257 }
303- final String type = (String ) currentField .getOrDefault ("type" , "object" );
304- final String format = (String ) currentField .get ("format" );
305- return new MappingTypeAndFormat (type , format );
306258 }
307259
308- static final Set <String > RANGE_TYPES = Set .of ("integer_range" , "float_range" , "long_range" , "double_range" , "ip_range" , "date_range" );
260+ private XContentBuilder createRangeEnrichMappingBuilder (EnrichPolicy enrichPolicy , List <Map <String , Object >> mappings ) {
261+ String matchFieldPath = "properties." + enrichPolicy .getMatchField ().replace ("." , ".properties." );
262+ List <Map <String , String >> matchFieldMappings = mappings .stream ()
263+ .map (map -> ObjectPath .<Map <String , String >>eval (matchFieldPath , map ))
264+ .filter (Objects ::nonNull )
265+ .toList ();
266+
267+ Set <String > types = matchFieldMappings .stream ().map (map -> map .get ("type" )).collect (Collectors .toSet ());
268+ if (types .size () == 1 ) {
269+ String type = types .iterator ().next ();
270+ if (type == null ) {
271+ // when no type is defined in a field mapping then it is of type object:
272+ throw new ElasticsearchException (
273+ "Field '{}' has type [object] which doesn't appear to be a range type" ,
274+ enrichPolicy .getMatchField (),
275+ type
276+ );
277+ }
309278
310- static Map <String , Object > mappingForMatchField (EnrichPolicy policy , List <Map <String , Object >> sourceMappings ) {
311- MappingTypeAndFormat typeAndFormat = validateAndGetMappingTypeAndFormat (policy .getMatchField (), policy , true , sourceMappings );
312- if (typeAndFormat == null ) {
313- throw new ElasticsearchException (
314- "Match field '{}' doesn't have a correct mapping type for policy type '{}'" ,
315- policy .getMatchField (),
316- policy .getType ()
317- );
318- }
319- return switch (policy .getType ()) {
320- case EnrichPolicy .MATCH_TYPE -> Map .of ("type" , "keyword" , "doc_values" , false );
321- case EnrichPolicy .GEO_MATCH_TYPE -> Map .of ("type" , "geo_shape" );
322- case EnrichPolicy .RANGE_TYPE -> {
323- if (RANGE_TYPES .contains (typeAndFormat .type ) == false ) {
279+ switch (type ) {
280+ case "integer_range" :
281+ case "float_range" :
282+ case "long_range" :
283+ case "double_range" :
284+ case "ip_range" :
285+ return createEnrichMappingBuilder ((builder ) -> builder .field ("type" , type ).field ("doc_values" , false ));
286+
287+ // date_range types mappings allow for the format to be specified, should be preserved in the created index
288+ case "date_range" :
289+ Set <String > formatEntries = matchFieldMappings .stream ().map (map -> map .get ("format" )).collect (Collectors .toSet ());
290+ if (formatEntries .size () == 1 ) {
291+ return createEnrichMappingBuilder ((builder ) -> {
292+ builder .field ("type" , type ).field ("doc_values" , false );
293+ String format = formatEntries .iterator ().next ();
294+ if (format != null ) {
295+ builder .field ("format" , format );
296+ }
297+ return builder ;
298+ });
299+ }
300+ if (formatEntries .isEmpty ()) {
301+ // no format specify rely on default
302+ return createEnrichMappingBuilder ((builder ) -> builder .field ("type" , type ).field ("doc_values" , false ));
303+ }
324304 throw new ElasticsearchException (
325- "Field '{}' has type [{}] which doesn't appear to be a range type" ,
326- policy .getMatchField (),
327- typeAndFormat .type
305+ "Multiple distinct date format specified for match field '{}' - indices({}) format entries({})" ,
306+ enrichPolicy .getMatchField (),
307+ Strings .collectionToCommaDelimitedString (enrichPolicy .getIndices ()),
308+ (formatEntries .contains (null ) ? "(DEFAULT), " : "" ) + Strings .collectionToCommaDelimitedString (formatEntries )
328309 );
329- }
330- Map <String , Object > mapping = Maps .newMapWithExpectedSize (3 );
331- mapping .put ("type" , typeAndFormat .type );
332- mapping .put ("doc_values" , false );
333- if (typeAndFormat .format != null ) {
334- mapping .put ("format" , typeAndFormat .format );
335- }
336- yield mapping ;
337- }
338- default -> throw new ElasticsearchException ("Unrecognized enrich policy type [{}]" , policy .getType ());
339- };
340- }
341310
342- private XContentBuilder createEnrichMapping (List <Map <String , Object >> sourceMappings ) {
343- Map <String , Map <String , Object >> fieldMappings = new HashMap <>();
344- Map <String , Object > mappingForMatchField = mappingForMatchField (policy , sourceMappings );
345- for (String enrichField : policy .getEnrichFields ()) {
346- if (enrichField .equals (policy .getMatchField ())) {
347- mappingForMatchField = new HashMap <>(mappingForMatchField );
348- mappingForMatchField .remove ("doc_values" ); // enable doc_values
349- } else {
350- var typeAndFormat = validateAndGetMappingTypeAndFormat (enrichField , policy , false , sourceMappings );
351- if (typeAndFormat != null ) {
352- Map <String , Object > mapping = Maps .newMapWithExpectedSize (3 );
353- mapping .put ("type" , typeAndFormat .type );
354- if (typeAndFormat .format != null ) {
355- mapping .put ("format" , typeAndFormat .format );
356- }
357- mapping .put ("index" , false ); // disable index
358- fieldMappings .put (enrichField , mapping );
359- }
311+ default :
312+ throw new ElasticsearchException (
313+ "Field '{}' has type [{}] which doesn't appear to be a range type" ,
314+ enrichPolicy .getMatchField (),
315+ type
316+ );
360317 }
361318 }
362- fieldMappings .put (policy .getMatchField (), mappingForMatchField );
319+ if (types .isEmpty ()) {
320+ throw new ElasticsearchException (
321+ "No mapping type found for match field '{}' - indices({})" ,
322+ enrichPolicy .getMatchField (),
323+ Strings .collectionToCommaDelimitedString (enrichPolicy .getIndices ())
324+ );
325+ }
326+ throw new ElasticsearchException (
327+ "Multiple distinct mapping types for match field '{}' - indices({}) types({})" ,
328+ enrichPolicy .getMatchField (),
329+ Strings .collectionToCommaDelimitedString (enrichPolicy .getIndices ()),
330+ Strings .collectionToCommaDelimitedString (types )
331+ );
332+ }
363333
334+ private XContentBuilder createEnrichMappingBuilder (CheckedFunction <XContentBuilder , XContentBuilder , IOException > matchFieldMapping ) {
364335 // Enable _source on enrich index. Explicitly mark key mapping type.
365336 try {
366337 XContentBuilder builder = JsonXContent .contentBuilder ();
@@ -376,7 +347,9 @@ private XContentBuilder createEnrichMapping(List<Map<String, Object>> sourceMapp
376347 builder .endObject ();
377348 builder .startObject ("properties" );
378349 {
379- builder .mapContents (fieldMappings );
350+ builder .startObject (policy .getMatchField ());
351+ matchFieldMapping .apply (builder );
352+ builder .endObject ();
380353 }
381354 builder .endObject ();
382355 builder .startObject ("_meta" );
@@ -407,7 +380,7 @@ private void prepareAndCreateEnrichIndex(List<Map<String, Object>> mappings) {
407380 .put ("index.warmer.enabled" , false )
408381 .build ();
409382 CreateIndexRequest createEnrichIndexRequest = new CreateIndexRequest (enrichIndexName , enrichIndexSettings );
410- createEnrichIndexRequest .mapping (createEnrichMapping ( mappings ));
383+ createEnrichIndexRequest .mapping (resolveEnrichMapping ( policy , mappings ));
411384 logger .debug ("Policy [{}]: Creating new enrich index [{}]" , policyName , enrichIndexName );
412385 enrichOriginClient ().admin ()
413386 .indices ()
0 commit comments