diff --git a/docs/changelog/86323.yaml b/docs/changelog/86323.yaml new file mode 100644 index 0000000000000..4abf25204c371 --- /dev/null +++ b/docs/changelog/86323.yaml @@ -0,0 +1,5 @@ +pr: 86323 +summary: Bulk merge field-caps responses using mapping hash +area: Search +type: enhancement +issues: [] diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java index f54c5b529caaa..6bb4dcac01cd3 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/fieldcaps/FieldCapabilitiesIT.java @@ -58,8 +58,10 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.IntStream; import static java.util.Collections.singletonList; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -70,6 +72,8 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; public class FieldCapabilitiesIT extends ESIntegTestCase { @@ -575,6 +579,67 @@ public void testRelocation() throws Exception { } } + public void testManyIndicesWithSameMapping() { + final String mapping = """ + { + "properties": { + "message_field": { "type": "text" }, + "value_field": { "type": "long" }, + "multi_field" : { "type" : "ip", "fields" : { "keyword" : { "type" : "keyword" } } }, + "timestamp": {"type": "date"} + } + } + """; + String[] indices = IntStream.range(0, between(1, 9)).mapToObj(n -> "test_many_index_" + n).toArray(String[]::new); + for (String index : indices) { + assertAcked(client().admin().indices().prepareCreate(index).setMapping(mapping).get()); + } + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest(); + request.indices("test_many_index_*"); + request.fields("*"); + boolean excludeMultiField = randomBoolean(); + if (excludeMultiField) { + request.filters("-multifield"); + } + Consumer verifyResponse = resp -> { + assertThat(resp.getIndices(), equalTo(indices)); + assertThat(resp.getField("message_field"), hasKey("text")); + assertThat(resp.getField("message_field").get("text").indices(), nullValue()); + assertTrue(resp.getField("message_field").get("text").isSearchable()); + assertFalse(resp.getField("message_field").get("text").isAggregatable()); + + assertThat(resp.getField("value_field"), hasKey("long")); + assertThat(resp.getField("value_field").get("long").indices(), nullValue()); + assertTrue(resp.getField("value_field").get("long").isSearchable()); + assertTrue(resp.getField("value_field").get("long").isAggregatable()); + + assertThat(resp.getField("timestamp"), hasKey("date")); + + assertThat(resp.getField("multi_field"), hasKey("ip")); + if (excludeMultiField) { + assertThat(resp.getField("multi_field.keyword"), not(hasKey("keyword"))); + } else { + assertThat(resp.getField("multi_field.keyword"), hasKey("keyword")); + } + }; + // Single mapping + verifyResponse.accept(client().execute(FieldCapabilitiesAction.INSTANCE, request).actionGet()); + + // add an extra field for some indices + String[] indicesWithExtraField = randomSubsetOf(between(1, indices.length), indices).stream().sorted().toArray(String[]::new); + ensureGreen(indices); + assertAcked(client().admin().indices().preparePutMapping(indicesWithExtraField).setSource("extra_field", "type=integer").get()); + for (String index : indicesWithExtraField) { + client().prepareIndex(index).setSource("extra_field", randomIntBetween(1, 1000)).get(); + } + FieldCapabilitiesResponse resp = client().execute(FieldCapabilitiesAction.INSTANCE, request).actionGet(); + verifyResponse.accept(resp); + assertThat(resp.getField("extra_field"), hasKey("integer")); + assertThat(resp.getField("extra_field").get("integer").indices(), nullValue()); + assertTrue(resp.getField("extra_field").get("integer").isSearchable()); + assertTrue(resp.getField("extra_field").get("integer").isAggregatable()); + } + private void assertIndices(FieldCapabilitiesResponse response, String... indices) { assertNotNull(response.getIndices()); Arrays.sort(indices); diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java index 5800c7f8a0350..eec23999dedca 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java @@ -486,23 +486,37 @@ static class Builder { private int dimensionIndices = 0; private TimeSeriesParams.MetricType metricType; private boolean hasConflictMetricType; - private final List indiceList; + private final List indicesList; private final Map> meta; + private int totalIndices; Builder(String name, String type) { this.name = name; this.type = type; this.metricType = null; this.hasConflictMetricType = false; - this.indiceList = new ArrayList<>(); + this.indicesList = new ArrayList<>(); this.meta = new HashMap<>(); } + private boolean assertIndicesSorted(String[] indices) { + for (int i = 1; i < indices.length; i++) { + assert indices[i - 1].compareTo(indices[i]) < 0 : "indices [" + Arrays.toString(indices) + "] aren't sorted"; + } + if (indicesList.isEmpty() == false) { + final IndexCaps lastCaps = indicesList.get(indicesList.size() - 1); + final String lastIndex = lastCaps.indices[lastCaps.indices.length - 1]; + assert lastIndex.compareTo(indices[0]) < 0 + : "indices aren't sorted; previous [" + lastIndex + "], current [" + indices[0] + "]"; + } + return true; + } + /** * Collect the field capabilities for an index. */ void add( - String index, + String[] indices, boolean isMetadataField, boolean search, boolean agg, @@ -510,82 +524,87 @@ void add( TimeSeriesParams.MetricType metricType, Map meta ) { - assert indiceList.isEmpty() || indiceList.get(indiceList.size() - 1).name.compareTo(index) < 0 - : "indices aren't sorted; previous [" + indiceList.get(indiceList.size() - 1).name + "], current [" + index + "]"; + assert assertIndicesSorted(indices); + totalIndices += indices.length; if (search) { - searchableIndices++; + searchableIndices += indices.length; } if (agg) { - aggregatableIndices++; + aggregatableIndices += indices.length; } if (isDimension) { - dimensionIndices++; + dimensionIndices += indices.length; } this.isMetadataField |= isMetadataField; // If we have discrepancy in metric types or in some indices this field is not marked as a metric field - we will // treat is a non-metric field and report this discrepancy in metricConflictsIndices - if (indiceList.isEmpty()) { + if (indicesList.isEmpty()) { this.metricType = metricType; } else if (this.metricType != metricType) { hasConflictMetricType = true; this.metricType = null; } - IndexCaps indexCaps = new IndexCaps(index, search, agg, isDimension, metricType); - indiceList.add(indexCaps); + indicesList.add(new IndexCaps(indices, search, agg, isDimension, metricType)); for (Map.Entry entry : meta.entrySet()) { this.meta.computeIfAbsent(entry.getKey(), key -> new HashSet<>()).add(entry.getValue()); } } Stream getIndices() { - return indiceList.stream().map(c -> c.name); + return indicesList.stream().flatMap(c -> Arrays.stream(c.indices)); } - private String[] getNonFeatureIndices(boolean featureInAll, int featureIndices, Predicate hasFeature) { - if (featureInAll || featureIndices == 0) { - return null; - } - String[] nonFeatureIndices = new String[indiceList.size() - featureIndices]; + private String[] filterIndices(int length, Predicate pred) { int index = 0; - for (IndexCaps indexCaps : indiceList) { - if (hasFeature.test(indexCaps) == false) { - nonFeatureIndices[index++] = indexCaps.name; + final String[] dst = new String[length]; + for (IndexCaps indexCaps : indicesList) { + if (pred.test(indexCaps)) { + System.arraycopy(indexCaps.indices, 0, dst, index, indexCaps.indices.length); + index += indexCaps.indices.length; } } - return nonFeatureIndices; + assert index == length : index + "!=" + length; + return dst; } FieldCapabilities build(boolean withIndices) { - final String[] indices; - if (withIndices) { - indices = indiceList.stream().map(caps -> caps.name).toArray(String[]::new); - } else { - indices = null; - } + final String[] indices = withIndices ? filterIndices(totalIndices, ic -> true) : null; // Iff this field is searchable in some indices AND non-searchable in others // we record the list of non-searchable indices - boolean isSearchable = searchableIndices == indiceList.size(); - String[] nonSearchableIndices = getNonFeatureIndices(isSearchable, searchableIndices, IndexCaps::isSearchable); + final boolean isSearchable = searchableIndices == totalIndices; + final String[] nonSearchableIndices; + if (isSearchable || searchableIndices == 0) { + nonSearchableIndices = null; + } else { + nonSearchableIndices = filterIndices(totalIndices - searchableIndices, ic -> ic.isSearchable == false); + } // Iff this field is aggregatable in some indices AND non-aggregatable in others // we keep the list of non-aggregatable indices - boolean isAggregatable = aggregatableIndices == indiceList.size(); - String[] nonAggregatableIndices = getNonFeatureIndices(isAggregatable, aggregatableIndices, IndexCaps::isAggregatable); + final boolean isAggregatable = aggregatableIndices == totalIndices; + final String[] nonAggregatableIndices; + if (isAggregatable || aggregatableIndices == 0) { + nonAggregatableIndices = null; + } else { + nonAggregatableIndices = filterIndices(totalIndices - aggregatableIndices, ic -> ic.isAggregatable == false); + } // Collect all indices that have dimension == false if this field is marked as a dimension in at least one index - boolean isDimension = dimensionIndices == indiceList.size(); - String[] nonDimensionIndices = getNonFeatureIndices(isDimension, dimensionIndices, IndexCaps::isDimension); + final boolean isDimension = dimensionIndices == totalIndices; + final String[] nonDimensionIndices; + if (isDimension || dimensionIndices == 0) { + nonDimensionIndices = null; + } else { + nonDimensionIndices = filterIndices(totalIndices - dimensionIndices, ic -> ic.isDimension == false); + } final String[] metricConflictsIndices; if (hasConflictMetricType) { // Collect all indices that have this field. If it is marked differently in different indices, we cannot really // make a decisions which index is "right" and which index is "wrong" so collecting all indices where this field // is present is probably the only sensible thing to do here - metricConflictsIndices = Objects.requireNonNullElseGet( - indices, - () -> indiceList.stream().map(caps -> caps.name).toArray(String[]::new) - ); + metricConflictsIndices = Objects.requireNonNullElseGet(indices, () -> filterIndices(totalIndices, ic -> true)); } else { metricConflictsIndices = null; } @@ -613,7 +632,7 @@ FieldCapabilities build(boolean withIndices) { } private record IndexCaps( - String name, + String[] indices, boolean isSearchable, boolean isAggregatable, boolean isDimension, diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index e84f9a4b69ea6..c0dae35971cdc 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -8,6 +8,7 @@ package org.elasticsearch.action.fieldcaps; +import org.apache.lucene.util.ArrayUtil; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; @@ -36,14 +37,15 @@ import org.elasticsearch.transport.TransportService; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.TreeMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -114,12 +116,12 @@ protected void doExecute(Task task, FieldCapabilitiesRequest request, final Acti final Map indexResponses = Collections.synchronizedMap(new HashMap<>()); // This map is used to share the index response for indices which have the same index mapping hash to reduce the memory usage. - final Map> indexMappingHashToResponses = Collections.synchronizedMap(new HashMap<>()); + final Map indexMappingHashToResponses = Collections.synchronizedMap(new HashMap<>()); final Consumer handleIndexResponse = resp -> { if (resp.canMatch() && resp.getIndexMappingHash() != null) { - Map curr = indexMappingHashToResponses.putIfAbsent(resp.getIndexMappingHash(), resp.get()); + FieldCapabilitiesIndexResponse curr = indexMappingHashToResponses.putIfAbsent(resp.getIndexMappingHash(), resp); if (curr != null) { - resp = new FieldCapabilitiesIndexResponse(resp.getIndexName(), resp.getIndexMappingHash(), curr, true); + resp = new FieldCapabilitiesIndexResponse(resp.getIndexName(), curr.getIndexMappingHash(), curr.get(), true); } } indexResponses.putIfAbsent(resp.getIndexName(), resp); @@ -228,15 +230,35 @@ private static FieldCapabilitiesRequest prepareRemoteRequest( return remoteRequest; } + private static boolean hasSameMappingHash(FieldCapabilitiesIndexResponse r1, FieldCapabilitiesIndexResponse r2) { + return r1.getIndexMappingHash() != null + && r2.getIndexMappingHash() != null + && r1.getIndexMappingHash().equals(r2.getIndexMappingHash()); + } + private FieldCapabilitiesResponse merge( Map indexResponsesMap, FieldCapabilitiesRequest request, List failures ) { - Map responses = new TreeMap<>(indexResponsesMap); - Map> responseMapBuilder = new HashMap<>(); - for (FieldCapabilitiesIndexResponse response : responses.values()) { - innerMerge(responseMapBuilder, request, response); + final FieldCapabilitiesIndexResponse[] indexResponses = indexResponsesMap.values() + .stream() + .sorted(Comparator.comparing(FieldCapabilitiesIndexResponse::getIndexName)) + .toArray(FieldCapabilitiesIndexResponse[]::new); + final String[] indices = Arrays.stream(indexResponses).map(FieldCapabilitiesIndexResponse::getIndexName).toArray(String[]::new); + final Map> responseMapBuilder = new HashMap<>(); + int lastPendingIndex = 0; + for (int i = 1; i <= indexResponses.length; i++) { + if (i == indexResponses.length || hasSameMappingHash(indexResponses[lastPendingIndex], indexResponses[i]) == false) { + final String[] subIndices; + if (lastPendingIndex == 0 && i == indexResponses.length) { + subIndices = indices; + } else { + subIndices = ArrayUtil.copyOfSubArray(indices, lastPendingIndex, i); + } + innerMerge(subIndices, responseMapBuilder, request, indexResponses[lastPendingIndex]); + lastPendingIndex = i; + } } Map> responseMap = new HashMap<>(); @@ -247,7 +269,7 @@ private FieldCapabilitiesResponse merge( if (request.includeUnmapped()) { // do this directly, rather than using the builder, to save creating a whole lot of objects we don't need unmapped = getUnmappedFields( - responses.keySet(), + indexResponsesMap.keySet(), entry.getKey(), typeMapBuilder.values().stream().flatMap(FieldCapabilities.Builder::getIndices).collect(Collectors.toSet()) ); @@ -266,7 +288,7 @@ private FieldCapabilitiesResponse merge( ) ); } - return new FieldCapabilitiesResponse(responses.keySet().toArray(String[]::new), Collections.unmodifiableMap(responseMap), failures); + return new FieldCapabilitiesResponse(indices, Collections.unmodifiableMap(responseMap), failures); } private static Optional> getUnmappedFields( @@ -287,6 +309,7 @@ private static Optional> getUnmappedFields( } private void innerMerge( + String[] indices, Map> responseMapBuilder, FieldCapabilitiesRequest request, FieldCapabilitiesIndexResponse response @@ -306,7 +329,7 @@ private void innerMerge( key -> new FieldCapabilities.Builder(field, key) ); builder.add( - response.getIndexName(), + indices, fieldCap.isMetadatafield(), fieldCap.isSearchable(), fieldCap.isAggregatable(), diff --git a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java index 637a26baeb8ac..9c354451c4090 100644 --- a/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java +++ b/server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesTests.java @@ -8,19 +8,20 @@ package org.elasticsearch.action.fieldcaps; +import org.apache.lucene.util.ArrayUtil; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.util.iterable.Iterables; -import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.index.mapper.TimeSeriesParams; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.IntStream; @@ -48,9 +49,9 @@ protected Writeable.Reader instanceReader() { public void testBuilder() { FieldCapabilities.Builder builder = new FieldCapabilities.Builder("field", "type"); - builder.add("index1", false, true, false, false, null, Collections.emptyMap()); - builder.add("index2", false, true, false, false, null, Collections.emptyMap()); - builder.add("index3", false, true, false, false, null, Collections.emptyMap()); + builder.add(new String[] { "index1" }, false, true, false, false, null, Collections.emptyMap()); + builder.add(new String[] { "index2" }, false, true, false, false, null, Collections.emptyMap()); + builder.add(new String[] { "index3" }, false, true, false, false, null, Collections.emptyMap()); { FieldCapabilities cap1 = builder.build(false); @@ -78,9 +79,9 @@ public void testBuilder() { } builder = new FieldCapabilities.Builder("field", "type"); - builder.add("index1", false, false, true, true, null, Collections.emptyMap()); - builder.add("index2", false, true, false, false, TimeSeriesParams.MetricType.counter, Collections.emptyMap()); - builder.add("index3", false, false, false, false, null, Collections.emptyMap()); + builder.add(new String[] { "index1" }, false, false, true, true, null, Collections.emptyMap()); + builder.add(new String[] { "index2" }, false, true, false, false, TimeSeriesParams.MetricType.counter, Collections.emptyMap()); + builder.add(new String[] { "index3" }, false, false, false, false, null, Collections.emptyMap()); { FieldCapabilities cap1 = builder.build(false); assertThat(cap1.isSearchable(), equalTo(false)); @@ -107,9 +108,9 @@ public void testBuilder() { } builder = new FieldCapabilities.Builder("field", "type"); - builder.add("index1", false, true, true, true, TimeSeriesParams.MetricType.counter, Collections.emptyMap()); - builder.add("index2", false, true, true, true, TimeSeriesParams.MetricType.counter, Map.of("foo", "bar")); - builder.add("index3", false, true, true, true, TimeSeriesParams.MetricType.counter, Map.of("foo", "quux")); + builder.add(new String[] { "index1" }, false, true, true, true, TimeSeriesParams.MetricType.counter, Collections.emptyMap()); + builder.add(new String[] { "index2" }, false, true, true, true, TimeSeriesParams.MetricType.counter, Map.of("foo", "bar")); + builder.add(new String[] { "index3" }, false, true, true, true, TimeSeriesParams.MetricType.counter, Map.of("foo", "quux")); { FieldCapabilities cap1 = builder.build(false); assertThat(cap1.isSearchable(), equalTo(true)); @@ -136,9 +137,9 @@ public void testBuilder() { } builder = new FieldCapabilities.Builder("field", "type"); - builder.add("index1", false, true, true, true, TimeSeriesParams.MetricType.counter, Collections.emptyMap()); - builder.add("index2", false, true, true, true, TimeSeriesParams.MetricType.gauge, Map.of("foo", "bar")); - builder.add("index3", false, true, true, true, TimeSeriesParams.MetricType.counter, Map.of("foo", "quux")); + builder.add(new String[] { "index1" }, false, true, true, true, TimeSeriesParams.MetricType.counter, Collections.emptyMap()); + builder.add(new String[] { "index2" }, false, true, true, true, TimeSeriesParams.MetricType.gauge, Map.of("foo", "bar")); + builder.add(new String[] { "index3" }, false, true, true, true, TimeSeriesParams.MetricType.counter, Map.of("foo", "quux")); { FieldCapabilities cap1 = builder.build(false); assertThat(cap1.isSearchable(), equalTo(true)); @@ -166,64 +167,73 @@ public void testBuilder() { } public void testRandomBuilder() { - List indices = IntStream.range(0, randomIntBetween(1, 50)).mapToObj(n -> formatted("index_%2d", n)).toList(); - Set searchableIndices = new HashSet<>(randomSubsetOf(indices)); - Set aggregatableIndices = new HashSet<>(randomSubsetOf(indices)); - Set dimensionIndices = new HashSet<>(randomSubsetOf(indices)); + String[] indices = IntStream.range(0, randomIntBetween(1, 50)) + .mapToObj(n -> String.format(Locale.ROOT, "index_%2d", n)) + .toArray(String[]::new); + + List nonSearchableIndices = new ArrayList<>(); + List nonAggregatableIndices = new ArrayList<>(); + List nonDimensionIndices = new ArrayList<>(); + FieldCapabilities.Builder builder = new FieldCapabilities.Builder("field", "type"); - for (String index : indices) { - builder.add( - index, - randomBoolean(), - searchableIndices.contains(index), - aggregatableIndices.contains(index), - dimensionIndices.contains(index), - null, - Map.of() - ); + for (int i = 0; i < indices.length;) { + int bulkSize = randomIntBetween(1, indices.length - i); + String[] groupIndices = ArrayUtil.copyOfSubArray(indices, i, i + bulkSize); + final boolean searchable = randomBoolean(); + if (searchable == false) { + nonSearchableIndices.addAll(Arrays.asList(groupIndices)); + } + final boolean aggregatable = randomBoolean(); + if (aggregatable == false) { + nonAggregatableIndices.addAll(Arrays.asList(groupIndices)); + } + final boolean isDimension = randomBoolean(); + if (isDimension == false) { + nonDimensionIndices.addAll(Arrays.asList(groupIndices)); + } + builder.add(groupIndices, false, searchable, aggregatable, isDimension, null, Map.of()); + i += bulkSize; + } + boolean withIndices = randomBoolean(); + FieldCapabilities fieldCaps = builder.build(withIndices); + if (withIndices) { + assertThat(fieldCaps.indices(), equalTo(indices)); } - FieldCapabilities fieldCaps = builder.build(randomBoolean()); // search - if (searchableIndices.isEmpty()) { - assertFalse(fieldCaps.isSearchable()); - assertNull(fieldCaps.nonSearchableIndices()); - } else if (searchableIndices.size() == indices.size()) { + if (nonSearchableIndices.isEmpty()) { assertTrue(fieldCaps.isSearchable()); assertNull(fieldCaps.nonSearchableIndices()); } else { assertFalse(fieldCaps.isSearchable()); - assertThat( - Sets.newHashSet(fieldCaps.nonSearchableIndices()), - equalTo(Sets.difference(Sets.newHashSet(indices), searchableIndices)) - ); + if (nonSearchableIndices.size() == indices.length) { + assertThat(fieldCaps.nonSearchableIndices(), equalTo(null)); + } else { + assertThat(fieldCaps.nonSearchableIndices(), equalTo(nonSearchableIndices.toArray(String[]::new))); + } } // aggregate - if (aggregatableIndices.isEmpty()) { - assertFalse(fieldCaps.isAggregatable()); - assertNull(fieldCaps.nonAggregatableIndices()); - } else if (aggregatableIndices.size() == indices.size()) { + if (nonAggregatableIndices.isEmpty()) { assertTrue(fieldCaps.isAggregatable()); assertNull(fieldCaps.nonAggregatableIndices()); } else { assertFalse(fieldCaps.isAggregatable()); - assertThat( - Sets.newHashSet(fieldCaps.nonAggregatableIndices()), - equalTo(Sets.difference(Sets.newHashSet(indices), aggregatableIndices)) - ); + if (nonAggregatableIndices.size() == indices.length) { + assertThat(fieldCaps.nonAggregatableIndices(), equalTo(null)); + } else { + assertThat(fieldCaps.nonAggregatableIndices(), equalTo(nonAggregatableIndices.toArray(String[]::new))); + } } // dimension - if (dimensionIndices.isEmpty()) { - assertFalse(fieldCaps.isDimension()); - assertNull(fieldCaps.nonDimensionIndices()); - } else if (dimensionIndices.size() == indices.size()) { + if (nonDimensionIndices.isEmpty()) { assertTrue(fieldCaps.isDimension()); assertNull(fieldCaps.nonDimensionIndices()); } else { assertFalse(fieldCaps.isDimension()); - assertThat( - Sets.newHashSet(fieldCaps.nonDimensionIndices()), - equalTo(Sets.difference(Sets.newHashSet(indices), dimensionIndices)) - ); + if (nonDimensionIndices.size() == indices.length) { + assertThat(fieldCaps.nonDimensionIndices(), equalTo(null)); + } else { + assertThat(fieldCaps.nonDimensionIndices(), equalTo(nonDimensionIndices.toArray(String[]::new))); + } } } @@ -232,7 +242,7 @@ public void testBuilderSingleMetricType() { TimeSeriesParams.MetricType metric = randomBoolean() ? null : randomFrom(TimeSeriesParams.MetricType.values()); FieldCapabilities.Builder builder = new FieldCapabilities.Builder("field", "type"); for (String index : indices) { - builder.add(index, randomBoolean(), randomBoolean(), randomBoolean(), randomBoolean(), metric, Map.of()); + builder.add(new String[] { index }, randomBoolean(), randomBoolean(), randomBoolean(), randomBoolean(), metric, Map.of()); } FieldCapabilities fieldCaps = builder.build(randomBoolean()); assertThat(fieldCaps.getMetricType(), equalTo(metric)); @@ -249,7 +259,15 @@ public void testBuilderMixedMetricType() { } FieldCapabilities.Builder builder = new FieldCapabilities.Builder("field", "type"); for (String index : indices) { - builder.add(index, randomBoolean(), randomBoolean(), randomBoolean(), randomBoolean(), metricTypes.get(index), Map.of()); + builder.add( + new String[] { index }, + randomBoolean(), + randomBoolean(), + randomBoolean(), + randomBoolean(), + metricTypes.get(index), + Map.of() + ); } FieldCapabilities fieldCaps = builder.build(randomBoolean()); if (metricTypes.isEmpty()) { @@ -269,7 +287,7 @@ public void testOutOfOrderIndices() { int numIndex = randomIntBetween(1, 5); for (int i = 1; i <= numIndex; i++) { builder.add( - "index-" + i, + new String[] { "index-" + i }, randomBoolean(), randomBoolean(), randomBoolean(), @@ -281,7 +299,7 @@ public void testOutOfOrderIndices() { final String outOfOrderIndex = randomBoolean() ? "abc" : "index-" + randomIntBetween(1, numIndex); AssertionError error = expectThrows(AssertionError.class, () -> { builder.add( - outOfOrderIndex, + new String[] { outOfOrderIndex }, randomBoolean(), randomBoolean(), randomBoolean(),