Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test/framework/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
api "org.elasticsearch:mocksocket:${versions.mocksocket}"

testImplementation project(':x-pack:plugin:mapper-unsigned-long')
testImplementation project(':x-pack:plugin:mapper-counted-keyword')
testImplementation project(":modules:mapper-extras")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import org.elasticsearch.logsdb.datageneration.datasource.DataSource;
import org.elasticsearch.logsdb.datageneration.fields.leaf.ByteFieldDataGenerator;
import org.elasticsearch.logsdb.datageneration.fields.leaf.CountedKeywordFieldDataGenerator;
import org.elasticsearch.logsdb.datageneration.fields.leaf.DoubleFieldDataGenerator;
import org.elasticsearch.logsdb.datageneration.fields.leaf.FloatFieldDataGenerator;
import org.elasticsearch.logsdb.datageneration.fields.leaf.HalfFloatFieldDataGenerator;
Expand All @@ -34,7 +35,8 @@ public enum FieldType {
DOUBLE("double"),
FLOAT("float"),
HALF_FLOAT("half_float"),
SCALED_FLOAT("scaled_float");
SCALED_FLOAT("scaled_float"),
COUNTED_KEYWORD("counted_keyword");

private final String name;

Expand All @@ -54,6 +56,7 @@ public FieldDataGenerator generator(String fieldName, DataSource dataSource) {
case FLOAT -> new FloatFieldDataGenerator(fieldName, dataSource);
case HALF_FLOAT -> new HalfFloatFieldDataGenerator(fieldName, dataSource);
case SCALED_FLOAT -> new ScaledFloatFieldDataGenerator(fieldName, dataSource);
case COUNTED_KEYWORD -> new CountedKeywordFieldDataGenerator(fieldName, dataSource);
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ default DataSourceResponse.ArrayWrapper handle(DataSourceRequest.ArrayWrapper re
return null;
}

default DataSourceResponse.RepeatingWrapper handle(DataSourceRequest.RepeatingWrapper request) {
return null;
}

default DataSourceResponse.ChildFieldGenerator handle(DataSourceRequest.ChildFieldGenerator request) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ public DataSourceResponse.ArrayWrapper accept(DataSourceHandler handler) {
}
}

record RepeatingWrapper() implements DataSourceRequest<DataSourceResponse.RepeatingWrapper> {
public DataSourceResponse.RepeatingWrapper accept(DataSourceHandler handler) {
return handler.handle(this);
}
}

record ChildFieldGenerator(DataGeneratorSpecification specification)
implements
DataSourceRequest<DataSourceResponse.ChildFieldGenerator> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ record NullWrapper(Function<Supplier<Object>, Supplier<Object>> wrapper) impleme

record ArrayWrapper(Function<Supplier<Object>, Supplier<Object>> wrapper) implements DataSourceResponse {}

record RepeatingWrapper(Function<Supplier<Object>, Supplier<Object>> wrapper) implements DataSourceResponse {}

interface ChildFieldGenerator extends DataSourceResponse {
int generateChildFieldCount();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public DataSourceResponse.LeafMappingParametersGenerator handle(DataSourceReques
case KEYWORD -> keywordMapping(request, map);
case LONG, INTEGER, SHORT, BYTE, DOUBLE, FLOAT, HALF_FLOAT, UNSIGNED_LONG -> plain(map);
case SCALED_FLOAT -> scaledFloatMapping(map);
case COUNTED_KEYWORD -> plain(Map.of("index", ESTestCase.randomBoolean()));
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import org.elasticsearch.test.ESTestCase;

import java.util.HashSet;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.IntStream;
Expand All @@ -26,6 +27,11 @@ public DataSourceResponse.ArrayWrapper handle(DataSourceRequest.ArrayWrapper ign
return new DataSourceResponse.ArrayWrapper(wrapInArray());
}

@Override
public DataSourceResponse.RepeatingWrapper handle(DataSourceRequest.RepeatingWrapper ignored) {
return new DataSourceResponse.RepeatingWrapper(repeatValues());
}

private static Function<Supplier<Object>, Supplier<Object>> injectNulls() {
// Inject some nulls but majority of data should be non-null (as it likely is in reality).
return (values) -> () -> ESTestCase.randomDouble() <= 0.05 ? null : values.get();
Expand All @@ -41,4 +47,19 @@ private static Function<Supplier<Object>, Supplier<Object>> wrapInArray() {
return values.get();
};
}

private static Function<Supplier<Object>, Supplier<Object>> repeatValues() {
return (values) -> {
HashSet<Object> previousValues = new HashSet<>();
return () -> {
if (previousValues.size() > 0 && ESTestCase.randomBoolean()) {
return ESTestCase.randomFrom(previousValues);
} else {
var value = values.get();
previousValues.add(value);
return value;
}
};
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.logsdb.datageneration.fields.leaf;

import org.elasticsearch.logsdb.datageneration.FieldDataGenerator;
import org.elasticsearch.logsdb.datageneration.datasource.DataSource;
import org.elasticsearch.logsdb.datageneration.datasource.DataSourceRequest;

import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;

public class CountedKeywordFieldDataGenerator implements FieldDataGenerator {
private final Supplier<Object> valueGenerator;
private final Set<String> previousStrings = new HashSet<>();

public CountedKeywordFieldDataGenerator(String fieldName, DataSource dataSource) {
var strings = dataSource.get(new DataSourceRequest.StringGenerator());
var nulls = dataSource.get(new DataSourceRequest.NullWrapper());
var arrays = dataSource.get(new DataSourceRequest.ArrayWrapper());
var repeats = dataSource.get(new DataSourceRequest.RepeatingWrapper());

this.valueGenerator = arrays.wrapper().compose(nulls.wrapper().compose(repeats.wrapper())).apply(() -> strings.generator().get());
}

@Override
public Object generateValue() {
return valueGenerator.get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.elasticsearch.xcontent.XContentBuilder;

import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -223,4 +224,68 @@ private static BigInteger toBigInteger(Object value) {
return (BigInteger) value;
}
}

class CountedKeywordMatcher implements FieldSpecificMatcher {
private final XContentBuilder actualMappings;
private final Settings.Builder actualSettings;
private final XContentBuilder expectedMappings;
private final Settings.Builder expectedSettings;

CountedKeywordMatcher(
XContentBuilder actualMappings,
Settings.Builder actualSettings,
XContentBuilder expectedMappings,
Settings.Builder expectedSettings
) {
this.actualMappings = actualMappings;
this.actualSettings = actualSettings;
this.expectedMappings = expectedMappings;
this.expectedSettings = expectedSettings;
}

private static List<String> normalize(List<Object> values) {
return values.stream().filter(Objects::nonNull).map(it -> (String) it).toList();
}

private static boolean matchCountsEqualExact(List<String> actualNormalized, List<String> expectedNormalized) {
HashMap<String, Integer> counts = new HashMap<>();
for (String value : actualNormalized) {
counts.put(value, counts.getOrDefault(value, 0) + 1);
}
for (String value : expectedNormalized) {
int newCount = counts.getOrDefault(value, 0) - 1;
if (newCount == 0) {
counts.remove(value);
} else {
counts.put(value, newCount);
}
}

return counts.isEmpty();
}

@Override
public MatchResult match(
List<Object> actual,
List<Object> expected,
Map<String, Object> actualMapping,
Map<String, Object> expectedMapping
) {
var actualNormalized = normalize(actual);
var expectedNormalized = normalize(expected);

return matchCountsEqualExact(actualNormalized, expectedNormalized)
? MatchResult.match()
: MatchResult.noMatch(
formatErrorMessage(
actualMappings,
actualSettings,
expectedMappings,
expectedSettings,
"Values of type [counted_keyword] don't match after normalization, normalized"
+ prettyPrintCollections(actualNormalized, expectedNormalized)
)
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ public SourceMatcher(
"scaled_float",
new FieldSpecificMatcher.ScaledFloatMatcher(actualMappings, actualSettings, expectedMappings, expectedSettings),
"unsigned_long",
new FieldSpecificMatcher.UnsignedLongMatcher(actualMappings, actualSettings, expectedMappings, expectedSettings)
new FieldSpecificMatcher.UnsignedLongMatcher(actualMappings, actualSettings, expectedMappings, expectedSettings),
"counted_keyword",
new FieldSpecificMatcher.CountedKeywordMatcher(actualMappings, actualSettings, expectedMappings, expectedSettings)
);
this.dynamicFieldMatcher = new DynamicFieldMatcher(actualMappings, actualSettings, expectedMappings, expectedSettings);
}
Expand Down Expand Up @@ -100,17 +102,8 @@ private MatchResult compareSource(Map<String, List<Object>> actual, Map<String,
var actualValues = actual.get(name);
var expectedValues = expectedFieldEntry.getValue();

// There are cases when field values are stored in ignored source
// so we try to match them as is first and then apply field specific matcher.
// This is temporary, we should be able to tell when source is exact using mappings.
// See #111916.
var genericMatchResult = matchWithGenericMatcher(actualValues, expectedValues);
if (genericMatchResult.isMatch()) {
continue;
}

var matchIncludingFieldSpecificMatchers = matchWithFieldSpecificMatcher(name, actualValues, expectedValues).orElse(
genericMatchResult
var matchIncludingFieldSpecificMatchers = matchWithFieldSpecificMatcher(name, actualValues, expectedValues).orElseGet(
() -> matchWithGenericMatcher(actualValues, expectedValues)
);
if (matchIncludingFieldSpecificMatchers.isMatch() == false) {
var message = "Source documents don't match for field [" + name + "]: " + matchIncludingFieldSpecificMatchers.getMessage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.countedkeyword.CountedKeywordMapperPlugin;
import org.elasticsearch.xpack.unsignedlong.UnsignedLongMapperPlugin;

import java.io.IOException;
Expand Down Expand Up @@ -110,7 +111,7 @@ public DataSourceResponse.FieldTypeGenerator handle(DataSourceRequest.FieldTypeG
var mappingService = new MapperServiceTestCase() {
@Override
protected Collection<? extends Plugin> getPlugins() {
return List.of(new UnsignedLongMapperPlugin(), new MapperExtrasPlugin());
return List.of(new UnsignedLongMapperPlugin(), new MapperExtrasPlugin(), new CountedKeywordMapperPlugin());
}
}.createMapperService(mappingXContent);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,38 @@ public void testMappedMismatch() throws IOException {
var sut = new SourceMatcher(mapping, Settings.builder(), mapping, Settings.builder(), actual, expected, false);
assertFalse(sut.match().isMatch());
}

public void testCountedKeywordMatch() throws IOException {
List<Map<String, Object>> actual = List.of(Map.of("field", List.of("a", "b", "a", "c", "b", "a")));
List<Map<String, Object>> expected = List.of(Map.of("field", List.of("a", "b", "a", "c", "b", "a")));

var mapping = XContentBuilder.builder(XContentType.JSON.xContent());
mapping.startObject();
mapping.startObject("_doc");
{
mapping.startObject("field").field("type", "counted_keyword").endObject();
}
mapping.endObject();
mapping.endObject();

var sut = new SourceMatcher(mapping, Settings.builder(), mapping, Settings.builder(), actual, expected, false);
assertTrue(sut.match().isMatch());
}

public void testCountedKeywordMismatch() throws IOException {
List<Map<String, Object>> actual = List.of(Map.of("field", List.of("a", "b", "a", "c", "b", "a")));
List<Map<String, Object>> expected = List.of(Map.of("field", List.of("a", "b", "c", "a")));

var mapping = XContentBuilder.builder(XContentType.JSON.xContent());
mapping.startObject();
mapping.startObject("_doc");
{
mapping.startObject("field").field("type", "counted_keyword").endObject();
}
mapping.endObject();
mapping.endObject();

var sut = new SourceMatcher(mapping, Settings.builder(), mapping, Settings.builder(), actual, expected, false);
assertFalse(sut.match().isMatch());
}
}