diff --git a/.buildkite/scripts/gradle-cache-validation.sh b/.buildkite/scripts/gradle-cache-validation.sh
new file mode 100755
index 0000000000000..fbb957bc3b26b
--- /dev/null
+++ b/.buildkite/scripts/gradle-cache-validation.sh
@@ -0,0 +1,79 @@
+#!/bin/bash
+
+set -euo pipefail
+
+VALIDATION_SCRIPTS_VERSION=2.5.1
+GRADLE_ENTERPRISE_ACCESS_KEY=$(vault kv get -field=value secret/ci/elastic-elasticsearch/gradle-enterprise-api-key)
+export GRADLE_ENTERPRISE_ACCESS_KEY
+
+curl -s -L -O https://github.com/gradle/gradle-enterprise-build-validation-scripts/releases/download/v$VALIDATION_SCRIPTS_VERSION/gradle-enterprise-gradle-build-validation-$VALIDATION_SCRIPTS_VERSION.zip && unzip -q -o gradle-enterprise-gradle-build-validation-$VALIDATION_SCRIPTS_VERSION.zip
+
+# Create a temporary file
+tmpOutputFile=$(mktemp)
+trap "rm $tmpOutputFile" EXIT
+
+gradle-enterprise-gradle-build-validation/03-validate-local-build-caching-different-locations.sh -r https://github.com/elastic/elasticsearch.git -b $BUILDKITE_BRANCH --gradle-enterprise-server https://gradle-enterprise.elastic.co -t precommit --fail-if-not-fully-cacheable | tee $tmpOutputFile
+
+# Capture the return value
+retval=$?
+
+# Now read the content from the temporary file into a variable
+perfOutput=$(cat $tmpOutputFile | sed -n '/Performance Characteristics/,/See https:\/\/gradle.com\/bvs\/main\/Gradle.md#performance-characteristics for details./p' | sed '$d' | sed 's/\x1b\[[0-9;]*m//g')
+investigationOutput=$(cat $tmpOutputFile | sed -n '/Investigation Quick Links/,$p' | sed 's/\x1b\[[0-9;]*m//g')
+
+# Initialize HTML output variable
+summaryHtml="
Performance Characteristics
"
+summaryHtml+="
"
+
+# Process each line of the string
+while IFS=: read -r label value; do
+ if [[ -n "$label" && -n "$value" ]]; then
+ # Trim whitespace from label and value
+ trimmed_label=$(echo "$label" | xargs)
+ trimmed_value=$(echo "$value" | xargs)
+
+ # Append to HTML output variable
+ summaryHtml+="
$trimmed_label: $trimmed_value
"
+ fi
+done <<< "$perfOutput"
+
+summaryHtml+="
"
+
+# generate html for links
+summaryHtml+="
Investigation Links
"
+summaryHtml+="
"
+
+# Process each line of the string
+while IFS= read -r line; do
+ if [[ "$line" =~ http.* ]]; then
+ # Extract URL and description using awk
+ url=$(echo "$line" | awk '{print $NF}')
+ description=$(echo "$line" | sed -e "s/:.*//")
+
+ # Append to HTML output variable
+ summaryHtml+="
"
+ fi
+done <<< "$investigationOutput"
+
+# End of the HTML content
+summaryHtml+="
"
+
+cat << EOF | buildkite-agent annotate --context "ctx-validation-summary" --style "info"
+$summaryHtml
+EOF
+
+# Check if the command was successful
+if [ $retval -eq 0 ]; then
+ echo "Experiment completed successfully"
+elif [ $retval -eq 1 ]; then
+ echo "An invalid input was provided while attempting to run the experiment"
+elif [ $retval -eq 2 ]; then
+ echo "One of the builds that is part of the experiment failed"
+elif [ $retval -eq 3 ]; then
+ echo "The build was not fully cacheable for the given task graph"
+elif [ $retval -eq 3 ]; then
+ echo "An unclassified, fatal error happened while running the experiment"
+fi
+
+exit $retval
+
diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/DependencyLicensesTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/DependencyLicensesTask.java
index 0099a4616f829..07817fdaed1fe 100644
--- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/DependencyLicensesTask.java
+++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/DependencyLicensesTask.java
@@ -23,11 +23,14 @@
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.specs.Spec;
+import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.PathSensitive;
+import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction;
import java.io.File;
@@ -89,6 +92,7 @@
* for the dependency. This artifact will be redistributed by us with the release to
* comply with the license terms.
*/
+@CacheableTask
public abstract class DependencyLicensesTask extends DefaultTask {
private final Pattern regex = Pattern.compile("-v?\\d+.*");
@@ -149,6 +153,7 @@ public DependencyLicensesTask(ObjectFactory objects, ProjectLayout projectLayout
}
@InputFiles
+ @PathSensitive(PathSensitivity.NAME_ONLY)
public FileCollection getDependencies() {
return dependencies;
}
@@ -159,6 +164,7 @@ public void setDependencies(FileCollection dependencies) {
@Optional
@InputDirectory
+ @PathSensitive(PathSensitivity.RELATIVE)
public File getLicensesDir() {
File asFile = licensesDir.get().getAsFile();
if (asFile.exists()) {
diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/SplitPackagesAuditTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/SplitPackagesAuditTask.java
index ec279589a6bed..f75adbe640297 100644
--- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/SplitPackagesAuditTask.java
+++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/SplitPackagesAuditTask.java
@@ -20,6 +20,7 @@
import org.gradle.api.provider.MapProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.SetProperty;
+import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.CompileClasspath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
@@ -56,6 +57,7 @@
/**
* Checks for split packages with dependencies. These are not allowed in a future modularized world.
*/
+@CacheableTask
public class SplitPackagesAuditTask extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(SplitPackagesAuditTask.class);
diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/AbstractVersionsTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/AbstractVersionsTask.java
index 0ab3a9b917d65..ad39faad1bc85 100644
--- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/AbstractVersionsTask.java
+++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/AbstractVersionsTask.java
@@ -8,19 +8,119 @@
package org.elasticsearch.gradle.internal.release;
+import com.github.javaparser.GeneratedJavaParserConstants;
+import com.github.javaparser.ast.CompilationUnit;
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
+import com.github.javaparser.ast.body.FieldDeclaration;
+import com.github.javaparser.ast.expr.IntegerLiteralExpr;
+import com.github.javaparser.ast.observer.ObservableProperty;
+import com.github.javaparser.printer.ConcreteSyntaxModel;
+import com.github.javaparser.printer.concretesyntaxmodel.CsmElement;
+import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
+
import org.gradle.api.DefaultTask;
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
import org.gradle.initialization.layout.BuildLayout;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import java.util.Map;
+import java.util.OptionalInt;
+import java.util.stream.Collectors;
+
+import static com.github.javaparser.ast.observer.ObservableProperty.TYPE_PARAMETERS;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmConditional.Condition.FLAG;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.block;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.child;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.comma;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.comment;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.conditional;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.list;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.newline;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.none;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.sequence;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.space;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.string;
+import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.token;
public abstract class AbstractVersionsTask extends DefaultTask {
+ static {
+ replaceDefaultJavaParserClassCsm();
+ }
+
+ /*
+ * The default JavaParser CSM which it uses to format any new declarations added to a class
+ * inserts two newlines after each declaration. Our version classes only have one newline.
+ * In order to get javaparser lexical printer to use our format, we have to completely replace
+ * the statically declared CSM pattern using hacky reflection
+ * to access the static map where these are stored, and insert a replacement that is identical
+ * apart from only one newline at the end of each member declaration, rather than two.
+ */
+ private static void replaceDefaultJavaParserClassCsm() {
+ try {
+ Field classCsms = ConcreteSyntaxModel.class.getDeclaredField("concreteSyntaxModelByClass");
+ classCsms.setAccessible(true);
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ Map csms = (Map) classCsms.get(null);
+
+ // copied from the static initializer in ConcreteSyntaxModel
+ csms.put(
+ ClassOrInterfaceDeclaration.class,
+ sequence(
+ comment(),
+ list(ObservableProperty.ANNOTATIONS, newline(), none(), newline()),
+ list(ObservableProperty.MODIFIERS, space(), none(), space()),
+ conditional(
+ ObservableProperty.INTERFACE,
+ FLAG,
+ token(GeneratedJavaParserConstants.INTERFACE),
+ token(GeneratedJavaParserConstants.CLASS)
+ ),
+ space(),
+ child(ObservableProperty.NAME),
+ list(
+ TYPE_PARAMETERS,
+ sequence(comma(), space()),
+ string(GeneratedJavaParserConstants.LT),
+ string(GeneratedJavaParserConstants.GT)
+ ),
+ list(
+ ObservableProperty.EXTENDED_TYPES,
+ sequence(string(GeneratedJavaParserConstants.COMMA), space()),
+ sequence(space(), token(GeneratedJavaParserConstants.EXTENDS), space()),
+ none()
+ ),
+ list(
+ ObservableProperty.IMPLEMENTED_TYPES,
+ sequence(string(GeneratedJavaParserConstants.COMMA), space()),
+ sequence(space(), token(GeneratedJavaParserConstants.IMPLEMENTS), space()),
+ none()
+ ),
+ space(),
+ block(sequence(newline(), list(ObservableProperty.MEMBERS, sequence(newline()/*, newline()*/), newline(), newline())))
+ )
+ );
+ } catch (ReflectiveOperationException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static final Logger LOGGER = Logging.getLogger(AbstractVersionsTask.class);
+
static final String TRANSPORT_VERSION_TYPE = "TransportVersion";
static final String INDEX_VERSION_TYPE = "IndexVersion";
static final String SERVER_MODULE_PATH = "server/src/main/java/";
- static final String TRANSPORT_VERSION_FILE_PATH = SERVER_MODULE_PATH + "org/elasticsearch/TransportVersions.java";
- static final String INDEX_VERSION_FILE_PATH = SERVER_MODULE_PATH + "org/elasticsearch/index/IndexVersions.java";
+
+ static final String VERSION_FILE_PATH = SERVER_MODULE_PATH + "org/elasticsearch/Version.java";
+ static final String TRANSPORT_VERSIONS_FILE_PATH = SERVER_MODULE_PATH + "org/elasticsearch/TransportVersions.java";
+ static final String INDEX_VERSIONS_FILE_PATH = SERVER_MODULE_PATH + "org/elasticsearch/index/IndexVersions.java";
static final String SERVER_RESOURCES_PATH = "server/src/main/resources/";
static final String TRANSPORT_VERSIONS_RECORD = SERVER_RESOURCES_PATH + "org/elasticsearch/TransportVersions.csv";
@@ -32,4 +132,34 @@ protected AbstractVersionsTask(BuildLayout layout) {
rootDir = layout.getRootDirectory().toPath();
}
+ static Map splitVersionIds(List version) {
+ return version.stream().map(l -> {
+ var split = l.split(":");
+ if (split.length != 2) throw new IllegalArgumentException("Invalid tag format [" + l + "]");
+ return split;
+ }).collect(Collectors.toMap(l -> l[0], l -> Integer.parseInt(l[1])));
+ }
+
+ static OptionalInt findSingleIntegerExpr(FieldDeclaration field) {
+ var ints = field.findAll(IntegerLiteralExpr.class);
+ switch (ints.size()) {
+ case 0 -> {
+ return OptionalInt.empty();
+ }
+ case 1 -> {
+ return OptionalInt.of(ints.get(0).asNumber().intValue());
+ }
+ default -> {
+ LOGGER.warn("Multiple integers found in version field declaration [{}]", field); // and ignore it
+ return OptionalInt.empty();
+ }
+ }
+ }
+
+ static void writeOutNewContents(Path file, CompilationUnit unit) throws IOException {
+ if (unit.containsData(LexicalPreservingPrinter.NODE_TEXT_DATA) == false) {
+ throw new IllegalArgumentException("CompilationUnit has no lexical information for output");
+ }
+ Files.writeString(file, LexicalPreservingPrinter.print(unit), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
+ }
}
diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ExtractCurrentVersionsTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ExtractCurrentVersionsTask.java
index 3530d7ef9e807..53dd55041f6bd 100644
--- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ExtractCurrentVersionsTask.java
+++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ExtractCurrentVersionsTask.java
@@ -11,7 +11,6 @@
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.FieldDeclaration;
-import com.github.javaparser.ast.expr.IntegerLiteralExpr;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
@@ -53,11 +52,11 @@ public void executeTask() throws IOException {
LOGGER.lifecycle("Extracting latest version information");
List output = new ArrayList<>();
- int transportVersion = readLatestVersion(rootDir.resolve(TRANSPORT_VERSION_FILE_PATH));
+ int transportVersion = readLatestVersion(rootDir.resolve(TRANSPORT_VERSIONS_FILE_PATH));
LOGGER.lifecycle("Transport version: {}", transportVersion);
output.add(TRANSPORT_VERSION_TYPE + ":" + transportVersion);
- int indexVersion = readLatestVersion(rootDir.resolve(INDEX_VERSION_FILE_PATH));
+ int indexVersion = readLatestVersion(rootDir.resolve(INDEX_VERSIONS_FILE_PATH));
LOGGER.lifecycle("Index version: {}", indexVersion);
output.add(INDEX_VERSION_TYPE + ":" + indexVersion);
@@ -74,21 +73,13 @@ Integer highestVersionId() {
@Override
public void accept(FieldDeclaration fieldDeclaration) {
- var ints = fieldDeclaration.findAll(IntegerLiteralExpr.class);
- switch (ints.size()) {
- case 0 -> {
- // No ints in the field declaration, ignore
+ findSingleIntegerExpr(fieldDeclaration).ifPresent(id -> {
+ if (highestVersionId != null && highestVersionId > id) {
+ LOGGER.warn("Version ids [{}, {}] out of order", highestVersionId, id);
+ } else {
+ highestVersionId = id;
}
- case 1 -> {
- int id = ints.get(0).asNumber().intValue();
- if (highestVersionId != null && highestVersionId > id) {
- LOGGER.warn("Version ids [{}, {}] out of order", highestVersionId, id);
- } else {
- highestVersionId = id;
- }
- }
- default -> LOGGER.warn("Multiple integers found in version field declaration [{}]", fieldDeclaration); // and ignore it
- }
+ });
}
}
diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ReleaseToolsPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ReleaseToolsPlugin.java
index 8001b82797557..08abb02ea831e 100644
--- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ReleaseToolsPlugin.java
+++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/ReleaseToolsPlugin.java
@@ -52,6 +52,7 @@ public void apply(Project project) {
project.getTasks().register("extractCurrentVersions", ExtractCurrentVersionsTask.class);
project.getTasks().register("tagVersions", TagVersionsTask.class);
+ project.getTasks().register("setCompatibleVersions", SetCompatibleVersionsTask.class);
final FileTree yamlFiles = projectDirectory.dir("docs/changelog")
.getAsFileTree()
diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/SetCompatibleVersionsTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/SetCompatibleVersionsTask.java
new file mode 100644
index 0000000000000..15e0a0cc345d5
--- /dev/null
+++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/SetCompatibleVersionsTask.java
@@ -0,0 +1,83 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.internal.release;
+
+import com.github.javaparser.StaticJavaParser;
+import com.github.javaparser.ast.CompilationUnit;
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
+import com.github.javaparser.ast.expr.NameExpr;
+import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
+
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.api.tasks.options.Option;
+import org.gradle.initialization.layout.BuildLayout;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+public class SetCompatibleVersionsTask extends AbstractVersionsTask {
+
+ private Map versionIds = Map.of();
+
+ @Inject
+ public SetCompatibleVersionsTask(BuildLayout layout) {
+ super(layout);
+ }
+
+ @Option(option = "version-id", description = "Version id used for the release. Of the form :.")
+ public void versionIds(List version) {
+ this.versionIds = splitVersionIds(version);
+ }
+
+ @TaskAction
+ public void executeTask() throws IOException {
+ if (versionIds.isEmpty()) {
+ throw new IllegalArgumentException("No version ids specified");
+ }
+ Integer transportVersion = versionIds.get(TRANSPORT_VERSION_TYPE);
+ if (transportVersion == null) {
+ throw new IllegalArgumentException("TransportVersion id not specified");
+ }
+
+ Path versionJava = rootDir.resolve(TRANSPORT_VERSIONS_FILE_PATH);
+ CompilationUnit file = LexicalPreservingPrinter.setup(StaticJavaParser.parse(versionJava));
+
+ Optional modifiedFile;
+
+ modifiedFile = setMinimumCcsTransportVersion(file, transportVersion);
+
+ if (modifiedFile.isPresent()) {
+ writeOutNewContents(versionJava, modifiedFile.get());
+ }
+ }
+
+ static Optional setMinimumCcsTransportVersion(CompilationUnit unit, int transportVersion) {
+ ClassOrInterfaceDeclaration transportVersions = unit.getClassByName("TransportVersions").get();
+
+ String tvConstantName = transportVersions.getFields().stream().filter(f -> {
+ var i = findSingleIntegerExpr(f);
+ return i.isPresent() && i.getAsInt() == transportVersion;
+ })
+ .map(f -> f.getVariable(0).getNameAsString())
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("Could not find constant for id " + transportVersion));
+
+ transportVersions.getFieldByName("MINIMUM_CCS_VERSION")
+ .orElseThrow(() -> new IllegalStateException("Could not find MINIMUM_CCS_VERSION constant"))
+ .getVariable(0)
+ .setInitializer(new NameExpr(tvConstantName));
+
+ return Optional.of(unit);
+ }
+}
diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/TagVersionsTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/TagVersionsTask.java
index fa11746543e82..a7f67f87b602e 100644
--- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/TagVersionsTask.java
+++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/TagVersionsTask.java
@@ -47,11 +47,7 @@ public void release(String version) {
@Option(option = "tag-version", description = "Version id to tag. Of the form :.")
public void tagVersions(List version) {
- this.tagVersions = version.stream().map(l -> {
- var split = l.split(":");
- if (split.length != 2) throw new IllegalArgumentException("Invalid tag format [" + l + "]");
- return split;
- }).collect(Collectors.toMap(l -> l[0], l -> Integer.parseInt(l[1])));
+ this.tagVersions = splitVersionIds(version);
}
@TaskAction
diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/UpdateVersionsTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/UpdateVersionsTask.java
index 9996ffe613545..b19e5c0beacf8 100644
--- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/UpdateVersionsTask.java
+++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/release/UpdateVersionsTask.java
@@ -8,7 +8,6 @@
package org.elasticsearch.gradle.internal.release;
-import com.github.javaparser.GeneratedJavaParserConstants;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.NodeList;
@@ -16,14 +15,10 @@
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.VariableDeclarator;
import com.github.javaparser.ast.expr.NameExpr;
-import com.github.javaparser.ast.observer.ObservableProperty;
-import com.github.javaparser.printer.ConcreteSyntaxModel;
-import com.github.javaparser.printer.concretesyntaxmodel.CsmElement;
import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
import com.google.common.annotations.VisibleForTesting;
import org.elasticsearch.gradle.Version;
-import org.gradle.api.DefaultTask;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.tasks.TaskAction;
@@ -31,10 +26,7 @@
import org.gradle.initialization.layout.BuildLayout;
import java.io.IOException;
-import java.lang.reflect.Field;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
@@ -47,93 +39,12 @@
import javax.annotation.Nullable;
import javax.inject.Inject;
-import static com.github.javaparser.ast.observer.ObservableProperty.TYPE_PARAMETERS;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmConditional.Condition.FLAG;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.block;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.child;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.comma;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.comment;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.conditional;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.list;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.newline;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.none;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.sequence;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.space;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.string;
-import static com.github.javaparser.printer.concretesyntaxmodel.CsmElement.token;
-
-public class UpdateVersionsTask extends DefaultTask {
-
- static {
- replaceDefaultJavaParserClassCsm();
- }
-
- /*
- * The default JavaParser CSM which it uses to format any new declarations added to a class
- * inserts two newlines after each declaration. Our version classes only have one newline.
- * In order to get javaparser lexical printer to use our format, we have to completely replace
- * the statically declared CSM pattern using hacky reflection
- * to access the static map where these are stored, and insert a replacement that is identical
- * apart from only one newline at the end of each member declaration, rather than two.
- */
- private static void replaceDefaultJavaParserClassCsm() {
- try {
- Field classCsms = ConcreteSyntaxModel.class.getDeclaredField("concreteSyntaxModelByClass");
- classCsms.setAccessible(true);
- @SuppressWarnings({ "unchecked", "rawtypes" })
- Map csms = (Map) classCsms.get(null);
-
- // copied from the static initializer in ConcreteSyntaxModel
- csms.put(
- ClassOrInterfaceDeclaration.class,
- sequence(
- comment(),
- list(ObservableProperty.ANNOTATIONS, newline(), none(), newline()),
- list(ObservableProperty.MODIFIERS, space(), none(), space()),
- conditional(
- ObservableProperty.INTERFACE,
- FLAG,
- token(GeneratedJavaParserConstants.INTERFACE),
- token(GeneratedJavaParserConstants.CLASS)
- ),
- space(),
- child(ObservableProperty.NAME),
- list(
- TYPE_PARAMETERS,
- sequence(comma(), space()),
- string(GeneratedJavaParserConstants.LT),
- string(GeneratedJavaParserConstants.GT)
- ),
- list(
- ObservableProperty.EXTENDED_TYPES,
- sequence(string(GeneratedJavaParserConstants.COMMA), space()),
- sequence(space(), token(GeneratedJavaParserConstants.EXTENDS), space()),
- none()
- ),
- list(
- ObservableProperty.IMPLEMENTED_TYPES,
- sequence(string(GeneratedJavaParserConstants.COMMA), space()),
- sequence(space(), token(GeneratedJavaParserConstants.IMPLEMENTS), space()),
- none()
- ),
- space(),
- block(sequence(newline(), list(ObservableProperty.MEMBERS, sequence(newline()/*, newline()*/), newline(), newline())))
- )
- );
- } catch (ReflectiveOperationException e) {
- throw new AssertionError(e);
- }
- }
+public class UpdateVersionsTask extends AbstractVersionsTask {
private static final Logger LOGGER = Logging.getLogger(UpdateVersionsTask.class);
- static final String SERVER_MODULE_PATH = "server/src/main/java/";
- static final String VERSION_FILE_PATH = SERVER_MODULE_PATH + "org/elasticsearch/Version.java";
-
static final Pattern VERSION_FIELD = Pattern.compile("V_(\\d+)_(\\d+)_(\\d+)(?:_(\\w+))?");
- final Path rootDir;
-
@Nullable
private Version addVersion;
private boolean setCurrent;
@@ -142,7 +53,7 @@ private static void replaceDefaultJavaParserClassCsm() {
@Inject
public UpdateVersionsTask(BuildLayout layout) {
- rootDir = layout.getRootDirectory().toPath();
+ super(layout);
}
@Option(option = "add-version", description = "Specifies the version to add")
@@ -287,11 +198,4 @@ static Optional removeVersionConstant(CompilationUnit versionJa
return Optional.of(versionJava);
}
-
- static void writeOutNewContents(Path file, CompilationUnit unit) throws IOException {
- if (unit.containsData(LexicalPreservingPrinter.NODE_TEXT_DATA) == false) {
- throw new IllegalArgumentException("CompilationUnit has no lexical information for output");
- }
- Files.writeString(file, LexicalPreservingPrinter.print(unit), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
- }
}
diff --git a/build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/SetCompatibleVersionsTaskTests.java b/build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/SetCompatibleVersionsTaskTests.java
new file mode 100644
index 0000000000000..eecb953a44eb6
--- /dev/null
+++ b/build-tools-internal/src/test/java/org/elasticsearch/gradle/internal/release/SetCompatibleVersionsTaskTests.java
@@ -0,0 +1,50 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.internal.release;
+
+import com.github.javaparser.StaticJavaParser;
+import com.github.javaparser.ast.CompilationUnit;
+
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasToString;
+
+public class SetCompatibleVersionsTaskTests {
+
+ @Test
+ public void updateMinCcsVersion() {
+ final String transportVersionsJava = """
+ public class TransportVersions {
+ public static final TransportVersion V1 = def(100);
+ public static final TransportVersion V2 = def(200);
+ public static final TransportVersion V3 = def(300);
+
+ public static final TransportVersion MINIMUM_CCS_VERSION = V2;
+ }""";
+ final String updatedJava = """
+ public class TransportVersions {
+
+ public static final TransportVersion V1 = def(100);
+
+ public static final TransportVersion V2 = def(200);
+
+ public static final TransportVersion V3 = def(300);
+
+ public static final TransportVersion MINIMUM_CCS_VERSION = V3;
+ }
+ """;
+
+ CompilationUnit unit = StaticJavaParser.parse(transportVersionsJava);
+
+ SetCompatibleVersionsTask.setMinimumCcsTransportVersion(unit, 300);
+
+ assertThat(unit, hasToString(updatedJava));
+ }
+}
diff --git a/docs/changelog/106252.yaml b/docs/changelog/106252.yaml
new file mode 100644
index 0000000000000..5e3f084632b9d
--- /dev/null
+++ b/docs/changelog/106252.yaml
@@ -0,0 +1,6 @@
+pr: 106252
+summary: Add min/max range of the `event.ingested` field to cluster state for searchable
+ snapshots
+area: Search
+type: enhancement
+issues: []
diff --git a/docs/changelog/107415.yaml b/docs/changelog/107415.yaml
new file mode 100644
index 0000000000000..8877d0426c60d
--- /dev/null
+++ b/docs/changelog/107415.yaml
@@ -0,0 +1,6 @@
+pr: 107415
+summary: Fix `DecayFunctions'` `toString`
+area: Search
+type: bug
+issues:
+ - 100870
diff --git a/docs/changelog/108606.yaml b/docs/changelog/108606.yaml
new file mode 100644
index 0000000000000..04780bff58800
--- /dev/null
+++ b/docs/changelog/108606.yaml
@@ -0,0 +1,14 @@
+pr: 108606
+summary: "Extend ISO8601 datetime parser to specify forbidden fields, allowing it to be used\
+ \ on more formats"
+area: Infra/Core
+type: enhancement
+issues: []
+highlight:
+ title: New custom parser for more ISO-8601 date formats
+ body: |-
+ Following on from #106486, this extends the custom ISO-8601 datetime parser to cover the `strict_year`,
+ `strict_year_month`, `strict_date_time`, `strict_date_time_no_millis`, `strict_date_hour_minute_second`,
+ `strict_date_hour_minute_second_millis`, and `strict_date_hour_minute_second_fraction` date formats.
+ As before, the parser will use the existing java.time parser if there are parsing issues, and the
+ `es.datetime.java_time_parsers=true` JVM property will force the use of the old parsers regardless.
diff --git a/docs/changelog/109807.yaml b/docs/changelog/109807.yaml
new file mode 100644
index 0000000000000..5cf8a2c896c4e
--- /dev/null
+++ b/docs/changelog/109807.yaml
@@ -0,0 +1,6 @@
+pr: 109807
+summary: "ESQL: Fix LOOKUP attribute shadowing"
+area: ES|QL
+type: bug
+issues:
+ - 109392
diff --git a/docs/changelog/109893.yaml b/docs/changelog/109893.yaml
new file mode 100644
index 0000000000000..df6d6e51236c8
--- /dev/null
+++ b/docs/changelog/109893.yaml
@@ -0,0 +1,5 @@
+pr: 109893
+summary: Add Anthropic messages integration to Inference API
+area: Machine Learning
+type: enhancement
+issues: [ ]
diff --git a/docs/changelog/110016.yaml b/docs/changelog/110016.yaml
new file mode 100644
index 0000000000000..28ad55aa796c8
--- /dev/null
+++ b/docs/changelog/110016.yaml
@@ -0,0 +1,5 @@
+pr: 110016
+summary: Opt in keyword field into fallback synthetic source if needed
+area: Mapping
+type: enhancement
+issues: []
diff --git a/docs/changelog/110059.yaml b/docs/changelog/110059.yaml
new file mode 100644
index 0000000000000..ba160c091cdc2
--- /dev/null
+++ b/docs/changelog/110059.yaml
@@ -0,0 +1,32 @@
+pr: 110059
+summary: Adds new `bit` `element_type` for `dense_vectors`
+area: Vector Search
+type: feature
+issues: []
+highlight:
+ title: Adds new `bit` `element_type` for `dense_vectors`
+ body: |-
+ This adds `bit` vector support by adding `element_type: bit` for
+ vectors. This new element type works for indexed and non-indexed
+ vectors. Additionally, it works with `hnsw` and `flat` index types. No
+ quantization based codec works with this element type, this is
+ consistent with `byte` vectors.
+
+ `bit` vectors accept up to `32768` dimensions in size and expect vectors
+ that are being indexed to be encoded either as a hexidecimal string or a
+ `byte[]` array where each element of the `byte` array represents `8`
+ bits of the vector.
+
+ `bit` vectors support script usage and regular query usage. When
+ indexed, all comparisons done are `xor` and `popcount` summations (aka,
+ hamming distance), and the scores are transformed and normalized given
+ the vector dimensions.
+
+ For scripts, `l1norm` is the same as `hamming` distance and `l2norm` is
+ `sqrt(l1norm)`. `dotProduct` and `cosineSimilarity` are not supported.
+
+ Note, the dimensions expected by this element_type are always to be
+ divisible by `8`, and the `byte[]` vectors provided for index must be
+ have size `dim/8` size, where each byte element represents `8` bits of
+ the vectors.
+ notable: true
diff --git a/docs/changelog/110066.yaml b/docs/changelog/110066.yaml
new file mode 100644
index 0000000000000..920c6304b63ae
--- /dev/null
+++ b/docs/changelog/110066.yaml
@@ -0,0 +1,6 @@
+pr: 110066
+summary: Support flattened fields and multi-fields as dimensions in downsampling
+area: Downsampling
+type: bug
+issues:
+ - 99297
diff --git a/docs/changelog/110102.yaml b/docs/changelog/110102.yaml
new file mode 100644
index 0000000000000..d1b9b53e2dfc5
--- /dev/null
+++ b/docs/changelog/110102.yaml
@@ -0,0 +1,6 @@
+pr: 110102
+summary: Optimize ST_DISTANCE filtering with Lucene circle intersection query
+area: ES|QL
+type: enhancement
+issues:
+ - 109972
diff --git a/docs/changelog/110103.yaml b/docs/changelog/110103.yaml
new file mode 100644
index 0000000000000..9f613ec2b446e
--- /dev/null
+++ b/docs/changelog/110103.yaml
@@ -0,0 +1,5 @@
+pr: 110103
+summary: Fix automatic tracking of collapse with `docvalue_fields`
+area: Search
+type: bug
+issues: []
diff --git a/docs/changelog/110112.yaml b/docs/changelog/110112.yaml
new file mode 100644
index 0000000000000..eca5fd9af15ce
--- /dev/null
+++ b/docs/changelog/110112.yaml
@@ -0,0 +1,5 @@
+pr: 110112
+summary: Increase response size limit for batched requests
+area: Machine Learning
+type: bug
+issues: []
diff --git a/docs/changelog/110146.yaml b/docs/changelog/110146.yaml
new file mode 100644
index 0000000000000..61ba35cec319b
--- /dev/null
+++ b/docs/changelog/110146.yaml
@@ -0,0 +1,5 @@
+pr: 110146
+summary: Fix trailing slash in `ml.get_categories` specification
+area: Machine Learning
+type: bug
+issues: []
diff --git a/docs/changelog/110160.yaml b/docs/changelog/110160.yaml
new file mode 100644
index 0000000000000..0c38c23c69067
--- /dev/null
+++ b/docs/changelog/110160.yaml
@@ -0,0 +1,5 @@
+pr: 110160
+summary: Opt in number fields into fallback synthetic source when doc values a…
+area: Mapping
+type: enhancement
+issues: []
diff --git a/docs/changelog/110176.yaml b/docs/changelog/110176.yaml
new file mode 100644
index 0000000000000..ae1d7d10d6dc4
--- /dev/null
+++ b/docs/changelog/110176.yaml
@@ -0,0 +1,5 @@
+pr: 110176
+summary: Fix trailing slash in two rollup specifications
+area: Rollup
+type: bug
+issues: []
diff --git a/docs/changelog/110177.yaml b/docs/changelog/110177.yaml
new file mode 100644
index 0000000000000..0ac5328d88df4
--- /dev/null
+++ b/docs/changelog/110177.yaml
@@ -0,0 +1,5 @@
+pr: 110177
+summary: Fix trailing slash in `security.put_privileges` specification
+area: Authorization
+type: bug
+issues: []
diff --git a/docs/changelog/110186.yaml b/docs/changelog/110186.yaml
new file mode 100644
index 0000000000000..23eaab118e2ab
--- /dev/null
+++ b/docs/changelog/110186.yaml
@@ -0,0 +1,6 @@
+pr: 110186
+summary: Don't sample calls to `ReduceContext#consumeBucketsAndMaybeBreak` ins `InternalDateHistogram`
+ and `InternalHistogram` during reduction
+area: Aggregations
+type: bug
+issues: []
diff --git a/docs/reference/connector/apis/set-connector-sync-job-stats-api.asciidoc b/docs/reference/connector/apis/set-connector-sync-job-stats-api.asciidoc
index 4dd9cc6e67ab2..1427269d22b86 100644
--- a/docs/reference/connector/apis/set-connector-sync-job-stats-api.asciidoc
+++ b/docs/reference/connector/apis/set-connector-sync-job-stats-api.asciidoc
@@ -53,6 +53,9 @@ This API is mainly used by the connector service for updating sync job informati
`last_seen`::
(Optional, instant) The timestamp to set the connector sync job's `last_seen` property.
+`metadata`::
+(Optional, object) The connector-specific metadata.
+
[[set-connector-sync-job-stats-api-response-codes]]
==== {api-response-codes-title}
diff --git a/docs/reference/data-streams/downsampling.asciidoc b/docs/reference/data-streams/downsampling.asciidoc
index b005e83e8c95d..0b08b0972f9a1 100644
--- a/docs/reference/data-streams/downsampling.asciidoc
+++ b/docs/reference/data-streams/downsampling.asciidoc
@@ -18,9 +18,9 @@ Metrics solutions collect large amounts of time series data that grow over time.
As that data ages, it becomes less relevant to the current state of the system.
The downsampling process rolls up documents within a fixed time interval into a
single summary document. Each summary document includes statistical
-representations of the original data: the `min`, `max`, `sum`, `value_count`,
-and `average` for each metric. Data stream <> are stored unchanged.
+representations of the original data: the `min`, `max`, `sum` and `value_count`
+for each metric. Data stream <>
+are stored unchanged.
Downsampling, in effect, lets you to trade data resolution and precision for
storage size. You can include it in an <>.
-[[dimension-limits]]
-.Dimension limits
-****
-In a TSDS, {es} uses dimensions to
-generate the document `_id` and <> values. The resulting `_id` is
-always a short encoded hash. To prevent the `_tsid` value from being overly
-large, {es} limits the number of dimensions for an index using the
-<>
-index setting. While you can increase this limit, the resulting document `_tsid`
-value can't exceed 32KB. Additionally the field name of a dimension cannot be
-longer than 512 bytes and the each dimension value can't exceed 1kb.
-****
-
[discrete]
[[time-series-metric]]
==== Metrics
@@ -290,11 +277,6 @@ created the initial backing index has:
Only data that falls inside that range can be indexed.
-In our <>,
-`index.look_ahead_time` is set to three hours, so only documents with a
-`@timestamp` value that is within three hours previous or subsequent to the
-present time are accepted for indexing.
-
You can use the <> to check the
accepted time range for writing to any TSDS.
diff --git a/docs/reference/mapping/types/dense-vector.asciidoc b/docs/reference/mapping/types/dense-vector.asciidoc
index 2f09e743faa7b..f2f0b3ae8bb23 100644
--- a/docs/reference/mapping/types/dense-vector.asciidoc
+++ b/docs/reference/mapping/types/dense-vector.asciidoc
@@ -183,11 +183,23 @@ The following mapping parameters are accepted:
`element_type`::
(Optional, string)
The data type used to encode vectors. The supported data types are
-`float` (default) and `byte`. `float` indexes a 4-byte floating-point
-value per dimension. `byte` indexes a 1-byte integer value per dimension.
-Using `byte` can result in a substantially smaller index size with the
-trade off of lower precision. Vectors using `byte` require dimensions with
-integer values between -128 to 127, inclusive for both indexing and searching.
+`float` (default), `byte`, and bit.
+
+.Valid values for `element_type`
+[%collapsible%open]
+====
+`float`:::
+indexes a 4-byte floating-point
+value per dimension. This is the default value.
+
+`byte`:::
+indexes a 1-byte integer value per dimension.
+
+`bit`:::
+indexes a single bit per dimension. Useful for very high-dimensional vectors or models that specifically support bit vectors.
+NOTE: when using `bit`, the number of dimensions must be a multiple of 8 and must represent the number of bits.
+
+====
`dims`::
(Optional, integer)
@@ -205,7 +217,11 @@ API>>. Defaults to `true`.
The vector similarity metric to use in kNN search. Documents are ranked by
their vector field's similarity to the query vector. The `_score` of each
document will be derived from the similarity, in a way that ensures scores are
-positive and that a larger score corresponds to a higher ranking. Defaults to `cosine`.
+positive and that a larger score corresponds to a higher ranking.
+Defaults to `l2_norm` when `element_type: bit` otherwise defaults to `cosine`.
+
+NOTE: `bit` vectors only support `l2_norm` as their similarity metric.
+
+
^*^ This parameter can only be specified when `index` is `true`.
+
@@ -217,6 +233,9 @@ Computes similarity based on the L^2^ distance (also known as Euclidean
distance) between the vectors. The document `_score` is computed as
`1 / (1 + l2_norm(query, vector)^2)`.
+For `bit` vectors, instead of using `l2_norm`, the `hamming` distance between the vectors is used. The `_score`
+transformation is `(numBits - hamming(a, b)) / numBits`
+
`dot_product`:::
Computes the dot product of two unit vectors. This option provides an optimized way
to perform cosine similarity. The constraints and computed score are defined
@@ -320,3 +339,112 @@ any issues, but features in technical preview are not subject to the support SLA
of official GA features.
`dense_vector` fields support <> .
+
+[[dense-vector-index-bit]]
+==== Indexing & Searching bit vectors
+
+When using `element_type: bit`, this will treat all vectors as bit vectors. Bit vectors utilize only a single
+bit per dimension and are internally encoded as bytes. This can be useful for very high-dimensional vectors or models.
+
+When using `bit`, the number of dimensions must be a multiple of 8 and must represent the number of bits. Additionally,
+with `bit` vectors, the typical vector similarity values are effectively all scored the same, e.g. with `hamming` distance.
+
+Let's compare two `byte[]` arrays, each representing 40 individual bits.
+
+`[-127, 0, 1, 42, 127]` in bits `1000000100000000000000010010101001111111`
+`[127, -127, 0, 1, 42]` in bits `0111111110000001000000000000000100101010`
+
+When comparing these two bit, vectors, we first take the {wikipedia}/Hamming_distance[`hamming` distance].
+
+`xor` result:
+```
+1000000100000000000000010010101001111111
+^
+0111111110000001000000000000000100101010
+=
+1111111010000001000000010010101101010101
+```
+
+Then, we gather the count of `1` bits in the `xor` result: `18`. To scale for scoring, we subtract from the total number
+of bits and divide by the total number of bits: `(40 - 18) / 40 = 0.55`. This would be the `_score` betwee these two
+vectors.
+
+Here is an example of indexing and searching bit vectors:
+
+[source,console]
+--------------------------------------------------
+PUT my-bit-vectors
+{
+ "mappings": {
+ "properties": {
+ "my_vector": {
+ "type": "dense_vector",
+ "dims": 40, <1>
+ "element_type": "bit"
+ }
+ }
+ }
+}
+--------------------------------------------------
+<1> The number of dimensions that represents the number of bits
+
+[source,console]
+--------------------------------------------------
+POST /my-bit-vectors/_bulk?refresh
+{"index": {"_id" : "1"}}
+{"my_vector": [127, -127, 0, 1, 42]} <1>
+{"index": {"_id" : "2"}}
+{"my_vector": "8100012a7f"} <2>
+--------------------------------------------------
+// TEST[continued]
+<1> 5 bytes representing the 40 bit dimensioned vector
+<2> A hexidecimal string representing the 40 bit dimensioned vector
+
+Then, when searching, you can use the `knn` query to search for similar bit vectors:
+
+[source,console]
+--------------------------------------------------
+POST /my-bit-vectors/_search?filter_path=hits.hits
+{
+ "query": {
+ "knn": {
+ "query_vector": [127, -127, 0, 1, 42],
+ "field": "my_vector"
+ }
+ }
+}
+--------------------------------------------------
+// TEST[continued]
+
+[source,console-result]
+----
+{
+ "hits": {
+ "hits": [
+ {
+ "_index": "my-bit-vectors",
+ "_id": "1",
+ "_score": 1.0,
+ "_source": {
+ "my_vector": [
+ 127,
+ -127,
+ 0,
+ 1,
+ 42
+ ]
+ }
+ },
+ {
+ "_index": "my-bit-vectors",
+ "_id": "2",
+ "_score": 0.55,
+ "_source": {
+ "my_vector": "8100012a7f"
+ }
+ }
+ ]
+ }
+}
+----
+
diff --git a/docs/reference/query-rules/apis/delete-query-rule.asciidoc b/docs/reference/query-rules/apis/delete-query-rule.asciidoc
new file mode 100644
index 0000000000000..01b73033aa361
--- /dev/null
+++ b/docs/reference/query-rules/apis/delete-query-rule.asciidoc
@@ -0,0 +1,74 @@
+[role="xpack"]
+[[delete-query-rule]]
+=== Delete query rule
+
+++++
+Delete query rule
+++++
+
+Removes an individual query rule within an existing query ruleset.
+This is a destructive action that is only recoverable by re-adding the same rule via the <> API.
+
+[[delete-query-rule-request]]
+==== {api-request-title}
+
+`DELETE _query_rules//_rule/`
+
+[[delete-query-rule-prereq]]
+==== {api-prereq-title}
+
+Requires the `manage_search_query_rules` privilege.
+
+[[delete-query_rule-path-params]]
+==== {api-path-parms-title}
+
+``::
+(Required, string)
+
+``::
+(Required, string)
+
+[[delete-query-rule-response-codes]]
+==== {api-response-codes-title}
+
+`400`::
+Missing `ruleset_id`, `rule_id`, or both.
+
+`404` (Missing resources)::
+No query ruleset matching `ruleset_id` could be found, or else no rule matching `rule_id` was found in that ruleset.
+
+[[delete-query-rule-example]]
+==== {api-examples-title}
+
+The following example deletes the query rule with ID `my-rule1` from the query ruleset named `my-ruleset`:
+
+////
+[source,console]
+----
+PUT _query_rules/my-ruleset
+{
+ "rules": [
+ {
+ "rule_id": "my-rule1",
+ "type": "pinned",
+ "criteria": [
+ {
+ "type": "exact",
+ "metadata": "query_string",
+ "values": [ "marvel" ]
+ }
+ ],
+ "actions": {
+ "ids": ["id1"]
+ }
+ }
+ ]
+}
+----
+// TESTSETUP
+////
+
+[source,console]
+----
+DELETE _query_rules/my-ruleset/_rule/my-rule1
+----
diff --git a/docs/reference/query-rules/apis/get-query-rule.asciidoc b/docs/reference/query-rules/apis/get-query-rule.asciidoc
new file mode 100644
index 0000000000000..56713965d7bdc
--- /dev/null
+++ b/docs/reference/query-rules/apis/get-query-rule.asciidoc
@@ -0,0 +1,130 @@
+[role="xpack"]
+[[get-query-rule]]
+=== Get query rule
+
+++++
+Get query rule
+++++
+
+Retrieves information about an individual query rule within a query ruleset.
+
+[[get-query-rule-request]]
+==== {api-request-title}
+
+`GET _query_rules//_rule/`
+
+[[get-query-rule-prereq]]
+==== {api-prereq-title}
+
+Requires the `manage_search_query_rules` privilege.
+
+[[get-query-rule-path-params]]
+==== {api-path-parms-title}
+
+``::
+(Required, string)
+
+``::
+(Required, string)
+
+[[get-query-rule-response-codes]]
+==== {api-response-codes-title}
+
+`400`::
+Missing `ruleset_id` or `rule_id`, or both.
+
+`404` (Missing resources)::
+Either no query ruleset matching `ruleset_id` could be found, or no rule matching `rule_id` could be found within that ruleset.
+
+[[get-query-rule-example]]
+==== {api-examples-title}
+
+The following example gets the query rule with ID `my-rule1` from the ruleset named `my-ruleset`:
+
+////
+
+[source,console]
+--------------------------------------------------
+PUT _query_rules/my-ruleset
+{
+ "rules": [
+ {
+ "rule_id": "my-rule1",
+ "type": "pinned",
+ "criteria": [
+ {
+ "type": "contains",
+ "metadata": "query_string",
+ "values": [ "pugs", "puggles" ]
+ }
+ ],
+ "actions": {
+ "ids": [
+ "id1",
+ "id2"
+ ]
+ }
+ },
+ {
+ "rule_id": "my-rule2",
+ "type": "pinned",
+ "criteria": [
+ {
+ "type": "fuzzy",
+ "metadata": "query_string",
+ "values": [ "rescue dogs" ]
+ }
+ ],
+ "actions": {
+ "docs": [
+ {
+ "_index": "index1",
+ "_id": "id3"
+ },
+ {
+ "_index": "index2",
+ "_id": "id4"
+ }
+ ]
+ }
+ }
+ ]
+}
+--------------------------------------------------
+// TESTSETUP
+
+[source,console]
+--------------------------------------------------
+DELETE _query_rules/my-ruleset
+--------------------------------------------------
+// TEARDOWN
+
+////
+
+[source,console]
+----
+GET _query_rules/my-ruleset/_rule/my-rule1
+----
+
+A sample response:
+
+[source,console-result]
+----
+{
+ "rule_id": "my-rule1",
+ "type": "pinned",
+ "criteria": [
+ {
+ "type": "contains",
+ "metadata": "query_string",
+ "values": [ "pugs", "puggles" ]
+ }
+ ],
+ "actions": {
+ "ids": [
+ "id1",
+ "id2"
+ ]
+ }
+}
+----
diff --git a/docs/reference/query-rules/apis/index.asciidoc b/docs/reference/query-rules/apis/index.asciidoc
index e72d56d2f4834..f7303647f8515 100644
--- a/docs/reference/query-rules/apis/index.asciidoc
+++ b/docs/reference/query-rules/apis/index.asciidoc
@@ -1,6 +1,8 @@
[[query-rules-apis]]
== Query rules APIs
+preview::[]
+
++++
Query rules APIs
++++
@@ -20,8 +22,15 @@ Use the following APIs to manage query rulesets:
* <>
* <>
* <>
+* <>
+* <>
+* <>
include::put-query-ruleset.asciidoc[]
include::get-query-ruleset.asciidoc[]
include::list-query-rulesets.asciidoc[]
include::delete-query-ruleset.asciidoc[]
+include::put-query-rule.asciidoc[]
+include::get-query-rule.asciidoc[]
+include::delete-query-rule.asciidoc[]
+
diff --git a/docs/reference/query-rules/apis/put-query-rule.asciidoc b/docs/reference/query-rules/apis/put-query-rule.asciidoc
new file mode 100644
index 0000000000000..2b9a6ba892b84
--- /dev/null
+++ b/docs/reference/query-rules/apis/put-query-rule.asciidoc
@@ -0,0 +1,144 @@
+[role="xpack"]
+[[put-query-rule]]
+=== Create or update query rule
+
+++++
+Create or update query rule
+++++
+
+Creates or updates an individual query rule within a query ruleset.
+
+[[put-query-rule-request]]
+==== {api-request-title}
+
+`PUT _query_rules//_rule/`
+
+[[put-query-rule-prereqs]]
+==== {api-prereq-title}
+
+Requires the `manage_search_query_rules` privilege.
+
+[role="child_attributes"]
+[[put-query-rule-request-body]]
+(Required, object) Contains parameters for a query rule:
+
+==== {api-request-body-title}
+
+`type`::
+(Required, string) The type of rule.
+At this time only `pinned` query rule types are allowed.
+
+`criteria`::
+(Required, array of objects) The criteria that must be met for the rule to be applied.
+If multiple criteria are specified for a rule, all criteria must be met for the rule to be applied.
+
+Criteria must have the following information:
+
+- `type` (Required, string) The type of criteria.
+The following criteria types are supported:
++
+--
+- `exact`
+Only exact matches meet the criteria defined by the rule.
+Applicable for string or numerical values.
+- `fuzzy`
+Exact matches or matches within the allowed {wikipedia}/Levenshtein_distance[Levenshtein Edit Distance] meet the criteria defined by the rule.
+Only applicable for string values.
+- `prefix`
+Matches that start with this value meet the criteria defined by the rule.
+Only applicable for string values.
+- `suffix`
+Matches that end with this value meet the criteria defined by the rule.
+Only applicable for string values.
+- `contains`
+Matches that contain this value anywhere in the field meet the criteria defined by the rule.
+Only applicable for string values.
+- `lt`
+Matches with a value less than this value meet the criteria defined by the rule.
+Only applicable for numerical values.
+- `lte`
+Matches with a value less than or equal to this value meet the criteria defined by the rule.
+Only applicable for numerical values.
+- `gt`
+Matches with a value greater than this value meet the criteria defined by the rule.
+Only applicable for numerical values.
+- `gte`
+Matches with a value greater than or equal to this value meet the criteria defined by the rule.
+Only applicable for numerical values.
+- `always`
+Matches all queries, regardless of input.
+--
+- `metadata` (Optional, string) The metadata field to match against.
+This metadata will be used to match against `match_criteria` sent in the <>.
+Required for all criteria types except `global`.
+- `values` (Optional, array of strings) The values to match against the metadata field.
+Only one value must match for the criteria to be met.
+Required for all criteria types except `global`.
+
+`actions`::
+(Required, object) The actions to take when the rule is matched.
+The format of this action depends on the rule type.
+
+Actions depend on the rule type.
+For `pinned` rules, actions follow the format specified by the <>.
+The following actions are allowed:
+
+- `ids` (Optional, array of strings) The unique <> of the documents to pin.
+Only one of `ids` or `docs` may be specified, and at least one must be specified.
+- `docs` (Optional, array of objects) The documents to pin.
+Only one of `ids` or `docs` may be specified, and at least one must be specified.
+You can specify the following attributes for each document:
++
+--
+- `_index` (Required, string) The index of the document to pin.
+- `_id` (Required, string) The unique <>.
+--
+
+IMPORTANT: Due to limitations within <>, you can only pin documents using `ids` or `docs`, but cannot use both in single rule.
+It is advised to use one or the other in query rulesets, to avoid errors.
+Additionally, pinned queries have a maximum limit of 100 pinned hits.
+If multiple matching rules pin more than 100 documents, only the first 100 documents are pinned in the order they are specified in the ruleset.
+
+[[put-query-rule-example]]
+==== {api-examples-title}
+
+The following example creates a new query rule with the ID `my-rule1` in a query ruleset called `my-ruleset`.
+
+`my-rule1` will pin documents with IDs `id1` and `id2` when `user_query` contains `pugs` _or_ `puggles` **and** `user_country` exactly matches `us`.
+
+[source,console]
+----
+PUT _query_rules/my-ruleset/_rule/my-rule1
+{
+ "type": "pinned",
+ "criteria": [
+ {
+ "type": "contains",
+ "metadata": "user_query",
+ "values": [ "pugs", "puggles" ]
+ },
+ {
+ "type": "exact",
+ "metadata": "user_country",
+ "values": [ "us" ]
+ }
+ ],
+ "actions": {
+ "ids": [
+ "id1",
+ "id2"
+ ]
+ }
+}
+----
+// TESTSETUP
+
+//////////////////////////
+
+[source,console]
+--------------------------------------------------
+DELETE _query_rules/my-ruleset
+--------------------------------------------------
+// TEARDOWN
+
+//////////////////////////
diff --git a/docs/reference/search/search-your-data/cohere-es.asciidoc b/docs/reference/search/search-your-data/cohere-es.asciidoc
index f12f23ad2c5dc..3029cfd9f098c 100644
--- a/docs/reference/search/search-your-data/cohere-es.asciidoc
+++ b/docs/reference/search/search-your-data/cohere-es.asciidoc
@@ -25,14 +25,15 @@ set.
Refer to https://docs.cohere.com/docs/elasticsearch-and-cohere[Cohere's tutorial]
for an example using a different data set.
+You can also review the https://colab.research.google.com/github/elastic/elasticsearch-labs/blob/main/notebooks/integrations/cohere/cohere-elasticsearch.ipynb[Colab notebook version of this tutorial].
+
[discrete]
[[cohere-es-req]]
==== Requirements
-* A https://cohere.com/[Cohere account],
-* an https://www.elastic.co/guide/en/cloud/current/ec-getting-started.html[Elastic Cloud]
-account,
+* A paid https://cohere.com/[Cohere account] is required to use the {infer-cap} API with the Cohere service as the Cohere free trial API usage is limited,
+* an https://www.elastic.co/guide/en/cloud/current/ec-getting-started.html[Elastic Cloud] account,
* Python 3.7 or higher.
@@ -329,17 +330,12 @@ they were sent to the {infer} endpoint.
[[cohere-es-rag]]
==== Retrieval Augmented Generation (RAG) with Cohere and {es}
-RAG is a method for generating text using additional information fetched from an
-external data source. With the ranked results, you can build a RAG system on the
-top of what you previously created by using
-https://docs.cohere.com/docs/chat-api[Cohere's Chat API].
+https://docs.cohere.com/docs/retrieval-augmented-generation-rag[RAG] is a method for generating text using additional information fetched from an external data source.
+With the ranked results, you can build a RAG system on the top of what you previously created by using https://docs.cohere.com/docs/chat-api[Cohere's Chat API].
-Pass in the retrieved documents and the query to receive a grounded response
-using Cohere's newest generative model
-https://docs.cohere.com/docs/command-r-plus[Command R+].
+Pass in the retrieved documents and the query to receive a grounded response using Cohere's newest generative model https://docs.cohere.com/docs/command-r-plus[Command R+].
-Then pass in the query and the documents to the Chat API, and print out the
-response.
+Then pass in the query and the documents to the Chat API, and print out the response.
[source,py]
--------------------------------------------------
diff --git a/docs/reference/search/search-your-data/knn-search.asciidoc b/docs/reference/search/search-your-data/knn-search.asciidoc
index 0e61b44eda413..70cf9eec121d7 100644
--- a/docs/reference/search/search-your-data/knn-search.asciidoc
+++ b/docs/reference/search/search-your-data/knn-search.asciidoc
@@ -410,6 +410,24 @@ post-filtering approach, where the filter is applied **after** the approximate
kNN search completes. Post-filtering has the downside that it sometimes
returns fewer than k results, even when there are enough matching documents.
+[discrete]
+[[approximate-knn-search-and-filtering]]
+==== Approximate kNN search and filtering
+
+Unlike conventional query filtering, where more restrictive filters typically lead to faster queries,
+applying filters in an approximate kNN search with an HNSW index can decrease performance.
+This is because searching the HNSW graph requires additional exploration to obtain the `num_candidates`
+that meet the filter criteria.
+
+To avoid significant performance drawbacks, Lucene implements the following strategies per segment:
+
+* If the filtered document count is less than or equal to num_candidates, the search bypasses the HNSW graph and
+uses a brute force search on the filtered documents.
+
+* While exploring the HNSW graph, if the number of nodes explored exceeds the number of documents that satisfy the filter,
+the search will stop exploring the graph and switch to a brute force search over the filtered documents.
+
+
[discrete]
==== Combine approximate kNN with other features
diff --git a/docs/reference/search/search-your-data/search-application-api.asciidoc b/docs/reference/search/search-your-data/search-application-api.asciidoc
index 6312751d37bca..7c9308e78ebea 100644
--- a/docs/reference/search/search-your-data/search-application-api.asciidoc
+++ b/docs/reference/search/search-your-data/search-application-api.asciidoc
@@ -295,6 +295,12 @@ This may be helpful when experimenting with specific search queries that you wan
If your search application's name is `my_search_application`, your alias will be `my_search_application`.
You can search this using the <>.
+[discrete]
+[[search-application-cross-cluster-search]]
+===== Cross cluster search
+
+Search applications do not currently support {ccs} because it is not possible to add a remote cluster's index or index pattern to an index alias.
+
[NOTE]
====
You should use the Search Applications management APIs to update your application and _not_ directly use {es} APIs such as the alias API.
diff --git a/docs/reference/settings/inference-settings.asciidoc b/docs/reference/settings/inference-settings.asciidoc
index fa0905cf0ef73..3476058a17b21 100644
--- a/docs/reference/settings/inference-settings.asciidoc
+++ b/docs/reference/settings/inference-settings.asciidoc
@@ -34,7 +34,7 @@ message can be logged again. Defaults to one hour (`1h`).
`xpack.inference.http.max_response_size`::
(<>) Specifies the maximum size in bytes an HTTP response is allowed to have,
-defaults to `10mb`, the maximum configurable value is `50mb`.
+defaults to `50mb`, the maximum configurable value is `100mb`.
`xpack.inference.http.max_total_connections`::
(<>) Specifies the maximum number of connections the internal connection pool can
diff --git a/docs/reference/vectors/vector-functions.asciidoc b/docs/reference/vectors/vector-functions.asciidoc
index 4e627ef18ec6c..2a80290cf9d3b 100644
--- a/docs/reference/vectors/vector-functions.asciidoc
+++ b/docs/reference/vectors/vector-functions.asciidoc
@@ -1,4 +1,3 @@
-[role="xpack"]
[[vector-functions]]
===== Functions for vector fields
@@ -17,6 +16,8 @@ This is the list of available vector functions and vector access methods:
6. <].vectorValue`>> – returns a vector's value as an array of floats
7. <].magnitude`>> – returns a vector's magnitude
+NOTE: The `cosineSimilarity` and `dotProduct` functions are not supported for `bit` vectors.
+
NOTE: The recommended way to access dense vectors is through the
`cosineSimilarity`, `dotProduct`, `l1norm` or `l2norm` functions. Please note
however, that you should call these functions only once per script. For example,
@@ -193,7 +194,7 @@ we added `1` in the denominator.
====== Hamming distance
The `hamming` function calculates {wikipedia}/Hamming_distance[Hamming distance] between a given query vector and
-document vectors. It is only available for byte vectors.
+document vectors. It is only available for byte and bit vectors.
[source,console]
--------------------------------------------------
@@ -278,10 +279,14 @@ You can access vector values directly through the following functions:
- `doc[].vectorValue` – returns a vector's value as an array of floats
+NOTE: For `bit` vectors, it does return a `float[]`, where each element represents 8 bits.
+
- `doc[].magnitude` – returns a vector's magnitude as a float
(for vectors created prior to version 7.5 the magnitude is not stored.
So this function calculates it anew every time it is called).
+NOTE: For `bit` vectors, this is just the square root of the sum of `1` bits.
+
For example, the script below implements a cosine similarity using these
two functions:
@@ -319,3 +324,14 @@ GET my-index-000001/_search
}
}
--------------------------------------------------
+[[vector-functions-bit-vectors]]
+====== Bit vectors and vector functions
+
+When using `bit` vectors, not all the vector functions are available. The supported functions are:
+
+* <> – calculates Hamming distance, the sum of the bitwise XOR of the two vectors
+* <> – calculates L^1^ distance, this is simply the `hamming` distance
+* <> - calculates L^2^ distance, this is the square root of the `hamming` distance
+
+Currently, the `cosineSimilarity` and `dotProduct` functions are not supported for `bit` vectors.
+
diff --git a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/adjacency/AdjacencyMatrixAggregator.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/adjacency/AdjacencyMatrixAggregator.java
index 07a363ed727c7..dfe0a0642ccc3 100644
--- a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/adjacency/AdjacencyMatrixAggregator.java
+++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/adjacency/AdjacencyMatrixAggregator.java
@@ -239,8 +239,7 @@ public InternalAggregation[] buildAggregations(long[] owningBucketOrds) throws I
@Override
public InternalAggregation buildEmptyAggregation() {
- List buckets = new ArrayList<>(0);
- return new InternalAdjacencyMatrix(name, buckets, metadata());
+ return new InternalAdjacencyMatrix(name, List.of(), metadata());
}
final long bucketOrd(long owningBucketOrdinal, int filterOrd) {
diff --git a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregator.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregator.java
index 53142f6cdf601..f238419687cfc 100644
--- a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregator.java
+++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/timeseries/TimeSeriesAggregator.java
@@ -105,7 +105,7 @@ public InternalAggregation[] buildAggregations(long[] owningBucketOrds) throws I
@Override
public InternalAggregation buildEmptyAggregation() {
- return new InternalTimeSeries(name, new ArrayList<>(), false, metadata());
+ return new InternalTimeSeries(name, List.of(), false, metadata());
}
@Override
diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/200_rollover_failure_store.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/200_rollover_failure_store.yml
index dcbb0d2e465db..0742435f045fb 100644
--- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/200_rollover_failure_store.yml
+++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/200_rollover_failure_store.yml
@@ -416,7 +416,7 @@ teardown:
"Rolling over a failure store on a data stream without the failure store enabled should work":
- do:
allowed_warnings:
- - "index template [my-other-template] has index patterns [data-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-template] will take precedence during new index creation"
+ - "index template [my-other-template] has index patterns [other-data-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-other-template] will take precedence during new index creation"
indices.put_index_template:
name: my-other-template
body:
diff --git a/modules/kibana/src/internalClusterTest/java/org/elasticsearch/kibana/KibanaThreadPoolIT.java b/modules/kibana/src/internalClusterTest/java/org/elasticsearch/kibana/KibanaThreadPoolIT.java
index 48e2b14e63fc7..b4cb4404525f4 100644
--- a/modules/kibana/src/internalClusterTest/java/org/elasticsearch/kibana/KibanaThreadPoolIT.java
+++ b/modules/kibana/src/internalClusterTest/java/org/elasticsearch/kibana/KibanaThreadPoolIT.java
@@ -11,8 +11,6 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.bulk.BulkResponse;
-import org.elasticsearch.action.search.SearchPhaseExecutionException;
-import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.common.settings.Settings;
@@ -37,7 +35,6 @@
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
-import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.startsWith;
/**
@@ -108,7 +105,6 @@ public void testKibanaThreadPoolByPassesBlockedThreadPools() throws Exception {
});
}
- @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/107625")
public void testBlockedThreadPoolsRejectUserRequests() throws Exception {
assertAcked(client().admin().indices().prepareCreate(USER_INDEX));
@@ -126,15 +122,16 @@ private void assertThreadPoolsBlocked() {
assertThat(e1.getMessage(), startsWith("rejected execution of TimedRunnable"));
var e2 = expectThrows(EsRejectedExecutionException.class, () -> client().prepareGet(USER_INDEX, "id").get());
assertThat(e2.getMessage(), startsWith("rejected execution of ActionRunnable"));
- var e3 = expectThrows(
- SearchPhaseExecutionException.class,
- () -> client().prepareSearch(USER_INDEX)
- .setQuery(QueryBuilders.matchAllQuery())
- // Request times out if max concurrent shard requests is set to 1
- .setMaxConcurrentShardRequests(usually() ? SearchRequest.DEFAULT_MAX_CONCURRENT_SHARD_REQUESTS : randomIntBetween(2, 10))
- .get()
- );
- assertThat(e3.getMessage(), containsString("all shards failed"));
+ // intentionally commented out this test until https://github.com/elastic/elasticsearch/issues/97916 is fixed
+ // var e3 = expectThrows(
+ // SearchPhaseExecutionException.class,
+ // () -> client().prepareSearch(USER_INDEX)
+ // .setQuery(QueryBuilders.matchAllQuery())
+ // // Request times out if max concurrent shard requests is set to 1
+ // .setMaxConcurrentShardRequests(usually() ? SearchRequest.DEFAULT_MAX_CONCURRENT_SHARD_REQUESTS : randomIntBetween(2, 10))
+ // .get()
+ // );
+ // assertThat(e3.getMessage(), containsString("all shards failed"));
}
protected void runWithBlockedThreadPools(Runnable runnable) throws Exception {
diff --git a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/140_dense_vector_basic.yml b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/140_dense_vector_basic.yml
index e49dc20e73406..25088f51e2b59 100644
--- a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/140_dense_vector_basic.yml
+++ b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/140_dense_vector_basic.yml
@@ -229,6 +229,7 @@ setup:
Content-Type: application/json
catch: bad_request
search:
+ allow_partial_search_results: false
body:
query:
script_score:
@@ -243,6 +244,7 @@ setup:
Content-Type: application/json
catch: bad_request
search:
+ allow_partial_search_results: false
body:
query:
script_score:
diff --git a/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/146_dense_vector_bit_basic.yml b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/146_dense_vector_bit_basic.yml
new file mode 100644
index 0000000000000..3eb686bda2174
--- /dev/null
+++ b/modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/146_dense_vector_bit_basic.yml
@@ -0,0 +1,392 @@
+setup:
+ - requires:
+ cluster_features: ["mapper.vectors.bit_vectors"]
+ reason: "support for bit vectors added in 8.15"
+ test_runner_features: headers
+
+ - do:
+ indices.create:
+ index: test-index
+ body:
+ mappings:
+ properties:
+ vector:
+ type: dense_vector
+ index: false
+ element_type: bit
+ dims: 40
+ indexed_vector:
+ type: dense_vector
+ element_type: bit
+ dims: 40
+ index: true
+ similarity: l2_norm
+
+ - do:
+ index:
+ index: test-index
+ id: "1"
+ body:
+ vector: [8, 5, -15, 1, -7]
+ indexed_vector: [8, 5, -15, 1, -7]
+
+ - do:
+ index:
+ index: test-index
+ id: "2"
+ body:
+ vector: [-1, 115, -3, 4, -128]
+ indexed_vector: [-1, 115, -3, 4, -128]
+
+ - do:
+ index:
+ index: test-index
+ id: "3"
+ body:
+ vector: [2, 18, -5, 0, -124]
+ indexed_vector: [2, 18, -5, 0, -124]
+
+ - do:
+ indices.refresh: {}
+
+---
+"Test vector magnitude equality":
+ - skip:
+ features: close_to
+
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "doc['vector'].magnitude"
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - close_to: {hits.hits.0._score: {value: 4.690416, error: 0.01}}
+
+ - match: {hits.hits.1._id: "1"}
+ - close_to: {hits.hits.1._score: {value: 3.8729835, error: 0.01}}
+
+ - match: {hits.hits.2._id: "3"}
+ - close_to: {hits.hits.2._score: {value: 3.4641016, error: 0.01}}
+
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "doc['indexed_vector'].magnitude"
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - close_to: {hits.hits.0._score: {value: 4.690416, error: 0.01}}
+
+ - match: {hits.hits.1._id: "1"}
+ - close_to: {hits.hits.1._score: {value: 3.8729835, error: 0.01}}
+
+ - match: {hits.hits.2._id: "3"}
+ - close_to: {hits.hits.2._score: {value: 3.4641016, error: 0.01}}
+
+---
+"Dot Product is not supported":
+ - do:
+ catch: bad_request
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "dotProduct(params.query_vector, 'vector')"
+ params:
+ query_vector: [0, 111, -13, 14, -124]
+ - do:
+ catch: bad_request
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "dotProduct(params.query_vector, 'vector')"
+ params:
+ query_vector: "006ff30e84"
+
+---
+"Cosine Similarity is not supported":
+ - do:
+ catch: bad_request
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "cosineSimilarity(params.query_vector, 'vector')"
+ params:
+ query_vector: [0, 111, -13, 14, -124]
+ - do:
+ catch: bad_request
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "cosineSimilarity(params.query_vector, 'vector')"
+ params:
+ query_vector: "006ff30e84"
+
+ - do:
+ catch: bad_request
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "cosineSimilarity(params.query_vector, 'indexed_vector')"
+ params:
+ query_vector: [0, 111, -13, 14, -124]
+---
+"L1 norm":
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "l1norm(params.query_vector, 'vector')"
+ params:
+ query_vector: [0, 111, -13, 14, -124]
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0._score: 17.0}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1._score: 16.0}
+
+ - match: {hits.hits.2._id: "3"}
+ - match: {hits.hits.2._score: 11.0}
+
+---
+"L1 norm hexidecimal":
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "l1norm(params.query_vector, 'vector')"
+ params:
+ query_vector: "006ff30e84"
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0._score: 17.0}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1._score: 16.0}
+
+ - match: {hits.hits.2._id: "3"}
+ - match: {hits.hits.2._score: 11.0}
+---
+"L2 norm":
+ - requires:
+ test_runner_features: close_to
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "l2norm(params.query_vector, 'vector')"
+ params:
+ query_vector: [0, 111, -13, 14, -124]
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - close_to: {hits.hits.0._score: {value: 4.123, error: 0.001}}
+
+ - match: {hits.hits.1._id: "1"}
+ - close_to: {hits.hits.1._score: {value: 4, error: 0.001}}
+
+ - match: {hits.hits.2._id: "3"}
+ - close_to: {hits.hits.2._score: {value: 3.316, error: 0.001}}
+---
+"L2 norm hexidecimal":
+ - requires:
+ test_runner_features: close_to
+
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "l2norm(params.query_vector, 'vector')"
+ params:
+ query_vector: "006ff30e84"
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - close_to: {hits.hits.0._score: {value: 4.123, error: 0.001}}
+
+ - match: {hits.hits.1._id: "1"}
+ - close_to: {hits.hits.1._score: {value: 4, error: 0.001}}
+
+ - match: {hits.hits.2._id: "3"}
+ - close_to: {hits.hits.2._score: {value: 3.316, error: 0.001}}
+---
+"Hamming distance":
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "hamming(params.query_vector, 'vector')"
+ params:
+ query_vector: [0, 111, -13, 14, -124]
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0._score: 17.0}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1._score: 16.0}
+
+ - match: {hits.hits.2._id: "3"}
+ - match: {hits.hits.2._score: 11.0}
+
+
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "hamming(params.query_vector, 'indexed_vector')"
+ params:
+ query_vector: [0, 111, -13, 14, -124]
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0._score: 17.0}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1._score: 16.0}
+
+ - match: {hits.hits.2._id: "3"}
+ - match: {hits.hits.2._score: 11.0}
+---
+"Hamming distance hexidecimal":
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "hamming(params.query_vector, 'vector')"
+ params:
+ query_vector: "006ff30e84"
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0._score: 17.0}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1._score: 16.0}
+
+ - match: {hits.hits.2._id: "3"}
+ - match: {hits.hits.2._score: 11.0}
+
+
+ - do:
+ headers:
+ Content-Type: application/json
+ search:
+ rest_total_hits_as_int: true
+ body:
+ query:
+ script_score:
+ query: {match_all: {} }
+ script:
+ source: "hamming(params.query_vector, 'indexed_vector')"
+ params:
+ query_vector: "006ff30e84"
+
+ - match: {hits.total: 3}
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0._score: 17.0}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1._score: 16.0}
+
+ - match: {hits.hits.2._id: "3"}
+ - match: {hits.hits.2._score: 11.0}
diff --git a/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4ChunkedContinuationsIT.java b/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4ChunkedContinuationsIT.java
index 5ad1152d65e85..d2f7f6ab61977 100644
--- a/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4ChunkedContinuationsIT.java
+++ b/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4ChunkedContinuationsIT.java
@@ -72,8 +72,10 @@
import org.elasticsearch.tasks.Task;
import org.elasticsearch.tasks.TaskCancelledException;
import org.elasticsearch.test.MockLog;
+import org.elasticsearch.test.ReachabilityChecker;
import org.elasticsearch.test.junit.annotations.TestLogging;
import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.LeakTracker;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.transport.netty4.Netty4Utils;
import org.elasticsearch.xcontent.ToXContentObject;
@@ -315,14 +317,20 @@ public void onFailure(Exception exception) {
private static Releasable withResourceTracker() {
assertNull(refs);
+ final ReachabilityChecker reachabilityChecker = new ReachabilityChecker();
final var latch = new CountDownLatch(1);
- refs = AbstractRefCounted.of(latch::countDown);
+ refs = LeakTracker.wrap(reachabilityChecker.register(AbstractRefCounted.of(latch::countDown)));
return () -> {
refs.decRef();
+ boolean success = false;
try {
safeAwait(latch);
+ success = true;
} finally {
refs = null;
+ if (success == false) {
+ reachabilityChecker.ensureUnreachable();
+ }
}
};
}
@@ -635,10 +643,10 @@ public void close() {
@Override
public void accept(RestChannel channel) {
- localRefs.mustIncRef();
client.execute(TYPE, new Request(), new RestActionListener<>(channel) {
@Override
protected void processResponse(Response response) {
+ localRefs.mustIncRef();
channel.sendResponse(RestResponse.chunked(RestStatus.OK, response.getResponseBodyPart(), () -> {
// cancellation notification only happens while processing a continuation, not while computing
// the next one; prompt cancellation requires use of something like RestCancellableNodeClient
diff --git a/muted-tests.yml b/muted-tests.yml
index 225bb7ac038eb..748f6f463f345 100644
--- a/muted-tests.yml
+++ b/muted-tests.yml
@@ -50,8 +50,6 @@ tests:
- class: "org.elasticsearch.xpack.rollup.job.RollupIndexerStateTests"
issue: "https://github.com/elastic/elasticsearch/issues/109627"
method: "testMultipleJobTriggering"
-- class: "org.elasticsearch.index.store.FsDirectoryFactoryTests"
- issue: "https://github.com/elastic/elasticsearch/issues/109681"
- class: "org.elasticsearch.xpack.test.rest.XPackRestIT"
issue: "https://github.com/elastic/elasticsearch/issues/109687"
method: "test {p0=sql/translate/Translate SQL}"
@@ -61,31 +59,29 @@ tests:
- class: org.elasticsearch.action.search.SearchProgressActionListenerIT
method: testSearchProgressWithHits
issue: https://github.com/elastic/elasticsearch/issues/109830
-- class: "org.elasticsearch.xpack.shutdown.NodeShutdownReadinessIT"
- issue: "https://github.com/elastic/elasticsearch/issues/109838"
- method: "testShutdownReadinessService"
- class: "org.elasticsearch.xpack.security.ScrollHelperIntegTests"
issue: "https://github.com/elastic/elasticsearch/issues/109905"
method: "testFetchAllEntities"
-- class: "org.elasticsearch.xpack.ml.integration.AutodetectMemoryLimitIT"
- issue: "https://github.com/elastic/elasticsearch/issues/109904"
- class: "org.elasticsearch.xpack.esql.action.AsyncEsqlQueryActionIT"
issue: "https://github.com/elastic/elasticsearch/issues/109944"
method: "testBasicAsyncExecution"
- class: "org.elasticsearch.xpack.security.authz.store.NativePrivilegeStoreCacheTests"
issue: "https://github.com/elastic/elasticsearch/issues/110015"
-- class: "org.elasticsearch.painless.LangPainlessClientYamlTestSuiteIT"
- issue: "https://github.com/elastic/elasticsearch/issues/110032"
- method: "test {yaml=painless/140_dense_vector_basic/Test hamming distance fails on float}"
- class: "org.elasticsearch.action.admin.indices.rollover.RolloverIT"
issue: "https://github.com/elastic/elasticsearch/issues/110034"
method: "testRolloverWithClosedWriteIndex"
-- class: org.elasticsearch.datastreams.DataStreamsClientYamlTestSuiteIT
- method: test {p0=data_stream/200_rollover_failure_store/Rolling over a failure store on a data stream without the failure store enabled should work}
- issue: https://github.com/elastic/elasticsearch/issues/110051
- class: org.elasticsearch.xpack.transform.transforms.TransformIndexerTests
method: testMaxPageSearchSizeIsResetToConfiguredValue
issue: https://github.com/elastic/elasticsearch/issues/109844
+- class: org.elasticsearch.index.store.FsDirectoryFactoryTests
+ method: testStoreDirectory
+ issue: https://github.com/elastic/elasticsearch/issues/110210
+- class: org.elasticsearch.index.store.FsDirectoryFactoryTests
+ method: testPreload
+ issue: https://github.com/elastic/elasticsearch/issues/110211
+- class: org.elasticsearch.synonyms.SynonymsManagementAPIServiceIT
+ method: testUpdateRuleWithMaxSynonyms
+ issue: https://github.com/elastic/elasticsearch/issues/110212
# Examples:
#
diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.get_categories.json b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.get_categories.json
index 6dfa2e64dd293..69f8dd74e3d55 100644
--- a/rest-api-spec/src/main/resources/rest-api-spec/api/ml.get_categories.json
+++ b/rest-api-spec/src/main/resources/rest-api-spec/api/ml.get_categories.json
@@ -30,7 +30,7 @@
}
},
{
- "path":"/_ml/anomaly_detectors/{job_id}/results/categories/",
+ "path":"/_ml/anomaly_detectors/{job_id}/results/categories",
"methods":[
"GET",
"POST"
diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/rollup.get_jobs.json b/rest-api-spec/src/main/resources/rest-api-spec/api/rollup.get_jobs.json
index 46ac1c4d304d1..e373c9f08bfd5 100644
--- a/rest-api-spec/src/main/resources/rest-api-spec/api/rollup.get_jobs.json
+++ b/rest-api-spec/src/main/resources/rest-api-spec/api/rollup.get_jobs.json
@@ -24,7 +24,7 @@
}
},
{
- "path":"/_rollup/job/",
+ "path":"/_rollup/job",
"methods":[
"GET"
]
diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/rollup.get_rollup_caps.json b/rest-api-spec/src/main/resources/rest-api-spec/api/rollup.get_rollup_caps.json
index 7dcc83ee0cd47..a72187f9ca926 100644
--- a/rest-api-spec/src/main/resources/rest-api-spec/api/rollup.get_rollup_caps.json
+++ b/rest-api-spec/src/main/resources/rest-api-spec/api/rollup.get_rollup_caps.json
@@ -24,7 +24,7 @@
}
},
{
- "path":"/_rollup/data/",
+ "path":"/_rollup/data",
"methods":[
"GET"
]
diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/security.put_privileges.json b/rest-api-spec/src/main/resources/rest-api-spec/api/security.put_privileges.json
index da63002b49485..8c920e10f285b 100644
--- a/rest-api-spec/src/main/resources/rest-api-spec/api/security.put_privileges.json
+++ b/rest-api-spec/src/main/resources/rest-api-spec/api/security.put_privileges.json
@@ -13,7 +13,7 @@
"url":{
"paths":[
{
- "path":"/_security/privilege/",
+ "path":"/_security/privilege",
"methods":[
"PUT",
"POST"
diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml
index 9fc82eb125def..dcd1f93e35da8 100644
--- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml
+++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.create/20_synthetic_source.yml
@@ -13,7 +13,7 @@ invalid:
mode: synthetic
properties:
kwd:
- type: keyword
+ type: boolean
doc_values: false
diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/101_knn_nested_search_bits.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/101_knn_nested_search_bits.yml
new file mode 100644
index 0000000000000..a3d920d903ae8
--- /dev/null
+++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/101_knn_nested_search_bits.yml
@@ -0,0 +1,301 @@
+setup:
+ - requires:
+ cluster_features: "mapper.vectors.bit_vectors"
+ test_runner_features: close_to
+ reason: 'bit vectors added in 8.15'
+ - do:
+ indices.create:
+ index: test
+ body:
+ settings:
+ index:
+ number_of_shards: 2
+ mappings:
+ properties:
+ name:
+ type: keyword
+ nested:
+ type: nested
+ properties:
+ paragraph_id:
+ type: keyword
+ vector:
+ type: dense_vector
+ dims: 40
+ index: true
+ element_type: bit
+ similarity: l2_norm
+
+ - do:
+ index:
+ index: test
+ id: "1"
+ body:
+ name: cow.jpg
+ nested:
+ - paragraph_id: 0
+ vector: [100, 20, -34, 15, -100]
+ - paragraph_id: 1
+ vector: [40, 30, -3, 1, -20]
+
+ - do:
+ index:
+ index: test
+ id: "2"
+ body:
+ name: moose.jpg
+ nested:
+ - paragraph_id: 0
+ vector: [-1, 100, -13, 14, -127]
+ - paragraph_id: 2
+ vector: [0, 100, 0, 15, -127]
+ - paragraph_id: 3
+ vector: [0, 1, 0, 2, -15]
+
+ - do:
+ index:
+ index: test
+ id: "3"
+ body:
+ name: rabbit.jpg
+ nested:
+ - paragraph_id: 0
+ vector: [1, 111, -13, 14, -1]
+
+ - do:
+ indices.refresh: {}
+
+---
+"nested kNN search only":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: nested.vector
+ query_vector: [-1, 90, -10, 14, -127]
+ k: 2
+ num_candidates: 3
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0.fields.name.0: "moose.jpg"}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1.fields.name.0: "cow.jpg"}
+
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: nested.vector
+ query_vector: [-1, 90, -10, 14, -127]
+ k: 2
+ num_candidates: 3
+ inner_hits: {size: 1, "fields": ["nested.paragraph_id"], _source: false}
+
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0.fields.name.0: "moose.jpg"}
+ - match: {hits.hits.0.inner_hits.nested.hits.hits.0.fields.nested.0.paragraph_id.0: "0"}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1.fields.name.0: "cow.jpg"}
+ - match: {hits.hits.1.inner_hits.nested.hits.hits.0.fields.nested.0.paragraph_id.0: "0"}
+
+---
+"nested kNN search filtered":
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: nested.vector
+ query_vector: [-1, 90, -10, 14, -127]
+ k: 2
+ num_candidates: 3
+ filter: {term: {name: "rabbit.jpg"}}
+
+ - match: {hits.total.value: 1}
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: nested.vector
+ query_vector: [-1, 90, -10, 14, -127]
+ k: 3
+ num_candidates: 3
+ filter: {term: {name: "rabbit.jpg"}}
+ inner_hits: {size: 1, fields: ["nested.paragraph_id"], _source: false}
+
+ - match: {hits.total.value: 1}
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+ - match: {hits.hits.0.inner_hits.nested.hits.hits.0.fields.nested.0.paragraph_id.0: "0"}
+---
+"nested kNN search inner_hits size > 1":
+ - do:
+ index:
+ index: test
+ id: "4"
+ body:
+ name: moose.jpg
+ nested:
+ - paragraph_id: 0
+ vector: [-1, 90, -10, 14, -127]
+ - paragraph_id: 2
+ vector: [ 0, 100.0, 0, 14, -127 ]
+ - paragraph_id: 3
+ vector: [ 0, 1.0, 0, 2, -15 ]
+
+ - do:
+ index:
+ index: test
+ id: "5"
+ body:
+ name: moose.jpg
+ nested:
+ - paragraph_id: 0
+ vector: [ -1, 100, -13, 14, -127 ]
+ - paragraph_id: 2
+ vector: [ 0, 100, 0, 15, -127 ]
+ - paragraph_id: 3
+ vector: [ 0, 1, 0, 2, -15 ]
+
+ - do:
+ index:
+ index: test
+ id: "6"
+ body:
+ name: moose.jpg
+ nested:
+ - paragraph_id: 0
+ vector: [ -1, 100, -13, 15, -127 ]
+ - paragraph_id: 2
+ vector: [ 0, 100, 0, 15, -127 ]
+ - paragraph_id: 3
+ vector: [ 0, 1, 0, 2, -15 ]
+ - do:
+ indices.refresh: { }
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: nested.vector
+ query_vector: [-1, 90, -10, 15, -127]
+ k: 3
+ num_candidates: 5
+ inner_hits: {size: 2, fields: ["nested.paragraph_id"], _source: false}
+
+ - match: {hits.total.value: 3}
+ - length: { hits.hits.0.inner_hits.nested.hits.hits: 2 }
+ - length: { hits.hits.1.inner_hits.nested.hits.hits: 2 }
+ - length: { hits.hits.2.inner_hits.nested.hits.hits: 2 }
+
+ - match: { hits.hits.0.fields.name.0: "moose.jpg" }
+ - match: { hits.hits.0.inner_hits.nested.hits.hits.0.fields.nested.0.paragraph_id.0: "0" }
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: nested.vector
+ query_vector: [-1, 90, -10, 15, -127]
+ k: 5
+ num_candidates: 5
+ inner_hits: {size: 2, fields: ["nested.paragraph_id"], _source: false}
+
+ - match: {hits.total.value: 5}
+ # All these initial matches are "moose.jpg", which has 3 nested vectors, but two are closest
+ - match: {hits.hits.0.fields.name.0: "moose.jpg"}
+ - length: { hits.hits.0.inner_hits.nested.hits.hits: 2 }
+ - match: { hits.hits.0.inner_hits.nested.hits.hits.0.fields.nested.0.paragraph_id.0: "0" }
+ - match: { hits.hits.0.inner_hits.nested.hits.hits.1.fields.nested.0.paragraph_id.0: "2" }
+ - match: {hits.hits.1.fields.name.0: "moose.jpg"}
+ - length: { hits.hits.1.inner_hits.nested.hits.hits: 2 }
+ - match: { hits.hits.1.inner_hits.nested.hits.hits.0.fields.nested.0.paragraph_id.0: "0" }
+ - match: { hits.hits.1.inner_hits.nested.hits.hits.1.fields.nested.0.paragraph_id.0: "2" }
+ - match: {hits.hits.2.fields.name.0: "moose.jpg"}
+ - length: { hits.hits.2.inner_hits.nested.hits.hits: 2 }
+ - match: { hits.hits.2.inner_hits.nested.hits.hits.0.fields.nested.0.paragraph_id.0: "0" }
+ - match: { hits.hits.2.inner_hits.nested.hits.hits.1.fields.nested.0.paragraph_id.0: "2" }
+ - match: {hits.hits.3.fields.name.0: "moose.jpg"}
+ - length: { hits.hits.3.inner_hits.nested.hits.hits: 2 }
+ - match: { hits.hits.3.inner_hits.nested.hits.hits.0.fields.nested.0.paragraph_id.0: "0" }
+ - match: { hits.hits.3.inner_hits.nested.hits.hits.1.fields.nested.0.paragraph_id.0: "2" }
+ # Rabbit only has one passage vector
+ - match: {hits.hits.4.fields.name.0: "cow.jpg"}
+ - length: { hits.hits.4.inner_hits.nested.hits.hits: 2 }
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: nested.vector
+ query_vector: [ -1, 90, -10, 15, -127 ]
+ k: 3
+ num_candidates: 3
+ filter: {term: {name: "cow.jpg"}}
+ inner_hits: {size: 3, fields: ["nested.paragraph_id"], _source: false}
+
+ - match: {hits.total.value: 1}
+ - match: { hits.hits.0._id: "1" }
+ - length: { hits.hits.0.inner_hits.nested.hits.hits: 2 }
+ - match: { hits.hits.0.inner_hits.nested.hits.hits.0.fields.nested.0.paragraph_id.0: "0" }
+ - match: { hits.hits.0.inner_hits.nested.hits.hits.1.fields.nested.0.paragraph_id.0: "1" }
+---
+"nested kNN search inner_hits & boosting":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: nested.vector
+ query_vector: [-1, 90, -10, 15, -127]
+ k: 3
+ num_candidates: 5
+ inner_hits: {size: 2, fields: ["nested.paragraph_id"], _source: false}
+
+ - close_to: { hits.hits.0._score: {value: 0.8, error: 0.00001} }
+ - close_to: { hits.hits.0.inner_hits.nested.hits.hits.0._score: {value: 0.8, error: 0.00001} }
+ - close_to: { hits.hits.1._score: {value: 0.625, error: 0.00001} }
+ - close_to: { hits.hits.1.inner_hits.nested.hits.hits.0._score: {value: 0.625, error: 0.00001} }
+ - close_to: { hits.hits.2._score: {value: 0.5, error: 0.00001} }
+ - close_to: { hits.hits.2.inner_hits.nested.hits.hits.0._score: {value: 0.5, error: 0.00001} }
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: nested.vector
+ query_vector: [-1, 90, -10, 15, -127]
+ k: 3
+ num_candidates: 5
+ boost: 2
+ inner_hits: {size: 2, fields: ["nested.paragraph_id"], _source: false}
+ - close_to: { hits.hits.0._score: {value: 1.6, error: 0.00001} }
+ - close_to: { hits.hits.0.inner_hits.nested.hits.hits.0._score: {value: 1.6, error: 0.00001} }
+ - close_to: { hits.hits.1._score: {value: 1.25, error: 0.00001} }
+ - close_to: { hits.hits.1.inner_hits.nested.hits.hits.0._score: {value: 1.25, error: 0.00001} }
+ - close_to: { hits.hits.2._score: {value: 1, error: 0.00001} }
+ - close_to: { hits.hits.2.inner_hits.nested.hits.hits.0._score: {value: 1.0, error: 0.00001} }
diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/170_knn_search_hex_encoded_byte_vectors.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/170_knn_search_hex_encoded_byte_vectors.yml
index 74fbe221c0fe7..f989e17e6ec30 100644
--- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/170_knn_search_hex_encoded_byte_vectors.yml
+++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/170_knn_search_hex_encoded_byte_vectors.yml
@@ -116,8 +116,9 @@ setup:
---
"Knn search with hex string for byte field - dimensions mismatch" :
# [64, 10, -30, 10] - is encoded as '400ae20a'
+ # the error message has been adjusted in later versions
- do:
- catch: /the query vector has a different dimension \[4\] than the index vectors \[3\]/
+ catch: /dimension|dimensions \[4\] than the document|index vectors \[3\]/
search:
index: knn_hex_vector_index
body:
diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/175_knn_query_hex_encoded_byte_vectors.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/175_knn_query_hex_encoded_byte_vectors.yml
index e01f3ec18b8c3..cd94275234661 100644
--- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/175_knn_query_hex_encoded_byte_vectors.yml
+++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/175_knn_query_hex_encoded_byte_vectors.yml
@@ -116,8 +116,9 @@ setup:
---
"Knn query with hex string for byte field - dimensions mismatch" :
# [64, 10, -30, 10] - is encoded as '400ae20a'
+ # the error message has been adjusted in later versions
- do:
- catch: /the query vector has a different dimension \[4\] than the index vectors \[3\]/
+ catch: /dimension|dimensions \[4\] than the document|index vectors \[3\]/
search:
index: knn_hex_vector_index
body:
diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_half_byte_quantized.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_half_byte_quantized.yml
index 24437e3db1379..cb5aae482507a 100644
--- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_half_byte_quantized.yml
+++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/41_knn_search_half_byte_quantized.yml
@@ -204,7 +204,7 @@ setup:
num_candidates: 3
k: 3
field: vector
- similarity: 10.3
+ similarity: 17
query_vector: [-0.5, 90.0, -10, 14.8]
- length: {hits.hits: 1}
@@ -222,7 +222,7 @@ setup:
num_candidates: 3
k: 3
field: vector
- similarity: 11
+ similarity: 17
query_vector: [-0.5, 90.0, -10, 14.8]
filter: {"term": {"name": "moose.jpg"}}
diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/45_knn_search_bit.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/45_knn_search_bit.yml
new file mode 100644
index 0000000000000..ed469ffd7ff16
--- /dev/null
+++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/45_knn_search_bit.yml
@@ -0,0 +1,356 @@
+setup:
+ - requires:
+ cluster_features: "mapper.vectors.bit_vectors"
+ reason: 'mapper.vectors.bit_vectors'
+
+ - do:
+ indices.create:
+ index: test
+ body:
+ mappings:
+ properties:
+ name:
+ type: keyword
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 40
+ index: true
+ similarity: l2_norm
+
+ - do:
+ index:
+ index: test
+ id: "1"
+ body:
+ name: cow.jpg
+ vector: [2, -1, 1, 4, -3]
+
+ - do:
+ index:
+ index: test
+ id: "2"
+ body:
+ name: moose.jpg
+ vector: [127.0, -128.0, 0.0, 1.0, -1.0]
+
+ - do:
+ index:
+ index: test
+ id: "3"
+ body:
+ name: rabbit.jpg
+ vector: [5, 4.0, 3, 2.0, 127]
+
+ - do:
+ indices.refresh: {}
+
+---
+"kNN search only":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [127, 127, -128, -128, 127]
+ k: 2
+ num_candidates: 3
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0.fields.name.0: "moose.jpg"}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1.fields.name.0: "cow.jpg"}
+
+---
+"kNN search plus query":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [127.0, -128.0, 0.0, 1.0, -1.0]
+ k: 2
+ num_candidates: 3
+ query:
+ term:
+ name: rabbit.jpg
+
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+
+ - match: {hits.hits.1._id: "2"}
+ - match: {hits.hits.1.fields.name.0: "moose.jpg"}
+
+---
+"kNN search with filter":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [5.0, 4, 3.0, 2, 127.0]
+ k: 2
+ num_candidates: 3
+ filter:
+ term:
+ name: "rabbit.jpg"
+
+ - match: {hits.total.value: 1}
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [2, -1, 1, 4, -3]
+ k: 2
+ num_candidates: 3
+ filter:
+ - term:
+ name: "rabbit.jpg"
+ - term:
+ _id: 2
+
+ - match: {hits.total.value: 0}
+
+---
+"Vector similarity search only":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ num_candidates: 3
+ k: 3
+ field: vector
+ similarity: 0.98
+ query_vector: [5, 4.0, 3, 2.0, 127]
+
+ - length: {hits.hits: 1}
+
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+---
+"Vector similarity with filter only":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ num_candidates: 3
+ k: 3
+ field: vector
+ similarity: 0.98
+ query_vector: [5, 4.0, 3, 2.0, 127]
+ filter: {"term": {"name": "rabbit.jpg"}}
+
+ - length: {hits.hits: 1}
+
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ num_candidates: 3
+ k: 3
+ field: vector
+ similarity: 0.98
+ query_vector: [5, 4.0, 3, 2.0, 127]
+ filter: {"term": {"name": "cow.jpg"}}
+
+ - length: {hits.hits: 0}
+---
+"dim mismatch":
+ - do:
+ catch: bad_request
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [1, 2, 3, 4, 5, 6]
+ k: 2
+ num_candidates: 3
+---
+"disallow quantized vector types":
+ - do:
+ catch: bad_request
+ indices.create:
+ index: test
+ body:
+ mappings:
+ properties:
+ name:
+ type: keyword
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 32
+ index: true
+ similarity: l2_norm
+ index_options:
+ type: int8_flat
+
+ - do:
+ catch: bad_request
+ indices.create:
+ index: test
+ body:
+ mappings:
+ properties:
+ name:
+ type: keyword
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 32
+ index: true
+ similarity: l2_norm
+ index_options:
+ type: int4_flat
+
+ - do:
+ catch: bad_request
+ indices.create:
+ index: test
+ body:
+ mappings:
+ properties:
+ name:
+ type: keyword
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 32
+ index: true
+ similarity: l2_norm
+ index_options:
+ type: int8_hnsw
+
+ - do:
+ catch: bad_request
+ indices.create:
+ index: test
+ body:
+ mappings:
+ properties:
+ name:
+ type: keyword
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 32
+ index: true
+ similarity: l2_norm
+ index_options:
+ type: int4_hnsw
+---
+"disallow vector index type change to quantized type":
+ - do:
+ catch: bad_request
+ indices.put_mapping:
+ index: test
+ body:
+ properties:
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 32
+ index: true
+ similarity: l2_norm
+ index_options:
+ type: int4_hnsw
+ - do:
+ catch: bad_request
+ indices.put_mapping:
+ index: test
+ body:
+ properties:
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 32
+ index: true
+ similarity: l2_norm
+ index_options:
+ type: int8_hnsw
+---
+"Defaults to l2_norm with bit vectors":
+ - do:
+ indices.create:
+ index: default_to_l2_norm_bit
+ body:
+ mappings:
+ properties:
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 40
+ index: true
+
+ - do:
+ indices.get_mapping:
+ index: default_to_l2_norm_bit
+
+ - match: { default_to_l2_norm_bit.mappings.properties.vector.similarity: l2_norm }
+
+---
+"Only allow l2_norm with bit vectors":
+ - do:
+ catch: bad_request
+ indices.create:
+ index: dot_product_fails_for_bits
+ body:
+ mappings:
+ properties:
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 40
+ index: true
+ similarity: dot_product
+
+ - do:
+ catch: bad_request
+ indices.create:
+ index: cosine_product_fails_for_bits
+ body:
+ mappings:
+ properties:
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 40
+ index: true
+ similarity: cosine
+
+ - do:
+ catch: bad_request
+ indices.create:
+ index: cosine_product_fails_for_bits
+ body:
+ mappings:
+ properties:
+ type: dense_vector
+ element_type: bit
+ dims: 40
+ index: true
+ similarity: max_inner_product
diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/45_knn_search_bit_flat.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/45_knn_search_bit_flat.yml
new file mode 100644
index 0000000000000..ec7bde4de8435
--- /dev/null
+++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/45_knn_search_bit_flat.yml
@@ -0,0 +1,223 @@
+setup:
+ - requires:
+ cluster_features: "mapper.vectors.bit_vectors"
+ reason: 'mapper.vectors.bit_vectors'
+
+ - do:
+ indices.create:
+ index: test
+ body:
+ mappings:
+ properties:
+ name:
+ type: keyword
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 40
+ index: true
+ similarity: l2_norm
+ index_options:
+ type: flat
+
+ - do:
+ index:
+ index: test
+ id: "1"
+ body:
+ name: cow.jpg
+ vector: [2, -1, 1, 4, -3]
+
+ - do:
+ index:
+ index: test
+ id: "2"
+ body:
+ name: moose.jpg
+ vector: [127.0, -128.0, 0.0, 1.0, -1.0]
+
+ - do:
+ index:
+ index: test
+ id: "3"
+ body:
+ name: rabbit.jpg
+ vector: [5, 4.0, 3, 2.0, 127]
+
+ - do:
+ indices.refresh: {}
+
+---
+"kNN search only":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [127, 127, -128, -128, 127]
+ k: 2
+ num_candidates: 3
+
+ - match: {hits.hits.0._id: "2"}
+ - match: {hits.hits.0.fields.name.0: "moose.jpg"}
+
+ - match: {hits.hits.1._id: "1"}
+ - match: {hits.hits.1.fields.name.0: "cow.jpg"}
+
+---
+"kNN search plus query":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [127.0, -128.0, 0.0, 1.0, -1.0]
+ k: 2
+ num_candidates: 3
+ query:
+ term:
+ name: rabbit.jpg
+
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+
+ - match: {hits.hits.1._id: "2"}
+ - match: {hits.hits.1.fields.name.0: "moose.jpg"}
+
+---
+"kNN search with filter":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [5.0, 4, 3.0, 2, 127.0]
+ k: 2
+ num_candidates: 3
+ filter:
+ term:
+ name: "rabbit.jpg"
+
+ - match: {hits.total.value: 1}
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [2, -1, 1, 4, -3]
+ k: 2
+ num_candidates: 3
+ filter:
+ - term:
+ name: "rabbit.jpg"
+ - term:
+ _id: 2
+
+ - match: {hits.total.value: 0}
+
+---
+"Vector similarity search only":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ num_candidates: 3
+ k: 3
+ field: vector
+ similarity: 0.98
+ query_vector: [5, 4.0, 3, 2.0, 127]
+
+ - length: {hits.hits: 1}
+
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+---
+"Vector similarity with filter only":
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ num_candidates: 3
+ k: 3
+ field: vector
+ similarity: 0.98
+ query_vector: [5, 4.0, 3, 2.0, 127]
+ filter: {"term": {"name": "rabbit.jpg"}}
+
+ - length: {hits.hits: 1}
+
+ - match: {hits.hits.0._id: "3"}
+ - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+
+ - do:
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ num_candidates: 3
+ k: 3
+ field: vector
+ similarity: 0.98
+ query_vector: [5, 4.0, 3, 2.0, 127]
+ filter: {"term": {"name": "cow.jpg"}}
+
+ - length: {hits.hits: 0}
+---
+"dim mismatch":
+ - do:
+ catch: bad_request
+ search:
+ index: test
+ body:
+ fields: [ "name" ]
+ knn:
+ field: vector
+ query_vector: [1, 2, 3, 4, 5, 6]
+ k: 2
+ num_candidates: 3
+---
+"disallow vector index type change to quantized type":
+ - do:
+ catch: bad_request
+ indices.put_mapping:
+ index: test
+ body:
+ properties:
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 32
+ index: true
+ similarity: l2_norm
+ index_options:
+ type: int4_hnsw
+ - do:
+ catch: bad_request
+ indices.put_mapping:
+ index: test
+ body:
+ properties:
+ vector:
+ type: dense_vector
+ element_type: bit
+ dims: 32
+ index: true
+ similarity: l2_norm
+ index_options:
+ type: int8_hnsw
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java
index b07a861a5a5ef..ee66d57bec5cc 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/ClusterStateDiffIT.java
@@ -8,6 +8,7 @@
package org.elasticsearch.cluster;
+import org.elasticsearch.TransportVersion;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.block.ClusterBlock;
import org.elasticsearch.cluster.block.ClusterBlocks;
@@ -48,6 +49,7 @@
import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.index.shard.IndexLongFieldRange;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.snapshots.Snapshot;
import org.elasticsearch.snapshots.SnapshotId;
@@ -568,6 +570,7 @@ public IndexMetadata randomCreate(String name) {
settingsBuilder.put(randomSettings(Settings.EMPTY)).put(IndexMetadata.SETTING_VERSION_CREATED, randomVersion(random()));
builder.settings(settingsBuilder);
builder.numberOfShards(randomIntBetween(1, 10)).numberOfReplicas(randomInt(10));
+ builder.eventIngestedRange(IndexLongFieldRange.UNKNOWN, TransportVersion.current());
int aliasCount = randomInt(10);
for (int i = 0; i < aliasCount; i++) {
builder.putAlias(randomAlias());
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java b/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java
index b563d6849c777..204d7131c44d2 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/indices/recovery/IndexRecoveryIT.java
@@ -1041,7 +1041,6 @@ public void testHistoryRetention() throws Exception {
assertThat(recoveryState.getTranslog().recoveredOperations(), greaterThan(0));
}
- @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105122")
public void testDoNotInfinitelyWaitForMapping() {
internalCluster().ensureAtLeastNumDataNodes(3);
createIndex(
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/FileSettingsServiceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/FileSettingsServiceIT.java
index 6e89c1447edb6..2fe808d813ccc 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/FileSettingsServiceIT.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/reservedstate/service/FileSettingsServiceIT.java
@@ -42,15 +42,16 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0, autoManageMasterNodes = false)
public class FileSettingsServiceIT extends ESIntegTestCase {
- private static AtomicLong versionCounter = new AtomicLong(1);
+ private static final AtomicLong versionCounter = new AtomicLong(1);
- private static String testJSON = """
+ private static final String testJSON = """
{
"metadata": {
"version": "%s",
@@ -63,7 +64,7 @@ public class FileSettingsServiceIT extends ESIntegTestCase {
}
}""";
- private static String testJSON43mb = """
+ private static final String testJSON43mb = """
{
"metadata": {
"version": "%s",
@@ -76,7 +77,7 @@ public class FileSettingsServiceIT extends ESIntegTestCase {
}
}""";
- private static String testCleanupJSON = """
+ private static final String testCleanupJSON = """
{
"metadata": {
"version": "%s",
@@ -87,7 +88,7 @@ public class FileSettingsServiceIT extends ESIntegTestCase {
}
}""";
- private static String testErrorJSON = """
+ private static final String testErrorJSON = """
{
"metadata": {
"version": "%s",
@@ -165,8 +166,7 @@ public void clusterChanged(ClusterChangedEvent event) {
private void assertClusterStateSaveOK(CountDownLatch savedClusterState, AtomicLong metadataVersion, String expectedBytesPerSec)
throws Exception {
- boolean awaitSuccessful = savedClusterState.await(20, TimeUnit.SECONDS);
- assertTrue(awaitSuccessful);
+ assertTrue(savedClusterState.await(20, TimeUnit.SECONDS));
final ClusterStateResponse clusterStateResponse = clusterAdmin().state(
new ClusterStateRequest().waitForMetadataVersion(metadataVersion.get())
@@ -180,11 +180,13 @@ private void assertClusterStateSaveOK(CountDownLatch savedClusterState, AtomicLo
ClusterUpdateSettingsRequest req = new ClusterUpdateSettingsRequest().persistentSettings(
Settings.builder().put(INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING.getKey(), "1234kb")
);
- assertEquals(
- "java.lang.IllegalArgumentException: Failed to process request "
- + "[org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest/unset] "
- + "with errors: [[indices.recovery.max_bytes_per_sec] set as read-only by [file_settings]]",
- expectThrows(ExecutionException.class, () -> clusterAdmin().updateSettings(req).get()).getMessage()
+ assertThat(
+ expectThrows(ExecutionException.class, () -> clusterAdmin().updateSettings(req).get()).getMessage(),
+ is(
+ "java.lang.IllegalArgumentException: Failed to process request "
+ + "[org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest/unset] "
+ + "with errors: [[indices.recovery.max_bytes_per_sec] set as read-only by [file_settings]]"
+ )
);
}
@@ -256,16 +258,15 @@ public void testReservedStatePersistsOnRestart() throws Exception {
internalCluster().restartNode(masterNode);
final ClusterStateResponse clusterStateResponse = clusterAdmin().state(new ClusterStateRequest()).actionGet();
- assertEquals(
- 1,
+ assertThat(
clusterStateResponse.getState()
.metadata()
.reservedStateMetadata()
.get(FileSettingsService.NAMESPACE)
.handlers()
.get(ReservedClusterSettingsAction.NAME)
- .keys()
- .size()
+ .keys(),
+ hasSize(1)
);
}
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/CollapseSearchResultsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/CollapseSearchResultsIT.java
index a12a26d69c5ff..f5fdd752a6f57 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/search/CollapseSearchResultsIT.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/search/CollapseSearchResultsIT.java
@@ -39,4 +39,26 @@ public void testCollapse() {
}
);
}
+
+ public void testCollapseWithDocValueFields() {
+ final String indexName = "test_collapse";
+ createIndex(indexName);
+ final String collapseField = "collapse_field";
+ final String otherField = "other_field";
+ assertAcked(indicesAdmin().preparePutMapping(indexName).setSource(collapseField, "type=keyword", otherField, "type=keyword"));
+ index(indexName, "id_1_0", Map.of(collapseField, "value1", otherField, "other_value1"));
+ index(indexName, "id_1_1", Map.of(collapseField, "value1", otherField, "other_value2"));
+ index(indexName, "id_2_0", Map.of(collapseField, "value2", otherField, "other_value3"));
+ refresh(indexName);
+
+ assertNoFailuresAndResponse(
+ prepareSearch(indexName).setQuery(new MatchAllQueryBuilder())
+ .addDocValueField(otherField)
+ .setCollapse(new CollapseBuilder(collapseField).setInnerHits(new InnerHitBuilder("ih").setSize(2))),
+ searchResponse -> {
+ assertEquals(collapseField, searchResponse.getHits().getCollapseField());
+ assertEquals(Set.of(new BytesRef("value1"), new BytesRef("value2")), Set.of(searchResponse.getHits().getCollapseValues()));
+ }
+ );
+ }
}
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/FunctionScorePluginIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/FunctionScorePluginIT.java
index 396af7e8501cf..d42a84677a8f7 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/FunctionScorePluginIT.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/FunctionScorePluginIT.java
@@ -146,7 +146,6 @@ private static class LinearMultScoreFunction implements DecayFunction {
@Override
public double evaluate(double value, double scale) {
-
return value;
}
diff --git a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SnapshotStressTestsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SnapshotStressTestsIT.java
index 9bcddd5c58d66..b8b6dcb25b557 100644
--- a/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SnapshotStressTestsIT.java
+++ b/server/src/internalClusterTest/java/org/elasticsearch/snapshots/SnapshotStressTestsIT.java
@@ -216,11 +216,13 @@ private static class TrackedCluster {
static final Logger logger = LogManager.getLogger(TrackedCluster.class);
static final String CLIENT = "client";
+ static final String NODE_RESTARTER = "node_restarter";
private final ThreadPool threadPool = new TestThreadPool(
"TrackedCluster",
// a single thread for "client" activities, to limit the number of activities all starting at once
- new ScalingExecutorBuilder(CLIENT, 1, 1, TimeValue.ZERO, true, CLIENT)
+ new ScalingExecutorBuilder(CLIENT, 1, 1, TimeValue.ZERO, true, CLIENT),
+ new ScalingExecutorBuilder(NODE_RESTARTER, 1, 5, TimeValue.ZERO, true, NODE_RESTARTER)
);
private final Executor clientExecutor = threadPool.executor(CLIENT);
@@ -1163,7 +1165,7 @@ private void startNodeRestarter() {
final String nodeName = trackedNode.nodeName;
final Releasable releaseAll = localReleasables.transfer();
- threadPool.generic().execute(mustSucceed(() -> {
+ threadPool.executor(NODE_RESTARTER).execute(mustSucceed(() -> {
logger.info("--> restarting [{}]", nodeName);
cluster.restartNode(nodeName);
logger.info("--> finished restarting [{}]", nodeName);
diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java
index db7e3d40518ba..e2810a6f5bf16 100644
--- a/server/src/main/java/module-info.java
+++ b/server/src/main/java/module-info.java
@@ -449,7 +449,10 @@
with
org.elasticsearch.index.codec.vectors.ES813FlatVectorFormat,
org.elasticsearch.index.codec.vectors.ES813Int8FlatVectorFormat,
- org.elasticsearch.index.codec.vectors.ES814HnswScalarQuantizedVectorsFormat;
+ org.elasticsearch.index.codec.vectors.ES814HnswScalarQuantizedVectorsFormat,
+ org.elasticsearch.index.codec.vectors.ES815HnswBitVectorsFormat,
+ org.elasticsearch.index.codec.vectors.ES815BitFlatVectorFormat;
+
provides org.apache.lucene.codecs.Codec with Elasticsearch814Codec;
provides org.apache.logging.log4j.core.util.ContextDataProvider with org.elasticsearch.common.logging.DynamicContextDataProvider;
diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java
index 977f292912746..28a75443b5842 100644
--- a/server/src/main/java/org/elasticsearch/TransportVersions.java
+++ b/server/src/main/java/org/elasticsearch/TransportVersions.java
@@ -199,6 +199,10 @@ static TransportVersion def(int id) {
public static final TransportVersion SNAPSHOT_REQUEST_TIMEOUTS = def(8_690_00_0);
public static final TransportVersion INDEX_METADATA_MAPPINGS_UPDATED_VERSION = def(8_691_00_0);
public static final TransportVersion ML_INFERENCE_ELAND_SETTINGS_ADDED = def(8_692_00_0);
+ public static final TransportVersion ML_ANTHROPIC_INTEGRATION_ADDED = def(8_693_00_0);
+ public static final TransportVersion ML_INFERENCE_GOOGLE_VERTEX_AI_EMBEDDINGS_ADDED = def(8_694_00_0);
+ public static final TransportVersion EVENT_INGESTED_RANGE_IN_CLUSTER_STATE = def(8_695_00_0);
+ public static final TransportVersion ESQL_ADD_AGGREGATE_TYPE = def(8_696_00_0);
public static final TransportVersion MULTI_PROJECT = def(8_999_00_0); // THIS IS A HACK FOR NOW (!)
/*
@@ -264,7 +268,7 @@ static TransportVersion def(int id) {
* Reference to the minimum transport version that can be used with CCS.
* This should be the transport version used by the previous minor release.
*/
- public static final TransportVersion MINIMUM_CCS_VERSION = V_8_13_0;
+ public static final TransportVersion MINIMUM_CCS_VERSION = SHUTDOWN_REQUEST_TIMEOUTS_FIX_8_14;
static final NavigableMap VERSION_IDS = getAllVersionIds(TransportVersions.class);
diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java b/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java
index 55c754545cbbe..82c498c64e1c9 100644
--- a/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java
+++ b/server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java
@@ -28,6 +28,7 @@
import org.elasticsearch.common.lucene.search.TopDocsAndMaxScore;
import org.elasticsearch.common.util.Maps;
import org.elasticsearch.common.util.concurrent.AtomicArray;
+import org.elasticsearch.index.fielddata.IndexFieldData;
import org.elasticsearch.lucene.grouping.TopFieldGroups;
import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.SearchHit;
@@ -301,11 +302,13 @@ private static Sort checkSameSortTypes(Collection results, SortField[]
}
private static SortField.Type getType(SortField sortField) {
- if (sortField instanceof SortedNumericSortField) {
- return ((SortedNumericSortField) sortField).getNumericType();
- }
- if (sortField instanceof SortedSetSortField) {
+ if (sortField instanceof SortedNumericSortField sf) {
+ return sf.getNumericType();
+ } else if (sortField instanceof SortedSetSortField) {
return SortField.Type.STRING;
+ } else if (sortField.getComparatorSource() instanceof IndexFieldData.XFieldComparatorSource cmp) {
+ // This can occur if the sort field wasn't rewritten by Lucene#rewriteMergeSortField because all search shards are local.
+ return cmp.reducedType();
} else {
return sortField.getType();
}
diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java
index f8d30786aca34..c2d1cdae85cd9 100644
--- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java
+++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java
@@ -292,24 +292,43 @@ public long buildTookInMillis() {
@Override
protected void doExecute(Task task, SearchRequest searchRequest, ActionListener listener) {
- ActionListener loggingAndMetrics = listener.delegateFailureAndWrap((l, searchResponse) -> {
- searchResponseMetrics.recordTookTime(searchResponse.getTookInMillis());
- if (searchResponse.getShardFailures() != null && searchResponse.getShardFailures().length > 0) {
- // Deduplicate failures by exception message and index
- ShardOperationFailedException[] groupedFailures = ExceptionsHelper.groupBy(searchResponse.getShardFailures());
- for (ShardOperationFailedException f : groupedFailures) {
- boolean causeHas500Status = false;
- if (f.getCause() != null) {
- causeHas500Status = ExceptionsHelper.status(f.getCause()).getStatus() >= 500;
- }
- if ((f.status().getStatus() >= 500 || causeHas500Status)
- && ExceptionsHelper.isNodeOrShardUnavailableTypeException(f.getCause()) == false) {
- logger.warn("TransportSearchAction shard failure (partial results response)", f);
+ ActionListener loggingAndMetrics = new ActionListener<>() {
+ @Override
+ public void onResponse(SearchResponse searchResponse) {
+ try {
+ searchResponseMetrics.recordTookTime(searchResponse.getTookInMillis());
+ SearchResponseMetrics.ResponseCountTotalStatus responseCountTotalStatus =
+ SearchResponseMetrics.ResponseCountTotalStatus.SUCCESS;
+ if (searchResponse.getShardFailures() != null && searchResponse.getShardFailures().length > 0) {
+ // Deduplicate failures by exception message and index
+ ShardOperationFailedException[] groupedFailures = ExceptionsHelper.groupBy(searchResponse.getShardFailures());
+ for (ShardOperationFailedException f : groupedFailures) {
+ boolean causeHas500Status = false;
+ if (f.getCause() != null) {
+ causeHas500Status = ExceptionsHelper.status(f.getCause()).getStatus() >= 500;
+ }
+ if ((f.status().getStatus() >= 500 || causeHas500Status)
+ && ExceptionsHelper.isNodeOrShardUnavailableTypeException(f.getCause()) == false) {
+ logger.warn("TransportSearchAction shard failure (partial results response)", f);
+ responseCountTotalStatus = SearchResponseMetrics.ResponseCountTotalStatus.PARTIAL_FAILURE;
+ }
+ }
}
+ listener.onResponse(searchResponse);
+ // increment after the delegated onResponse to ensure we don't
+ // record both a success and a failure if there is an exception
+ searchResponseMetrics.incrementResponseCount(responseCountTotalStatus);
+ } catch (Exception e) {
+ onFailure(e);
}
}
- l.onResponse(searchResponse);
- });
+
+ @Override
+ public void onFailure(Exception e) {
+ searchResponseMetrics.incrementResponseCount(SearchResponseMetrics.ResponseCountTotalStatus.FAILURE);
+ listener.onFailure(e);
+ }
+ };
executeRequest((SearchTask) task, searchRequest, loggingAndMetrics, AsyncSearchActionProvider::new);
}
diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchScrollAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchScrollAction.java
index c1c43310b0e11..d60033786abeb 100644
--- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchScrollAction.java
+++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchScrollAction.java
@@ -55,20 +55,39 @@ public TransportSearchScrollAction(
@Override
protected void doExecute(Task task, SearchScrollRequest request, ActionListener listener) {
- ActionListener loggingAndMetrics = listener.delegateFailureAndWrap((l, searchResponse) -> {
- searchResponseMetrics.recordTookTime(searchResponse.getTookInMillis());
- if (searchResponse.getShardFailures() != null && searchResponse.getShardFailures().length > 0) {
- ShardOperationFailedException[] groupedFailures = ExceptionsHelper.groupBy(searchResponse.getShardFailures());
- for (ShardOperationFailedException f : groupedFailures) {
- Throwable cause = f.getCause() == null ? f : f.getCause();
- if (ExceptionsHelper.status(cause).getStatus() >= 500
- && ExceptionsHelper.isNodeOrShardUnavailableTypeException(cause) == false) {
- logger.warn("TransportSearchScrollAction shard failure (partial results response)", f);
+ ActionListener loggingAndMetrics = new ActionListener<>() {
+ @Override
+ public void onResponse(SearchResponse searchResponse) {
+ try {
+ searchResponseMetrics.recordTookTime(searchResponse.getTookInMillis());
+ SearchResponseMetrics.ResponseCountTotalStatus responseCountTotalStatus =
+ SearchResponseMetrics.ResponseCountTotalStatus.SUCCESS;
+ if (searchResponse.getShardFailures() != null && searchResponse.getShardFailures().length > 0) {
+ ShardOperationFailedException[] groupedFailures = ExceptionsHelper.groupBy(searchResponse.getShardFailures());
+ for (ShardOperationFailedException f : groupedFailures) {
+ Throwable cause = f.getCause() == null ? f : f.getCause();
+ if (ExceptionsHelper.status(cause).getStatus() >= 500
+ && ExceptionsHelper.isNodeOrShardUnavailableTypeException(cause) == false) {
+ logger.warn("TransportSearchScrollAction shard failure (partial results response)", f);
+ responseCountTotalStatus = SearchResponseMetrics.ResponseCountTotalStatus.PARTIAL_FAILURE;
+ }
+ }
}
+ listener.onResponse(searchResponse);
+ // increment after the delegated onResponse to ensure we don't
+ // record both a success and a failure if there is an exception
+ searchResponseMetrics.incrementResponseCount(responseCountTotalStatus);
+ } catch (Exception e) {
+ onFailure(e);
}
}
- l.onResponse(searchResponse);
- });
+
+ @Override
+ public void onFailure(Exception e) {
+ searchResponseMetrics.incrementResponseCount(SearchResponseMetrics.ResponseCountTotalStatus.FAILURE);
+ listener.onFailure(e);
+ }
+ };
try {
ParsedScrollId scrollId = parseScrollId(request.scrollId());
Runnable action = switch (scrollId.getType()) {
diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java
index aabc213235dc7..988e38f35391e 100644
--- a/server/src/main/java/org/elasticsearch/cluster/ClusterState.java
+++ b/server/src/main/java/org/elasticsearch/cluster/ClusterState.java
@@ -47,6 +47,7 @@
import org.elasticsearch.common.xcontent.ChunkedToXContentHelper;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.SuppressForbidden;
+import org.elasticsearch.index.shard.IndexLongFieldRange;
import org.elasticsearch.indices.SystemIndexDescriptor;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContent;
@@ -233,6 +234,27 @@ public ClusterState(
this.minVersions = blocks.hasGlobalBlock(STATE_NOT_RECOVERED_BLOCK)
? new CompatibilityVersions(TransportVersions.MINIMUM_COMPATIBLE, Map.of()) // empty map because cluster state is unknown
: CompatibilityVersions.minimumVersions(compatibilityVersions.values());
+
+ assert compatibilityVersions.isEmpty()
+ || blocks.hasGlobalBlock(STATE_NOT_RECOVERED_BLOCK)
+ || assertEventIngestedIsUnknownInMixedClusters(metadata, this.minVersions);
+ }
+
+ private boolean assertEventIngestedIsUnknownInMixedClusters(Metadata metadata, CompatibilityVersions compatibilityVersions) {
+ if (compatibilityVersions.transportVersion().before(TransportVersions.EVENT_INGESTED_RANGE_IN_CLUSTER_STATE)
+ && metadata != null
+ && metadata.indices() != null) {
+ for (IndexMetadata indexMetadata : metadata.indices().values()) {
+ assert indexMetadata.getEventIngestedRange() == IndexLongFieldRange.UNKNOWN
+ : "event.ingested range should be UNKNOWN but is "
+ + indexMetadata.getEventIngestedRange()
+ + " for index: "
+ + indexMetadata.getIndex()
+ + " minTransportVersion: "
+ + compatibilityVersions.transportVersion();
+ }
+ }
+ return true;
}
private static boolean assertConsistentRoutingNodes(
diff --git a/server/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java b/server/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java
index 51fca588699e2..a01383b3eaa93 100644
--- a/server/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java
+++ b/server/src/main/java/org/elasticsearch/cluster/action/shard/ShardStateAction.java
@@ -12,6 +12,8 @@
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ExceptionsHelper;
+import org.elasticsearch.TransportVersion;
+import org.elasticsearch.TransportVersions;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ResultDeduplicator;
import org.elasticsearch.action.support.ChannelActionListener;
@@ -543,9 +545,10 @@ public void shardStarted(
final long primaryTerm,
final String message,
final ShardLongFieldRange timestampRange,
+ final ShardLongFieldRange eventIngestedRange,
final ActionListener listener
) {
- shardStarted(shardRouting, primaryTerm, message, timestampRange, listener, clusterService.state());
+ shardStarted(shardRouting, primaryTerm, message, timestampRange, eventIngestedRange, listener, clusterService.state());
}
public void shardStarted(
@@ -553,11 +556,19 @@ public void shardStarted(
final long primaryTerm,
final String message,
final ShardLongFieldRange timestampRange,
+ final ShardLongFieldRange eventIngestedRange,
final ActionListener listener,
final ClusterState currentState
) {
remoteShardStateUpdateDeduplicator.executeOnce(
- new StartedShardEntry(shardRouting.shardId(), shardRouting.allocationId().getId(), primaryTerm, message, timestampRange),
+ new StartedShardEntry(
+ shardRouting.shardId(),
+ shardRouting.allocationId().getId(),
+ primaryTerm,
+ message,
+ timestampRange,
+ eventIngestedRange
+ ),
listener,
(req, l) -> sendShardAction(SHARD_STARTED_ACTION_NAME, currentState, req, l)
);
@@ -585,6 +596,14 @@ public void messageReceived(StartedShardEntry request, TransportChannel channel,
}
}
+ /**
+ * Holder of the pair of time ranges needed in cluster state - one for @timestamp, the other for 'event.ingested'.
+ * Since 'event.ingested' was added well after @timestamp, it can be UNKNOWN when @timestamp range is present.
+ * @param timestampRange range for @timestamp
+ * @param eventIngestedRange range for event.ingested
+ */
+ record ClusterStateTimeRanges(IndexLongFieldRange timestampRange, IndexLongFieldRange eventIngestedRange) {}
+
public static class ShardStartedClusterStateTaskExecutor implements ClusterStateTaskExecutor {
private final AllocationService allocationService;
private final RerouteService rerouteService;
@@ -599,37 +618,42 @@ public ClusterState execute(BatchExecutionContext batchE
List> tasksToBeApplied = new ArrayList<>();
List shardRoutingsToBeApplied = new ArrayList<>(batchExecutionContext.taskContexts().size());
Set seenShardRoutings = new HashSet<>(); // to prevent duplicates
- final Map updatedTimestampRanges = new HashMap<>();
+ final Map updatedTimestampRanges = new HashMap<>();
final ClusterState initialState = batchExecutionContext.initialState();
for (var taskContext : batchExecutionContext.taskContexts()) {
final var task = taskContext.getTask();
- StartedShardEntry entry = task.getEntry();
- final ShardRouting matched = initialState.getRoutingTable().getByAllocationId(entry.shardId, entry.allocationId);
+ StartedShardEntry startedShardEntry = task.getEntry();
+ final ShardRouting matched = initialState.getRoutingTable()
+ .getByAllocationId(startedShardEntry.shardId, startedShardEntry.allocationId);
if (matched == null) {
// tasks that correspond to non-existent shards are marked as successful. The reason is that we resend shard started
// events on every cluster state publishing that does not contain the shard as started yet. This means that old stale
// requests might still be in flight even after the shard has already been started or failed on the master. We just
// ignore these requests for now.
- logger.debug("{} ignoring shard started task [{}] (shard does not exist anymore)", entry.shardId, entry);
+ logger.debug(
+ "{} ignoring shard started task [{}] (shard does not exist anymore)",
+ startedShardEntry.shardId,
+ startedShardEntry
+ );
taskContext.success(task::onSuccess);
} else {
- if (matched.primary() && entry.primaryTerm > 0) {
- final IndexMetadata indexMetadata = initialState.metadata().index(entry.shardId.getIndex());
+ if (matched.primary() && startedShardEntry.primaryTerm > 0) {
+ final IndexMetadata indexMetadata = initialState.metadata().index(startedShardEntry.shardId.getIndex());
assert indexMetadata != null;
- final long currentPrimaryTerm = indexMetadata.primaryTerm(entry.shardId.id());
- if (currentPrimaryTerm != entry.primaryTerm) {
- assert currentPrimaryTerm > entry.primaryTerm
+ final long currentPrimaryTerm = indexMetadata.primaryTerm(startedShardEntry.shardId.id());
+ if (currentPrimaryTerm != startedShardEntry.primaryTerm) {
+ assert currentPrimaryTerm > startedShardEntry.primaryTerm
: "received a primary term with a higher term than in the "
+ "current cluster state (received ["
- + entry.primaryTerm
+ + startedShardEntry.primaryTerm
+ "] but current is ["
+ currentPrimaryTerm
+ "])";
logger.debug(
"{} ignoring shard started task [{}] (primary term {} does not match current term {})",
- entry.shardId,
- entry,
- entry.primaryTerm,
+ startedShardEntry.shardId,
+ startedShardEntry,
+ startedShardEntry.primaryTerm,
currentPrimaryTerm
);
taskContext.success(task::onSuccess);
@@ -637,12 +661,12 @@ public ClusterState execute(BatchExecutionContext batchE
}
}
if (matched.initializing() == false) {
- assert matched.active() : "expected active shard routing for task " + entry + " but found " + matched;
+ assert matched.active() : "expected active shard routing for task " + startedShardEntry + " but found " + matched;
// same as above, this might have been a stale in-flight request, so we just ignore.
logger.debug(
"{} ignoring shard started task [{}] (shard exists but is not initializing: {})",
- entry.shardId,
- entry,
+ startedShardEntry.shardId,
+ startedShardEntry,
matched
);
taskContext.success(task::onSuccess);
@@ -651,32 +675,66 @@ public ClusterState execute(BatchExecutionContext batchE
if (seenShardRoutings.contains(matched)) {
logger.trace(
"{} ignoring shard started task [{}] (already scheduled to start {})",
- entry.shardId,
- entry,
+ startedShardEntry.shardId,
+ startedShardEntry,
matched
);
tasksToBeApplied.add(taskContext);
} else {
- logger.debug("{} starting shard {} (shard started task: [{}])", entry.shardId, matched, entry);
+ logger.debug(
+ "{} starting shard {} (shard started task: [{}])",
+ startedShardEntry.shardId,
+ matched,
+ startedShardEntry
+ );
tasksToBeApplied.add(taskContext);
shardRoutingsToBeApplied.add(matched);
seenShardRoutings.add(matched);
- // expand the timestamp range recorded in the index metadata if needed
- final Index index = entry.shardId.getIndex();
- IndexLongFieldRange currentTimestampMillisRange = updatedTimestampRanges.get(index);
+ // expand the timestamp range(s) recorded in the index metadata if needed
+ final Index index = startedShardEntry.shardId.getIndex();
+ ClusterStateTimeRanges clusterStateTimeRanges = updatedTimestampRanges.get(index);
+ IndexLongFieldRange currentTimestampMillisRange = clusterStateTimeRanges == null
+ ? null
+ : clusterStateTimeRanges.timestampRange();
+ IndexLongFieldRange currentEventIngestedMillisRange = clusterStateTimeRanges == null
+ ? null
+ : clusterStateTimeRanges.eventIngestedRange();
+
final IndexMetadata indexMetadata = initialState.metadata().index(index);
if (currentTimestampMillisRange == null) {
currentTimestampMillisRange = indexMetadata.getTimestampRange();
}
- final IndexLongFieldRange newTimestampMillisRange;
- newTimestampMillisRange = currentTimestampMillisRange.extendWithShardRange(
- entry.shardId.id(),
+ if (currentEventIngestedMillisRange == null) {
+ currentEventIngestedMillisRange = indexMetadata.getEventIngestedRange();
+ }
+
+ final IndexLongFieldRange newTimestampMillisRange = currentTimestampMillisRange.extendWithShardRange(
+ startedShardEntry.shardId.id(),
indexMetadata.getNumberOfShards(),
- entry.timestampRange
+ startedShardEntry.timestampRange
);
- if (newTimestampMillisRange != currentTimestampMillisRange) {
- updatedTimestampRanges.put(index, newTimestampMillisRange);
+ /*
+ * Only track 'event.ingested' range this if the cluster state min transport version is on/after the version
+ * where we added 'event.ingested'. If we don't do that, we will have different cluster states on different
+ * nodes because we can't send this data over the wire to older nodes.
+ */
+ IndexLongFieldRange newEventIngestedMillisRange = IndexLongFieldRange.UNKNOWN;
+ TransportVersion minTransportVersion = batchExecutionContext.initialState().getMinTransportVersion();
+ if (minTransportVersion.onOrAfter(TransportVersions.EVENT_INGESTED_RANGE_IN_CLUSTER_STATE)) {
+ newEventIngestedMillisRange = currentEventIngestedMillisRange.extendWithShardRange(
+ startedShardEntry.shardId.id(),
+ indexMetadata.getNumberOfShards(),
+ startedShardEntry.eventIngestedRange
+ );
+ }
+
+ if (newTimestampMillisRange != currentTimestampMillisRange
+ || newEventIngestedMillisRange != currentEventIngestedMillisRange) {
+ updatedTimestampRanges.put(
+ index,
+ new ClusterStateTimeRanges(newTimestampMillisRange, newEventIngestedMillisRange)
+ );
}
}
}
@@ -690,10 +748,12 @@ public ClusterState execute(BatchExecutionContext batchE
if (updatedTimestampRanges.isEmpty() == false) {
final Metadata.Builder metadataBuilder = Metadata.builder(maybeUpdatedState.metadata());
- for (Map.Entry updatedTimestampRangeEntry : updatedTimestampRanges.entrySet()) {
+ for (Map.Entry updatedTimeRangesEntry : updatedTimestampRanges.entrySet()) {
+ ClusterStateTimeRanges timeRanges = updatedTimeRangesEntry.getValue();
metadataBuilder.put(
- IndexMetadata.builder(metadataBuilder.getSafe(updatedTimestampRangeEntry.getKey()))
- .timestampRange(updatedTimestampRangeEntry.getValue())
+ IndexMetadata.builder(metadataBuilder.getSafe(updatedTimeRangesEntry.getKey()))
+ .timestampRange(timeRanges.timestampRange())
+ .eventIngestedRange(timeRanges.eventIngestedRange(), maybeUpdatedState.getMinTransportVersion())
);
}
maybeUpdatedState = ClusterState.builder(maybeUpdatedState).metadata(metadataBuilder).build();
@@ -725,6 +785,15 @@ private static boolean assertStartedIndicesHaveCompleteTimestampRanges(ClusterSt
+ clusterState.metadata().index(cursor.getKey()).getTimestampRange()
+ " for "
+ cursor.getValue().prettyPrint();
+
+ assert cursor.getValue().allPrimaryShardsActive() == false
+ || clusterState.metadata().index(cursor.getKey()).getEventIngestedRange().isComplete()
+ : "index ["
+ + cursor.getKey()
+ + "] should have complete event.ingested range, but got "
+ + clusterState.metadata().index(cursor.getKey()).getEventIngestedRange()
+ + " for "
+ + cursor.getValue().prettyPrint();
}
return true;
}
@@ -748,6 +817,7 @@ public static class StartedShardEntry extends TransportRequest {
final long primaryTerm;
final String message;
final ShardLongFieldRange timestampRange;
+ final ShardLongFieldRange eventIngestedRange;
StartedShardEntry(StreamInput in) throws IOException {
super(in);
@@ -756,6 +826,11 @@ public static class StartedShardEntry extends TransportRequest {
primaryTerm = in.readVLong();
this.message = in.readString();
this.timestampRange = ShardLongFieldRange.readFrom(in);
+ if (in.getTransportVersion().onOrAfter(TransportVersions.EVENT_INGESTED_RANGE_IN_CLUSTER_STATE)) {
+ this.eventIngestedRange = ShardLongFieldRange.readFrom(in);
+ } else {
+ this.eventIngestedRange = ShardLongFieldRange.UNKNOWN;
+ }
}
public StartedShardEntry(
@@ -763,13 +838,15 @@ public StartedShardEntry(
final String allocationId,
final long primaryTerm,
final String message,
- final ShardLongFieldRange timestampRange
+ final ShardLongFieldRange timestampRange,
+ final ShardLongFieldRange eventIngestedRange
) {
this.shardId = shardId;
this.allocationId = allocationId;
this.primaryTerm = primaryTerm;
this.message = message;
this.timestampRange = timestampRange;
+ this.eventIngestedRange = eventIngestedRange;
}
@Override
@@ -780,6 +857,9 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeVLong(primaryTerm);
out.writeString(message);
timestampRange.writeTo(out);
+ if (out.getTransportVersion().onOrAfter(TransportVersions.EVENT_INGESTED_RANGE_IN_CLUSTER_STATE)) {
+ eventIngestedRange.writeTo(out);
+ }
}
@Override
@@ -802,12 +882,13 @@ public boolean equals(Object o) {
&& shardId.equals(that.shardId)
&& allocationId.equals(that.allocationId)
&& message.equals(that.message)
- && timestampRange.equals(that.timestampRange);
+ && timestampRange.equals(that.timestampRange)
+ && eventIngestedRange.equals(that.eventIngestedRange);
}
@Override
public int hashCode() {
- return Objects.hash(shardId, allocationId, primaryTerm, message, timestampRange);
+ return Objects.hash(shardId, allocationId, primaryTerm, message, timestampRange, eventIngestedRange);
}
}
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java
index 7bfb3b9c8ae76..391ae451c8cef 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java
@@ -137,6 +137,9 @@ public class IndexMetadata implements Diffable, ToXContentFragmen
EnumSet.of(ClusterBlockLevel.WRITE)
);
+ // 'event.ingested' (part of Elastic Common Schema) range is tracked in cluster state, along with @timestamp
+ public static final String EVENT_INGESTED_FIELD_NAME = "event.ingested";
+
@Nullable
public String getDownsamplingInterval() {
return settings.get(IndexMetadata.INDEX_DOWNSAMPLE_INTERVAL_KEY);
@@ -538,6 +541,7 @@ public Iterator> settings() {
static final String KEY_MAPPINGS_UPDATED_VERSION = "mappings_updated_version";
static final String KEY_SYSTEM = "system";
static final String KEY_TIMESTAMP_RANGE = "timestamp_range";
+ static final String KEY_EVENT_INGESTED_RANGE = "event_ingested_range";
public static final String KEY_PRIMARY_TERMS = "primary_terms";
public static final String KEY_STATS = "stats";
@@ -603,7 +607,10 @@ public Iterator> settings() {
private final boolean isSystem;
private final boolean isHidden;
+ // range for the @timestamp field for the Index
private final IndexLongFieldRange timestampRange;
+ // range for the event.ingested field for the Index
+ private final IndexLongFieldRange eventIngestedRange;
private final int priority;
@@ -670,6 +677,7 @@ private IndexMetadata(
final boolean isSystem,
final boolean isHidden,
final IndexLongFieldRange timestampRange,
+ final IndexLongFieldRange eventIngestedRange,
final int priority,
final long creationDate,
final boolean ignoreDiskWatermarks,
@@ -724,6 +732,7 @@ private IndexMetadata(
assert isHidden == INDEX_HIDDEN_SETTING.get(settings);
this.isHidden = isHidden;
this.timestampRange = timestampRange;
+ this.eventIngestedRange = eventIngestedRange;
this.priority = priority;
this.creationDate = creationDate;
this.ignoreDiskWatermarks = ignoreDiskWatermarks;
@@ -780,6 +789,7 @@ IndexMetadata withMappingMetadata(MappingMetadata mapping) {
this.isSystem,
this.isHidden,
this.timestampRange,
+ this.eventIngestedRange,
this.priority,
this.creationDate,
this.ignoreDiskWatermarks,
@@ -840,6 +850,7 @@ public IndexMetadata withInSyncAllocationIds(int shardId, Set inSyncSet)
this.isSystem,
this.isHidden,
this.timestampRange,
+ this.eventIngestedRange,
this.priority,
this.creationDate,
this.ignoreDiskWatermarks,
@@ -898,6 +909,7 @@ public IndexMetadata withIncrementedPrimaryTerm(int shardId) {
this.isSystem,
this.isHidden,
this.timestampRange,
+ this.eventIngestedRange,
this.priority,
this.creationDate,
this.ignoreDiskWatermarks,
@@ -919,13 +931,24 @@ public IndexMetadata withIncrementedPrimaryTerm(int shardId) {
}
/**
- * @param timestampRange new timestamp range
+ * @param timestampRange new @timestamp range
+ * @param eventIngestedRange new 'event.ingested' range
+ * @param minClusterTransportVersion minimum transport version used between nodes of this cluster
* @return copy of this instance with updated timestamp range
*/
- public IndexMetadata withTimestampRange(IndexLongFieldRange timestampRange) {
- if (timestampRange.equals(this.timestampRange)) {
+ public IndexMetadata withTimestampRanges(
+ IndexLongFieldRange timestampRange,
+ IndexLongFieldRange eventIngestedRange,
+ TransportVersion minClusterTransportVersion
+ ) {
+ if (timestampRange.equals(this.timestampRange) && eventIngestedRange.equals(this.eventIngestedRange)) {
return this;
}
+ IndexLongFieldRange allowedEventIngestedRange = eventIngestedRange;
+ // remove this check when the EVENT_INGESTED_RANGE_IN_CLUSTER_STATE version is removed
+ if (minClusterTransportVersion.before(TransportVersions.EVENT_INGESTED_RANGE_IN_CLUSTER_STATE)) {
+ allowedEventIngestedRange = IndexLongFieldRange.UNKNOWN;
+ }
return new IndexMetadata(
this.index,
this.version,
@@ -956,6 +979,7 @@ public IndexMetadata withTimestampRange(IndexLongFieldRange timestampRange) {
this.isSystem,
this.isHidden,
timestampRange,
+ allowedEventIngestedRange,
this.priority,
this.creationDate,
this.ignoreDiskWatermarks,
@@ -1010,6 +1034,7 @@ public IndexMetadata withIncrementedVersion() {
this.isSystem,
this.isHidden,
this.timestampRange,
+ this.eventIngestedRange,
this.priority,
this.creationDate,
this.ignoreDiskWatermarks,
@@ -1360,6 +1385,10 @@ public IndexLongFieldRange getTimestampRange() {
return timestampRange;
}
+ public IndexLongFieldRange getEventIngestedRange() {
+ return eventIngestedRange;
+ }
+
/**
* @return whether this index has a time series timestamp range
*/
@@ -1512,7 +1541,12 @@ private static class IndexMetadataDiff implements Diff {
private final Diff> rolloverInfos;
private final IndexVersion mappingsUpdatedVersion;
private final boolean isSystem;
+
+ // range for the @timestamp field for the Index
private final IndexLongFieldRange timestampRange;
+ // range for the event.ingested field for the Index
+ private final IndexLongFieldRange eventIngestedRange;
+
private final IndexMetadataStats stats;
private final Double indexWriteLoadForecast;
private final Long shardSizeInBytesForecast;
@@ -1551,6 +1585,7 @@ private static class IndexMetadataDiff implements Diff {
mappingsUpdatedVersion = after.mappingsUpdatedVersion;
isSystem = after.isSystem;
timestampRange = after.timestampRange;
+ eventIngestedRange = after.eventIngestedRange;
stats = after.stats;
indexWriteLoadForecast = after.writeLoadForecast;
shardSizeInBytesForecast = after.shardSizeInBytesForecast;
@@ -1629,6 +1664,11 @@ private static class IndexMetadataDiff implements Diff {
indexWriteLoadForecast = null;
shardSizeInBytesForecast = null;
}
+ if (in.getTransportVersion().onOrAfter(TransportVersions.EVENT_INGESTED_RANGE_IN_CLUSTER_STATE)) {
+ eventIngestedRange = IndexLongFieldRange.readFrom(in);
+ } else {
+ eventIngestedRange = IndexLongFieldRange.UNKNOWN;
+ }
}
@Override
@@ -1670,6 +1710,12 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeOptionalDouble(indexWriteLoadForecast);
out.writeOptionalLong(shardSizeInBytesForecast);
}
+ if (out.getTransportVersion().onOrAfter(TransportVersions.EVENT_INGESTED_RANGE_IN_CLUSTER_STATE)) {
+ eventIngestedRange.writeTo(out);
+ } else {
+ assert eventIngestedRange == IndexLongFieldRange.UNKNOWN
+ : "eventIngestedRange should be UNKNOWN until all nodes are on the new version but is " + eventIngestedRange;
+ }
}
@Override
@@ -1698,6 +1744,7 @@ public IndexMetadata apply(IndexMetadata part) {
builder.rolloverInfos.putAllFromMap(rolloverInfos.apply(part.rolloverInfos));
builder.system(isSystem);
builder.timestampRange(timestampRange);
+ builder.eventIngestedRange(eventIngestedRange);
builder.stats(stats);
builder.indexWriteLoadForecast(indexWriteLoadForecast);
builder.shardSizeInBytesForecast(shardSizeInBytesForecast);
@@ -1775,6 +1822,11 @@ public static IndexMetadata readFrom(StreamInput in, @Nullable Function mappingsMetadata = new HashMap<>();
DocumentMapper docMapper = documentMapperSupplier.get();
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexStateService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexStateService.java
index 34f71d315f97a..be6d6f3ef1e53 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexStateService.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexStateService.java
@@ -886,6 +886,7 @@ static Tuple> closeRoutingTable(
final IndexMetadata.Builder updatedMetadata = IndexMetadata.builder(indexMetadata).state(IndexMetadata.State.CLOSE);
metadata.put(
updatedMetadata.timestampRange(IndexLongFieldRange.NO_SHARDS)
+ .eventIngestedRange(IndexLongFieldRange.NO_SHARDS, currentState.getMinTransportVersion())
.settingsVersion(indexMetadata.getSettingsVersion() + 1)
.settings(Settings.builder().put(indexMetadata.getSettings()).put(VERIFIED_BEFORE_CLOSE_SETTING.getKey(), true))
);
@@ -1132,6 +1133,7 @@ private ClusterState openIndices(final Index[] indices, final ClusterState curre
.settingsVersion(indexMetadata.getSettingsVersion() + 1)
.settings(updatedSettings)
.timestampRange(IndexLongFieldRange.NO_SHARDS)
+ .eventIngestedRange(IndexLongFieldRange.NO_SHARDS, currentState.getMinTransportVersion())
.build();
// The index might be closed because we couldn't import it due to an old incompatible
diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
index bce727f5790ff..a798ed58833b8 100644
--- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
+++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
@@ -48,6 +48,7 @@
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.mapper.MapperService.MergeReason;
import org.elasticsearch.index.mapper.RoutingFieldMapper;
+import org.elasticsearch.index.shard.IndexLongFieldRange;
import org.elasticsearch.indices.IndexTemplateMissingException;
import org.elasticsearch.indices.IndicesService;
import org.elasticsearch.indices.InvalidIndexTemplateException;
@@ -1652,7 +1653,12 @@ private static void validateCompositeTemplate(
final ClusterState stateWithIndex = ClusterState.builder(stateWithTemplate)
.metadata(
Metadata.builder(stateWithTemplate.metadata())
- .put(IndexMetadata.builder(temporaryIndexName).settings(finalResolvedSettings))
+ .put(
+ IndexMetadata.builder(temporaryIndexName)
+ // necessary to pass asserts in ClusterState constructor
+ .eventIngestedRange(IndexLongFieldRange.UNKNOWN, state.getMinTransportVersion())
+ .settings(finalResolvedSettings)
+ )
.build()
)
.build();
diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/IndexMetadataUpdater.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/IndexMetadataUpdater.java
index e8231f8c09387..98885acd127e2 100644
--- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/IndexMetadataUpdater.java
+++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/IndexMetadataUpdater.java
@@ -9,6 +9,7 @@
package org.elasticsearch.cluster.routing.allocation;
import org.apache.logging.log4j.Logger;
+import org.elasticsearch.TransportVersion;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.Metadata;
@@ -104,9 +105,10 @@ public void relocationCompleted(ShardRouting removedRelocationSource) {
*
* @param oldMetadata {@link Metadata} object from before the routing nodes was changed.
* @param newRoutingTable {@link RoutingTable} object after routing changes were applied.
+ * @param minClusterTransportVersion minimum TransportVersion used between nodes of this cluster
* @return adapted {@link Metadata}, potentially the original one if no change was needed.
*/
- public Metadata applyChanges(Metadata oldMetadata, RoutingTable newRoutingTable) {
+ public Metadata applyChanges(Metadata oldMetadata, RoutingTable newRoutingTable, TransportVersion minClusterTransportVersion) {
Map>> changesGroupedByIndex = shardChanges.entrySet()
.stream()
.collect(Collectors.groupingBy(e -> e.getKey().getIndex()));
@@ -119,7 +121,14 @@ public Metadata applyChanges(Metadata oldMetadata, RoutingTable newRoutingTable)
for (Map.Entry shardEntry : indexChanges.getValue()) {
ShardId shardId = shardEntry.getKey();
Updates updates = shardEntry.getValue();
- updatedIndexMetadata = updateInSyncAllocations(newRoutingTable, oldIndexMetadata, updatedIndexMetadata, shardId, updates);
+ updatedIndexMetadata = updateInSyncAllocations(
+ newRoutingTable,
+ oldIndexMetadata,
+ updatedIndexMetadata,
+ shardId,
+ updates,
+ minClusterTransportVersion
+ );
updatedIndexMetadata = updates.increaseTerm
? updatedIndexMetadata.withIncrementedPrimaryTerm(shardId.id())
: updatedIndexMetadata;
@@ -140,7 +149,8 @@ private static IndexMetadata updateInSyncAllocations(
IndexMetadata oldIndexMetadata,
IndexMetadata updatedIndexMetadata,
ShardId shardId,
- Updates updates
+ Updates updates,
+ TransportVersion minClusterTransportVersion
) {
assert Sets.haveEmptyIntersection(updates.addedAllocationIds, updates.removedAllocationIds)
: "allocation ids cannot be both added and removed in the same allocation round, added ids: "
@@ -167,10 +177,13 @@ private static IndexMetadata updateInSyncAllocations(
updatedIndexMetadata = updatedIndexMetadata.withInSyncAllocationIds(shardId.id(), Set.of());
} else {
final String allocationId;
+
if (recoverySource == RecoverySource.ExistingStoreRecoverySource.FORCE_STALE_PRIMARY_INSTANCE) {
allocationId = RecoverySource.ExistingStoreRecoverySource.FORCED_ALLOCATION_ID;
- updatedIndexMetadata = updatedIndexMetadata.withTimestampRange(
- updatedIndexMetadata.getTimestampRange().removeShard(shardId.id(), oldIndexMetadata.getNumberOfShards())
+ updatedIndexMetadata = updatedIndexMetadata.withTimestampRanges(
+ updatedIndexMetadata.getTimestampRange().removeShard(shardId.id(), oldIndexMetadata.getNumberOfShards()),
+ updatedIndexMetadata.getEventIngestedRange().removeShard(shardId.id(), oldIndexMetadata.getNumberOfShards()),
+ minClusterTransportVersion
);
} else {
assert recoverySource instanceof RecoverySource.SnapshotRecoverySource
diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/RoutingAllocation.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/RoutingAllocation.java
index 382e49135ea8d..af5f8cd7bd8c6 100644
--- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/RoutingAllocation.java
+++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/RoutingAllocation.java
@@ -339,7 +339,7 @@ public RoutingChangesObserver changes() {
* Returns updated {@link Metadata} based on the changes that were made to the routing nodes
*/
public Metadata updateMetadataWithRoutingChanges(RoutingTable newRoutingTable) {
- Metadata metadata = indexMetadataUpdater.applyChanges(metadata(), newRoutingTable);
+ Metadata metadata = indexMetadataUpdater.applyChanges(metadata(), newRoutingTable, clusterState.getMinTransportVersion());
return resizeSourceIndexUpdater.applyChanges(metadata, newRoutingTable);
}
diff --git a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java
index 55c421b87196d..ae8f8cb28da11 100644
--- a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java
+++ b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java
@@ -44,6 +44,7 @@
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.NANO_OF_SECOND;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
+import static org.elasticsearch.common.util.ArrayUtils.prepend;
public class DateFormatters {
@@ -202,7 +203,11 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
new JavaTimeDateTimePrinter(STRICT_DATE_OPTIONAL_TIME_PRINTER),
JAVA_TIME_PARSERS_ONLY
? new DateTimeParser[] { javaTimeParser }
- : new DateTimeParser[] { new Iso8601DateTimeParser(Set.of(), false).withLocale(Locale.ROOT), javaTimeParser }
+ : new DateTimeParser[] {
+ new Iso8601DateTimeParser(Set.of(), false, null, DecimalSeparator.BOTH, TimezonePresence.OPTIONAL).withLocale(
+ Locale.ROOT
+ ),
+ javaTimeParser }
);
}
@@ -266,7 +271,13 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
JAVA_TIME_PARSERS_ONLY
? new DateTimeParser[] { javaTimeParser }
: new DateTimeParser[] {
- new Iso8601DateTimeParser(Set.of(HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE), true).withLocale(Locale.ROOT),
+ new Iso8601DateTimeParser(
+ Set.of(HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE),
+ true,
+ null,
+ DecimalSeparator.BOTH,
+ TimezonePresence.OPTIONAL
+ ).withLocale(Locale.ROOT),
javaTimeParser }
);
}
@@ -316,7 +327,11 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
new JavaTimeDateTimePrinter(STRICT_DATE_OPTIONAL_TIME_PRINTER),
JAVA_TIME_PARSERS_ONLY
? new DateTimeParser[] { javaTimeParser }
- : new DateTimeParser[] { new Iso8601DateTimeParser(Set.of(), false).withLocale(Locale.ROOT), javaTimeParser }
+ : new DateTimeParser[] {
+ new Iso8601DateTimeParser(Set.of(), false, null, DecimalSeparator.BOTH, TimezonePresence.OPTIONAL).withLocale(
+ Locale.ROOT
+ ),
+ javaTimeParser }
);
}
@@ -739,24 +754,53 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
/*
* A strict formatter that formats or parses a year and a month, such as '2011-12'.
*/
- private static final DateFormatter STRICT_YEAR_MONTH = newDateFormatter(
- "strict_year_month",
- new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4, 4, SignStyle.EXCEEDS_PAD)
+ private static final DateFormatter STRICT_YEAR_MONTH;
+ static {
+ DateTimeFormatter javaTimeFormatter = new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4, 4, SignStyle.EXCEEDS_PAD)
.appendLiteral("-")
.appendValue(MONTH_OF_YEAR, 2, 2, SignStyle.NOT_NEGATIVE)
.toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT)
- );
+ .withResolverStyle(ResolverStyle.STRICT);
+ DateTimeParser javaTimeParser = new JavaTimeDateTimeParser(javaTimeFormatter);
+
+ STRICT_YEAR_MONTH = new JavaDateFormatter(
+ "strict_year_month",
+ new JavaTimeDateTimePrinter(javaTimeFormatter),
+ JAVA_TIME_PARSERS_ONLY
+ ? new DateTimeParser[] { javaTimeParser }
+ : new DateTimeParser[] {
+ new Iso8601DateTimeParser(
+ Set.of(MONTH_OF_YEAR),
+ false,
+ MONTH_OF_YEAR,
+ DecimalSeparator.BOTH,
+ TimezonePresence.FORBIDDEN
+ ).withLocale(Locale.ROOT),
+ javaTimeParser }
+ );
+ }
/*
* A strict formatter that formats or parses a year, such as '2011'.
*/
- private static final DateFormatter STRICT_YEAR = newDateFormatter(
- "strict_year",
- new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4, 4, SignStyle.EXCEEDS_PAD)
+ private static final DateFormatter STRICT_YEAR;
+ static {
+ DateTimeFormatter javaTimeFormatter = new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4, 4, SignStyle.EXCEEDS_PAD)
.toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT)
- );
+ .withResolverStyle(ResolverStyle.STRICT);
+ DateTimeParser javaTimeParser = new JavaTimeDateTimeParser(javaTimeFormatter);
+
+ STRICT_YEAR = new JavaDateFormatter(
+ "strict_year",
+ new JavaTimeDateTimePrinter(javaTimeFormatter),
+ JAVA_TIME_PARSERS_ONLY
+ ? new DateTimeParser[] { javaTimeParser }
+ : new DateTimeParser[] {
+ new Iso8601DateTimeParser(Set.of(), false, ChronoField.YEAR, DecimalSeparator.BOTH, TimezonePresence.FORBIDDEN)
+ .withLocale(Locale.ROOT),
+ javaTimeParser }
+ );
+ }
/*
* A strict formatter that formats or parses a hour, minute and second, such as '09:43:25'.
@@ -787,18 +831,39 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
* Returns a formatter that combines a full date and time, separated by a 'T'
* (uuuu-MM-dd'T'HH:mm:ss.SSSZZ).
*/
- private static final DateFormatter STRICT_DATE_TIME = newDateFormatter(
- "strict_date_time",
- STRICT_DATE_PRINTER,
- new DateTimeFormatterBuilder().append(STRICT_DATE_FORMATTER)
- .appendZoneOrOffsetId()
- .toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT),
- new DateTimeFormatterBuilder().append(STRICT_DATE_FORMATTER)
- .append(TIME_ZONE_FORMATTER_NO_COLON)
- .toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT)
- );
+ private static final DateFormatter STRICT_DATE_TIME;
+ static {
+ DateTimeParser[] javaTimeParsers = new DateTimeParser[] {
+ new JavaTimeDateTimeParser(
+ new DateTimeFormatterBuilder().append(STRICT_DATE_FORMATTER)
+ .appendZoneOrOffsetId()
+ .toFormatter(Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT)
+ ),
+ new JavaTimeDateTimeParser(
+ new DateTimeFormatterBuilder().append(STRICT_DATE_FORMATTER)
+ .append(TIME_ZONE_FORMATTER_NO_COLON)
+ .toFormatter(Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT)
+ ) };
+
+ STRICT_DATE_TIME = new JavaDateFormatter(
+ "strict_date_time",
+ new JavaTimeDateTimePrinter(STRICT_DATE_PRINTER),
+ JAVA_TIME_PARSERS_ONLY
+ ? javaTimeParsers
+ : prepend(
+ new Iso8601DateTimeParser(
+ Set.of(MONTH_OF_YEAR, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE),
+ false,
+ null,
+ DecimalSeparator.DOT,
+ TimezonePresence.MANDATORY
+ ).withLocale(Locale.ROOT),
+ javaTimeParsers
+ )
+ );
+ }
private static final DateTimeFormatter STRICT_ORDINAL_DATE_TIME_NO_MILLIS_BASE = new DateTimeFormatterBuilder().appendValue(
ChronoField.YEAR,
@@ -841,21 +906,44 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
* Returns a formatter that combines a full date and time without millis,
* separated by a 'T' (uuuu-MM-dd'T'HH:mm:ssZZ).
*/
- private static final DateFormatter STRICT_DATE_TIME_NO_MILLIS = newDateFormatter(
- "strict_date_time_no_millis",
- new DateTimeFormatterBuilder().append(STRICT_DATE_TIME_NO_MILLIS_FORMATTER)
- .appendOffset("+HH:MM", "Z")
- .toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT),
- new DateTimeFormatterBuilder().append(STRICT_DATE_TIME_NO_MILLIS_FORMATTER)
- .appendZoneOrOffsetId()
- .toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT),
- new DateTimeFormatterBuilder().append(STRICT_DATE_TIME_NO_MILLIS_FORMATTER)
- .append(TIME_ZONE_FORMATTER_NO_COLON)
- .toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT)
- );
+ private static final DateFormatter STRICT_DATE_TIME_NO_MILLIS;
+ static {
+ DateTimeParser[] javaTimeParsers = new DateTimeParser[] {
+ new JavaTimeDateTimeParser(
+ new DateTimeFormatterBuilder().append(STRICT_DATE_TIME_NO_MILLIS_FORMATTER)
+ .appendZoneOrOffsetId()
+ .toFormatter(Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT)
+ ),
+ new JavaTimeDateTimeParser(
+ new DateTimeFormatterBuilder().append(STRICT_DATE_TIME_NO_MILLIS_FORMATTER)
+ .append(TIME_ZONE_FORMATTER_NO_COLON)
+ .toFormatter(Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT)
+ ) };
+
+ STRICT_DATE_TIME_NO_MILLIS = new JavaDateFormatter(
+ "strict_date_time_no_millis",
+ new JavaTimeDateTimePrinter(
+ new DateTimeFormatterBuilder().append(STRICT_DATE_TIME_NO_MILLIS_FORMATTER)
+ .appendOffset("+HH:MM", "Z")
+ .toFormatter(Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT)
+ ),
+ JAVA_TIME_PARSERS_ONLY
+ ? javaTimeParsers
+ : prepend(
+ new Iso8601DateTimeParser(
+ Set.of(MONTH_OF_YEAR, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE),
+ false,
+ SECOND_OF_MINUTE,
+ DecimalSeparator.BOTH,
+ TimezonePresence.MANDATORY
+ ).withLocale(Locale.ROOT),
+ javaTimeParsers
+ )
+ );
+ }
// NOTE: this is not a strict formatter to retain the joda time based behaviour, even though it's named like this
private static final DateTimeFormatter STRICT_HOUR_MINUTE_SECOND_MILLIS_FORMATTER = new DateTimeFormatterBuilder().append(
@@ -891,37 +979,75 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
* two digit minute of hour, two digit second of minute, and three digit
* fraction of second (uuuu-MM-dd'T'HH:mm:ss.SSS).
*/
- private static final DateFormatter STRICT_DATE_HOUR_MINUTE_SECOND_FRACTION = newDateFormatter(
- "strict_date_hour_minute_second_fraction",
- new DateTimeFormatterBuilder().append(STRICT_YEAR_MONTH_DAY_FORMATTER)
- .appendLiteral("T")
- .append(STRICT_HOUR_MINUTE_SECOND_MILLIS_PRINTER)
- .toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT),
- new DateTimeFormatterBuilder().append(STRICT_YEAR_MONTH_DAY_FORMATTER)
- .appendLiteral("T")
- .append(STRICT_HOUR_MINUTE_SECOND_FORMATTER)
- // this one here is lenient as well to retain joda time based bwc compatibility
- .appendFraction(NANO_OF_SECOND, 1, 9, true)
- .toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT)
- );
+ private static final DateFormatter STRICT_DATE_HOUR_MINUTE_SECOND_FRACTION;
+ static {
+ DateTimeParser javaTimeParser = new JavaTimeDateTimeParser(
+ new DateTimeFormatterBuilder().append(STRICT_YEAR_MONTH_DAY_FORMATTER)
+ .appendLiteral("T")
+ .append(STRICT_HOUR_MINUTE_SECOND_FORMATTER)
+ // this one here is lenient as well to retain joda time based bwc compatibility
+ .appendFraction(NANO_OF_SECOND, 1, 9, true)
+ .toFormatter(Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT)
+ );
- private static final DateFormatter STRICT_DATE_HOUR_MINUTE_SECOND_MILLIS = newDateFormatter(
- "strict_date_hour_minute_second_millis",
- new DateTimeFormatterBuilder().append(STRICT_YEAR_MONTH_DAY_FORMATTER)
- .appendLiteral("T")
- .append(STRICT_HOUR_MINUTE_SECOND_MILLIS_PRINTER)
- .toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT),
- new DateTimeFormatterBuilder().append(STRICT_YEAR_MONTH_DAY_FORMATTER)
- .appendLiteral("T")
- .append(STRICT_HOUR_MINUTE_SECOND_FORMATTER)
- // this one here is lenient as well to retain joda time based bwc compatibility
- .appendFraction(NANO_OF_SECOND, 1, 9, true)
- .toFormatter(Locale.ROOT)
- .withResolverStyle(ResolverStyle.STRICT)
- );
+ STRICT_DATE_HOUR_MINUTE_SECOND_FRACTION = new JavaDateFormatter(
+ "strict_date_hour_minute_second_fraction",
+ new JavaTimeDateTimePrinter(
+ new DateTimeFormatterBuilder().append(STRICT_YEAR_MONTH_DAY_FORMATTER)
+ .appendLiteral("T")
+ .append(STRICT_HOUR_MINUTE_SECOND_MILLIS_PRINTER)
+ .toFormatter(Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT)
+ ),
+ JAVA_TIME_PARSERS_ONLY
+ ? new DateTimeParser[] { javaTimeParser }
+ : new DateTimeParser[] {
+ new Iso8601DateTimeParser(
+ Set.of(MONTH_OF_YEAR, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE, NANO_OF_SECOND),
+ false,
+ null,
+ DecimalSeparator.DOT,
+ TimezonePresence.FORBIDDEN
+ ).withLocale(Locale.ROOT),
+ javaTimeParser }
+ );
+ }
+
+ private static final DateFormatter STRICT_DATE_HOUR_MINUTE_SECOND_MILLIS;
+ static {
+ DateTimeParser javaTimeParser = new JavaTimeDateTimeParser(
+ new DateTimeFormatterBuilder().append(STRICT_YEAR_MONTH_DAY_FORMATTER)
+ .appendLiteral("T")
+ .append(STRICT_HOUR_MINUTE_SECOND_FORMATTER)
+ // this one here is lenient as well to retain joda time based bwc compatibility
+ .appendFraction(NANO_OF_SECOND, 1, 9, true)
+ .toFormatter(Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT)
+ );
+
+ STRICT_DATE_HOUR_MINUTE_SECOND_MILLIS = new JavaDateFormatter(
+ "strict_date_hour_minute_second_millis",
+ new JavaTimeDateTimePrinter(
+ new DateTimeFormatterBuilder().append(STRICT_YEAR_MONTH_DAY_FORMATTER)
+ .appendLiteral("T")
+ .append(STRICT_HOUR_MINUTE_SECOND_MILLIS_PRINTER)
+ .toFormatter(Locale.ROOT)
+ .withResolverStyle(ResolverStyle.STRICT)
+ ),
+ JAVA_TIME_PARSERS_ONLY
+ ? new DateTimeParser[] { javaTimeParser }
+ : new DateTimeParser[] {
+ new Iso8601DateTimeParser(
+ Set.of(MONTH_OF_YEAR, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE, NANO_OF_SECOND),
+ false,
+ null,
+ DecimalSeparator.DOT,
+ TimezonePresence.FORBIDDEN
+ ).withLocale(Locale.ROOT),
+ javaTimeParser }
+ );
+ }
/*
* Returns a formatter for a two digit hour of day. (HH)
@@ -1235,10 +1361,27 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p
* two digit minute of hour, and two digit second of
* minute. (uuuu-MM-dd'T'HH:mm:ss)
*/
- private static final DateFormatter STRICT_DATE_HOUR_MINUTE_SECOND = newDateFormatter(
- "strict_date_hour_minute_second",
- DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss", Locale.ROOT)
- );
+ private static final DateFormatter STRICT_DATE_HOUR_MINUTE_SECOND;
+ static {
+ DateTimeFormatter javaTimeFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss", Locale.ROOT);
+ DateTimeParser javaTimeParser = new JavaTimeDateTimeParser(javaTimeFormatter);
+
+ STRICT_DATE_HOUR_MINUTE_SECOND = new JavaDateFormatter(
+ "strict_date_hour_minute_second",
+ new JavaTimeDateTimePrinter(javaTimeFormatter),
+ JAVA_TIME_PARSERS_ONLY
+ ? new DateTimeParser[] { javaTimeParser }
+ : new DateTimeParser[] {
+ new Iso8601DateTimeParser(
+ Set.of(MONTH_OF_YEAR, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE),
+ false,
+ SECOND_OF_MINUTE,
+ DecimalSeparator.BOTH,
+ TimezonePresence.FORBIDDEN
+ ).withLocale(Locale.ROOT),
+ javaTimeParser }
+ );
+ }
/*
* A basic formatter for a full date as four digit year, two digit
diff --git a/server/src/main/java/org/elasticsearch/common/time/DecimalSeparator.java b/server/src/main/java/org/elasticsearch/common/time/DecimalSeparator.java
new file mode 100644
index 0000000000000..3598599e1f759
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/common/time/DecimalSeparator.java
@@ -0,0 +1,15 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.common.time;
+
+enum DecimalSeparator {
+ DOT,
+ COMMA,
+ BOTH
+}
diff --git a/server/src/main/java/org/elasticsearch/common/time/Iso8601DateTimeParser.java b/server/src/main/java/org/elasticsearch/common/time/Iso8601DateTimeParser.java
index cce4b13f4a166..027c1ec94a411 100644
--- a/server/src/main/java/org/elasticsearch/common/time/Iso8601DateTimeParser.java
+++ b/server/src/main/java/org/elasticsearch/common/time/Iso8601DateTimeParser.java
@@ -24,8 +24,14 @@ class Iso8601DateTimeParser implements DateTimeParser {
// and we already account for . or , in decimals
private final Locale locale;
- Iso8601DateTimeParser(Set mandatoryFields, boolean optionalTime) {
- parser = new Iso8601Parser(mandatoryFields, optionalTime, Map.of());
+ Iso8601DateTimeParser(
+ Set mandatoryFields,
+ boolean optionalTime,
+ ChronoField maxAllowedField,
+ DecimalSeparator decimalSeparator,
+ TimezonePresence timezonePresence
+ ) {
+ parser = new Iso8601Parser(mandatoryFields, optionalTime, maxAllowedField, decimalSeparator, timezonePresence, Map.of());
timezone = null;
locale = null;
}
@@ -57,7 +63,18 @@ public DateTimeParser withLocale(Locale locale) {
}
Iso8601DateTimeParser withDefaults(Map defaults) {
- return new Iso8601DateTimeParser(new Iso8601Parser(parser.mandatoryFields(), parser.optionalTime(), defaults), timezone, locale);
+ return new Iso8601DateTimeParser(
+ new Iso8601Parser(
+ parser.mandatoryFields(),
+ parser.optionalTime(),
+ parser.maxAllowedField(),
+ parser.decimalSeparator(),
+ parser.timezonePresence(),
+ defaults
+ ),
+ timezone,
+ locale
+ );
}
@Override
diff --git a/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java b/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java
index fe92ff62b6ddc..6e420df9c72dd 100644
--- a/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java
+++ b/server/src/main/java/org/elasticsearch/common/time/Iso8601Parser.java
@@ -13,16 +13,18 @@
import java.time.DateTimeException;
import java.time.ZoneId;
import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
* Parses datetimes in ISO8601 format (and subsequences thereof).
*
- * This is faster than the generic parsing in {@link java.time.format.DateTimeFormatter}, as this is hard-coded and specific to ISO-8601.
+ * This is faster than the generic parsing in {@link DateTimeFormatter}, as this is hard-coded and specific to ISO-8601.
* Various public libraries provide their own variant of this mechanism. We use our own for a few reasons:
*
*
@@ -37,13 +39,14 @@
*/
class Iso8601Parser {
- private static final Set VALID_MANDATORY_FIELDS = EnumSet.of(
+ private static final Set VALID_SPECIFIED_FIELDS = EnumSet.of(
ChronoField.YEAR,
ChronoField.MONTH_OF_YEAR,
ChronoField.DAY_OF_MONTH,
ChronoField.HOUR_OF_DAY,
ChronoField.MINUTE_OF_HOUR,
- ChronoField.SECOND_OF_MINUTE
+ ChronoField.SECOND_OF_MINUTE,
+ ChronoField.NANO_OF_SECOND
);
private static final Set VALID_DEFAULT_FIELDS = EnumSet.of(
@@ -57,31 +60,51 @@ class Iso8601Parser {
private final Set mandatoryFields;
private final boolean optionalTime;
+ @Nullable
+ private final ChronoField maxAllowedField;
+ private final DecimalSeparator decimalSeparator;
+ private final TimezonePresence timezonePresence;
private final Map defaults;
/**
* Constructs a new {@code Iso8601Parser} object
*
- * @param mandatoryFields
- * The set of fields that must be present for a valid parse. These should be specified in field order
- * (eg if {@link ChronoField#DAY_OF_MONTH} is specified, {@link ChronoField#MONTH_OF_YEAR} should also be specified).
- * {@link ChronoField#YEAR} is always mandatory.
- * @param optionalTime
- * {@code false} if the presence of time fields follows {@code mandatoryFields},
- * {@code true} if a time component is always optional, despite the presence of time fields in {@code mandatoryFields}.
- * This makes it possible to specify 'time is optional, but if it is present, it must have these fields'
- * by settings {@code optionalTime = true} and putting time fields such as {@link ChronoField#HOUR_OF_DAY}
- * and {@link ChronoField#MINUTE_OF_HOUR} in {@code mandatoryFields}.
- * @param defaults
- * Map of default field values, if they are not present in the parsed string.
+ * @param mandatoryFields The set of fields that must be present for a valid parse. These should be specified in field order
+ * (eg if {@link ChronoField#DAY_OF_MONTH} is specified,
+ * {@link ChronoField#MONTH_OF_YEAR} should also be specified).
+ * {@link ChronoField#YEAR} is always mandatory.
+ * @param optionalTime {@code false} if the presence of time fields follows {@code mandatoryFields},
+ * {@code true} if a time component is always optional,
+ * despite the presence of time fields in {@code mandatoryFields}.
+ * This makes it possible to specify 'time is optional, but if it is present, it must have these fields'
+ * by settings {@code optionalTime = true} and putting time fields such as {@link ChronoField#HOUR_OF_DAY}
+ * and {@link ChronoField#MINUTE_OF_HOUR} in {@code mandatoryFields}.
+ * @param maxAllowedField The most-specific field allowed in the parsed string,
+ * or {@code null} if everything up to nanoseconds is allowed.
+ * @param decimalSeparator The decimal separator that is allowed.
+ * @param timezonePresence Specifies if the timezone is optional, mandatory, or forbidden.
+ * @param defaults Map of default field values, if they are not present in the parsed string.
*/
- Iso8601Parser(Set mandatoryFields, boolean optionalTime, Map defaults) {
- checkChronoFields(mandatoryFields, VALID_MANDATORY_FIELDS);
+ Iso8601Parser(
+ Set mandatoryFields,
+ boolean optionalTime,
+ @Nullable ChronoField maxAllowedField,
+ DecimalSeparator decimalSeparator,
+ TimezonePresence timezonePresence,
+ Map defaults
+ ) {
+ checkChronoFields(mandatoryFields, VALID_SPECIFIED_FIELDS);
+ if (maxAllowedField != null && VALID_SPECIFIED_FIELDS.contains(maxAllowedField) == false) {
+ throw new IllegalArgumentException("Invalid chrono field specified " + maxAllowedField);
+ }
checkChronoFields(defaults.keySet(), VALID_DEFAULT_FIELDS);
this.mandatoryFields = EnumSet.of(ChronoField.YEAR); // year is always mandatory
this.mandatoryFields.addAll(mandatoryFields);
this.optionalTime = optionalTime;
+ this.maxAllowedField = maxAllowedField;
+ this.decimalSeparator = Objects.requireNonNull(decimalSeparator);
+ this.timezonePresence = Objects.requireNonNull(timezonePresence);
this.defaults = defaults.isEmpty() ? Map.of() : new EnumMap<>(defaults);
}
@@ -103,6 +126,18 @@ Set mandatoryFields() {
return mandatoryFields;
}
+ ChronoField maxAllowedField() {
+ return maxAllowedField;
+ }
+
+ DecimalSeparator decimalSeparator() {
+ return decimalSeparator;
+ }
+
+ TimezonePresence timezonePresence() {
+ return timezonePresence;
+ }
+
private boolean isOptional(ChronoField field) {
return mandatoryFields.contains(field) == false;
}
@@ -186,7 +221,7 @@ private ParseResult parse(CharSequence str, @Nullable ZoneId defaultTimezone) {
: ParseResult.error(4);
}
- if (str.charAt(4) != '-') return ParseResult.error(4);
+ if (str.charAt(4) != '-' || maxAllowedField == ChronoField.YEAR) return ParseResult.error(4);
// MONTHS
Integer months = parseInt(str, 5, 7);
@@ -208,7 +243,7 @@ private ParseResult parse(CharSequence str, @Nullable ZoneId defaultTimezone) {
: ParseResult.error(7);
}
- if (str.charAt(7) != '-') return ParseResult.error(7);
+ if (str.charAt(7) != '-' || maxAllowedField == ChronoField.MONTH_OF_YEAR) return ParseResult.error(7);
// DAYS
Integer days = parseInt(str, 8, 10);
@@ -230,7 +265,7 @@ private ParseResult parse(CharSequence str, @Nullable ZoneId defaultTimezone) {
: ParseResult.error(10);
}
- if (str.charAt(10) != 'T') return ParseResult.error(10);
+ if (str.charAt(10) != 'T' || maxAllowedField == ChronoField.DAY_OF_MONTH) return ParseResult.error(10);
if (len == 11) {
return isOptional(ChronoField.HOUR_OF_DAY)
? new ParseResult(
@@ -252,7 +287,7 @@ private ParseResult parse(CharSequence str, @Nullable ZoneId defaultTimezone) {
Integer hours = parseInt(str, 11, 13);
if (hours == null || hours > 23) return ParseResult.error(11);
if (len == 13) {
- return isOptional(ChronoField.MINUTE_OF_HOUR)
+ return isOptional(ChronoField.MINUTE_OF_HOUR) && timezonePresence != TimezonePresence.MANDATORY
? new ParseResult(
withZoneOffset(
years,
@@ -285,13 +320,13 @@ private ParseResult parse(CharSequence str, @Nullable ZoneId defaultTimezone) {
: ParseResult.error(13);
}
- if (str.charAt(13) != ':') return ParseResult.error(13);
+ if (str.charAt(13) != ':' || maxAllowedField == ChronoField.HOUR_OF_DAY) return ParseResult.error(13);
// MINUTES + timezone
Integer minutes = parseInt(str, 14, 16);
if (minutes == null || minutes > 59) return ParseResult.error(14);
if (len == 16) {
- return isOptional(ChronoField.SECOND_OF_MINUTE)
+ return isOptional(ChronoField.SECOND_OF_MINUTE) && timezonePresence != TimezonePresence.MANDATORY
? new ParseResult(
withZoneOffset(
years,
@@ -324,15 +359,17 @@ private ParseResult parse(CharSequence str, @Nullable ZoneId defaultTimezone) {
: ParseResult.error(16);
}
- if (str.charAt(16) != ':') return ParseResult.error(16);
+ if (str.charAt(16) != ':' || maxAllowedField == ChronoField.MINUTE_OF_HOUR) return ParseResult.error(16);
// SECONDS + timezone
Integer seconds = parseInt(str, 17, 19);
if (seconds == null || seconds > 59) return ParseResult.error(17);
if (len == 19) {
- return new ParseResult(
- withZoneOffset(years, months, days, hours, minutes, seconds, defaultZero(ChronoField.NANO_OF_SECOND), defaultTimezone)
- );
+ return isOptional(ChronoField.NANO_OF_SECOND) && timezonePresence != TimezonePresence.MANDATORY
+ ? new ParseResult(
+ withZoneOffset(years, months, days, hours, minutes, seconds, defaultZero(ChronoField.NANO_OF_SECOND), defaultTimezone)
+ )
+ : ParseResult.error(19);
}
if (isZoneId(str, 19)) {
ZoneId timezone = parseZoneId(str, 19);
@@ -343,11 +380,9 @@ private ParseResult parse(CharSequence str, @Nullable ZoneId defaultTimezone) {
: ParseResult.error(19);
}
- char decSeparator = str.charAt(19);
- if (decSeparator != '.' && decSeparator != ',') return ParseResult.error(19);
+ if (checkDecimalSeparator(str.charAt(19)) == false || maxAllowedField == ChronoField.SECOND_OF_MINUTE) return ParseResult.error(19);
// NANOS + timezone
- // nanos are always optional
// the last number could be millis or nanos, or any combination in the middle
// so we keep parsing numbers until we get to not a number
int nanos = 0;
@@ -364,7 +399,9 @@ private ParseResult parse(CharSequence str, @Nullable ZoneId defaultTimezone) {
nanos *= NANO_MULTIPLICANDS[29 - pos];
if (len == pos) {
- return new ParseResult(withZoneOffset(years, months, days, hours, minutes, seconds, nanos, defaultTimezone));
+ return timezonePresence != TimezonePresence.MANDATORY
+ ? new ParseResult(withZoneOffset(years, months, days, hours, minutes, seconds, nanos, defaultTimezone))
+ : ParseResult.error(pos);
}
if (isZoneId(str, pos)) {
ZoneId timezone = parseZoneId(str, pos);
@@ -377,6 +414,16 @@ private ParseResult parse(CharSequence str, @Nullable ZoneId defaultTimezone) {
return ParseResult.error(pos);
}
+ private boolean checkDecimalSeparator(char separator) {
+ boolean isDot = separator == '.';
+ boolean isComma = separator == ',';
+ return switch (decimalSeparator) {
+ case DOT -> isDot;
+ case COMMA -> isComma;
+ case BOTH -> isDot || isComma;
+ };
+ }
+
private static boolean isZoneId(CharSequence str, int pos) {
// all region zoneIds must start with [A-Za-z] (see ZoneId#of)
// this also covers Z and UT/UTC/GMT zone variants
@@ -385,10 +432,14 @@ private static boolean isZoneId(CharSequence str, int pos) {
}
/**
- * This parses the zone offset, which is of the format accepted by {@link java.time.ZoneId#of(String)}.
+ * This parses the zone offset, which is of the format accepted by {@link ZoneId#of(String)}.
* It has fast paths for numerical offsets, but falls back on {@code ZoneId.of} for non-trivial zone ids.
*/
private ZoneId parseZoneId(CharSequence str, int pos) {
+ if (timezonePresence == TimezonePresence.FORBIDDEN) {
+ return null;
+ }
+
int len = str.length();
char first = str.charAt(pos);
diff --git a/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java
index e8d729f9e9977..79b0c44d39108 100644
--- a/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java
+++ b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java
@@ -18,7 +18,6 @@
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -149,19 +148,24 @@ static DateFormatter combined(String input, List formatters) {
assert formatters.isEmpty() == false;
DateTimePrinter printer = null;
- List parsers = new ArrayList<>(formatters.size());
- List roundUpParsers = new ArrayList<>(formatters.size());
+ List parsers = new ArrayList<>(formatters.size());
+ List roundUpParsers = new ArrayList<>(formatters.size());
for (DateFormatter formatter : formatters) {
JavaDateFormatter javaDateFormatter = (JavaDateFormatter) formatter;
if (printer == null) {
printer = javaDateFormatter.printer;
}
- Collections.addAll(parsers, javaDateFormatter.parsers);
- Collections.addAll(roundUpParsers, javaDateFormatter.roundupParsers);
+ parsers.add(javaDateFormatter.parsers);
+ roundUpParsers.add(javaDateFormatter.roundupParsers);
}
- return new JavaDateFormatter(input, printer, roundUpParsers.toArray(DateTimeParser[]::new), parsers.toArray(DateTimeParser[]::new));
+ return new JavaDateFormatter(
+ input,
+ printer,
+ roundUpParsers.stream().flatMap(Arrays::stream).toArray(DateTimeParser[]::new),
+ parsers.stream().flatMap(Arrays::stream).toArray(DateTimeParser[]::new)
+ );
}
private JavaDateFormatter(String format, DateTimePrinter printer, DateTimeParser[] roundupParsers, DateTimeParser[] parsers) {
diff --git a/server/src/main/java/org/elasticsearch/common/time/TimezonePresence.java b/server/src/main/java/org/elasticsearch/common/time/TimezonePresence.java
new file mode 100644
index 0000000000000..fd8cdcc28976d
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/common/time/TimezonePresence.java
@@ -0,0 +1,15 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.common.time;
+
+enum TimezonePresence {
+ OPTIONAL,
+ MANDATORY,
+ FORBIDDEN
+}
diff --git a/server/src/main/java/org/elasticsearch/common/util/ArrayUtils.java b/server/src/main/java/org/elasticsearch/common/util/ArrayUtils.java
index 2f1264fa88247..0b48a298fe59a 100644
--- a/server/src/main/java/org/elasticsearch/common/util/ArrayUtils.java
+++ b/server/src/main/java/org/elasticsearch/common/util/ArrayUtils.java
@@ -68,6 +68,21 @@ public static T[] concat(T[] one, T[] other) {
return target;
}
+ /**
+ * Copy the given element and array into a new array of size {@code array.length + 1}.
+ * @param added first element in the newly created array
+ * @param array array to copy to the end of new returned array copy
+ * @return copy that contains added element and array
+ * @param type of the array elements
+ */
+ public static T[] prepend(T added, T[] array) {
+ @SuppressWarnings("unchecked")
+ T[] updated = (T[]) Array.newInstance(array.getClass().getComponentType(), array.length + 1);
+ updated[0] = added;
+ System.arraycopy(array, 0, updated, 1, array.length);
+ return updated;
+ }
+
/**
* Copy the given array and the added element into a new array of size {@code array.length + 1}.
* @param array array to copy to the beginning of new returned array copy
@@ -76,9 +91,7 @@ public static T[] concat(T[] one, T[] other) {
* @param type of the array elements
*/
public static T[] append(T[] array, T added) {
- @SuppressWarnings("unchecked")
- final T[] updated = (T[]) Array.newInstance(added.getClass(), array.length + 1);
- System.arraycopy(array, 0, updated, 0, array.length);
+ T[] updated = Arrays.copyOf(array, array.length + 1);
updated[array.length] = added;
return updated;
}
diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/ES813FlatVectorFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/ES813FlatVectorFormat.java
index 690b580d0c322..861f5ecd56f5a 100644
--- a/server/src/main/java/org/elasticsearch/index/codec/vectors/ES813FlatVectorFormat.java
+++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/ES813FlatVectorFormat.java
@@ -54,11 +54,11 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException
return new ES813FlatVectorReader(format.fieldsReader(state));
}
- public static class ES813FlatVectorWriter extends KnnVectorsWriter {
+ static class ES813FlatVectorWriter extends KnnVectorsWriter {
private final FlatVectorsWriter writer;
- public ES813FlatVectorWriter(FlatVectorsWriter writer) {
+ ES813FlatVectorWriter(FlatVectorsWriter writer) {
super();
this.writer = writer;
}
@@ -94,11 +94,11 @@ public void mergeOneField(FieldInfo fieldInfo, MergeState mergeState) throws IOE
}
}
- public static class ES813FlatVectorReader extends KnnVectorsReader {
+ static class ES813FlatVectorReader extends KnnVectorsReader {
private final FlatVectorsReader reader;
- public ES813FlatVectorReader(FlatVectorsReader reader) {
+ ES813FlatVectorReader(FlatVectorsReader reader) {
super();
this.reader = reader;
}
diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/ES815BitFlatVectorFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/ES815BitFlatVectorFormat.java
new file mode 100644
index 0000000000000..86bc58c5862ee
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/ES815BitFlatVectorFormat.java
@@ -0,0 +1,47 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.codec.vectors;
+
+import org.apache.lucene.codecs.KnnVectorsFormat;
+import org.apache.lucene.codecs.KnnVectorsReader;
+import org.apache.lucene.codecs.KnnVectorsWriter;
+import org.apache.lucene.codecs.hnsw.FlatVectorsFormat;
+import org.apache.lucene.index.SegmentReadState;
+import org.apache.lucene.index.SegmentWriteState;
+
+import java.io.IOException;
+
+public class ES815BitFlatVectorFormat extends KnnVectorsFormat {
+
+ static final String NAME = "ES815BitFlatVectorFormat";
+
+ private final FlatVectorsFormat format = new ES815BitFlatVectorsFormat();
+
+ /**
+ * Sole constructor
+ */
+ public ES815BitFlatVectorFormat() {
+ super(NAME);
+ }
+
+ @Override
+ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException {
+ return new ES813FlatVectorFormat.ES813FlatVectorWriter(format.fieldsWriter(state));
+ }
+
+ @Override
+ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException {
+ return new ES813FlatVectorFormat.ES813FlatVectorReader(format.fieldsReader(state));
+ }
+
+ @Override
+ public String toString() {
+ return NAME;
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/ES815BitFlatVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/ES815BitFlatVectorsFormat.java
new file mode 100644
index 0000000000000..659cc89bfe46d
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/ES815BitFlatVectorsFormat.java
@@ -0,0 +1,143 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.codec.vectors;
+
+import org.apache.lucene.codecs.hnsw.FlatVectorsFormat;
+import org.apache.lucene.codecs.hnsw.FlatVectorsReader;
+import org.apache.lucene.codecs.hnsw.FlatVectorsScorer;
+import org.apache.lucene.codecs.hnsw.FlatVectorsWriter;
+import org.apache.lucene.codecs.lucene99.Lucene99FlatVectorsFormat;
+import org.apache.lucene.index.SegmentReadState;
+import org.apache.lucene.index.SegmentWriteState;
+import org.apache.lucene.index.VectorSimilarityFunction;
+import org.apache.lucene.util.VectorUtil;
+import org.apache.lucene.util.hnsw.RandomAccessVectorValues;
+import org.apache.lucene.util.hnsw.RandomVectorScorer;
+import org.apache.lucene.util.hnsw.RandomVectorScorerSupplier;
+import org.apache.lucene.util.quantization.RandomAccessQuantizedByteVectorValues;
+
+import java.io.IOException;
+
+class ES815BitFlatVectorsFormat extends FlatVectorsFormat {
+
+ private final FlatVectorsFormat delegate = new Lucene99FlatVectorsFormat(FlatBitVectorScorer.INSTANCE);
+
+ @Override
+ public FlatVectorsWriter fieldsWriter(SegmentWriteState segmentWriteState) throws IOException {
+ return delegate.fieldsWriter(segmentWriteState);
+ }
+
+ @Override
+ public FlatVectorsReader fieldsReader(SegmentReadState segmentReadState) throws IOException {
+ return delegate.fieldsReader(segmentReadState);
+ }
+
+ static class FlatBitVectorScorer implements FlatVectorsScorer {
+
+ static final FlatBitVectorScorer INSTANCE = new FlatBitVectorScorer();
+
+ static void checkDimensions(int queryLen, int fieldLen) {
+ if (queryLen != fieldLen) {
+ throw new IllegalArgumentException("vector query dimension: " + queryLen + " differs from field dimension: " + fieldLen);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString();
+ }
+
+ @Override
+ public RandomVectorScorerSupplier getRandomVectorScorerSupplier(
+ VectorSimilarityFunction vectorSimilarityFunction,
+ RandomAccessVectorValues randomAccessVectorValues
+ ) throws IOException {
+ assert randomAccessVectorValues instanceof RandomAccessVectorValues.Bytes;
+ assert vectorSimilarityFunction == VectorSimilarityFunction.EUCLIDEAN;
+ if (randomAccessVectorValues instanceof RandomAccessVectorValues.Bytes randomAccessVectorValuesBytes) {
+ assert randomAccessVectorValues instanceof RandomAccessQuantizedByteVectorValues == false;
+ return switch (vectorSimilarityFunction) {
+ case DOT_PRODUCT, MAXIMUM_INNER_PRODUCT, COSINE, EUCLIDEAN -> new HammingScorerSupplier(randomAccessVectorValuesBytes);
+ };
+ }
+ throw new IllegalArgumentException("Unsupported vector type or similarity function");
+ }
+
+ @Override
+ public RandomVectorScorer getRandomVectorScorer(
+ VectorSimilarityFunction vectorSimilarityFunction,
+ RandomAccessVectorValues randomAccessVectorValues,
+ byte[] bytes
+ ) {
+ assert randomAccessVectorValues instanceof RandomAccessVectorValues.Bytes;
+ assert vectorSimilarityFunction == VectorSimilarityFunction.EUCLIDEAN;
+ if (randomAccessVectorValues instanceof RandomAccessVectorValues.Bytes randomAccessVectorValuesBytes) {
+ checkDimensions(bytes.length, randomAccessVectorValuesBytes.dimension());
+ return switch (vectorSimilarityFunction) {
+ case DOT_PRODUCT, MAXIMUM_INNER_PRODUCT, COSINE, EUCLIDEAN -> new HammingVectorScorer(
+ randomAccessVectorValuesBytes,
+ bytes
+ );
+ };
+ }
+ throw new IllegalArgumentException("Unsupported vector type or similarity function");
+ }
+
+ @Override
+ public RandomVectorScorer getRandomVectorScorer(
+ VectorSimilarityFunction vectorSimilarityFunction,
+ RandomAccessVectorValues randomAccessVectorValues,
+ float[] floats
+ ) {
+ throw new IllegalArgumentException("Unsupported vector type");
+ }
+ }
+
+ static float hammingScore(byte[] a, byte[] b) {
+ return ((a.length * Byte.SIZE) - VectorUtil.xorBitCount(a, b)) / (float) (a.length * Byte.SIZE);
+ }
+
+ static class HammingVectorScorer extends RandomVectorScorer.AbstractRandomVectorScorer {
+ private final byte[] query;
+ private final RandomAccessVectorValues.Bytes byteValues;
+
+ HammingVectorScorer(RandomAccessVectorValues.Bytes byteValues, byte[] query) {
+ super(byteValues);
+ this.query = query;
+ this.byteValues = byteValues;
+ }
+
+ @Override
+ public float score(int i) throws IOException {
+ return hammingScore(byteValues.vectorValue(i), query);
+ }
+ }
+
+ static class HammingScorerSupplier implements RandomVectorScorerSupplier {
+ private final RandomAccessVectorValues.Bytes byteValues, byteValues1, byteValues2;
+
+ HammingScorerSupplier(RandomAccessVectorValues.Bytes byteValues) throws IOException {
+ this.byteValues = byteValues;
+ this.byteValues1 = byteValues.copy();
+ this.byteValues2 = byteValues.copy();
+ }
+
+ @Override
+ public RandomVectorScorer scorer(int i) throws IOException {
+ byte[] query = byteValues1.vectorValue(i);
+ return new HammingVectorScorer(byteValues2, query);
+ }
+
+ @Override
+ public RandomVectorScorerSupplier copy() throws IOException {
+ return new HammingScorerSupplier(byteValues);
+ }
+ }
+
+}
diff --git a/server/src/main/java/org/elasticsearch/index/codec/vectors/ES815HnswBitVectorsFormat.java b/server/src/main/java/org/elasticsearch/index/codec/vectors/ES815HnswBitVectorsFormat.java
new file mode 100644
index 0000000000000..f7884c0b73688
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/index/codec/vectors/ES815HnswBitVectorsFormat.java
@@ -0,0 +1,74 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.codec.vectors;
+
+import org.apache.lucene.codecs.KnnVectorsFormat;
+import org.apache.lucene.codecs.KnnVectorsReader;
+import org.apache.lucene.codecs.KnnVectorsWriter;
+import org.apache.lucene.codecs.hnsw.FlatVectorsFormat;
+import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsReader;
+import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsWriter;
+import org.apache.lucene.index.SegmentReadState;
+import org.apache.lucene.index.SegmentWriteState;
+
+import java.io.IOException;
+
+public class ES815HnswBitVectorsFormat extends KnnVectorsFormat {
+
+ static final String NAME = "ES815HnswBitVectorsFormat";
+
+ static final int MAXIMUM_MAX_CONN = 512;
+ static final int MAXIMUM_BEAM_WIDTH = 3200;
+
+ private final int maxConn;
+ private final int beamWidth;
+
+ private final FlatVectorsFormat flatVectorsFormat = new ES815BitFlatVectorsFormat();
+
+ public ES815HnswBitVectorsFormat() {
+ this(16, 100);
+ }
+
+ public ES815HnswBitVectorsFormat(int maxConn, int beamWidth) {
+ super(NAME);
+ if (maxConn <= 0 || maxConn > MAXIMUM_MAX_CONN) {
+ throw new IllegalArgumentException(
+ "maxConn must be positive and less than or equal to " + MAXIMUM_MAX_CONN + "; maxConn=" + maxConn
+ );
+ }
+ if (beamWidth <= 0 || beamWidth > MAXIMUM_BEAM_WIDTH) {
+ throw new IllegalArgumentException(
+ "beamWidth must be positive and less than or equal to " + MAXIMUM_BEAM_WIDTH + "; beamWidth=" + beamWidth
+ );
+ }
+ this.maxConn = maxConn;
+ this.beamWidth = beamWidth;
+ }
+
+ @Override
+ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException {
+ return new Lucene99HnswVectorsWriter(state, maxConn, beamWidth, flatVectorsFormat.fieldsWriter(state), 1, null);
+ }
+
+ @Override
+ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException {
+ return new Lucene99HnswVectorsReader(state, flatVectorsFormat.fieldsReader(state));
+ }
+
+ @Override
+ public String toString() {
+ return "ES815HnswBitVectorsFormat(name=ES815HnswBitVectorsFormat, maxConn="
+ + maxConn
+ + ", beamWidth="
+ + beamWidth
+ + ", flatVectorFormat="
+ + flatVectorsFormat
+ + ")";
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java
index 72bd15c3c3daa..34c518a93404b 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java
@@ -1022,7 +1022,11 @@ private String originalName() {
@Override
protected SyntheticSourceMode syntheticSourceMode() {
- return SyntheticSourceMode.NATIVE;
+ if (fieldType.stored() || hasDocValues) {
+ return SyntheticSourceMode.NATIVE;
+ }
+
+ return SyntheticSourceMode.FALLBACK;
}
@Override
@@ -1044,6 +1048,7 @@ public SourceLoader.SyntheticFieldLoader syntheticFieldLoader(String simpleName)
"field [" + fullPath() + "] of type [" + typeName() + "] doesn't support synthetic source because it declares a normalizer"
);
}
+
if (fieldType.stored()) {
return new StringStoredFieldFieldLoader(
fullPath(),
@@ -1057,33 +1062,29 @@ protected void write(XContentBuilder b, Object value) throws IOException {
}
};
}
- if (hasDocValues == false) {
- throw new IllegalArgumentException(
- "field ["
- + fullPath()
- + "] of type ["
- + typeName()
- + "] doesn't support synthetic source because it doesn't have doc values and isn't stored"
- );
- }
- return new SortedSetDocValuesSyntheticFieldLoader(
- fullPath(),
- simpleName,
- fieldType().ignoreAbove == Defaults.IGNORE_ABOVE ? null : originalName(),
- false
- ) {
- @Override
- protected BytesRef convert(BytesRef value) {
- return value;
- }
+ if (hasDocValues) {
+ return new SortedSetDocValuesSyntheticFieldLoader(
+ fullPath(),
+ simpleName,
+ fieldType().ignoreAbove == Defaults.IGNORE_ABOVE ? null : originalName(),
+ false
+ ) {
- @Override
- protected BytesRef preserve(BytesRef value) {
- // Preserve must make a deep copy because convert gets a shallow copy from the iterator
- return BytesRef.deepCopyOf(value);
- }
- };
+ @Override
+ protected BytesRef convert(BytesRef value) {
+ return value;
+ }
+
+ @Override
+ protected BytesRef preserve(BytesRef value) {
+ // Preserve must make a deep copy because convert gets a shallow copy from the iterator
+ return BytesRef.deepCopyOf(value);
+ }
+ };
+ }
+
+ return super.syntheticFieldLoader();
}
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java
index ab5e731c1430a..f7d9b2b4cbd28 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperFeatures.java
@@ -25,7 +25,8 @@ public Set getFeatures() {
PassThroughObjectMapper.PASS_THROUGH_PRIORITY,
RangeFieldMapper.NULL_VALUES_OFF_BY_ONE_FIX,
SourceFieldMapper.SYNTHETIC_SOURCE_FALLBACK,
- DenseVectorFieldMapper.INT4_QUANTIZATION
+ DenseVectorFieldMapper.INT4_QUANTIZATION,
+ DenseVectorFieldMapper.BIT_VECTORS
);
}
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java
index 140a7849754a8..1e5143a58f20a 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java
@@ -1988,7 +1988,11 @@ public void doValidate(MappingLookup lookup) {
@Override
protected SyntheticSourceMode syntheticSourceMode() {
- return SyntheticSourceMode.NATIVE;
+ if (hasDocValues) {
+ return SyntheticSourceMode.NATIVE;
+ }
+
+ return SyntheticSourceMode.FALLBACK;
}
@Override
@@ -1996,21 +2000,16 @@ public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() {
if (hasScript()) {
return SourceLoader.SyntheticFieldLoader.NOTHING;
}
- if (hasDocValues == false) {
- throw new IllegalArgumentException(
- "field ["
- + fullPath()
- + "] of type ["
- + typeName()
- + "] doesn't support synthetic source because it doesn't have doc values"
- );
- }
if (copyTo.copyToFields().isEmpty() != true) {
throw new IllegalArgumentException(
"field [" + fullPath() + "] of type [" + typeName() + "] doesn't support synthetic source because it declares copy_to"
);
}
- return type.syntheticFieldLoader(fullPath(), leafName(), ignoreMalformed.value());
+ if (hasDocValues) {
+ return type.syntheticFieldLoader(fullPath(), leafName(), ignoreMalformed.value());
+ }
+
+ return super.syntheticFieldLoader();
}
// For testing only:
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java
index eb57e068bd89f..85407fe824275 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java
@@ -275,6 +275,10 @@ public String typeName() {
return CONTENT_TYPE;
}
+ public String rootName() {
+ return this.rootName;
+ }
+
public String key() {
return key;
}
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java
index 15cd10b4f67dc..3a50fe6f28a6a 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java
@@ -31,6 +31,7 @@
import org.apache.lucene.search.FieldExistsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.join.BitSetProducer;
+import org.apache.lucene.util.BitUtil;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.VectorUtil;
import org.elasticsearch.common.ParsingException;
@@ -41,6 +42,8 @@
import org.elasticsearch.index.codec.vectors.ES813FlatVectorFormat;
import org.elasticsearch.index.codec.vectors.ES813Int8FlatVectorFormat;
import org.elasticsearch.index.codec.vectors.ES814HnswScalarQuantizedVectorsFormat;
+import org.elasticsearch.index.codec.vectors.ES815BitFlatVectorFormat;
+import org.elasticsearch.index.codec.vectors.ES815HnswBitVectorsFormat;
import org.elasticsearch.index.fielddata.FieldDataContext;
import org.elasticsearch.index.fielddata.IndexFieldData;
import org.elasticsearch.index.mapper.ArraySourceValueFetcher;
@@ -100,6 +103,7 @@ static boolean isNotUnitVector(float magnitude) {
}
public static final NodeFeature INT4_QUANTIZATION = new NodeFeature("mapper.vectors.int4_quantization");
+ public static final NodeFeature BIT_VECTORS = new NodeFeature("mapper.vectors.bit_vectors");
public static final IndexVersion MAGNITUDE_STORED_INDEX_VERSION = IndexVersions.V_7_5_0;
public static final IndexVersion INDEXED_BY_DEFAULT_INDEX_VERSION = IndexVersions.FIRST_DETACHED_INDEX_VERSION;
@@ -109,6 +113,7 @@ static boolean isNotUnitVector(float magnitude) {
public static final String CONTENT_TYPE = "dense_vector";
public static short MAX_DIMS_COUNT = 4096; // maximum allowed number of dimensions
+ public static int MAX_DIMS_COUNT_BIT = 4096 * Byte.SIZE; // maximum allowed number of dimensions
public static short MIN_DIMS_FOR_DYNAMIC_FLOAT_MAPPING = 128; // minimum number of dims for floats to be dynamically mapped to vector
public static final int MAGNITUDE_BYTES = 4;
@@ -134,17 +139,28 @@ public static class Builder extends FieldMapper.Builder {
throw new MapperParsingException("Property [dims] on field [" + n + "] must be an integer but got [" + o + "]");
}
int dims = XContentMapValues.nodeIntegerValue(o);
- if (dims < 1 || dims > MAX_DIMS_COUNT) {
+ int maxDims = elementType.getValue() == ElementType.BIT ? MAX_DIMS_COUNT_BIT : MAX_DIMS_COUNT;
+ int minDims = elementType.getValue() == ElementType.BIT ? Byte.SIZE : 1;
+ if (dims < minDims || dims > maxDims) {
throw new MapperParsingException(
"The number of dimensions for field ["
+ n
- + "] should be in the range [1, "
- + MAX_DIMS_COUNT
+ + "] should be in the range ["
+ + minDims
+ + ", "
+ + maxDims
+ "] but was ["
+ dims
+ "]"
);
}
+ if (elementType.getValue() == ElementType.BIT) {
+ if (dims % Byte.SIZE != 0) {
+ throw new MapperParsingException(
+ "The number of dimensions for field [" + n + "] should be a multiple of 8 but was [" + dims + "]"
+ );
+ }
+ }
return dims;
}, m -> toType(m).fieldType().dims, XContentBuilder::field, Object::toString).setSerializerCheck((id, ic, v) -> v != null)
.setMergeValidator((previous, current, c) -> previous == null || Objects.equals(previous, current));
@@ -171,13 +187,27 @@ public Builder(String name, IndexVersion indexVersionCreated) {
"similarity",
false,
m -> toType(m).fieldType().similarity,
- (Supplier) () -> indexedByDefault && indexed.getValue() ? VectorSimilarity.COSINE : null,
+ (Supplier) () -> {
+ if (indexedByDefault && indexed.getValue()) {
+ return elementType.getValue() == ElementType.BIT ? VectorSimilarity.L2_NORM : VectorSimilarity.COSINE;
+ }
+ return null;
+ },
VectorSimilarity.class
- ).acceptsNull().setSerializerCheck((id, ic, v) -> v != null);
+ ).acceptsNull().setSerializerCheck((id, ic, v) -> v != null).addValidator(vectorSim -> {
+ if (vectorSim == null) {
+ return;
+ }
+ if (elementType.getValue() == ElementType.BIT && vectorSim != VectorSimilarity.L2_NORM) {
+ throw new IllegalArgumentException(
+ "The [" + VectorSimilarity.L2_NORM + "] similarity is the only supported similarity for bit vectors"
+ );
+ }
+ });
this.indexOptions = new Parameter<>(
"index_options",
true,
- () -> defaultInt8Hnsw && elementType.getValue() != ElementType.BYTE && this.indexed.getValue()
+ () -> defaultInt8Hnsw && elementType.getValue() == ElementType.FLOAT && this.indexed.getValue()
? new Int8HnswIndexOptions(
Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN,
Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH,
@@ -266,7 +296,7 @@ public DenseVectorFieldMapper build(MapperBuilderContext context) {
public enum ElementType {
- BYTE(1) {
+ BYTE {
@Override
public String toString() {
@@ -371,7 +401,7 @@ void checkVectorMagnitude(
}
@Override
- public double computeDotProduct(VectorData vectorData) {
+ public double computeSquaredMagnitude(VectorData vectorData) {
return VectorUtil.dotProduct(vectorData.asByteVector(), vectorData.asByteVector());
}
@@ -428,7 +458,7 @@ private VectorData parseHexEncodedVector(DocumentParserContext context, DenseVec
byte[] decodedVector = HexFormat.of().parseHex(context.parser().text());
fieldMapper.checkDimensionMatches(decodedVector.length, context);
VectorData vectorData = VectorData.fromBytes(decodedVector);
- double squaredMagnitude = computeDotProduct(vectorData);
+ double squaredMagnitude = computeSquaredMagnitude(vectorData);
checkVectorMagnitude(
fieldMapper.fieldType().similarity,
errorByteElementsAppender(decodedVector),
@@ -463,7 +493,7 @@ public void parseKnnVectorAndIndex(DocumentParserContext context, DenseVectorFie
@Override
int getNumBytes(int dimensions) {
- return dimensions * elementBytes;
+ return dimensions;
}
@Override
@@ -494,7 +524,7 @@ int parseDimensionCount(DocumentParserContext context) throws IOException {
}
},
- FLOAT(4) {
+ FLOAT {
@Override
public String toString() {
@@ -596,7 +626,7 @@ void checkVectorMagnitude(
}
@Override
- public double computeDotProduct(VectorData vectorData) {
+ public double computeSquaredMagnitude(VectorData vectorData) {
return VectorUtil.dotProduct(vectorData.asFloatVector(), vectorData.asFloatVector());
}
@@ -656,7 +686,7 @@ VectorData parseKnnVector(DocumentParserContext context, DenseVectorFieldMapper
@Override
int getNumBytes(int dimensions) {
- return dimensions * elementBytes;
+ return dimensions * Float.BYTES;
}
@Override
@@ -665,13 +695,249 @@ ByteBuffer createByteBuffer(IndexVersion indexVersion, int numBytes) {
? ByteBuffer.wrap(new byte[numBytes]).order(ByteOrder.LITTLE_ENDIAN)
: ByteBuffer.wrap(new byte[numBytes]);
}
- };
+ },
- final int elementBytes;
+ BIT {
- ElementType(int elementBytes) {
- this.elementBytes = elementBytes;
- }
+ @Override
+ public String toString() {
+ return "bit";
+ }
+
+ @Override
+ public void writeValue(ByteBuffer byteBuffer, float value) {
+ byteBuffer.put((byte) value);
+ }
+
+ @Override
+ public void readAndWriteValue(ByteBuffer byteBuffer, XContentBuilder b) throws IOException {
+ b.value(byteBuffer.get());
+ }
+
+ private KnnByteVectorField createKnnVectorField(String name, byte[] vector, VectorSimilarityFunction function) {
+ if (vector == null) {
+ throw new IllegalArgumentException("vector value must not be null");
+ }
+ FieldType denseVectorFieldType = new FieldType();
+ denseVectorFieldType.setVectorAttributes(vector.length, VectorEncoding.BYTE, function);
+ denseVectorFieldType.freeze();
+ return new KnnByteVectorField(name, vector, denseVectorFieldType);
+ }
+
+ @Override
+ IndexFieldData.Builder fielddataBuilder(DenseVectorFieldType denseVectorFieldType, FieldDataContext fieldDataContext) {
+ return new VectorIndexFieldData.Builder(
+ denseVectorFieldType.name(),
+ CoreValuesSourceType.KEYWORD,
+ denseVectorFieldType.indexVersionCreated,
+ this,
+ denseVectorFieldType.dims,
+ denseVectorFieldType.indexed,
+ r -> r
+ );
+ }
+
+ @Override
+ public void checkVectorBounds(float[] vector) {
+ checkNanAndInfinite(vector);
+
+ StringBuilder errorBuilder = null;
+
+ for (int index = 0; index < vector.length; ++index) {
+ float value = vector[index];
+
+ if (value % 1.0f != 0.0f) {
+ errorBuilder = new StringBuilder(
+ "element_type ["
+ + this
+ + "] vectors only support non-decimal values but found decimal value ["
+ + value
+ + "] at dim ["
+ + index
+ + "];"
+ );
+ break;
+ }
+
+ if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) {
+ errorBuilder = new StringBuilder(
+ "element_type ["
+ + this
+ + "] vectors only support integers between ["
+ + Byte.MIN_VALUE
+ + ", "
+ + Byte.MAX_VALUE
+ + "] but found ["
+ + value
+ + "] at dim ["
+ + index
+ + "];"
+ );
+ break;
+ }
+ }
+
+ if (errorBuilder != null) {
+ throw new IllegalArgumentException(appendErrorElements(errorBuilder, vector).toString());
+ }
+ }
+
+ @Override
+ void checkVectorMagnitude(
+ VectorSimilarity similarity,
+ Function appender,
+ float squaredMagnitude
+ ) {}
+
+ @Override
+ public double computeSquaredMagnitude(VectorData vectorData) {
+ int count = 0;
+ int i = 0;
+ byte[] byteBits = vectorData.asByteVector();
+ for (int upperBound = byteBits.length & -8; i < upperBound; i += 8) {
+ count += Long.bitCount((long) BitUtil.VH_NATIVE_LONG.get(byteBits, i));
+ }
+
+ while (i < byteBits.length) {
+ count += Integer.bitCount(byteBits[i] & 255);
+ ++i;
+ }
+ return count;
+ }
+
+ private VectorData parseVectorArray(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException {
+ int index = 0;
+ byte[] vector = new byte[fieldMapper.fieldType().dims / Byte.SIZE];
+ for (XContentParser.Token token = context.parser().nextToken(); token != Token.END_ARRAY; token = context.parser()
+ .nextToken()) {
+ fieldMapper.checkDimensionExceeded(index, context);
+ ensureExpectedToken(Token.VALUE_NUMBER, token, context.parser());
+ final int value;
+ if (context.parser().numberType() != XContentParser.NumberType.INT) {
+ float floatValue = context.parser().floatValue(true);
+ if (floatValue % 1.0f != 0.0f) {
+ throw new IllegalArgumentException(
+ "element_type ["
+ + this
+ + "] vectors only support non-decimal values but found decimal value ["
+ + floatValue
+ + "] at dim ["
+ + index
+ + "];"
+ );
+ }
+ value = (int) floatValue;
+ } else {
+ value = context.parser().intValue(true);
+ }
+ if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) {
+ throw new IllegalArgumentException(
+ "element_type ["
+ + this
+ + "] vectors only support integers between ["
+ + Byte.MIN_VALUE
+ + ", "
+ + Byte.MAX_VALUE
+ + "] but found ["
+ + value
+ + "] at dim ["
+ + index
+ + "];"
+ );
+ }
+ if (index >= vector.length) {
+ throw new IllegalArgumentException(
+ "The number of dimensions for field ["
+ + fieldMapper.fieldType().name()
+ + "] should be ["
+ + fieldMapper.fieldType().dims
+ + "] but found ["
+ + (index + 1) * Byte.SIZE
+ + "]"
+ );
+ }
+ vector[index++] = (byte) value;
+ }
+ fieldMapper.checkDimensionMatches(index * Byte.SIZE, context);
+ return VectorData.fromBytes(vector);
+ }
+
+ private VectorData parseHexEncodedVector(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException {
+ byte[] decodedVector = HexFormat.of().parseHex(context.parser().text());
+ fieldMapper.checkDimensionMatches(decodedVector.length * Byte.SIZE, context);
+ return VectorData.fromBytes(decodedVector);
+ }
+
+ @Override
+ VectorData parseKnnVector(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException {
+ XContentParser.Token token = context.parser().currentToken();
+ return switch (token) {
+ case START_ARRAY -> parseVectorArray(context, fieldMapper);
+ case VALUE_STRING -> parseHexEncodedVector(context, fieldMapper);
+ default -> throw new ParsingException(
+ context.parser().getTokenLocation(),
+ format("Unsupported type [%s] for provided value [%s]", token, context.parser().text())
+ );
+ };
+ }
+
+ @Override
+ public void parseKnnVectorAndIndex(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException {
+ VectorData vectorData = parseKnnVector(context, fieldMapper);
+ Field field = createKnnVectorField(
+ fieldMapper.fieldType().name(),
+ vectorData.asByteVector(),
+ fieldMapper.fieldType().similarity.vectorSimilarityFunction(fieldMapper.indexCreatedVersion, this)
+ );
+ context.doc().addWithKey(fieldMapper.fieldType().name(), field);
+ }
+
+ @Override
+ int getNumBytes(int dimensions) {
+ assert dimensions % Byte.SIZE == 0;
+ return dimensions / Byte.SIZE;
+ }
+
+ @Override
+ ByteBuffer createByteBuffer(IndexVersion indexVersion, int numBytes) {
+ return ByteBuffer.wrap(new byte[numBytes]);
+ }
+
+ @Override
+ int parseDimensionCount(DocumentParserContext context) throws IOException {
+ XContentParser.Token currentToken = context.parser().currentToken();
+ return switch (currentToken) {
+ case START_ARRAY -> {
+ int index = 0;
+ for (Token token = context.parser().nextToken(); token != Token.END_ARRAY; token = context.parser().nextToken()) {
+ index++;
+ }
+ yield index * Byte.SIZE;
+ }
+ case VALUE_STRING -> {
+ byte[] decodedVector = HexFormat.of().parseHex(context.parser().text());
+ yield decodedVector.length * Byte.SIZE;
+ }
+ default -> throw new ParsingException(
+ context.parser().getTokenLocation(),
+ format("Unsupported type [%s] for provided value [%s]", currentToken, context.parser().text())
+ );
+ };
+ }
+
+ @Override
+ public void checkDimensions(int dvDims, int qvDims) {
+ if (dvDims != qvDims * Byte.SIZE) {
+ throw new IllegalArgumentException(
+ "The query vector has a different number of dimensions ["
+ + qvDims * Byte.SIZE
+ + "] than the document vectors ["
+ + dvDims
+ + "]."
+ );
+ }
+ }
+ };
public abstract void writeValue(ByteBuffer byteBuffer, float value);
@@ -695,6 +961,14 @@ abstract void checkVectorMagnitude(
float squaredMagnitude
);
+ public void checkDimensions(int dvDims, int qvDims) {
+ if (dvDims != qvDims) {
+ throw new IllegalArgumentException(
+ "The query vector has a different number of dimensions [" + qvDims + "] than the document vectors [" + dvDims + "]."
+ );
+ }
+ }
+
int parseDimensionCount(DocumentParserContext context) throws IOException {
int index = 0;
for (Token token = context.parser().nextToken(); token != Token.END_ARRAY; token = context.parser().nextToken()) {
@@ -775,7 +1049,7 @@ static Function errorByteElementsAppender(byte[] v
return sb -> appendErrorElements(sb, vector);
}
- public abstract double computeDotProduct(VectorData vectorData);
+ public abstract double computeSquaredMagnitude(VectorData vectorData);
public static ElementType fromString(String name) {
return valueOf(name.trim().toUpperCase(Locale.ROOT));
@@ -786,7 +1060,9 @@ public static ElementType fromString(String name) {
ElementType.BYTE.toString(),
ElementType.BYTE,
ElementType.FLOAT.toString(),
- ElementType.FLOAT
+ ElementType.FLOAT,
+ ElementType.BIT.toString(),
+ ElementType.BIT
);
public enum VectorSimilarity {
@@ -795,6 +1071,7 @@ public enum VectorSimilarity {
float score(float similarity, ElementType elementType, int dim) {
return switch (elementType) {
case BYTE, FLOAT -> 1f / (1f + similarity * similarity);
+ case BIT -> (dim - similarity) / dim;
};
}
@@ -806,8 +1083,10 @@ public VectorSimilarityFunction vectorSimilarityFunction(IndexVersion indexVersi
COSINE {
@Override
float score(float similarity, ElementType elementType, int dim) {
+ assert elementType != ElementType.BIT;
return switch (elementType) {
case BYTE, FLOAT -> (1 + similarity) / 2f;
+ default -> throw new IllegalArgumentException("Unsupported element type [" + elementType + "]");
};
}
@@ -824,6 +1103,7 @@ float score(float similarity, ElementType elementType, int dim) {
return switch (elementType) {
case BYTE -> 0.5f + similarity / (float) (dim * (1 << 15));
case FLOAT -> (1 + similarity) / 2f;
+ default -> throw new IllegalArgumentException("Unsupported element type [" + elementType + "]");
};
}
@@ -837,6 +1117,7 @@ public VectorSimilarityFunction vectorSimilarityFunction(IndexVersion indexVersi
float score(float similarity, ElementType elementType, int dim) {
return switch (elementType) {
case BYTE, FLOAT -> similarity < 0 ? 1 / (1 + -1 * similarity) : similarity + 1;
+ default -> throw new IllegalArgumentException("Unsupported element type [" + elementType + "]");
};
}
@@ -863,7 +1144,7 @@ abstract static class IndexOptions implements ToXContent {
this.type = type;
}
- abstract KnnVectorsFormat getVectorsFormat();
+ abstract KnnVectorsFormat getVectorsFormat(ElementType elementType);
boolean supportsElementType(ElementType elementType) {
return true;
@@ -1002,7 +1283,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
}
@Override
- KnnVectorsFormat getVectorsFormat() {
+ KnnVectorsFormat getVectorsFormat(ElementType elementType) {
+ assert elementType == ElementType.FLOAT;
return new ES813Int8FlatVectorFormat(confidenceInterval, 7, false);
}
@@ -1021,7 +1303,7 @@ public int hashCode() {
@Override
boolean supportsElementType(ElementType elementType) {
- return elementType != ElementType.BYTE;
+ return elementType == ElementType.FLOAT;
}
@Override
@@ -1047,7 +1329,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
}
@Override
- KnnVectorsFormat getVectorsFormat() {
+ KnnVectorsFormat getVectorsFormat(ElementType elementType) {
+ if (elementType.equals(ElementType.BIT)) {
+ return new ES815BitFlatVectorFormat();
+ }
return new ES813FlatVectorFormat();
}
@@ -1083,7 +1368,8 @@ static class Int4HnswIndexOptions extends IndexOptions {
}
@Override
- public KnnVectorsFormat getVectorsFormat() {
+ public KnnVectorsFormat getVectorsFormat(ElementType elementType) {
+ assert elementType == ElementType.FLOAT;
return new ES814HnswScalarQuantizedVectorsFormat(m, efConstruction, confidenceInterval, 4, true);
}
@@ -1126,7 +1412,7 @@ public String toString() {
@Override
boolean supportsElementType(ElementType elementType) {
- return elementType != ElementType.BYTE;
+ return elementType == ElementType.FLOAT;
}
@Override
@@ -1153,7 +1439,8 @@ static class Int4FlatIndexOptions extends IndexOptions {
}
@Override
- public KnnVectorsFormat getVectorsFormat() {
+ public KnnVectorsFormat getVectorsFormat(ElementType elementType) {
+ assert elementType == ElementType.FLOAT;
return new ES813Int8FlatVectorFormat(confidenceInterval, 4, true);
}
@@ -1186,7 +1473,7 @@ public String toString() {
@Override
boolean supportsElementType(ElementType elementType) {
- return elementType != ElementType.BYTE;
+ return elementType == ElementType.FLOAT;
}
@Override
@@ -1216,7 +1503,8 @@ static class Int8HnswIndexOptions extends IndexOptions {
}
@Override
- public KnnVectorsFormat getVectorsFormat() {
+ public KnnVectorsFormat getVectorsFormat(ElementType elementType) {
+ assert elementType == ElementType.FLOAT;
return new ES814HnswScalarQuantizedVectorsFormat(m, efConstruction, confidenceInterval, 7, false);
}
@@ -1261,7 +1549,7 @@ public String toString() {
@Override
boolean supportsElementType(ElementType elementType) {
- return elementType != ElementType.BYTE;
+ return elementType == ElementType.FLOAT;
}
@Override
@@ -1291,7 +1579,10 @@ static class HnswIndexOptions extends IndexOptions {
}
@Override
- public KnnVectorsFormat getVectorsFormat() {
+ public KnnVectorsFormat getVectorsFormat(ElementType elementType) {
+ if (elementType == ElementType.BIT) {
+ return new ES815HnswBitVectorsFormat(m, efConstruction);
+ }
return new Lucene99HnswVectorsFormat(m, efConstruction, 1, null);
}
@@ -1412,48 +1703,6 @@ public Query termQuery(Object value, SearchExecutionContext context) {
throw new IllegalArgumentException("Field [" + name() + "] of type [" + typeName() + "] doesn't support term queries");
}
- public Query createKnnQuery(
- byte[] queryVector,
- int numCands,
- Query filter,
- Float similarityThreshold,
- BitSetProducer parentFilter
- ) {
- if (isIndexed() == false) {
- throw new IllegalArgumentException(
- "to perform knn search on field [" + name() + "], its mapping must have [index] set to [true]"
- );
- }
-
- if (queryVector.length != dims) {
- throw new IllegalArgumentException(
- "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]"
- );
- }
-
- if (elementType != ElementType.BYTE) {
- throw new IllegalArgumentException(
- "only [" + ElementType.BYTE + "] elements are supported when querying field [" + name() + "]"
- );
- }
-
- if (similarity == VectorSimilarity.DOT_PRODUCT || similarity == VectorSimilarity.COSINE) {
- float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector);
- elementType.checkVectorMagnitude(similarity, ElementType.errorByteElementsAppender(queryVector), squaredMagnitude);
- }
- Query knnQuery = parentFilter != null
- ? new ESDiversifyingChildrenByteKnnVectorQuery(name(), queryVector, filter, numCands, parentFilter)
- : new ESKnnByteVectorQuery(name(), queryVector, numCands, filter);
- if (similarityThreshold != null) {
- knnQuery = new VectorSimilarityQuery(
- knnQuery,
- similarityThreshold,
- similarity.score(similarityThreshold, elementType, dims)
- );
- }
- return knnQuery;
- }
-
public Query createExactKnnQuery(VectorData queryVector) {
if (isIndexed() == false) {
throw new IllegalArgumentException(
@@ -1463,15 +1712,17 @@ public Query createExactKnnQuery(VectorData queryVector) {
return switch (elementType) {
case BYTE -> createExactKnnByteQuery(queryVector.asByteVector());
case FLOAT -> createExactKnnFloatQuery(queryVector.asFloatVector());
+ case BIT -> createExactKnnBitQuery(queryVector.asByteVector());
};
}
+ private Query createExactKnnBitQuery(byte[] queryVector) {
+ elementType.checkDimensions(dims, queryVector.length);
+ return new DenseVectorQuery.Bytes(queryVector, name());
+ }
+
private Query createExactKnnByteQuery(byte[] queryVector) {
- if (queryVector.length != dims) {
- throw new IllegalArgumentException(
- "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]"
- );
- }
+ elementType.checkDimensions(dims, queryVector.length);
if (similarity == VectorSimilarity.DOT_PRODUCT || similarity == VectorSimilarity.COSINE) {
float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector);
elementType.checkVectorMagnitude(similarity, ElementType.errorByteElementsAppender(queryVector), squaredMagnitude);
@@ -1480,11 +1731,7 @@ private Query createExactKnnByteQuery(byte[] queryVector) {
}
private Query createExactKnnFloatQuery(float[] queryVector) {
- if (queryVector.length != dims) {
- throw new IllegalArgumentException(
- "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]"
- );
- }
+ elementType.checkDimensions(dims, queryVector.length);
elementType.checkVectorBounds(queryVector);
if (similarity == VectorSimilarity.DOT_PRODUCT || similarity == VectorSimilarity.COSINE) {
float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector);
@@ -1521,21 +1768,39 @@ public Query createKnnQuery(
return switch (getElementType()) {
case BYTE -> createKnnByteQuery(queryVector.asByteVector(), numCands, filter, similarityThreshold, parentFilter);
case FLOAT -> createKnnFloatQuery(queryVector.asFloatVector(), numCands, filter, similarityThreshold, parentFilter);
+ case BIT -> createKnnBitQuery(queryVector.asByteVector(), numCands, filter, similarityThreshold, parentFilter);
};
}
- private Query createKnnByteQuery(
+ private Query createKnnBitQuery(
byte[] queryVector,
int numCands,
Query filter,
Float similarityThreshold,
BitSetProducer parentFilter
) {
- if (queryVector.length != dims) {
- throw new IllegalArgumentException(
- "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]"
+ elementType.checkDimensions(dims, queryVector.length);
+ Query knnQuery = parentFilter != null
+ ? new ESDiversifyingChildrenByteKnnVectorQuery(name(), queryVector, filter, numCands, parentFilter)
+ : new ESKnnByteVectorQuery(name(), queryVector, numCands, filter);
+ if (similarityThreshold != null) {
+ knnQuery = new VectorSimilarityQuery(
+ knnQuery,
+ similarityThreshold,
+ similarity.score(similarityThreshold, elementType, dims)
);
}
+ return knnQuery;
+ }
+
+ private Query createKnnByteQuery(
+ byte[] queryVector,
+ int numCands,
+ Query filter,
+ Float similarityThreshold,
+ BitSetProducer parentFilter
+ ) {
+ elementType.checkDimensions(dims, queryVector.length);
if (similarity == VectorSimilarity.DOT_PRODUCT || similarity == VectorSimilarity.COSINE) {
float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector);
@@ -1561,11 +1826,7 @@ private Query createKnnFloatQuery(
Float similarityThreshold,
BitSetProducer parentFilter
) {
- if (queryVector.length != dims) {
- throw new IllegalArgumentException(
- "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]"
- );
- }
+ elementType.checkDimensions(dims, queryVector.length);
elementType.checkVectorBounds(queryVector);
if (similarity == VectorSimilarity.DOT_PRODUCT || similarity == VectorSimilarity.COSINE) {
float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector);
@@ -1701,7 +1962,7 @@ private void parseBinaryDocValuesVectorAndIndex(DocumentParserContext context) t
vectorData.addToBuffer(byteBuffer);
if (indexCreatedVersion.onOrAfter(MAGNITUDE_STORED_INDEX_VERSION)) {
// encode vector magnitude at the end
- double dotProduct = elementType.computeDotProduct(vectorData);
+ double dotProduct = elementType.computeSquaredMagnitude(vectorData);
float vectorMagnitude = (float) Math.sqrt(dotProduct);
byteBuffer.putFloat(vectorMagnitude);
}
@@ -1780,9 +2041,9 @@ private static IndexOptions parseIndexOptions(String fieldName, Object propNode)
public KnnVectorsFormat getKnnVectorsFormatForField(KnnVectorsFormat defaultFormat) {
final KnnVectorsFormat format;
if (indexOptions == null) {
- format = defaultFormat;
+ format = fieldType().elementType == ElementType.BIT ? new ES815HnswBitVectorsFormat() : defaultFormat;
} else {
- format = indexOptions.getVectorsFormat();
+ format = indexOptions.getVectorsFormat(fieldType().elementType);
}
// It's legal to reuse the same format name as this is the same on-disk format.
return new KnnVectorsFormat(format.getName()) {
diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/VectorDVLeafFieldData.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/VectorDVLeafFieldData.java
index d66b429e6dd95..f35ba3a0fd5b8 100644
--- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/VectorDVLeafFieldData.java
+++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/VectorDVLeafFieldData.java
@@ -17,6 +17,8 @@
import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.ElementType;
import org.elasticsearch.script.field.DocValuesScriptFieldFactory;
import org.elasticsearch.script.field.vectors.BinaryDenseVectorDocValuesField;
+import org.elasticsearch.script.field.vectors.BitBinaryDenseVectorDocValuesField;
+import org.elasticsearch.script.field.vectors.BitKnnDenseVectorDocValuesField;
import org.elasticsearch.script.field.vectors.ByteBinaryDenseVectorDocValuesField;
import org.elasticsearch.script.field.vectors.ByteKnnDenseVectorDocValuesField;
import org.elasticsearch.script.field.vectors.KnnDenseVectorDocValuesField;
@@ -58,12 +60,14 @@ public DocValuesScriptFieldFactory getScriptFieldFactory(String name) {
return switch (elementType) {
case BYTE -> new ByteKnnDenseVectorDocValuesField(reader.getByteVectorValues(field), name, dims);
case FLOAT -> new KnnDenseVectorDocValuesField(reader.getFloatVectorValues(field), name, dims);
+ case BIT -> new BitKnnDenseVectorDocValuesField(reader.getByteVectorValues(field), name, dims);
};
} else {
BinaryDocValues values = DocValues.getBinary(reader, field);
return switch (elementType) {
case BYTE -> new ByteBinaryDenseVectorDocValuesField(values, name, elementType, dims);
case FLOAT -> new BinaryDenseVectorDocValuesField(values, name, elementType, dims, indexVersion);
+ case BIT -> new BitBinaryDenseVectorDocValuesField(values, name, elementType, dims);
};
}
} catch (IOException e) {
diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/ExponentialDecayFunctionBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/ExponentialDecayFunctionBuilder.java
index 2c361fe025dfa..ca6dfa5ef6e51 100644
--- a/server/src/main/java/org/elasticsearch/index/query/functionscore/ExponentialDecayFunctionBuilder.java
+++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/ExponentialDecayFunctionBuilder.java
@@ -76,10 +76,7 @@ public int hashCode() {
@Override
public boolean equals(Object obj) {
- if (super.equals(obj)) {
- return true;
- }
- return obj != null && getClass() != obj.getClass();
+ return obj == this || (obj != null && obj.getClass() == this.getClass());
}
}
diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/GaussDecayFunctionBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/GaussDecayFunctionBuilder.java
index 4415c87e9815e..1cc9335b5963e 100644
--- a/server/src/main/java/org/elasticsearch/index/query/functionscore/GaussDecayFunctionBuilder.java
+++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/GaussDecayFunctionBuilder.java
@@ -83,10 +83,7 @@ public int hashCode() {
@Override
public boolean equals(Object obj) {
- if (super.equals(obj)) {
- return true;
- }
- return obj != null && getClass() != obj.getClass();
+ return obj == this || (obj != null && obj.getClass() == this.getClass());
}
}
}
diff --git a/server/src/main/java/org/elasticsearch/index/query/functionscore/LinearDecayFunctionBuilder.java b/server/src/main/java/org/elasticsearch/index/query/functionscore/LinearDecayFunctionBuilder.java
index ff22e1d57f832..70c3c4458a217 100644
--- a/server/src/main/java/org/elasticsearch/index/query/functionscore/LinearDecayFunctionBuilder.java
+++ b/server/src/main/java/org/elasticsearch/index/query/functionscore/LinearDecayFunctionBuilder.java
@@ -86,10 +86,7 @@ public int hashCode() {
@Override
public boolean equals(Object obj) {
- if (super.equals(obj)) {
- return true;
- }
- return obj != null && getClass() != obj.getClass();
+ return obj == this || (obj != null && obj.getClass() == this.getClass());
}
}
}
diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java
index b3f19b1b7a81d..881f4602be1c7 100644
--- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java
+++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java
@@ -2230,10 +2230,19 @@ public RecoveryState recoveryState() {
@Override
public ShardLongFieldRange getTimestampRange() {
+ return determineShardLongFieldRange(DataStream.TIMESTAMP_FIELD_NAME);
+ }
+
+ @Override
+ public ShardLongFieldRange getEventIngestedRange() {
+ return determineShardLongFieldRange(IndexMetadata.EVENT_INGESTED_FIELD_NAME);
+ }
+
+ private ShardLongFieldRange determineShardLongFieldRange(String fieldName) {
if (mapperService() == null) {
return ShardLongFieldRange.UNKNOWN; // no mapper service, no idea if the field even exists
}
- final MappedFieldType mappedFieldType = mapperService().fieldType(DataStream.TIMESTAMP_FIELD_NAME);
+ final MappedFieldType mappedFieldType = mapperService().fieldType(fieldName);
if (mappedFieldType instanceof DateFieldMapper.DateFieldType == false) {
return ShardLongFieldRange.UNKNOWN; // field missing or not a date
}
@@ -2243,10 +2252,10 @@ public ShardLongFieldRange getTimestampRange() {
final ShardLongFieldRange rawTimestampFieldRange;
try {
- rawTimestampFieldRange = getEngine().getRawFieldRange(DataStream.TIMESTAMP_FIELD_NAME);
+ rawTimestampFieldRange = getEngine().getRawFieldRange(fieldName);
assert rawTimestampFieldRange != null;
} catch (IOException | AlreadyClosedException e) {
- logger.debug("exception obtaining range for timestamp field", e);
+ logger.debug("exception obtaining range for field " + fieldName, e);
return ShardLongFieldRange.UNKNOWN;
}
if (rawTimestampFieldRange == ShardLongFieldRange.UNKNOWN) {
@@ -3337,7 +3346,7 @@ private void executeRecovery(
markAsRecovering(reason, recoveryState); // mark the shard as recovering on the cluster state thread
threadPool.generic().execute(ActionRunnable.wrap(ActionListener.wrap(r -> {
if (r) {
- recoveryListener.onRecoveryDone(recoveryState, getTimestampRange());
+ recoveryListener.onRecoveryDone(recoveryState, getTimestampRange(), getEventIngestedRange());
}
}, e -> recoveryListener.onRecoveryFailure(new RecoveryFailedException(recoveryState, null, e), true)), action));
}
diff --git a/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java b/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java
index d409c3009ef5b..dd5ad26c58b12 100644
--- a/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java
+++ b/server/src/main/java/org/elasticsearch/indices/cluster/IndicesClusterStateService.java
@@ -46,6 +46,7 @@
import org.elasticsearch.common.util.concurrent.ThrottledTaskRunner;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.Releasable;
+import org.elasticsearch.core.Strings;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.env.ShardLockObtainFailedException;
import org.elasticsearch.gateway.GatewayService;
@@ -417,7 +418,10 @@ protected void doRun() throws Exception {
// lock is released so it's guaranteed to be deleted by the time we get the lock
indicesService.processPendingDeletes(index, indexSettings, timeout);
} catch (ShardLockObtainFailedException exc) {
- logger.warn("[{}] failed to lock all shards for index - timed out after [{}]]", index, timeout);
+ logger.warn(
+ Strings.format("[%s] failed to lock all shards for index - timed out after [%s]]", index, timeout),
+ exc
+ );
} catch (InterruptedException e) {
logger.warn("[{}] failed to lock all shards for index - interrupted", index);
}
@@ -905,6 +909,7 @@ private void updateShard(ShardRouting shardRouting, Shard shard, ClusterState cl
+ state
+ "], mark shard as started",
shard.getTimestampRange(),
+ shard.getEventIngestedRange(),
ActionListener.noop(),
clusterState
);
@@ -966,12 +971,17 @@ private RecoveryListener(final ShardRouting shardRouting, final long primaryTerm
}
@Override
- public void onRecoveryDone(final RecoveryState state, ShardLongFieldRange timestampMillisFieldRange) {
+ public void onRecoveryDone(
+ final RecoveryState state,
+ ShardLongFieldRange timestampMillisFieldRange,
+ ShardLongFieldRange eventIngestedMillisFieldRange
+ ) {
shardStateAction.shardStarted(
shardRouting,
primaryTerm,
"after " + state.getRecoverySource(),
timestampMillisFieldRange,
+ eventIngestedMillisFieldRange,
ActionListener.noop()
);
}
@@ -1123,6 +1133,13 @@ public interface Shard {
@Nullable
ShardLongFieldRange getTimestampRange();
+ /**
+ * @return the range of the {@code @event.ingested} field for this shard, or {@link ShardLongFieldRange#EMPTY} if this field is not
+ * found, or {@link ShardLongFieldRange#UNKNOWN} if its range is not fixed.
+ */
+ @Nullable
+ ShardLongFieldRange getEventIngestedRange();
+
/**
* Updates the shard state based on an incoming cluster state:
* - Updates and persists the new routing value.
diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetService.java b/server/src/main/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetService.java
index 3447cc73a4288..ac618ac9308c4 100644
--- a/server/src/main/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetService.java
+++ b/server/src/main/java/org/elasticsearch/indices/recovery/PeerRecoveryTargetService.java
@@ -517,7 +517,11 @@ private static void logGlobalCheckpointWarning(Logger logger, long startingSeqNo
}
public interface RecoveryListener {
- void onRecoveryDone(RecoveryState state, ShardLongFieldRange timestampMillisFieldRange);
+ void onRecoveryDone(
+ RecoveryState state,
+ ShardLongFieldRange timestampMillisFieldRange,
+ ShardLongFieldRange eventIngestedMillisFieldRange
+ );
void onRecoveryFailure(RecoveryFailedException e, boolean sendShardFailure);
}
diff --git a/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java b/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java
index dda7203fa7b0e..3232099831d8b 100644
--- a/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java
+++ b/server/src/main/java/org/elasticsearch/indices/recovery/RecoveryTarget.java
@@ -323,7 +323,7 @@ public void markAsDone() {
indexShard.postRecovery("peer recovery done", ActionListener.runBefore(new ActionListener<>() {
@Override
public void onResponse(Void unused) {
- listener.onRecoveryDone(state(), indexShard.getTimestampRange());
+ listener.onRecoveryDone(state(), indexShard.getTimestampRange(), indexShard.getEventIngestedRange());
}
@Override
diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/SearchResponseMetrics.java b/server/src/main/java/org/elasticsearch/rest/action/search/SearchResponseMetrics.java
index 00f1f5d5804d6..2e2be59689b65 100644
--- a/server/src/main/java/org/elasticsearch/rest/action/search/SearchResponseMetrics.java
+++ b/server/src/main/java/org/elasticsearch/rest/action/search/SearchResponseMetrics.java
@@ -8,14 +8,37 @@
package org.elasticsearch.rest.action.search;
+import org.elasticsearch.telemetry.metric.LongCounter;
import org.elasticsearch.telemetry.metric.LongHistogram;
import org.elasticsearch.telemetry.metric.MeterRegistry;
+import java.util.Map;
+
public class SearchResponseMetrics {
+ public enum ResponseCountTotalStatus {
+ SUCCESS("succes"),
+ PARTIAL_FAILURE("partial_failure"),
+ FAILURE("failure");
+
+ private final String displayName;
+
+ ResponseCountTotalStatus(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+ }
+
+ public static final String RESPONSE_COUNT_TOTAL_STATUS_ATTRIBUTE_NAME = "status";
+
public static final String TOOK_DURATION_TOTAL_HISTOGRAM_NAME = "es.search_response.took_durations.histogram";
+ public static final String RESPONSE_COUNT_TOTAL_COUNTER_NAME = "es.search_response.response_count.total";
private final LongHistogram tookDurationTotalMillisHistogram;
+ private final LongCounter responseCountTotalCounter;
public SearchResponseMetrics(MeterRegistry meterRegistry) {
this(
@@ -23,16 +46,31 @@ public SearchResponseMetrics(MeterRegistry meterRegistry) {
TOOK_DURATION_TOTAL_HISTOGRAM_NAME,
"The SearchResponse.took durations in milliseconds, expressed as a histogram",
"millis"
+ ),
+ meterRegistry.registerLongCounter(
+ RESPONSE_COUNT_TOTAL_COUNTER_NAME,
+ "The cumulative total of search responses with an attribute to describe "
+ + "success, partial failure, or failure, expressed as a single total counter and individual "
+ + "attribute counters",
+ "count"
)
);
}
- private SearchResponseMetrics(LongHistogram tookDurationTotalMillisHistogram) {
+ private SearchResponseMetrics(LongHistogram tookDurationTotalMillisHistogram, LongCounter responseCountTotalCounter) {
this.tookDurationTotalMillisHistogram = tookDurationTotalMillisHistogram;
+ this.responseCountTotalCounter = responseCountTotalCounter;
}
public long recordTookTime(long tookTime) {
tookDurationTotalMillisHistogram.record(tookTime);
return tookTime;
}
+
+ public void incrementResponseCount(ResponseCountTotalStatus responseCountTotalStatus) {
+ responseCountTotalCounter.incrementBy(
+ 1L,
+ Map.of(RESPONSE_COUNT_TOTAL_STATUS_ATTRIBUTE_NAME, responseCountTotalStatus.getDisplayName())
+ );
+ }
}
diff --git a/server/src/main/java/org/elasticsearch/script/VectorScoreScriptUtils.java b/server/src/main/java/org/elasticsearch/script/VectorScoreScriptUtils.java
index bccdd5782f277..ad7d74824a1d4 100644
--- a/server/src/main/java/org/elasticsearch/script/VectorScoreScriptUtils.java
+++ b/server/src/main/java/org/elasticsearch/script/VectorScoreScriptUtils.java
@@ -56,7 +56,7 @@ public static class ByteDenseVectorFunction extends DenseVectorFunction {
*/
public ByteDenseVectorFunction(ScoreScript scoreScript, DenseVectorDocValuesField field, List queryVector) {
super(scoreScript, field);
- DenseVector.checkDimensions(field.get().getDims(), queryVector.size());
+ field.getElementType().checkDimensions(field.get().getDims(), queryVector.size());
this.queryVector = new byte[queryVector.size()];
float[] validateValues = new float[queryVector.size()];
int queryMagnitude = 0;
@@ -168,7 +168,7 @@ public static final class L1Norm {
public L1Norm(ScoreScript scoreScript, Object queryVector, String fieldName) {
DenseVectorDocValuesField field = (DenseVectorDocValuesField) scoreScript.field(fieldName);
function = switch (field.getElementType()) {
- case BYTE -> {
+ case BYTE, BIT -> {
if (queryVector instanceof List) {
yield new ByteL1Norm(scoreScript, field, (List) queryVector);
} else if (queryVector instanceof String s) {
@@ -219,8 +219,8 @@ public static final class Hamming {
@SuppressWarnings("unchecked")
public Hamming(ScoreScript scoreScript, Object queryVector, String fieldName) {
DenseVectorDocValuesField field = (DenseVectorDocValuesField) scoreScript.field(fieldName);
- if (field.getElementType() != DenseVectorFieldMapper.ElementType.BYTE) {
- throw new IllegalArgumentException("hamming distance is only supported for byte vectors");
+ if (field.getElementType() == DenseVectorFieldMapper.ElementType.FLOAT) {
+ throw new IllegalArgumentException("hamming distance is only supported for byte or bit vectors");
}
if (queryVector instanceof List) {
function = new ByteHammingDistance(scoreScript, field, (List) queryVector);
@@ -278,7 +278,7 @@ public static final class L2Norm {
public L2Norm(ScoreScript scoreScript, Object queryVector, String fieldName) {
DenseVectorDocValuesField field = (DenseVectorDocValuesField) scoreScript.field(fieldName);
function = switch (field.getElementType()) {
- case BYTE -> {
+ case BYTE, BIT -> {
if (queryVector instanceof List) {
yield new ByteL2Norm(scoreScript, field, (List) queryVector);
} else if (queryVector instanceof String s) {
@@ -342,7 +342,7 @@ public static final class DotProduct {
public DotProduct(ScoreScript scoreScript, Object queryVector, String fieldName) {
DenseVectorDocValuesField field = (DenseVectorDocValuesField) scoreScript.field(fieldName);
function = switch (field.getElementType()) {
- case BYTE -> {
+ case BYTE, BIT -> {
if (queryVector instanceof List) {
yield new ByteDotProduct(scoreScript, field, (List) queryVector);
} else if (queryVector instanceof String s) {
@@ -406,7 +406,7 @@ public static final class CosineSimilarity {
public CosineSimilarity(ScoreScript scoreScript, Object queryVector, String fieldName) {
DenseVectorDocValuesField field = (DenseVectorDocValuesField) scoreScript.field(fieldName);
function = switch (field.getElementType()) {
- case BYTE -> {
+ case BYTE, BIT -> {
if (queryVector instanceof List) {
yield new ByteCosineSimilarity(scoreScript, field, (List) queryVector);
} else if (queryVector instanceof String s) {
diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/BitBinaryDenseVector.java b/server/src/main/java/org/elasticsearch/script/field/vectors/BitBinaryDenseVector.java
new file mode 100644
index 0000000000000..10420543ad181
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/script/field/vectors/BitBinaryDenseVector.java
@@ -0,0 +1,88 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script.field.vectors;
+
+import org.apache.lucene.util.BytesRef;
+
+import java.util.List;
+
+public class BitBinaryDenseVector extends ByteBinaryDenseVector {
+
+ public BitBinaryDenseVector(byte[] vectorValue, BytesRef docVector, int dims) {
+ super(vectorValue, docVector, dims);
+ }
+
+ @Override
+ public void checkDimensions(int qvDims) {
+ if (qvDims != dims) {
+ throw new IllegalArgumentException(
+ "The query vector has a different number of dimensions ["
+ + qvDims * Byte.SIZE
+ + "] than the document vectors ["
+ + dims * Byte.SIZE
+ + "]."
+ );
+ }
+ }
+
+ @Override
+ public int l1Norm(byte[] queryVector) {
+ return hamming(queryVector);
+ }
+
+ @Override
+ public double l1Norm(List queryVector) {
+ return hamming(queryVector);
+ }
+
+ @Override
+ public double l2Norm(byte[] queryVector) {
+ return Math.sqrt(hamming(queryVector));
+ }
+
+ @Override
+ public double l2Norm(List queryVector) {
+ return Math.sqrt(hamming(queryVector));
+ }
+
+ @Override
+ public int dotProduct(byte[] queryVector) {
+ throw new UnsupportedOperationException("dotProduct is not supported for bit vectors.");
+ }
+
+ @Override
+ public double cosineSimilarity(float[] queryVector, boolean normalizeQueryVector) {
+ throw new UnsupportedOperationException("cosineSimilarity is not supported for bit vectors.");
+ }
+
+ @Override
+ public double dotProduct(List queryVector) {
+ throw new UnsupportedOperationException("dotProduct is not supported for bit vectors.");
+ }
+
+ @Override
+ public double cosineSimilarity(byte[] queryVector, float qvMagnitude) {
+ throw new UnsupportedOperationException("cosineSimilarity is not supported for bit vectors.");
+ }
+
+ @Override
+ public double cosineSimilarity(List queryVector) {
+ throw new UnsupportedOperationException("cosineSimilarity is not supported for bit vectors.");
+ }
+
+ @Override
+ public double dotProduct(float[] queryVector) {
+ throw new UnsupportedOperationException("dotProduct is not supported for bit vectors.");
+ }
+
+ @Override
+ public int getDims() {
+ return dims * Byte.SIZE;
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/BitBinaryDenseVectorDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/vectors/BitBinaryDenseVectorDocValuesField.java
new file mode 100644
index 0000000000000..cb123c54dfecf
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/script/field/vectors/BitBinaryDenseVectorDocValuesField.java
@@ -0,0 +1,24 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script.field.vectors;
+
+import org.apache.lucene.index.BinaryDocValues;
+import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.ElementType;
+
+public class BitBinaryDenseVectorDocValuesField extends ByteBinaryDenseVectorDocValuesField {
+
+ public BitBinaryDenseVectorDocValuesField(BinaryDocValues input, String name, ElementType elementType, int dims) {
+ super(input, name, elementType, dims / 8);
+ }
+
+ @Override
+ protected DenseVector getVector() {
+ return new BitBinaryDenseVector(vectorValue, value, dims);
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/BitKnnDenseVector.java b/server/src/main/java/org/elasticsearch/script/field/vectors/BitKnnDenseVector.java
new file mode 100644
index 0000000000000..ce9d990c75851
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/script/field/vectors/BitKnnDenseVector.java
@@ -0,0 +1,95 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script.field.vectors;
+
+import java.util.List;
+
+public class BitKnnDenseVector extends ByteKnnDenseVector {
+
+ public BitKnnDenseVector(byte[] vector) {
+ super(vector);
+ }
+
+ @Override
+ public void checkDimensions(int qvDims) {
+ if (qvDims != docVector.length) {
+ throw new IllegalArgumentException(
+ "The query vector has a different number of dimensions ["
+ + qvDims * Byte.SIZE
+ + "] than the document vectors ["
+ + docVector.length * Byte.SIZE
+ + "]."
+ );
+ }
+ }
+
+ @Override
+ public float getMagnitude() {
+ if (magnitudeCalculated == false) {
+ magnitude = DenseVector.getBitMagnitude(docVector, docVector.length);
+ magnitudeCalculated = true;
+ }
+ return magnitude;
+ }
+
+ @Override
+ public int l1Norm(byte[] queryVector) {
+ return hamming(queryVector);
+ }
+
+ @Override
+ public double l1Norm(List queryVector) {
+ return hamming(queryVector);
+ }
+
+ @Override
+ public double l2Norm(byte[] queryVector) {
+ return Math.sqrt(hamming(queryVector));
+ }
+
+ @Override
+ public double l2Norm(List queryVector) {
+ return Math.sqrt(hamming(queryVector));
+ }
+
+ @Override
+ public int dotProduct(byte[] queryVector) {
+ throw new UnsupportedOperationException("dotProduct is not supported for bit vectors.");
+ }
+
+ @Override
+ public double cosineSimilarity(float[] queryVector, boolean normalizeQueryVector) {
+ throw new UnsupportedOperationException("cosineSimilarity is not supported for bit vectors.");
+ }
+
+ @Override
+ public double dotProduct(List queryVector) {
+ throw new UnsupportedOperationException("dotProduct is not supported for bit vectors.");
+ }
+
+ @Override
+ public double cosineSimilarity(byte[] queryVector, float qvMagnitude) {
+ throw new UnsupportedOperationException("cosineSimilarity is not supported for bit vectors.");
+ }
+
+ @Override
+ public double cosineSimilarity(List queryVector) {
+ throw new UnsupportedOperationException("cosineSimilarity is not supported for bit vectors.");
+ }
+
+ @Override
+ public double dotProduct(float[] queryVector) {
+ throw new UnsupportedOperationException("dotProduct is not supported for bit vectors.");
+ }
+
+ @Override
+ public int getDims() {
+ return docVector.length * Byte.SIZE;
+ }
+}
diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/BitKnnDenseVectorDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/vectors/BitKnnDenseVectorDocValuesField.java
new file mode 100644
index 0000000000000..10421d992727e
--- /dev/null
+++ b/server/src/main/java/org/elasticsearch/script/field/vectors/BitKnnDenseVectorDocValuesField.java
@@ -0,0 +1,26 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script.field.vectors;
+
+import org.apache.lucene.index.ByteVectorValues;
+import org.elasticsearch.core.Nullable;
+import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper;
+
+public class BitKnnDenseVectorDocValuesField extends ByteKnnDenseVectorDocValuesField {
+
+ public BitKnnDenseVectorDocValuesField(@Nullable ByteVectorValues input, String name, int dims) {
+ super(input, name, dims / 8, DenseVectorFieldMapper.ElementType.BIT);
+ }
+
+ @Override
+ protected DenseVector getVector() {
+ return new BitKnnDenseVector(vector);
+ }
+
+}
diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteBinaryDenseVector.java b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteBinaryDenseVector.java
index c009397452c8a..f2ff8fbccd2fb 100644
--- a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteBinaryDenseVector.java
+++ b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteBinaryDenseVector.java
@@ -21,7 +21,7 @@ public class ByteBinaryDenseVector implements DenseVector {
private final BytesRef docVector;
private final byte[] vectorValue;
- private final int dims;
+ protected final int dims;
private float[] floatDocVector;
private boolean magnitudeDecoded;
diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteBinaryDenseVectorDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteBinaryDenseVectorDocValuesField.java
index b767cd72c4341..c7ce8cd5e937f 100644
--- a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteBinaryDenseVectorDocValuesField.java
+++ b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteBinaryDenseVectorDocValuesField.java
@@ -17,11 +17,11 @@
public class ByteBinaryDenseVectorDocValuesField extends DenseVectorDocValuesField {
- private final BinaryDocValues input;
- private final int dims;
- private final byte[] vectorValue;
- private boolean decoded;
- private BytesRef value;
+ protected final BinaryDocValues input;
+ protected final int dims;
+ protected final byte[] vectorValue;
+ protected boolean decoded;
+ protected BytesRef value;
public ByteBinaryDenseVectorDocValuesField(BinaryDocValues input, String name, ElementType elementType, int dims) {
super(name, elementType);
@@ -50,13 +50,17 @@ public boolean isEmpty() {
return value == null;
}
+ protected DenseVector getVector() {
+ return new ByteBinaryDenseVector(vectorValue, value, dims);
+ }
+
@Override
public DenseVector get() {
if (isEmpty()) {
return DenseVector.EMPTY;
}
decodeVectorIfNecessary();
- return new ByteBinaryDenseVector(vectorValue, value, dims);
+ return getVector();
}
@Override
@@ -65,7 +69,7 @@ public DenseVector get(DenseVector defaultValue) {
return defaultValue;
}
decodeVectorIfNecessary();
- return new ByteBinaryDenseVector(vectorValue, value, dims);
+ return getVector();
}
@Override
diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteKnnDenseVectorDocValuesField.java b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteKnnDenseVectorDocValuesField.java
index a2a9ba1c1d750..a41e166d1d8f3 100644
--- a/server/src/main/java/org/elasticsearch/script/field/vectors/ByteKnnDenseVectorDocValuesField.java
+++ b/server/src/main/java/org/elasticsearch/script/field/vectors/ByteKnnDenseVectorDocValuesField.java
@@ -23,7 +23,11 @@ public class ByteKnnDenseVectorDocValuesField extends DenseVectorDocValuesField
protected final int dims;
public ByteKnnDenseVectorDocValuesField(@Nullable ByteVectorValues input, String name, int dims) {
- super(name, ElementType.BYTE);
+ this(input, name, dims, ElementType.BYTE);
+ }
+
+ protected ByteKnnDenseVectorDocValuesField(@Nullable ByteVectorValues input, String name, int dims, ElementType elementType) {
+ super(name, elementType);
this.dims = dims;
this.input = input;
}
@@ -57,13 +61,17 @@ public boolean isEmpty() {
return vector == null;
}
+ protected DenseVector getVector() {
+ return new ByteKnnDenseVector(vector);
+ }
+
@Override
public DenseVector get() {
if (isEmpty()) {
return DenseVector.EMPTY;
}
- return new ByteKnnDenseVector(vector);
+ return getVector();
}
@Override
@@ -72,7 +80,7 @@ public DenseVector get(DenseVector defaultValue) {
return defaultValue;
}
- return new ByteKnnDenseVector(vector);
+ return getVector();
}
@Override
diff --git a/server/src/main/java/org/elasticsearch/script/field/vectors/DenseVector.java b/server/src/main/java/org/elasticsearch/script/field/vectors/DenseVector.java
index a768e8add6663..d93daecf695a8 100644
--- a/server/src/main/java/org/elasticsearch/script/field/vectors/DenseVector.java
+++ b/server/src/main/java/org/elasticsearch/script/field/vectors/DenseVector.java
@@ -8,6 +8,7 @@
package org.elasticsearch.script.field.vectors;
+import org.apache.lucene.util.BitUtil;
import org.apache.lucene.util.VectorUtil;
import java.util.List;
@@ -25,6 +26,10 @@ class of the argument and checks dimensionality.
*/
public interface DenseVector {
+ default void checkDimensions(int qvDims) {
+ checkDimensions(getDims(), qvDims);
+ }
+
float[] getVector();
float getMagnitude();
@@ -38,13 +43,13 @@ public interface DenseVector {
@SuppressWarnings("unchecked")
default double dotProduct(Object queryVector) {
if (queryVector instanceof float[] floats) {
- checkDimensions(getDims(), floats.length);
+ checkDimensions(floats.length);
return dotProduct(floats);
} else if (queryVector instanceof List> list) {
- checkDimensions(getDims(), list.size());
+ checkDimensions(list.size());
return dotProduct((List) list);
} else if (queryVector instanceof byte[] bytes) {
- checkDimensions(getDims(), bytes.length);
+ checkDimensions(bytes.length);
return dotProduct(bytes);
}
@@ -60,13 +65,13 @@ default double dotProduct(Object queryVector) {
@SuppressWarnings("unchecked")
default double l1Norm(Object queryVector) {
if (queryVector instanceof float[] floats) {
- checkDimensions(getDims(), floats.length);
+ checkDimensions(floats.length);
return l1Norm(floats);
} else if (queryVector instanceof List> list) {
- checkDimensions(getDims(), list.size());
+ checkDimensions(list.size());
return l1Norm((List) list);
} else if (queryVector instanceof byte[] bytes) {
- checkDimensions(getDims(), bytes.length);
+ checkDimensions(bytes.length);
return l1Norm(bytes);
}
@@ -80,11 +85,11 @@ default double l1Norm(Object queryVector) {
@SuppressWarnings("unchecked")
default int hamming(Object queryVector) {
if (queryVector instanceof List> list) {
- checkDimensions(getDims(), list.size());
+ checkDimensions(list.size());
return hamming((List) list);
}
if (queryVector instanceof byte[] bytes) {
- checkDimensions(getDims(), bytes.length);
+ checkDimensions(bytes.length);
return hamming(bytes);
}
@@ -100,13 +105,13 @@ default int hamming(Object queryVector) {
@SuppressWarnings("unchecked")
default double l2Norm(Object queryVector) {
if (queryVector instanceof float[] floats) {
- checkDimensions(getDims(), floats.length);
+ checkDimensions(floats.length);
return l2Norm(floats);
} else if (queryVector instanceof List> list) {
- checkDimensions(getDims(), list.size());
+ checkDimensions(list.size());
return l2Norm((List) list);
} else if (queryVector instanceof byte[] bytes) {
- checkDimensions(getDims(), bytes.length);
+ checkDimensions(bytes.length);
return l2Norm(bytes);
}
@@ -150,13 +155,13 @@ default double cosineSimilarity(float[] queryVector) {
@SuppressWarnings("unchecked")
default double cosineSimilarity(Object queryVector) {
if (queryVector instanceof float[] floats) {
- checkDimensions(getDims(), floats.length);
+ checkDimensions(floats.length);
return cosineSimilarity(floats);
} else if (queryVector instanceof List> list) {
- checkDimensions(getDims(), list.size());
+ checkDimensions(list.size());
return cosineSimilarity((List) list);
} else if (queryVector instanceof byte[] bytes) {
- checkDimensions(getDims(), bytes.length);
+ checkDimensions(bytes.length);
return cosineSimilarity(bytes);
}
@@ -184,6 +189,20 @@ static float getMagnitude(byte[] vector, int dims) {
return (float) Math.sqrt(mag);
}
+ static float getBitMagnitude(byte[] vector, int dims) {
+ int count = 0;
+ int i = 0;
+ for (int upperBound = dims & -8; i < upperBound; i += 8) {
+ count += Long.bitCount((long) BitUtil.VH_NATIVE_LONG.get(vector, i));
+ }
+
+ while (i < dims) {
+ count += Integer.bitCount(vector[i] & 255);
+ ++i;
+ }
+ return (float) Math.sqrt(count);
+ }
+
static float getMagnitude(float[] vector) {
return (float) Math.sqrt(VectorUtil.dotProduct(vector, vector));
}
diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/AggregatorBase.java b/server/src/main/java/org/elasticsearch/search/aggregations/AggregatorBase.java
index 795f51a729ed6..0cebf3d79d754 100644
--- a/server/src/main/java/org/elasticsearch/search/aggregations/AggregatorBase.java
+++ b/server/src/main/java/org/elasticsearch/search/aggregations/AggregatorBase.java
@@ -316,7 +316,10 @@ protected void doClose() {}
protected void doPostCollection() throws IOException {}
protected final InternalAggregations buildEmptySubAggregations() {
- List aggs = new ArrayList<>();
+ if (subAggregators.length == 0) {
+ return InternalAggregations.EMPTY;
+ }
+ List aggs = new ArrayList<>(subAggregators.length);
for (Aggregator aggregator : subAggregators) {
aggs.add(aggregator.buildEmptyAggregation());
}
diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java b/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java
index b65f6b01de348..07e72404eefe9 100644
--- a/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java
+++ b/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java
@@ -34,7 +34,6 @@
import java.util.Optional;
import java.util.stream.Collectors;
-import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;
import static org.elasticsearch.common.xcontent.XContentParserUtils.parseTypedKeysObject;
@@ -71,7 +70,7 @@ public Iterator iterator() {
* The list of {@link InternalAggregation}s.
*/
public List asList() {
- return unmodifiableList(aggregations);
+ return aggregations;
}
/**
@@ -263,7 +262,7 @@ public static InternalAggregations reduce(List aggregation
}
// handle special case when there is just one aggregation
if (aggregationsList.size() == 1) {
- final List internalAggregations = aggregationsList.iterator().next().asList();
+ final List internalAggregations = aggregationsList.get(0).asList();
final List reduced = new ArrayList<>(internalAggregations.size());
for (InternalAggregation aggregation : internalAggregations) {
if (aggregation.mustReduceOnSingleInternalAgg()) {
diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java b/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java
index 6dd691bbf5aaa..de19c26daff92 100644
--- a/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java
+++ b/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java
@@ -132,7 +132,7 @@ static Object resolvePropertyFromPath(List path, List extends Internal
*/
public static int countInnerBucket(InternalBucket bucket) {
int count = 0;
- for (Aggregation agg : bucket.getAggregations().asList()) {
+ for (Aggregation agg : bucket.getAggregations()) {
count += countInnerBucket(agg);
}
return count;
@@ -146,12 +146,12 @@ public static int countInnerBucket(Aggregation agg) {
if (agg instanceof MultiBucketsAggregation multi) {
for (MultiBucketsAggregation.Bucket bucket : multi.getBuckets()) {
++size;
- for (Aggregation bucketAgg : bucket.getAggregations().asList()) {
+ for (Aggregation bucketAgg : bucket.getAggregations()) {
size += countInnerBucket(bucketAgg);
}
}
} else if (agg instanceof SingleBucketAggregation single) {
- for (Aggregation bucketAgg : single.getAggregations().asList()) {
+ for (Aggregation bucketAgg : single.getAggregations()) {
size += countInnerBucket(bucketAgg);
}
}
diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java
index 225b99fe40739..1fb7464dd5066 100644
--- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java
+++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/InternalSingleBucketAggregation.java
@@ -131,7 +131,7 @@ public final InternalAggregation reducePipelines(
InternalAggregation reduced = this;
if (pipelineTree.hasSubTrees()) {
List aggs = new ArrayList<>();
- for (InternalAggregation agg : getAggregations().asList()) {
+ for (InternalAggregation agg : getAggregations()) {
PipelineTree subTree = pipelineTree.subTree(agg.getName());
aggs.add(agg.reducePipelines(agg, reduceContext, subTree));
}
diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java
index e75b2d2002b0f..e0de42cebcc7d 100644
--- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java
+++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java
@@ -327,7 +327,6 @@ public Bucket createBucket(InternalAggregations aggregations, Bucket prototype)
}
private List reduceBuckets(final PriorityQueue> pq, AggregationReduceContext reduceContext) {
- int consumeBucketCount = 0;
List reducedBuckets = new ArrayList<>();
if (pq.size() > 0) {
// list of buckets coming from different shards that have the same key
@@ -340,13 +339,7 @@ private List reduceBuckets(final PriorityQueue= minDocCount || reduceContext.isFinalReduce() == false) {
- if (consumeBucketCount++ >= REPORT_EMPTY_EVERY) {
- reduceContext.consumeBucketsAndMaybeBreak(consumeBucketCount);
- consumeBucketCount = 0;
- }
- reducedBuckets.add(reduced);
- }
+ maybeAddBucket(reduceContext, reducedBuckets, reduced);
currentBuckets.clear();
key = top.current().key;
}
@@ -364,19 +357,21 @@ private List reduceBuckets(final PriorityQueue= minDocCount || reduceContext.isFinalReduce() == false) {
- reducedBuckets.add(reduced);
- if (consumeBucketCount++ >= REPORT_EMPTY_EVERY) {
- reduceContext.consumeBucketsAndMaybeBreak(consumeBucketCount);
- consumeBucketCount = 0;
- }
- }
+ maybeAddBucket(reduceContext, reducedBuckets, reduced);
}
}
- reduceContext.consumeBucketsAndMaybeBreak(consumeBucketCount);
return reducedBuckets;
}
+ private void maybeAddBucket(AggregationReduceContext reduceContext, List reducedBuckets, Bucket reduced) {
+ if (reduced.getDocCount() >= minDocCount || reduceContext.isFinalReduce() == false) {
+ reduceContext.consumeBucketsAndMaybeBreak(1);
+ reducedBuckets.add(reduced);
+ } else {
+ reduceContext.consumeBucketsAndMaybeBreak(-countInnerBucket(reduced));
+ }
+ }
+
/**
* Reduce a list of same-keyed buckets (from multiple shards) to a single bucket. This
* requires all buckets to have the same key.
diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java
index 7b264ccb022e5..098bd5ebc7b3d 100644
--- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java
+++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java
@@ -291,7 +291,6 @@ public Bucket createBucket(InternalAggregations aggregations, Bucket prototype)
}
private List reduceBuckets(PriorityQueue> pq, AggregationReduceContext reduceContext) {
- int consumeBucketCount = 0;
List reducedBuckets = new ArrayList<>();
if (pq.size() > 0) {
// list of buckets coming from different shards that have the same key
@@ -305,13 +304,7 @@ private List reduceBuckets(PriorityQueue> pq,
// The key changes, reduce what we already buffered and reset the buffer for current buckets.
// Using Double.compare instead of != to handle NaN correctly.
final Bucket reduced = reduceBucket(currentBuckets, reduceContext);
- if (reduced.getDocCount() >= minDocCount || reduceContext.isFinalReduce() == false) {
- reducedBuckets.add(reduced);
- if (consumeBucketCount++ >= REPORT_EMPTY_EVERY) {
- reduceContext.consumeBucketsAndMaybeBreak(consumeBucketCount);
- consumeBucketCount = 0;
- }
- }
+ maybeAddBucket(reduceContext, reducedBuckets, reduced);
currentBuckets.clear();
key = top.current().key;
}
@@ -329,20 +322,21 @@ private List reduceBuckets(PriorityQueue> pq,
if (currentBuckets.isEmpty() == false) {
final Bucket reduced = reduceBucket(currentBuckets, reduceContext);
- if (reduced.getDocCount() >= minDocCount || reduceContext.isFinalReduce() == false) {
- reducedBuckets.add(reduced);
- if (consumeBucketCount++ >= REPORT_EMPTY_EVERY) {
- reduceContext.consumeBucketsAndMaybeBreak(consumeBucketCount);
- consumeBucketCount = 0;
- }
- }
+ maybeAddBucket(reduceContext, reducedBuckets, reduced);
}
}
-
- reduceContext.consumeBucketsAndMaybeBreak(consumeBucketCount);
return reducedBuckets;
}
+ private void maybeAddBucket(AggregationReduceContext reduceContext, List reducedBuckets, Bucket reduced) {
+ if (reduced.getDocCount() >= minDocCount || reduceContext.isFinalReduce() == false) {
+ reduceContext.consumeBucketsAndMaybeBreak(1);
+ reducedBuckets.add(reduced);
+ } else {
+ reduceContext.consumeBucketsAndMaybeBreak(-countInnerBucket(reduced));
+ }
+ }
+
private Bucket reduceBucket(List buckets, AggregationReduceContext context) {
assert buckets.isEmpty() == false;
try (BucketReducer reducer = new BucketReducer<>(buckets.get(0), context, buckets.size())) {
diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricAggregator.java
index 7e749b06442f6..38bcc912c29d4 100644
--- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricAggregator.java
+++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricAggregator.java
@@ -28,6 +28,7 @@
import java.io.IOException;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import static java.util.Collections.singletonList;
@@ -146,9 +147,11 @@ private State aggStateForResult(long owningBucketOrdinal) {
return state;
}
+ private static final List