This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new a910c615ac [global-index-eslib]use thin packaging and fix pom
bundle,fix query compatibility and vector metrics, and (#8618)
a910c615ac is described below
commit a910c615ac1e7ca44301043c938533180f3b9566
Author: CrownChu <[email protected]>
AuthorDate: Tue Jul 14 16:30:33 2026 +0800
[global-index-eslib]use thin packaging and fix pom bundle,fix query
compatibility and vector metrics, and (#8618)
This is a follow-up to #8000. It fixes persisted full-text query
compatibility and vector metric handling, and changes `paimon-eslib` to
a thin JAR so that ESLib and Lucene classes are no longer bundled into
the Apache artifact.
---
paimon-eslib/README.md | 5 +
paimon-eslib/pom.xml | 49 +------
.../eslib/index/ESIndexGlobalIndexReader.java | 68 ++++++++-
.../paimon/eslib/index/ESIndexGlobalIndexer.java | 55 ++++++-
.../apache/paimon/eslib/index/ESIndexOptions.java | 20 +++
.../index/ESIndexFullTextQueryParserTest.java | 26 ++++
.../eslib/index/ESIndexGlobalIndexE2ETest.java | 34 +++++
.../eslib/index/ESIndexVectorMetricTest.java | 162 +++++++++++++++++++++
8 files changed, 367 insertions(+), 52 deletions(-)
diff --git a/paimon-eslib/README.md b/paimon-eslib/README.md
index d64ef34a49..94a860bc3e 100644
--- a/paimon-eslib/README.md
+++ b/paimon-eslib/README.md
@@ -35,6 +35,11 @@ indexed according to their data type and per-field options.
> `es-index`. The module and its ESLib/Lucene dependencies require Java 11 or
> newer; the root Maven
> build intentionally skips `paimon-eslib` when it runs on JDK 8.
+`paimon-eslib` is distributed as a thin JAR and does not embed ESLib or Lucene
classes. Maven and
+Gradle resolve these dependencies transitively. When installing JARs manually,
place `eslib-core`,
+`eslib-simdvec`, and their Lucene 9.12 dependencies in the same runtime
classloader as
+`paimon-eslib`.
+
See the general [Global Index](../docs/docs/multimodal-table/global-index.mdx)
documentation for the
required Data Evolution table properties, coverage/freshness behavior, and
shared build options.
diff --git a/paimon-eslib/pom.xml b/paimon-eslib/pom.xml
index 8b9ea3ce80..a5fa8f5ee3 100644
--- a/paimon-eslib/pom.xml
+++ b/paimon-eslib/pom.xml
@@ -114,56 +114,11 @@ under the License.
<repositories>
<repository>
- <id>eslib-oss</id>
-
<url>https://paimon-es-public-bucket.oss-cn-beijing.aliyuncs.com/maven/</url>
+ <id>eslib-github</id>
+
<url>https://raw.githubusercontent.com/CrownChu/es-paimon-lib-releases/main/repository</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-shade-plugin</artifactId>
- <executions>
- <execution>
- <id>shade-eslib</id>
- <phase>package</phase>
- <goals>
- <goal>shade</goal>
- </goals>
- <configuration>
- <artifactSet>
- <includes>
-
<include>io.github.paimon.eslib:eslib-core</include>
-
<include>io.github.paimon.eslib:eslib-simdvec</include>
- <include>org.apache.lucene:*</include>
- </includes>
- </artifactSet>
- <relocations>
- <relocation>
- <pattern>org.apache.lucene</pattern>
-
<shadedPattern>org.apache.paimon.shade.lucene912</shadedPattern>
- </relocation>
- </relocations>
- <transformers>
- <transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
- </transformers>
- <filters>
- <filter>
- <artifact>*:*</artifact>
- <excludes>
- <exclude>module-info.class</exclude>
- <exclude>META-INF/MANIFEST.MF</exclude>
- <exclude>META-INF/versions/**</exclude>
- </excludes>
- </filter>
- </filters>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
</project>
diff --git
a/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexReader.java
b/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexReader.java
index f95a269eb1..8e5bcd8ab7 100644
---
a/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexReader.java
+++
b/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexReader.java
@@ -350,6 +350,19 @@ public class ESIndexGlobalIndexReader implements
GlobalIndexReader {
return queryExecutor;
}
+ /** Returns the primary vector field's effective metric in Paimon's metric
vocabulary. */
+ String primaryVectorMetric() {
+ if (fields.isEmpty()) {
+ return null;
+ }
+ String physicalField = physicalField(fields.get(0).name());
+ FieldIndexConfig config =
+ physicalField == null ? null :
indexOptions.getConfig(physicalField);
+ return config != null && config.indexType() ==
FieldIndexConfig.IndexType.VECTOR
+ ? ESIndexOptions.toPaimonVectorMetric(config.metric())
+ : null;
+ }
+
int openStreamCount() {
synchronized (openProviders) {
return openProviders.size();
@@ -440,7 +453,7 @@ public class ESIndexGlobalIndexReader implements
GlobalIndexReader {
result.count,
DEFAULT_SAMPLE_LIMIT));
}
- return toScoredResult(result, topK);
+ return toVectorScoredResult(result, topK,
config.metric());
} catch (IOException e) {
throw new RuntimeException("Vector search failed", e);
}
@@ -632,9 +645,13 @@ public class ESIndexGlobalIndexReader implements
GlobalIndexReader {
}
if (q.has("match")) {
JsonNode m = requireObject(q.get("match"), "match query");
+ // Older SQL serialization includes a logical "column" routing
hint. Accept it for
+ // compatibility, but keep the already resolved physical field
authoritative so column
+ // renames continue to work.
requireOnlyFields(
m,
"match query",
+ "column",
"query",
"terms",
"operator",
@@ -650,7 +667,7 @@ public class ESIndexGlobalIndexReader implements
GlobalIndexReader {
requireObject(
q.has("match_phrase") ? q.get("match_phrase") :
q.get("phrase"),
"phrase query");
- requireOnlyFields(p, "phrase query", "query", "terms", "slop");
+ requireOnlyFields(p, "phrase query", "column", "query", "terms",
"slop");
int slop = p.has("slop") ? intValue(p.get("slop"), "slop") : 0;
return new FullTextQuerySpec.Phrase(field, queryText(p), slop);
} else if (q.has("boost")) {
@@ -967,6 +984,53 @@ public class ESIndexGlobalIndexReader implements
GlobalIndexReader {
return Optional.of(ScoredGlobalIndexResult.create(bitmap,
scoreMap::get));
}
+ private Optional<ScoredGlobalIndexResult> toVectorScoredResult(
+ SearchResult result, int limit, String metric) {
+ if (result == null || result.count == 0) {
+ return Optional.empty();
+ }
+
+ int count = Math.min(result.count, limit);
+ RoaringNavigableMap64 bitmap = new RoaringNavigableMap64();
+ Map<Long, Float> scoreMap = new HashMap<>(count);
+ for (int i = 0; i < count; i++) {
+ long id = result.ids[i];
+ bitmap.add(id);
+ scoreMap.put(id, toPaimonVectorScore(result.scores[i], metric));
+ }
+
+ return Optional.of(ScoredGlobalIndexResult.create(bitmap,
scoreMap::get));
+ }
+
+ /** Converts Lucene's metric-specific score into Paimon's exact
vector-search score. */
+ static float toPaimonVectorScore(float luceneScore, String metric) {
+ if (!Float.isFinite(luceneScore)) {
+ throw new IllegalArgumentException(
+ "Non-finite Lucene vector score for metric '" + metric +
"': " + luceneScore);
+ }
+ String normalized = metric == null ? "l2" :
metric.toLowerCase(Locale.ROOT);
+ switch (normalized) {
+ case "l2":
+ case "euclidean":
+ return luceneScore;
+ case "cosine":
+ case "dot_product":
+ case "dp":
+ return 2.0f * luceneScore - 1.0f;
+ case "inner_product":
+ case "mip":
+ case "maximum_inner_product":
+ if (luceneScore <= 0.0f) {
+ throw new IllegalArgumentException(
+ "Lucene maximum-inner-product score must be
positive; got: "
+ + luceneScore);
+ }
+ return luceneScore < 1.0f ? 1.0f - 1.0f / luceneScore :
luceneScore - 1.0f;
+ default:
+ throw new IllegalArgumentException("Unknown ESLib vector
metric: " + metric);
+ }
+ }
+
private void checkNotClosed() throws IOException {
if (closed) {
throw new IOException("Reader already closed");
diff --git
a/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexer.java
b/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexer.java
index 293e33a595..0167e24066 100644
---
a/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexer.java
+++
b/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexer.java
@@ -21,12 +21,14 @@ package org.apache.paimon.eslib.index;
import org.apache.paimon.globalindex.GlobalIndexIOMeta;
import org.apache.paimon.globalindex.GlobalIndexReader;
import org.apache.paimon.globalindex.GlobalIndexWriter;
-import org.apache.paimon.globalindex.GlobalIndexer;
+import org.apache.paimon.globalindex.VectorGlobalIndexer;
import org.apache.paimon.globalindex.io.GlobalIndexFileReader;
import org.apache.paimon.globalindex.io.GlobalIndexFileWriter;
import org.apache.paimon.options.Options;
import org.apache.paimon.types.DataField;
+import org.elasticsearch.eslib.api.model.FieldIndexConfig;
+
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@@ -37,14 +39,17 @@ import java.util.concurrent.ExecutorService;
* ES multi-index global indexer using ESLib. Builds Lucene-based indexes
supporting vector,
* fulltext, and scalar fields.
*/
-public class ESIndexGlobalIndexer implements GlobalIndexer {
+public class ESIndexGlobalIndexer implements VectorGlobalIndexer {
private final List<DataField> fields;
private final ESIndexOptions indexOptions;
+ private final String configuredVectorMetric;
+ private volatile String readerVectorMetric;
public ESIndexGlobalIndexer(List<DataField> fields, Options options) {
this.fields = Collections.unmodifiableList(new ArrayList<>(fields));
this.indexOptions = new ESIndexOptions(this.fields, options);
+ this.configuredVectorMetric = primaryVectorMetric(this.fields,
this.indexOptions);
}
@Override
@@ -57,6 +62,50 @@ public class ESIndexGlobalIndexer implements GlobalIndexer {
GlobalIndexFileReader fileReader,
List<GlobalIndexIOMeta> files,
ExecutorService executor) {
- return new ESIndexGlobalIndexReader(fileReader, files, fields,
indexOptions, executor);
+ ESIndexGlobalIndexReader reader =
+ new ESIndexGlobalIndexReader(fileReader, files, fields,
indexOptions, executor);
+ try {
+ registerReaderVectorMetric(reader.primaryVectorMetric());
+ return reader;
+ } catch (RuntimeException e) {
+ try {
+ reader.close();
+ } catch (IOException closeFailure) {
+ e.addSuppressed(closeFailure);
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public String metric() {
+ String metric = readerVectorMetric;
+ return metric == null ? configuredVectorMetric : metric;
+ }
+
+ private synchronized void registerReaderVectorMetric(String metric) {
+ if (metric == null) {
+ return;
+ }
+ if (readerVectorMetric == null) {
+ readerVectorMetric = metric;
+ } else if (!readerVectorMetric.equals(metric)) {
+ throw new IllegalArgumentException(
+ "Cannot combine es-index shards with different vector
metrics: "
+ + readerVectorMetric
+ + " and "
+ + metric
+ + ".");
+ }
+ }
+
+ private static String primaryVectorMetric(List<DataField> fields,
ESIndexOptions indexOptions) {
+ if (fields.isEmpty()) {
+ return null;
+ }
+ FieldIndexConfig config = indexOptions.getConfig(fields.get(0).name());
+ return config != null && config.indexType() ==
FieldIndexConfig.IndexType.VECTOR
+ ? ESIndexOptions.toPaimonVectorMetric(config.metric())
+ : null;
}
}
diff --git
a/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexOptions.java
b/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexOptions.java
index 530e5fbe00..d1936ec21c 100644
---
a/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexOptions.java
+++
b/paimon-eslib/src/main/java/org/apache/paimon/eslib/index/ESIndexOptions.java
@@ -159,6 +159,26 @@ public class ESIndexOptions {
return fieldConfigs.get(fieldName);
}
+ /** Maps an ESLib/Lucene vector metric name to Paimon's exact-search
metric vocabulary. */
+ static String toPaimonVectorMetric(String metric) {
+ String normalized = metric == null ? "l2" :
metric.toLowerCase(Locale.ROOT);
+ switch (normalized) {
+ case "l2":
+ case "euclidean":
+ return "l2";
+ case "cosine":
+ return "cosine";
+ case "dot_product":
+ case "dp":
+ case "inner_product":
+ case "mip":
+ case "maximum_inner_product":
+ return "inner_product";
+ default:
+ throw new IllegalArgumentException("Unknown ESLib vector
metric: " + metric);
+ }
+ }
+
/** Returns the keyword multi-field sub-field name for {@code fieldName}
if one exists. */
public String keywordSubField(String fieldName) {
String subField = fieldName + KEYWORD_SUBFIELD_SUFFIX;
diff --git
a/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexFullTextQueryParserTest.java
b/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexFullTextQueryParserTest.java
index adb86557bc..73657470f3 100644
---
a/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexFullTextQueryParserTest.java
+++
b/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexFullTextQueryParserTest.java
@@ -27,6 +27,32 @@ import static
org.assertj.core.api.Assertions.assertThatThrownBy;
class ESIndexFullTextQueryParserTest {
+ @Test
+ void acceptsPersistedColumnRoutingHints() {
+ FullTextQuerySpec.Match match =
+ (FullTextQuerySpec.Match)
+ ESIndexGlobalIndexReader.parseSpec(
+ "physical_text",
+
"{\"match\":{\"column\":\"logical_text\",\"terms\":\"paimon\"}}");
+ assertThat(match.field()).isEqualTo("physical_text");
+ assertThat(match.text()).isEqualTo("paimon");
+
+ FullTextQuerySpec.Phrase phrase =
+ (FullTextQuerySpec.Phrase)
+ ESIndexGlobalIndexReader.parseSpec(
+ "physical_text",
+
"{\"match_phrase\":{\"column\":\"logical_text\",\"terms\":\"apache
paimon\",\"slop\":1}}");
+ assertThat(phrase.field()).isEqualTo("physical_text");
+ assertThat(phrase.text()).isEqualTo("apache paimon");
+ assertThat(phrase.slop()).isEqualTo(1);
+
+ assertThat(
+ ESIndexGlobalIndexReader.parseSpec(
+ "physical_text",
+
"{\"boolean\":{\"queries\":[[\"Must\",{\"match\":{\"column\":\"logical_text\",\"terms\":\"paimon\"}}],[\"Should\",{\"phrase\":{\"column\":\"logical_text\",\"terms\":\"apache
paimon\"}}]]}}"))
+ .isNotNull();
+ }
+
@Test
void parsesCaseInsensitiveBooleanOperatorsAndRejectsTypos() {
FullTextQuerySpec.Match and =
diff --git
a/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexE2ETest.java
b/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexE2ETest.java
index 27ec952689..464d1ac8a4 100644
---
a/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexE2ETest.java
+++
b/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexGlobalIndexE2ETest.java
@@ -1636,6 +1636,40 @@ class ESIndexGlobalIndexE2ETest {
entry.meta());
}
+ @Test
+ void vectorSearchReturnsPaimonCosineScoreScale(@TempDir java.nio.file.Path
tmp)
+ throws Exception {
+ List<DataField> fields =
+ List.of(new DataField(0, "embedding",
DataTypes.ARRAY(DataTypes.FLOAT())));
+ Map<String, String> optionMap = new HashMap<>();
+ optionMap.put("global-index.es-index.fields.embedding.dimension", "2");
+ optionMap.put("global-index.es-index.fields.embedding.metric",
"cosine");
+ ESIndexOptions options = new ESIndexOptions(fields,
Options.fromMap(optionMap));
+
+ java.nio.file.Path archiveDir = tmp.resolve("cosine-scores");
+ Files.createDirectories(archiveDir);
+ ESIndexGlobalIndexWriter writer =
+ new ESIndexGlobalIndexWriter(new LocalDirWriter(archiveDir),
fields, options);
+ writer.write(new float[] {1.0f, 0.0f}, 0L);
+ writer.write(new float[] {0.0f, 1.0f}, 1L);
+ ResultEntry entry = writer.finish().get(0);
+
+ ESIndexGlobalIndexReader reader =
+ new ESIndexGlobalIndexReader(
+ new LocalFileReader(), List.of(ioMeta(archiveDir,
entry)), fields, options);
+ try {
+ ScoredGlobalIndexResult result =
+ reader.visitVectorSearch(
+ new VectorSearch(new float[] {1.0f, 0.0f},
2, "embedding"))
+ .join()
+ .orElseThrow(AssertionError::new);
+ assertEquals(1.0f, result.scoreGetter().score(0L), 0.000001f);
+ assertEquals(0.0f, result.scoreGetter().score(1L), 0.000001f);
+ } finally {
+ reader.close();
+ }
+ }
+
/**
* Verifies the DiskBBQ vector codec write path through {@link
ESIndexGlobalIndexWriter}.
*
diff --git
a/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexVectorMetricTest.java
b/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexVectorMetricTest.java
new file mode 100644
index 0000000000..fceab62411
--- /dev/null
+++
b/paimon-eslib/src/test/java/org/apache/paimon/eslib/index/ESIndexVectorMetricTest.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.eslib.index;
+
+import org.apache.paimon.globalindex.GlobalIndexIOMeta;
+import org.apache.paimon.globalindex.GlobalIndexReader;
+import org.apache.paimon.globalindex.GlobalIndexer;
+import org.apache.paimon.globalindex.VectorGlobalIndexer;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
+
+import org.elasticsearch.eslib.api.model.FieldIndexConfig;
+import org.elasticsearch.eslib.api.model.VectorAlgorithm;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class ESIndexVectorMetricTest {
+
+ private static final List<DataField> VECTOR_FIELDS =
+ List.of(new DataField(0, "embedding", DataTypes.VECTOR(2,
DataTypes.FLOAT())));
+
+ @Test
+ void exposesCanonicalPaimonMetric() {
+ GlobalIndexer indexer = indexer("dp");
+ assertThat(indexer).isInstanceOf(VectorGlobalIndexer.class);
+ assertThat(((VectorGlobalIndexer)
indexer).metric()).isEqualTo("inner_product");
+
+ assertThat(indexer("euclidean").metric()).isEqualTo("l2");
+ assertThat(indexer("cosine").metric()).isEqualTo("cosine");
+ assertThat(indexer("dot_product").metric()).isEqualTo("inner_product");
+
assertThat(indexer("inner_product").metric()).isEqualTo("inner_product");
+ assertThat(indexer("mip").metric()).isEqualTo("inner_product");
+
assertThat(indexer("maximum_inner_product").metric()).isEqualTo("inner_product");
+ }
+
+ @Test
+ void persistedMetricOverridesCurrentTableConfiguration(@TempDir Path
tempDir) throws Exception {
+ ESIndexGlobalIndexer indexer = indexer("cosine");
+ assertThat(indexer.metric()).isEqualTo("cosine");
+
+ GlobalIndexReader reader =
+ indexer.createReader(
+ meta -> null,
+ List.of(vectorFile(tempDir, "persisted-l2",
"euclidean")),
+ null);
+ try {
+ assertThat(((ESIndexGlobalIndexReader)
reader).primaryVectorMetric()).isEqualTo("l2");
+ assertThat(indexer.metric()).isEqualTo("l2");
+ } finally {
+ reader.close();
+ }
+ }
+
+ @Test
+ void rejectsShardsWithDifferentPersistedMetrics(@TempDir Path tempDir)
throws Exception {
+ ESIndexGlobalIndexer indexer = indexer("cosine");
+ GlobalIndexReader first =
+ indexer.createReader(
+ meta -> null, List.of(vectorFile(tempDir, "first",
"euclidean")), null);
+ try {
+ assertThatThrownBy(
+ () ->
+ indexer.createReader(
+ meta -> null,
+ List.of(vectorFile(tempDir,
"second", "cosine")),
+ null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("different vector metrics")
+ .hasMessageContaining("l2")
+ .hasMessageContaining("cosine");
+ } finally {
+ first.close();
+ }
+ }
+
+ @Test
+ void convertsLuceneScoresToPaimonScoreScale() {
+ assertThat(ESIndexGlobalIndexReader.toPaimonVectorScore(0.25f,
"l2")).isEqualTo(0.25f);
+ assertThat(ESIndexGlobalIndexReader.toPaimonVectorScore(0.25f,
"euclidean"))
+ .isEqualTo(0.25f);
+
+ assertThat(ESIndexGlobalIndexReader.toPaimonVectorScore(0.75f,
"cosine")).isEqualTo(0.5f);
+ assertThat(ESIndexGlobalIndexReader.toPaimonVectorScore(0.75f,
"dot_product"))
+ .isEqualTo(0.5f);
+ assertThat(ESIndexGlobalIndexReader.toPaimonVectorScore(0.25f,
"dp")).isEqualTo(-0.5f);
+
+ assertThat(ESIndexGlobalIndexReader.toPaimonVectorScore(3.0f,
"inner_product"))
+ .isEqualTo(2.0f);
+ assertThat(ESIndexGlobalIndexReader.toPaimonVectorScore(0.5f,
"mip")).isEqualTo(-1.0f);
+ assertThat(ESIndexGlobalIndexReader.toPaimonVectorScore(0.25f,
"maximum_inner_product"))
+ .isEqualTo(-3.0f);
+
+ assertThatThrownBy(() ->
ESIndexGlobalIndexReader.toPaimonVectorScore(Float.NaN, "cosine"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Non-finite");
+ assertThatThrownBy(() ->
ESIndexGlobalIndexReader.toPaimonVectorScore(0.0f, "mip"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("must be positive");
+ }
+
+ private static ESIndexGlobalIndexer indexer(String metric) {
+ Map<String, String> options = new LinkedHashMap<>();
+ options.put("global-index.es-index.fields.embedding.metric", metric);
+ return new ESIndexGlobalIndexer(VECTOR_FIELDS,
Options.fromMap(options));
+ }
+
+ private static GlobalIndexIOMeta vectorFile(Path tempDir, String name,
String metric)
+ throws Exception {
+ Path segment = tempDir.resolve("segments_" + name);
+ Files.write(segment, new byte[] {1});
+
+ Map<String, FieldIndexConfig> configs = new LinkedHashMap<>();
+ configs.put(
+ "embedding",
+ FieldIndexConfig.builder("embedding",
FieldIndexConfig.IndexType.VECTOR)
+ .algorithm(VectorAlgorithm.HNSW)
+ .dimension(2)
+ .metric(metric)
+ .build());
+ byte[] metadata =
+ ESIndexFileMeta.write(
+ new java.io.File[] {segment.toFile()},
+ List.of("embedding"),
+
List.of(VECTOR_FIELDS.get(0).type().copy(true).asSQLString()),
+ configs);
+
+ long archiveSize = 0L;
+ for (long[] range :
ESIndexFileMeta.read(metadata).fileOffsets().values()) {
+ archiveSize = Math.max(archiveSize, range[0] + range[1]);
+ }
+ return new GlobalIndexIOMeta(
+ new org.apache.paimon.fs.Path(tempDir.resolve(name +
".index").toString()),
+ archiveSize,
+ metadata);
+ }
+}