Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
6 changes: 6 additions & 0 deletions docs/changelog/135886.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 135886
summary: Provide defaults for index sort settings
area: Mapping
type: bug
issues:
- 129062
3 changes: 3 additions & 0 deletions rest-api-spec/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ tasks.named("yamlRestCompatTestTransform").configure ({ task ->
task.skipTest("update/100_synthetic_source/stored text", "synthetic recovery source means _recovery_source field will not be present")
task.skipTest("logsdb/10_settings/start time not allowed in logs mode", "we don't validate for index_mode=tsdb when setting start_date/end_date anymore")
task.skipTest("logsdb/10_settings/end time not allowed in logs mode", "we don't validate for index_mode=tsdb when setting start_date/end_date anymore")
task.skipTest("logsdb/10_settings/override sort mode settings", "we changed the error message")
task.skipTest("logsdb/10_settings/override sort missing settings", "we changed the error message")
task.skipTest("logsdb/10_settings/override sort order settings", "we changed the error message")
task.skipTest("tsdb/10_settings/set start_time and end_time without timeseries mode", "we don't validate for index_mode=tsdb when setting start_date/end_date anymore")
task.skipTest("tsdb/10_settings/set start_time, end_time and routing_path via put settings api without time_series mode", "we don't validate for index_mode=tsdb when setting start_date/end_date anymore")
// Expected deprecation warning to compat yaml tests:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ override sort order settings:
type: text

- match: { error.type: "illegal_argument_exception" }
- match: { error.reason: "index.sort.fields:[] index.sort.order:[asc, asc], size mismatch" }
- match: { error.reason: "setting [index.sort.order] requires [index.sort.field] to be configured" }

---
override sort missing settings:
Expand Down Expand Up @@ -312,7 +312,7 @@ override sort missing settings:
type: text

- match: { error.type: "illegal_argument_exception" }
- match: { error.reason: "index.sort.fields:[] index.sort.missing:[_last, _first], size mismatch" }
- match: { error.reason: "setting [index.sort.missing] requires [index.sort.field] to be configured" }

---
override sort mode settings:
Expand Down Expand Up @@ -348,7 +348,7 @@ override sort mode settings:
type: text

- match: { error.type: "illegal_argument_exception" }
- match: { error.reason: "index.sort.fields:[] index.sort.mode:[MAX, MAX], size mismatch" }
- match: { error.reason: "setting [index.sort.mode] requires [index.sort.field] to be configured" }

---
override sort field using nested field type in sorting:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,29 @@ check time_series empty time bound value:
"@timestamp": "2021-09-26T03:09:52.123456789Z",
"metricset": "pod"
}

---
default sort field:
- requires:
cluster_features: [ "mapper.provide_index_sort_setting_defaults" ]
reason: "testing index sort setting defaults"

- do:
indices.create:
index: test_index
body:
settings:
index:
mode: time_series
routing_path: foo
time_series:
start_time: 2021-04-28T00:00:00Z
end_time: 2021-04-29T00:00:00Z

- do:
indices.get_settings:
index: test_index
include_defaults: true
- match: { .test_index.settings.index.mode: time_series }
- match: { .test_index.defaults.index.sort.field: [ "_tsid", "@timestamp" ] }
- match: { .test_index.defaults.index.sort.order: [ "asc", "desc" ] }
Original file line number Diff line number Diff line change
Expand Up @@ -1821,11 +1821,19 @@ public static Setting<List<String>> stringListSetting(String key, Property... pr
}

public static Setting<List<String>> stringListSetting(String key, List<String> defValue, Property... properties) {
return new ListSetting<>(key, null, s -> defValue, s -> parseableStringToList(s, Function.identity()), v -> {}, properties) {
return stringListSettingWithDefaultProvider(key, s -> defValue, properties);
}

public static Setting<List<String>> stringListSettingWithDefaultProvider(
String key,
Function<Settings, List<String>> defValueProvider,
Property... properties
) {
return new ListSetting<>(key, null, defValueProvider, s -> parseableStringToList(s, Function.identity()), v -> {}, properties) {
@Override
public List<String> get(Settings settings) {
checkDeprecation(settings);
return settings.getAsList(getKey(), defValue);
return settings.getAsList(getKey(), defValueProvider.apply(settings));
}
};
}
Expand All @@ -1852,6 +1860,15 @@ public static <T> Setting<List<T>> listSetting(
return listSetting(key, null, singleValueParser, s -> defaultStringValue, properties);
}

public static <T> Setting<List<T>> listSetting(
final String key,
final Function<Settings, List<String>> defaultStringValueProvider,
final Function<String, T> singleValueParser,
final Property... properties
) {
return listSetting(key, null, singleValueParser, defaultStringValueProvider, properties);
}

public static <T> Setting<List<T>> listSetting(
final String key,
final List<String> defaultStringValue,
Expand Down Expand Up @@ -1981,7 +1998,7 @@ public void diff(Settings.Builder builder, Settings source, Settings defaultSett
if (exists(source) == false) {
List<String> asList = defaultSettings.getAsList(getKey(), null);
if (asList == null) {
builder.putList(getKey(), defaultStringValue.apply(defaultSettings));
builder.putList(getKey(), defaultStringValue.apply(source));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a bug, but I'm not sure why we never saw it before. Maybe we never had string list settings whose default value depended on other settings before?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find, definitely looks like a bug to me

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find, I agree that the current behaviour isn't correct and actual settings need to be pushed down instead of default settings..

} else {
builder.putList(getKey(), asList);
}
Expand Down
5 changes: 4 additions & 1 deletion server/src/main/java/org/elasticsearch/index/IndexMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,11 @@ void validateWithOtherSettings(Map<Setting<?>, Object> settings) {
if (settings.get(IndexMetadata.INDEX_ROUTING_PARTITION_SIZE_SETTING) != Integer.valueOf(1)) {
throw new IllegalArgumentException(error(IndexMetadata.INDEX_ROUTING_PARTITION_SIZE_SETTING));
}

var settingsWithIndexMode = Settings.builder().put(IndexSettings.MODE.getKey(), getName()).build();

for (Setting<?> unsupported : TIME_SERIES_UNSUPPORTED) {
if (false == Objects.equals(unsupported.getDefault(Settings.EMPTY), settings.get(unsupported))) {
if (false == Objects.equals(unsupported.getDefault(settingsWithIndexMode), settings.get(unsupported))) {
throw new IllegalArgumentException(error(unsupported));
}
}
Expand Down
177 changes: 126 additions & 51 deletions server/src/main/java/org/elasticsearch/index/IndexSortConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.SortedNumericSortField;
import org.apache.lucene.search.SortedSetSortField;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.settings.Setting;
Expand All @@ -25,9 +26,10 @@
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.search.sort.SortOrder;

import java.util.Collections;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
Expand Down Expand Up @@ -59,8 +61,9 @@ public final class IndexSortConfig {
/**
* The list of field names
*/
public static final Setting<List<String>> INDEX_SORT_FIELD_SETTING = Setting.stringListSetting(
public static final Setting<List<String>> INDEX_SORT_FIELD_SETTING = Setting.stringListSettingWithDefaultProvider(
"index.sort.field",
IndexSortConfigDefaults::getDefaultSortFields,
Setting.Property.IndexScope,
Setting.Property.Final,
Setting.Property.ServerlessPublic
Expand All @@ -71,7 +74,7 @@ public final class IndexSortConfig {
*/
public static final Setting<List<SortOrder>> INDEX_SORT_ORDER_SETTING = Setting.listSetting(
"index.sort.order",
Collections.emptyList(),
IndexSortConfigDefaults::getDefaultSortOrder,
IndexSortConfig::parseOrderMode,
Setting.Property.IndexScope,
Setting.Property.Final,
Expand All @@ -83,7 +86,7 @@ public final class IndexSortConfig {
*/
public static final Setting<List<MultiValueMode>> INDEX_SORT_MODE_SETTING = Setting.listSetting(
"index.sort.mode",
Collections.emptyList(),
IndexSortConfigDefaults::getDefaultSortMode,
IndexSortConfig::parseMultiValueMode,
Setting.Property.IndexScope,
Setting.Property.Final,
Expand All @@ -95,30 +98,91 @@ public final class IndexSortConfig {
*/
public static final Setting<List<String>> INDEX_SORT_MISSING_SETTING = Setting.listSetting(
"index.sort.missing",
Collections.emptyList(),
IndexSortConfigDefaults::getDefaultSortMissing,
IndexSortConfig::validateMissingValue,
Setting.Property.IndexScope,
Setting.Property.Final,
Setting.Property.ServerlessPublic
);

public static final FieldSortSpec[] TIME_SERIES_SORT, TIMESTAMP_SORT, HOSTNAME_TIMESTAMP_SORT, HOSTNAME_TIMESTAMP_BWC_SORT;
static {
FieldSortSpec timeStampSpec = new FieldSortSpec(DataStreamTimestampFieldMapper.DEFAULT_PATH);
timeStampSpec.order = SortOrder.DESC;
TIME_SERIES_SORT = new FieldSortSpec[] { new FieldSortSpec(TimeSeriesIdFieldMapper.NAME), timeStampSpec };
TIMESTAMP_SORT = new FieldSortSpec[] { timeStampSpec };

FieldSortSpec hostnameSpec = new FieldSortSpec(IndexMode.HOST_NAME);
hostnameSpec.order = SortOrder.ASC;
hostnameSpec.missingValue = "_last";
hostnameSpec.mode = MultiValueMode.MIN;
HOSTNAME_TIMESTAMP_SORT = new FieldSortSpec[] { hostnameSpec, timeStampSpec };

// Older indexes use ascending ordering for host name and timestamp.
HOSTNAME_TIMESTAMP_BWC_SORT = new FieldSortSpec[] {
new FieldSortSpec(IndexMode.HOST_NAME),
new FieldSortSpec(DataStreamTimestampFieldMapper.DEFAULT_PATH) };
public static class IndexSortConfigDefaults {
public static final FieldSortSpec[] TIME_SERIES_SORT, TIMESTAMP_SORT, HOSTNAME_TIMESTAMP_SORT, HOSTNAME_TIMESTAMP_BWC_SORT;

static {
FieldSortSpec timeStampSpec = new FieldSortSpec(DataStreamTimestampFieldMapper.DEFAULT_PATH);
timeStampSpec.order = SortOrder.DESC;
TIME_SERIES_SORT = new FieldSortSpec[] { new FieldSortSpec(TimeSeriesIdFieldMapper.NAME), timeStampSpec };
TIMESTAMP_SORT = new FieldSortSpec[] { timeStampSpec };

FieldSortSpec hostnameSpec = new FieldSortSpec(IndexMode.HOST_NAME);
hostnameSpec.order = SortOrder.ASC;
hostnameSpec.missingValue = "_last";
hostnameSpec.mode = MultiValueMode.MIN;
HOSTNAME_TIMESTAMP_SORT = new FieldSortSpec[] { hostnameSpec, timeStampSpec };

// Older indexes use ascending ordering for host name and timestamp.
HOSTNAME_TIMESTAMP_BWC_SORT = new FieldSortSpec[] {
new FieldSortSpec(IndexMode.HOST_NAME),
new FieldSortSpec(DataStreamTimestampFieldMapper.DEFAULT_PATH) };
}

public static FieldSortSpec[] getDefaultSortSpecs(Settings settings) {
if (settings.isEmpty()) {
return new FieldSortSpec[0];
}

String indexMode = settings.get(IndexSettings.MODE.getKey());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't use IndexSettings.MODE.get(settings) here because the validation logic for IndexSettings.MODE uses the default value of index.sort.*, which causes infinite recursion (since we're already in the default value provider for those settings).
So we need to get the mode while bypassing the validation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add this also as a comment?

if (indexMode != null) {
indexMode = indexMode.toLowerCase(Locale.ROOT);
}

if (IndexMode.TIME_SERIES.getName().equals(indexMode)) {
return TIME_SERIES_SORT;
} else if (IndexMode.LOGSDB.getName().equals(indexMode)) {
var version = IndexMetadata.SETTING_INDEX_VERSION_CREATED.get(settings);
if (version.onOrAfter(IndexVersions.LOGSB_OPTIONAL_SORTING_ON_HOST_NAME)
|| version.between(
IndexVersions.LOGSB_OPTIONAL_SORTING_ON_HOST_NAME_BACKPORT,
IndexVersions.UPGRADE_TO_LUCENE_10_0_0
)) {
return (IndexSettings.LOGSDB_SORT_ON_HOST_NAME.get(settings)) ? HOSTNAME_TIMESTAMP_SORT : TIMESTAMP_SORT;
} else {
return HOSTNAME_TIMESTAMP_BWC_SORT;
}
}

return new FieldSortSpec[0];
}

public static List<String> getDefaultSortFields(Settings settings) {
return Arrays.stream(getDefaultSortSpecs(settings)).map(sortSpec -> sortSpec.field).toList();
}

public static List<String> getDefaultSortOrder(Settings settings) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there are situations where non-default and default settings can be mixed incorrectly. For example, if mode=logsdb, and only a sort field is set, the order, mode, missing settings will use the defaults for timestamp sort.

this test (added to 80_index_sort_defaults.yml) shows the issue:

---
create logsdb data stream with non-default sort field:
  - do:
      cluster.put_component_template:
        name: "logsdb-mappings"
        body:
          template:
            settings:
              mode: "logsdb"
              index.sort.field: ["some_field"]
            mappings:
              properties:
                some_field:
                  type: "keyword"
                "@timestamp":
                  type: "date"

  - do:
      indices.put_index_template:
        name: "logsdb-index-template"
        body:
          index_patterns: ["logsdb"]
          data_stream: {}
          composed_of: ["logsdb-mappings"]
      allowed_warnings:
        - "index template [logsdb-index-template] has index patterns [logsdb] matching patterns from existing older templates [global] with patterns (global => [*]); this template [logsdb-index-template] will take precedence during new index creation"

  - do:
      indices.create_data_stream:
        name: "logsdb"

  - is_true: acknowledged
  - do:
      indices.get_data_stream:
        name: "logsdb"
        expand_wildcards: hidden
  - length: { data_streams: 1 }
  - set: { data_streams.0.indices.0.index_name: backing_index }

  - do:
      indices.get_settings:
        index: $backing_index
        include_defaults: true
  - match: { .$backing_index.settings.index.mode: logsdb }
  - match: { .$backing_index.settings.index.sort.field: [ "some_field" ] }
  - match: { .$backing_index.defaults.index.sort.order: [ "asc" ] }
  - match: { .$backing_index.defaults.index.sort.mode: [ "min" ] }
  - match: { .$backing_index.defaults.index.sort.missing: [ "_last" ] }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The current logic does ensure the defaults aren't actually applied, but they're still reported via the settings api. I'll update the logic so that if a custom sort is defined, the defaults match index.sort.fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, should be fixed as of b8ff892

return Arrays.stream(getDefaultSortSpecs(settings))
.map(sortSpec -> sortSpec.order != null ? sortSpec.order : SortOrder.ASC)
.map(Enum::toString)
.toList();
}

public static List<String> getDefaultSortMode(Settings settings) {
return Arrays.stream(getDefaultSortSpecs(settings)).map(sortSpec -> {
if (sortSpec.mode != null) {
return sortSpec.mode;
} else if (sortSpec.order == SortOrder.DESC) {
return MultiValueMode.MAX;
} else {
return MultiValueMode.MIN;
}
}).map(order -> order.toString().toLowerCase(Locale.ROOT)).toList();
}

public static List<String> getDefaultSortMissing(Settings settings) {
// _last is the default per IndexFieldData.XFieldComparatorSource.Nested#sortMissingLast
return Arrays.stream(getDefaultSortSpecs(settings))
.map(sortSpec -> sortSpec.missingValue != null ? sortSpec.missingValue : "_last")
.toList();
}
}

private static String validateMissingValue(String missing) {
Expand Down Expand Up @@ -146,6 +210,41 @@ private static MultiValueMode parseMultiValueMode(String value) {
return mode;
}

private static void checkSizeMismatch(String firstKey, List<?> first, String secondKey, List<?> second) {
if (first.size() != second.size()) {
throw new IllegalArgumentException(firstKey + ":" + first + " " + secondKey + ":" + second + ", size mismatch");
}
}

private static void validateSortSettings(Settings settings) {
if (INDEX_SORT_FIELD_SETTING.exists(settings) == false) {
for (Setting<?> setting : new Setting<?>[] { INDEX_SORT_ORDER_SETTING, INDEX_SORT_MODE_SETTING, INDEX_SORT_MISSING_SETTING }) {
if (setting.exists(settings)) {
throw new IllegalArgumentException(
"setting [" + setting.getKey() + "] requires [" + INDEX_SORT_FIELD_SETTING.getKey() + "] to be configured"
);
}
}
}

List<String> fields = INDEX_SORT_FIELD_SETTING.get(settings);

if (INDEX_SORT_ORDER_SETTING.exists(settings)) {
var order = INDEX_SORT_ORDER_SETTING.get(settings);
checkSizeMismatch(INDEX_SORT_FIELD_SETTING.getKey(), fields, INDEX_SORT_ORDER_SETTING.getKey(), order);
}

if (INDEX_SORT_MODE_SETTING.exists(settings)) {
var mode = INDEX_SORT_MODE_SETTING.get(settings);
checkSizeMismatch(INDEX_SORT_FIELD_SETTING.getKey(), fields, INDEX_SORT_MODE_SETTING.getKey(), mode);
}

if (INDEX_SORT_MISSING_SETTING.exists(settings)) {
var missing = INDEX_SORT_MISSING_SETTING.get(settings);
checkSizeMismatch(INDEX_SORT_FIELD_SETTING.getKey(), fields, INDEX_SORT_MISSING_SETTING.getKey(), missing);
}
}

// visible for tests
final FieldSortSpec[] sortSpecs;
private final IndexVersion indexCreatedVersion;
Expand All @@ -158,37 +257,13 @@ public IndexSortConfig(IndexSettings indexSettings) {
this.indexName = indexSettings.getIndex().getName();
this.indexMode = indexSettings.getMode();

if (indexMode == IndexMode.TIME_SERIES) {
sortSpecs = TIME_SERIES_SORT;
return;
}
validateSortSettings(settings);

List<String> fields = INDEX_SORT_FIELD_SETTING.get(settings);
if (indexMode == IndexMode.LOGSDB && INDEX_SORT_FIELD_SETTING.exists(settings) == false) {
if (INDEX_SORT_ORDER_SETTING.exists(settings)) {
var order = INDEX_SORT_ORDER_SETTING.get(settings);
throw new IllegalArgumentException("index.sort.fields:" + fields + " index.sort.order:" + order + ", size mismatch");
}
if (INDEX_SORT_MODE_SETTING.exists(settings)) {
var mode = INDEX_SORT_MODE_SETTING.get(settings);
throw new IllegalArgumentException("index.sort.fields:" + fields + " index.sort.mode:" + mode + ", size mismatch");
}
if (INDEX_SORT_MISSING_SETTING.exists(settings)) {
var missing = INDEX_SORT_MISSING_SETTING.get(settings);
throw new IllegalArgumentException("index.sort.fields:" + fields + " index.sort.missing:" + missing + ", size mismatch");
}
var version = indexSettings.getIndexVersionCreated();
if (version.onOrAfter(IndexVersions.LOGSB_OPTIONAL_SORTING_ON_HOST_NAME)
|| version.between(IndexVersions.LOGSB_OPTIONAL_SORTING_ON_HOST_NAME_BACKPORT, IndexVersions.UPGRADE_TO_LUCENE_10_0_0)) {
sortSpecs = (IndexSettings.LOGSDB_SORT_ON_HOST_NAME.get(settings)) ? HOSTNAME_TIMESTAMP_SORT : TIMESTAMP_SORT;
} else {
sortSpecs = HOSTNAME_TIMESTAMP_BWC_SORT;
}
return;
}
boolean applyDefaults = INDEX_SORT_FIELD_SETTING.exists(settings) == false;
sortSpecs = fields.stream().map(FieldSortSpec::new).toArray(FieldSortSpec[]::new);

if (INDEX_SORT_ORDER_SETTING.exists(settings)) {
if (INDEX_SORT_ORDER_SETTING.exists(settings) || applyDefaults) {
List<SortOrder> orders = INDEX_SORT_ORDER_SETTING.get(settings);
if (orders.size() != sortSpecs.length) {
throw new IllegalArgumentException("index.sort.field:" + fields + " index.sort.order:" + orders + ", size mismatch");
Expand All @@ -198,7 +273,7 @@ public IndexSortConfig(IndexSettings indexSettings) {
}
}

if (INDEX_SORT_MODE_SETTING.exists(settings)) {
if (INDEX_SORT_MODE_SETTING.exists(settings) || applyDefaults) {
List<MultiValueMode> modes = INDEX_SORT_MODE_SETTING.get(settings);
if (modes.size() != sortSpecs.length) {
throw new IllegalArgumentException("index.sort.field:" + fields + " index.sort.mode:" + modes + ", size mismatch");
Expand All @@ -208,7 +283,7 @@ public IndexSortConfig(IndexSettings indexSettings) {
}
}

if (INDEX_SORT_MISSING_SETTING.exists(settings)) {
if (INDEX_SORT_MISSING_SETTING.exists(settings) || applyDefaults) {
List<String> missingValues = INDEX_SORT_MISSING_SETTING.get(settings);
if (missingValues.size() != sortSpecs.length) {
throw new IllegalArgumentException(
Expand Down
Loading