15
15
import org .elasticsearch .action .admin .indices .get .GetIndexRequest ;
16
16
import org .elasticsearch .action .admin .indices .get .GetIndexRequest .Feature ;
17
17
import org .elasticsearch .action .admin .indices .get .GetIndexResponse ;
18
+ import org .elasticsearch .action .fieldcaps .FieldCapabilities ;
19
+ import org .elasticsearch .action .fieldcaps .FieldCapabilitiesRequest ;
18
20
import org .elasticsearch .action .support .IndicesOptions ;
19
21
import org .elasticsearch .action .support .IndicesOptions .Option ;
20
22
import org .elasticsearch .action .support .IndicesOptions .WildcardStates ;
24
26
import org .elasticsearch .common .Strings ;
25
27
import org .elasticsearch .common .collect .ImmutableOpenMap ;
26
28
import org .elasticsearch .index .IndexNotFoundException ;
29
+ import org .elasticsearch .xpack .sql .SqlIllegalArgumentException ;
30
+ import org .elasticsearch .xpack .sql .type .DataType ;
31
+ import org .elasticsearch .xpack .sql .type .DateEsField ;
27
32
import org .elasticsearch .xpack .sql .type .EsField ;
33
+ import org .elasticsearch .xpack .sql .type .KeywordEsField ;
34
+ import org .elasticsearch .xpack .sql .type .TextEsField ;
28
35
import org .elasticsearch .xpack .sql .type .Types ;
36
+ import org .elasticsearch .xpack .sql .type .UnsupportedEsField ;
29
37
import org .elasticsearch .xpack .sql .util .CollectionUtils ;
30
38
31
39
import java .util .ArrayList ;
40
+ import java .util .Arrays ;
32
41
import java .util .Collections ;
33
42
import java .util .Comparator ;
34
43
import java .util .EnumSet ;
44
+ import java .util .LinkedHashMap ;
35
45
import java .util .List ;
36
46
import java .util .Locale ;
37
47
import java .util .Map ;
48
+ import java .util .Map .Entry ;
49
+ import java .util .NavigableSet ;
38
50
import java .util .Objects ;
39
51
import java .util .Set ;
52
+ import java .util .TreeMap ;
40
53
import java .util .TreeSet ;
41
54
import java .util .regex .Pattern ;
42
55
43
- import static java .util .Collections .emptyList ;
56
+ import static java .util .Collections .emptyMap ;
44
57
45
58
public class IndexResolver {
46
59
@@ -222,64 +235,157 @@ private void filterResults(String javaRegex, GetAliasesResponse aliases, GetInde
222
235
listener .onResponse (result );
223
236
}
224
237
225
-
226
238
/**
227
239
* Resolves a pattern to one (potentially compound meaning that spawns multiple indices) mapping.
228
240
*/
229
- public void resolveWithSameMapping (String indexWildcard , String javaRegex , ActionListener <IndexResolution > listener ) {
230
- GetIndexRequest getIndexRequest = createGetIndexRequest (indexWildcard );
231
- client .admin ().indices ().getIndex (getIndexRequest , ActionListener .wrap (response -> {
232
- ImmutableOpenMap <String , ImmutableOpenMap <String , MappingMetaData >> mappings = response .getMappings ();
233
-
234
- List <IndexResolution > resolutions ;
235
- if (mappings .size () > 0 ) {
236
- resolutions = new ArrayList <>(mappings .size ());
237
- Pattern pattern = javaRegex != null ? Pattern .compile (javaRegex ) : null ;
238
- for (ObjectObjectCursor <String , ImmutableOpenMap <String , MappingMetaData >> indexMappings : mappings ) {
239
- String concreteIndex = indexMappings .key ;
240
- if (pattern == null || pattern .matcher (concreteIndex ).matches ()) {
241
- resolutions .add (buildGetIndexResult (concreteIndex , concreteIndex , indexMappings .value ));
241
+ public void resolveAsMergedMapping (String indexWildcard , String javaRegex , ActionListener <IndexResolution > listener ) {
242
+ FieldCapabilitiesRequest fieldRequest = createFieldCapsRequest (indexWildcard );
243
+ client .fieldCaps (fieldRequest ,
244
+ ActionListener .wrap (response -> listener .onResponse (mergedMapping (indexWildcard , response .get ())), listener ::onFailure ));
245
+ }
246
+
247
+ static IndexResolution mergedMapping (String indexPattern , Map <String , Map <String , FieldCapabilities >> fieldCaps ) {
248
+ if (fieldCaps == null || fieldCaps .isEmpty ()) {
249
+ return IndexResolution .notFound (indexPattern );
250
+ }
251
+
252
+ StringBuilder errorMessage = new StringBuilder ();
253
+
254
+ NavigableSet <Entry <String , Map <String , FieldCapabilities >>> sortedFields = new TreeSet <>(
255
+ // for some reason .reversed doesn't work (prolly due to inference)
256
+ Collections .reverseOrder (Comparator .comparing (Entry ::getKey )));
257
+ sortedFields .addAll (fieldCaps .entrySet ());
258
+
259
+ Map <String , EsField > hierarchicalMapping = new TreeMap <>();
260
+ Map <String , EsField > flattedMapping = new LinkedHashMap <>();
261
+
262
+ // sort keys descending in order to easily detect multi-fields (a.b.c multi-field of a.b)
263
+ // without sorting, they can still be detected however without the emptyMap optimization
264
+ // (fields without multi-fields have no children)
265
+ for (Entry <String , Map <String , FieldCapabilities >> entry : sortedFields ) {
266
+ String name = entry .getKey ();
267
+ // skip internal fields
268
+ if (!name .startsWith ("_" )) {
269
+ Map <String , FieldCapabilities > types = entry .getValue ();
270
+ // field is mapped differently across indices
271
+ if (types .size () > 1 ) {
272
+ // build error message
273
+ for (Entry <String , FieldCapabilities > type : types .entrySet ()) {
274
+ if (errorMessage .length () > 0 ) {
275
+ errorMessage .append (", " );
276
+ }
277
+ errorMessage .append ("[" );
278
+ errorMessage .append (type .getKey ());
279
+ errorMessage .append ("] in " );
280
+ errorMessage .append (Arrays .toString (type .getValue ().indices ()));
242
281
}
282
+
283
+ errorMessage .insert (0 ,
284
+ "[" + indexPattern + "] points to indices with incompatible mappings; " +
285
+ "field [" + name + "] is mapped in [" + types .size () + "] different ways: " );
286
+ }
287
+ if (errorMessage .length () > 0 ) {
288
+ return IndexResolution .invalid (errorMessage .toString ());
289
+ }
290
+
291
+ FieldCapabilities fieldCap = types .values ().iterator ().next ();
292
+ // validate search/agg-able
293
+ if (fieldCap .isAggregatable () && fieldCap .nonAggregatableIndices () != null ) {
294
+ errorMessage .append ("[" + indexPattern + "] points to indices with incompatible mappings: " );
295
+ errorMessage .append ("field [" + name + "] is aggregateable except in " );
296
+ errorMessage .append (Arrays .toString (fieldCap .nonAggregatableIndices ()));
297
+ }
298
+ if (fieldCap .isSearchable () && fieldCap .nonSearchableIndices () != null ) {
299
+ if (errorMessage .length () > 0 ) {
300
+ errorMessage .append ("," );
301
+ }
302
+ errorMessage .append ("[" + indexPattern + "] points to indices with incompatible mappings: " );
303
+ errorMessage .append ("field [" + name + "] is searchable except in " );
304
+ errorMessage .append (Arrays .toString (fieldCap .nonSearchableIndices ()));
305
+ }
306
+ if (errorMessage .length () > 0 ) {
307
+ return IndexResolution .invalid (errorMessage .toString ());
308
+ }
309
+
310
+ // validation passes - create the field
311
+ // and name wasn't added before
312
+ if (!flattedMapping .containsKey (name )) {
313
+ createField (name , fieldCap , fieldCaps , hierarchicalMapping , flattedMapping , false );
243
314
}
244
- } else {
245
- resolutions = emptyList ();
246
315
}
316
+ }
247
317
248
- listener .onResponse (merge (resolutions , indexWildcard ));
249
- }, listener ::onFailure ));
318
+ return IndexResolution .valid (new EsIndex (indexPattern , hierarchicalMapping ));
250
319
}
251
320
252
- static IndexResolution merge (List <IndexResolution > resolutions , String indexWildcard ) {
253
- IndexResolution merged = null ;
254
- for (IndexResolution resolution : resolutions ) {
255
- // everything that follows gets compared
256
- if (!resolution .isValid ()) {
257
- return resolution ;
258
- }
259
- // initialize resolution on first run
260
- if (merged == null ) {
261
- merged = resolution ;
262
- }
263
- // need the same mapping across all resolutions
264
- if (!merged .get ().mapping ().equals (resolution .get ().mapping ())) {
265
- return IndexResolution .invalid (
266
- "[" + indexWildcard + "] points to indices [" + merged .get ().name () + "] "
267
- + "and [" + resolution .get ().name () + "] which have different mappings. "
268
- + "When using multiple indices, the mappings must be identical." );
321
+ private static EsField createField (String fieldName , FieldCapabilities caps , Map <String , Map <String , FieldCapabilities >> globalCaps ,
322
+ Map <String , EsField > hierarchicalMapping , Map <String , EsField > flattedMapping , boolean hasChildren ) {
323
+
324
+ Map <String , EsField > parentProps = hierarchicalMapping ;
325
+
326
+ int dot = fieldName .lastIndexOf ('.' );
327
+ String fullFieldName = fieldName ;
328
+
329
+ if (dot >= 0 ) {
330
+ String parentName = fieldName .substring (0 , dot );
331
+ fieldName = fieldName .substring (dot + 1 );
332
+ EsField parent = flattedMapping .get (parentName );
333
+ if (parent == null ) {
334
+ Map <String , FieldCapabilities > map = globalCaps .get (parentName );
335
+ if (map == null ) {
336
+ throw new SqlIllegalArgumentException ("Cannot find field {}; this is likely a bug" , parentName );
337
+ }
338
+ FieldCapabilities parentCap = map .values ().iterator ().next ();
339
+ parent = createField (parentName , parentCap , globalCaps , hierarchicalMapping , flattedMapping , true );
269
340
}
341
+ parentProps = parent .getProperties ();
270
342
}
271
- if (merged != null ) {
272
- // at this point, we are sure there's the same mapping across all (if that's the case) indices
273
- // to keep things simple, use the given pattern as index name
274
- merged = IndexResolution .valid (new EsIndex (indexWildcard , merged .get ().mapping ()));
275
- } else {
276
- merged = IndexResolution .notFound (indexWildcard );
343
+
344
+ EsField field = null ;
345
+ Map <String , EsField > props = hasChildren ? new TreeMap <>() : emptyMap ();
346
+
347
+ DataType esType = DataType .fromEsType (caps .getType ());
348
+ switch (esType ) {
349
+ case TEXT :
350
+ field = new TextEsField (fieldName , props , false );
351
+ break ;
352
+ case KEYWORD :
353
+ int length = DataType .KEYWORD .defaultPrecision ;
354
+ // TODO: to check whether isSearchable/isAggregateable takes into account the presence of the normalizer
355
+ boolean normalized = false ;
356
+ field = new KeywordEsField (fieldName , props , caps .isAggregatable (), length , normalized );
357
+ break ;
358
+ case DATE :
359
+ field = new DateEsField (fieldName , props , caps .isAggregatable ());
360
+ break ;
361
+ case UNSUPPORTED :
362
+ field = new UnsupportedEsField (fieldName , caps .getType ());
363
+ break ;
364
+ default :
365
+ field = new EsField (fieldName , esType , props , caps .isAggregatable ());
277
366
}
278
- return merged ;
367
+
368
+ parentProps .put (fieldName , field );
369
+ flattedMapping .put (fullFieldName , field );
370
+
371
+ return field ;
372
+ }
373
+
374
+ private static FieldCapabilitiesRequest createFieldCapsRequest (String index ) {
375
+ return new FieldCapabilitiesRequest ()
376
+ .indices (Strings .commaDelimitedListToStringArray (index ))
377
+ .fields ("*" )
378
+ //lenient because we throw our own errors looking at the response e.g. if something was not resolved
379
+ //also because this way security doesn't throw authorization exceptions but rather honors ignore_unavailable
380
+ .indicesOptions (IndicesOptions .lenientExpandOpen ());
279
381
}
280
382
383
+ // TODO: Concrete indices still uses get mapping
384
+ // waiting on https://github.com/elastic/elasticsearch/pull/34071
385
+ //
386
+
281
387
/**
282
- * Resolves a pattern to multiple, separate indices.
388
+ * Resolves a pattern to multiple, separate indices. Doesn't perform validation.
283
389
*/
284
390
public void resolveAsSeparateMappings (String indexWildcard , String javaRegex , ActionListener <List <EsIndex >> listener ) {
285
391
GetIndexRequest getIndexRequest = createGetIndexRequest (indexWildcard );
@@ -306,7 +412,7 @@ public void resolveAsSeparateMappings(String indexWildcard, String javaRegex, Ac
306
412
listener .onResponse (results );
307
413
}, listener ::onFailure ));
308
414
}
309
-
415
+
310
416
private static GetIndexRequest createGetIndexRequest (String index ) {
311
417
return new GetIndexRequest ()
312
418
.local (true )
0 commit comments