-
Notifications
You must be signed in to change notification settings - Fork 25.9k
SQL: Support pattern against compatible indices #34718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,8 @@ | |
| import org.elasticsearch.action.admin.indices.get.GetIndexRequest; | ||
| import org.elasticsearch.action.admin.indices.get.GetIndexRequest.Feature; | ||
| import org.elasticsearch.action.admin.indices.get.GetIndexResponse; | ||
| import org.elasticsearch.action.fieldcaps.FieldCapabilities; | ||
| import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; | ||
| import org.elasticsearch.action.support.IndicesOptions; | ||
| import org.elasticsearch.action.support.IndicesOptions.Option; | ||
| import org.elasticsearch.action.support.IndicesOptions.WildcardStates; | ||
|
|
@@ -24,23 +26,34 @@ | |
| import org.elasticsearch.common.Strings; | ||
| import org.elasticsearch.common.collect.ImmutableOpenMap; | ||
| import org.elasticsearch.index.IndexNotFoundException; | ||
| import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; | ||
| import org.elasticsearch.xpack.sql.type.DataType; | ||
| import org.elasticsearch.xpack.sql.type.DateEsField; | ||
| import org.elasticsearch.xpack.sql.type.EsField; | ||
| import org.elasticsearch.xpack.sql.type.KeywordEsField; | ||
| import org.elasticsearch.xpack.sql.type.TextEsField; | ||
| import org.elasticsearch.xpack.sql.type.Types; | ||
| import org.elasticsearch.xpack.sql.type.UnsupportedEsField; | ||
| import org.elasticsearch.xpack.sql.util.CollectionUtils; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.Comparator; | ||
| import java.util.EnumSet; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
| import java.util.Map.Entry; | ||
| import java.util.NavigableSet; | ||
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.TreeMap; | ||
| import java.util.TreeSet; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| import static java.util.Collections.emptyList; | ||
| import static java.util.Collections.emptyMap; | ||
|
|
||
| public class IndexResolver { | ||
|
|
||
|
|
@@ -222,64 +235,157 @@ private void filterResults(String javaRegex, GetAliasesResponse aliases, GetInde | |
| listener.onResponse(result); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Resolves a pattern to one (potentially compound meaning that spawns multiple indices) mapping. | ||
| */ | ||
| public void resolveWithSameMapping(String indexWildcard, String javaRegex, ActionListener<IndexResolution> listener) { | ||
| GetIndexRequest getIndexRequest = createGetIndexRequest(indexWildcard); | ||
| client.admin().indices().getIndex(getIndexRequest, ActionListener.wrap(response -> { | ||
| ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = response.getMappings(); | ||
|
|
||
| List<IndexResolution> resolutions; | ||
| if (mappings.size() > 0) { | ||
| resolutions = new ArrayList<>(mappings.size()); | ||
| Pattern pattern = javaRegex != null ? Pattern.compile(javaRegex) : null; | ||
| for (ObjectObjectCursor<String, ImmutableOpenMap<String, MappingMetaData>> indexMappings : mappings) { | ||
| String concreteIndex = indexMappings.key; | ||
| if (pattern == null || pattern.matcher(concreteIndex).matches()) { | ||
| resolutions.add(buildGetIndexResult(concreteIndex, concreteIndex, indexMappings.value)); | ||
| public void resolveAsMergedMapping(String indexWildcard, String javaRegex, ActionListener<IndexResolution> listener) { | ||
| FieldCapabilitiesRequest fieldRequest = createFieldCapsRequest(indexWildcard); | ||
| client.fieldCaps(fieldRequest, | ||
| ActionListener.wrap(response -> listener.onResponse(mergedMapping(indexWildcard, response.get())), listener::onFailure)); | ||
| } | ||
|
|
||
| static IndexResolution mergedMapping(String indexPattern, Map<String, Map<String, FieldCapabilities>> fieldCaps) { | ||
| if (fieldCaps == null || fieldCaps.isEmpty()) { | ||
| return IndexResolution.notFound(indexPattern); | ||
| } | ||
|
|
||
| StringBuilder errorMessage = new StringBuilder(); | ||
|
|
||
| NavigableSet<Entry<String, Map<String, FieldCapabilities>>> sortedFields = new TreeSet<>( | ||
| // for some reason .reversed doesn't work (prolly due to inference) | ||
| Collections.reverseOrder(Comparator.comparing(Entry::getKey))); | ||
| sortedFields.addAll(fieldCaps.entrySet()); | ||
|
|
||
| Map<String, EsField> hierarchicalMapping = new TreeMap<>(); | ||
| Map<String, EsField> flattedMapping = new LinkedHashMap<>(); | ||
|
|
||
| // sort keys descending in order to easily detect multi-fields (a.b.c multi-field of a.b) | ||
| // without sorting, they can still be detected however without the emptyMap optimization | ||
| // (fields without multi-fields have no children) | ||
| for (Entry<String, Map<String, FieldCapabilities>> entry : sortedFields) { | ||
| String name = entry.getKey(); | ||
| // skip internal fields | ||
| if (!name.startsWith("_")) { | ||
| Map<String, FieldCapabilities> types = entry.getValue(); | ||
| // field is mapped differently across indices | ||
| if (types.size() > 1) { | ||
| // build error message | ||
| for (Entry<String, FieldCapabilities> type : types.entrySet()) { | ||
| if (errorMessage.length() > 0) { | ||
| errorMessage.append(", "); | ||
| } | ||
| errorMessage.append("["); | ||
| errorMessage.append(type.getKey()); | ||
| errorMessage.append("] in "); | ||
| errorMessage.append(Arrays.toString(type.getValue().indices())); | ||
| } | ||
|
|
||
| errorMessage.insert(0, | ||
| "[" + indexPattern + "] points to indices with incompatible mappings; " + | ||
| "field [" + name + "] is mapped in [" + types.size() + "] different ways: "); | ||
| } | ||
| if (errorMessage.length() > 0) { | ||
| return IndexResolution.invalid(errorMessage.toString()); | ||
| } | ||
|
|
||
| FieldCapabilities fieldCap = types.values().iterator().next(); | ||
| // validate search/agg-able | ||
| if (fieldCap.isAggregatable() && fieldCap.nonAggregatableIndices() != null) { | ||
| errorMessage.append("[" + indexPattern + "] points to indices with incompatible mappings: "); | ||
| errorMessage.append("field [" + name + "] is aggregateable except in "); | ||
| errorMessage.append(Arrays.toString(fieldCap.nonAggregatableIndices())); | ||
| } | ||
| if (fieldCap.isSearchable() && fieldCap.nonSearchableIndices() != null) { | ||
| if (errorMessage.length() > 0) { | ||
| errorMessage.append(","); | ||
| } | ||
| errorMessage.append("[" + indexPattern + "] points to indices with incompatible mappings: "); | ||
|
costin marked this conversation as resolved.
|
||
| errorMessage.append("field [" + name + "] is searchable except in "); | ||
| errorMessage.append(Arrays.toString(fieldCap.nonSearchableIndices())); | ||
| } | ||
| if (errorMessage.length() > 0) { | ||
| return IndexResolution.invalid(errorMessage.toString()); | ||
| } | ||
|
|
||
| // validation passes - create the field | ||
| // and name wasn't added before | ||
| if (!flattedMapping.containsKey(name)) { | ||
| createField(name, fieldCap, fieldCaps, hierarchicalMapping, flattedMapping, false); | ||
| } | ||
| } else { | ||
| resolutions = emptyList(); | ||
| } | ||
| } | ||
|
|
||
| listener.onResponse(merge(resolutions, indexWildcard)); | ||
| }, listener::onFailure)); | ||
| return IndexResolution.valid(new EsIndex(indexPattern, hierarchicalMapping)); | ||
| } | ||
|
|
||
| static IndexResolution merge(List<IndexResolution> resolutions, String indexWildcard) { | ||
| IndexResolution merged = null; | ||
| for (IndexResolution resolution : resolutions) { | ||
| // everything that follows gets compared | ||
| if (!resolution.isValid()) { | ||
| return resolution; | ||
| } | ||
| // initialize resolution on first run | ||
| if (merged == null) { | ||
| merged = resolution; | ||
| } | ||
| // need the same mapping across all resolutions | ||
| if (!merged.get().mapping().equals(resolution.get().mapping())) { | ||
| return IndexResolution.invalid( | ||
| "[" + indexWildcard + "] points to indices [" + merged.get().name() + "] " | ||
| + "and [" + resolution.get().name() + "] which have different mappings. " | ||
| + "When using multiple indices, the mappings must be identical."); | ||
| private static EsField createField(String fieldName, FieldCapabilities caps, Map<String, Map<String, FieldCapabilities>> globalCaps, | ||
| Map<String, EsField> hierarchicalMapping, Map<String, EsField> flattedMapping, boolean hasChildren) { | ||
|
|
||
| Map<String, EsField> parentProps = hierarchicalMapping; | ||
|
|
||
| int dot = fieldName.lastIndexOf('.'); | ||
| String fullFieldName = fieldName; | ||
|
|
||
| if (dot >= 0) { | ||
| String parentName = fieldName.substring(0, dot); | ||
| fieldName = fieldName.substring(dot + 1); | ||
| EsField parent = flattedMapping.get(parentName); | ||
| if (parent == null) { | ||
| Map<String, FieldCapabilities> map = globalCaps.get(parentName); | ||
| if (map == null) { | ||
| throw new SqlIllegalArgumentException("Cannot find field {}; this is likely a bug", parentName); | ||
| } | ||
| FieldCapabilities parentCap = map.values().iterator().next(); | ||
| parent = createField(parentName, parentCap, globalCaps, hierarchicalMapping, flattedMapping, true); | ||
|
costin marked this conversation as resolved.
|
||
| } | ||
| parentProps = parent.getProperties(); | ||
| } | ||
| if (merged != null) { | ||
| // at this point, we are sure there's the same mapping across all (if that's the case) indices | ||
| // to keep things simple, use the given pattern as index name | ||
| merged = IndexResolution.valid(new EsIndex(indexWildcard, merged.get().mapping())); | ||
| } else { | ||
| merged = IndexResolution.notFound(indexWildcard); | ||
|
|
||
| EsField field = null; | ||
| Map<String, EsField> props = hasChildren ? new TreeMap<>() : emptyMap(); | ||
|
|
||
| DataType esType = DataType.fromEsType(caps.getType()); | ||
| switch (esType) { | ||
| case TEXT: | ||
| field = new TextEsField(fieldName, props, false); | ||
| break; | ||
| case KEYWORD: | ||
| int length = DataType.KEYWORD.defaultPrecision; | ||
| // TODO: to check whether isSearchable/isAggregateable takes into account the presence of the normalizer | ||
| boolean normalized = false; | ||
| field = new KeywordEsField(fieldName, props, caps.isAggregatable(), length, normalized); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why just not passing
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mainly to not forget about it - I've added a comment regarding its presence. |
||
| break; | ||
| case DATE: | ||
| field = new DateEsField(fieldName, props, caps.isAggregatable()); | ||
| break; | ||
| case UNSUPPORTED: | ||
| field = new UnsupportedEsField(fieldName, caps.getType()); | ||
| break; | ||
| default: | ||
| field = new EsField(fieldName, esType, props, caps.isAggregatable()); | ||
| } | ||
| return merged; | ||
|
|
||
| parentProps.put(fieldName, field); | ||
| flattedMapping.put(fullFieldName, field); | ||
|
|
||
| return field; | ||
| } | ||
|
|
||
| private static FieldCapabilitiesRequest createFieldCapsRequest(String index) { | ||
| return new FieldCapabilitiesRequest() | ||
| .indices(Strings.commaDelimitedListToStringArray(index)) | ||
| .fields("*") | ||
| //lenient because we throw our own errors looking at the response e.g. if something was not resolved | ||
| //also because this way security doesn't throw authorization exceptions but rather honors ignore_unavailable | ||
| .indicesOptions(IndicesOptions.lenientExpandOpen()); | ||
| } | ||
|
|
||
| // TODO: Concrete indices still uses get mapping | ||
| // waiting on https://github.com/elastic/elasticsearch/pull/34071 | ||
| // | ||
|
|
||
| /** | ||
| * Resolves a pattern to multiple, separate indices. | ||
| * Resolves a pattern to multiple, separate indices. Doesn't perform validation. | ||
| */ | ||
| public void resolveAsSeparateMappings(String indexWildcard, String javaRegex, ActionListener<List<EsIndex>> listener) { | ||
| GetIndexRequest getIndexRequest = createGetIndexRequest(indexWildcard); | ||
|
|
@@ -306,7 +412,7 @@ public void resolveAsSeparateMappings(String indexWildcard, String javaRegex, Ac | |
| listener.onResponse(results); | ||
| }, listener::onFailure)); | ||
| } | ||
|
|
||
| private static GetIndexRequest createGetIndexRequest(String index) { | ||
| return new GetIndexRequest() | ||
| .local(true) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this tested with multiple entries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're asking about the error message, yes in
IndexResolverTests(e.g.testMergeIncompatibleTypes).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was confused with the StringBuilder here, as you later on
insert(0for the prefix of the message.You could use an ArrayList for the
type.getKey()and a StringJoiner to append them to the StringBuilder.Really minor, just a suggestion.