This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new d947f9823c [#11934] feat(clickhouse): read text index metadata (#13419)
d947f9823c is described below
commit d947f9823c2a46794c57278893eb61cc4cce2f85
Author: Thunderkeg <[email protected]>
AuthorDate: Wed Oct 28 08:36:54 2026 +0800
[#11934] feat(clickhouse): read text index metadata (#13419)
### What changes were proposed in this pull request?
This change adds read-only loading of native ClickHouse text
data-skipping index metadata. It recognizes the `text`, `gin`,
`full_text`, and `inverted` type names, restores supported `tokenizer`
and `ngram_size` properties from `type_full`, and preserves fallback to
legacy `type` metadata. For the `ngrams` tokenizer, it preserves sizes
from 1 through 8. The new `DATA_SKIPPING_TEXT` enum is covered by
JSON/OpenAPI updates and catalog documentation.
### Why are the changes needed?
Loading a ClickHouse table through Gravitino should preserve its native
text-index type and supported tokenizer metadata through the existing
table API.
Fix: #11934
### Does this PR introduce any user-facing change?
Yes. Loaded table metadata exposes the `DATA_SKIPPING_TEXT` type and
supported tokenizer values (`tokens` or `ngrams`); `ngram_size` values
from 1 through 8 are exposed for `ngrams`. Creating or altering text
indexes through Gravitino remains unsupported by this read-only change.
### How was this patch tested?
* `./gradlew :catalogs-contrib:catalog-jdbc-clickhouse:test --tests
org.apache.gravitino.catalog.clickhouse.operations.TestClickHouseTableOperationsUnit
-PskipITs` — 67 tests passed on the final read-path commit, including
modern `ngrams(1)`, legacy `inverted(1)`, and rejection of sizes 0 and
9.
* `./gradlew :common:test --tests
org.apache.gravitino.json.TestJsonUtils` — 13 tests passed.
* `./gradlew :docs:build` — passed.
* `./gradlew :catalogs-contrib:catalog-jdbc-clickhouse:test --tests
org.apache.gravitino.catalog.clickhouse.integration.test.CatalogClickHouseIT.testLoadNativeTextDataSkippingIndexes
-PskipDockerTests=false` — 1 test passed.
* `./gradlew :catalogs-contrib:catalog-jdbc-clickhouse:test --tests
org.apache.gravitino.catalog.clickhouse.integration.test.CatalogClickHouseClusterIT
-PskipDockerTests=false` — 20 tests passed; the Docker integration
results were run before the final parser-boundary-only follow-up.
---------
Signed-off-by: jiangxt2 <[email protected]>
---
.../org/apache/gravitino/rel/indexes/Index.java | 7 +-
.../catalog/clickhouse/ClickHouseConstants.java | 21 ++
.../operations/ClickHouseTableOperations.java | 319 ++++++++++++++++++++-
.../integration/test/CatalogClickHouseIT.java | 40 +++
.../TestClickHouseTableOperationsUnit.java | 169 +++++++++++
.../gravitino/api/rel/indexes/index.py | 8 +-
.../tests/unittests/dto/rel/test_index_serdes.py | 33 +++
.../tests/unittests/dto/rel/test_table_dto.py | 24 ++
.../org/apache/gravitino/json/TestJsonUtils.java | 11 +
docs/jdbc-clickhouse-catalog.md | 1 +
docs/open-api/indexes.yaml | 1 +
11 files changed, 623 insertions(+), 11 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/rel/indexes/Index.java
b/api/src/main/java/org/apache/gravitino/rel/indexes/Index.java
index e7cf8b429c..b8e3fcc543 100644
--- a/api/src/main/java/org/apache/gravitino/rel/indexes/Index.java
+++ b/api/src/main/java/org/apache/gravitino/rel/indexes/Index.java
@@ -54,8 +54,8 @@ public interface Index {
}
/**
- * The index type. Catalogs may support generic key types as well as
connector-specific index
- * types.
+ * Identifies an index type in the Gravitino table model. Catalogs may
support generic key types
+ * and connector-specific index types, and supported operations may vary by
catalog.
*/
enum IndexType {
/**
@@ -148,6 +148,9 @@ public interface Index {
*/
DATA_SKIPPING_TOKENBFV1,
+ /** Full-text data skipping index in ClickHouse. */
+ DATA_SKIPPING_TEXT,
+
/**
* Vector similarity data skipping index for approximate nearest-neighbor
search in ClickHouse.
*/
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/ClickHouseConstants.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/ClickHouseConstants.java
index 6263ac27c8..9e2b6b5753 100644
---
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/ClickHouseConstants.java
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/ClickHouseConstants.java
@@ -84,6 +84,21 @@ public class ClickHouseConstants {
/** The name of the data skipping index type for tokenbf_v1 in ClickHouse.
*/
public static final String DATA_SKIPPING_TOKENBFV1 = "tokenbf_v1";
+ /** The current name of ClickHouse's full-text data skipping index. */
+ public static final String DATA_SKIPPING_TEXT = "text";
+
+ /** Historical name of ClickHouse's full-text data skipping index. */
+ public static final String DATA_SKIPPING_GIN = "gin";
+
+ /** Historical name of ClickHouse's full-text data skipping index. */
+ public static final String DATA_SKIPPING_FULL_TEXT = "full_text";
+
+ /** Historical name of ClickHouse's full-text data skipping index. */
+ public static final String DATA_SKIPPING_INVERTED = "inverted";
+
+ /** Property key for the ClickHouse full-text tokenizer mode. */
+ public static final String TOKENIZER = "tokenizer";
+
/** The name of the vector similarity data skipping index type in
ClickHouse. */
public static final String DATA_SKIPPING_VECTOR_SIMILARITY =
"vector_similarity";
@@ -138,5 +153,11 @@ public class ClickHouseConstants {
/** Property key for the n-gram size in ngrambf_v1 index properties. */
public static final String NGRAM_SIZE = "ngram_size";
+
+ /** Gravitino tokenizer property value for ClickHouse word-token mode. */
+ public static final String TEXT_INDEX_TOKENS = "tokens";
+
+ /** Gravitino tokenizer property value for ClickHouse n-gram mode. */
+ public static final String TEXT_INDEX_NGRAMS = "ngrams";
}
}
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
index 8c8660f8fd..f8ca0ffc5b 100644
---
a/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/main/java/org/apache/gravitino/catalog/clickhouse/operations/ClickHouseTableOperations.java
@@ -24,9 +24,13 @@ import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexC
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.CLICKHOUSE_TYPE_FULL;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_ANNOY;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_BLOOM_FILTER;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_FULL_TEXT;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_GIN;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_INVERTED;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_MINMAX_VALUE;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_NGRAMBFV1;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_SET;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_TEXT;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_TOKENBFV1;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_USEARCH;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.DATA_SKIPPING_VECTOR_SIMILARITY;
@@ -37,6 +41,9 @@ import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexC
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.NGRAM_SIZE;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.RANDOM_SEED;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.SET_MAX_VALUES;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.TEXT_INDEX_NGRAMS;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.TEXT_INDEX_TOKENS;
+import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.TOKENIZER;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.USEARCH_DISTANCE_FUNCTION;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.USEARCH_SCALAR_KIND;
import static
org.apache.gravitino.catalog.clickhouse.ClickHouseConstants.IndexConstants.VECTOR_SIMILARITY_DIMENSIONS;
@@ -2115,6 +2122,7 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
if (indexType == Index.IndexType.DATA_SKIPPING_SET
|| isParameterizedBloomFilterIndex(indexType)
+ || indexType == Index.IndexType.DATA_SKIPPING_TEXT
|| isLegacyAnnUsearchIndex(indexType)
|| indexType == Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY)
{
try {
@@ -2177,6 +2185,28 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
databaseName,
tableName);
}
+ if (indexType == Index.IndexType.DATA_SKIPPING_TEXT) {
+ if (parameterProperties.isEmpty()) {
+ LOG.warn(
+ "ClickHouse text index '{}' on {}.{} has no supported
tokenizer metadata in "
+ + "{} '{}'; loaded Index.properties() is incomplete",
+ name,
+ databaseName,
+ tableName,
+ parameterSourceName,
+ parameterSource);
+ } else if
(TEXT_INDEX_NGRAMS.equals(parameterProperties.get(TOKENIZER))
+ && !parameterProperties.containsKey(NGRAM_SIZE)) {
+ LOG.warn(
+ "ClickHouse text index '{}' on {}.{} uses ngrams but {} '{}'
does not expose "
+ + "ngram_size; loaded Index.properties() is incomplete",
+ name,
+ databaseName,
+ tableName,
+ parameterSourceName,
+ parameterSource);
+ }
+ }
properties.putAll(parameterProperties);
secondaryIndexes.add(Indexes.of(indexType, name, fields,
properties));
}
@@ -2287,6 +2317,8 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
case DATA_SKIPPING_TOKENBFV1:
return parseBloomFilterPropertiesForQuery(
indexType, parameterSource, indexName, allowBareLegacyType);
+ case DATA_SKIPPING_TEXT:
+ return parseTextIndexPropertiesForQuery(parameterSource, indexName);
case DATA_SKIPPING_ANNOY:
case DATA_SKIPPING_USEARCH:
return parseLegacyAnnUsearchProperties(indexType, parameterSource,
indexName);
@@ -2577,6 +2609,271 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
|| indexType == Index.IndexType.DATA_SKIPPING_TOKENBFV1;
}
+ @VisibleForTesting
+ static Map<String, String> parseTextIndexPropertiesForQuery(String typeFull,
String indexName) {
+ String normalizedTypeFull = StringUtils.trimToEmpty(typeFull);
+ int paramsStart = normalizedTypeFull.indexOf('(');
+ if (paramsStart < 0) {
+ return Collections.emptyMap();
+ }
+ Preconditions.checkArgument(
+ normalizedTypeFull.endsWith(")"),
+ "Invalid text-index metadata '%s' for index '%s'",
+ typeFull,
+ indexName);
+
+ String clickHouseType = normalizedTypeFull.substring(0,
paramsStart).trim();
+ Preconditions.checkArgument(
+ isClickHouseTextIndexTypeName(clickHouseType),
+ "Text-index metadata '%s' has an unsupported type for index '%s'",
+ typeFull,
+ indexName);
+
+ String rawParameters =
+ normalizedTypeFull.substring(paramsStart + 1,
normalizedTypeFull.length() - 1);
+ if (isLegacyTextIndexTypeName(clickHouseType)) {
+ List<String> parameters = splitTextIndexParameters(rawParameters,
typeFull, indexName);
+ Preconditions.checkArgument(
+ parameters.size() == 1,
+ "Invalid %s metadata '%s' for index '%s': expected one tokenizer
parameter",
+ clickHouseType,
+ typeFull,
+ indexName);
+ return parseLegacyTextTokenizer(parameters.get(0), typeFull,
clickHouseType, indexName);
+ }
+
+ Map<String, String> namedParameters = new HashMap<>();
+ List<String> unsupportedParameters = new ArrayList<>();
+ for (String parameter : splitTextIndexParameters(rawParameters, typeFull,
indexName)) {
+ int assignmentIndex = parameter.indexOf('=');
+ if (assignmentIndex <= 0) {
+ unsupportedParameters.add(parameter.trim());
+ continue;
+ }
+ String key = StringUtils.lowerCase(parameter.substring(0,
assignmentIndex).trim());
+ String value = parameter.substring(assignmentIndex + 1).trim();
+ if (!TOKENIZER.equals(key) && !NGRAM_SIZE.equals(key)) {
+ unsupportedParameters.add(key);
+ continue;
+ }
+ Preconditions.checkArgument(
+ !namedParameters.containsKey(key),
+ "Duplicate text-index parameter '%s' in '%s' for index '%s'",
+ key,
+ typeFull,
+ indexName);
+ namedParameters.put(key, value);
+ }
+ if (!unsupportedParameters.isEmpty()) {
+ LOG.warn(
+ "ClickHouse text index '{}' contains unsupported metadata parameters
{}; "
+ + "loaded Index.properties() is incomplete",
+ indexName,
+ unsupportedParameters);
+ }
+
+ String tokenizer =
stripTextParameterQuotes(namedParameters.get(TOKENIZER));
+ if (StringUtils.isBlank(tokenizer)) {
+ return Collections.emptyMap();
+ }
+ if (StringUtils.equalsAnyIgnoreCase(tokenizer, "default", "tokens",
"splitByNonAlpha")) {
+ Preconditions.checkArgument(
+ !namedParameters.containsKey(NGRAM_SIZE),
+ "ngram_size is only valid for ngrams tokenizer in text-index
metadata '%s' for index '%s'",
+ typeFull,
+ indexName);
+ return Map.of(TOKENIZER, TEXT_INDEX_TOKENS);
+ }
+
+ String embeddedNgramSize = null;
+ String normalizedTokenizer = tokenizer;
+ int tokenizerParamsStart = tokenizer.indexOf('(');
+ if (tokenizerParamsStart >= 0 && tokenizer.endsWith(")")) {
+ normalizedTokenizer = tokenizer.substring(0,
tokenizerParamsStart).trim();
+ embeddedNgramSize =
+ tokenizer.substring(tokenizerParamsStart + 1, tokenizer.length() -
1).trim();
+ }
+ if (StringUtils.equalsAnyIgnoreCase(normalizedTokenizer, "ngram",
"ngrams")) {
+ String configuredNgramSize = namedParameters.get(NGRAM_SIZE);
+ Preconditions.checkArgument(
+ embeddedNgramSize == null || configuredNgramSize == null,
+ "ngram_size is specified twice in text-index metadata '%s' for index
'%s'",
+ typeFull,
+ indexName);
+ String ngramSize = embeddedNgramSize == null ? configuredNgramSize :
embeddedNgramSize;
+ Map<String, String> properties = new HashMap<>();
+ properties.put(TOKENIZER, TEXT_INDEX_NGRAMS);
+ if (StringUtils.isNotBlank(ngramSize)) {
+ properties.put(NGRAM_SIZE, requireTextNgramSize(ngramSize, typeFull,
indexName));
+ }
+ return Map.copyOf(properties);
+ }
+
+ return Collections.emptyMap();
+ }
+
+ private static Map<String, String> parseLegacyTextTokenizer(
+ String rawParameter, String typeFull, String clickHouseType, String
indexName) {
+ String parameter = stripTextParameterQuotes(rawParameter);
+ Preconditions.checkArgument(
+ StringUtils.isNumeric(parameter),
+ "Invalid %s metadata '%s' for index '%s': tokenizer parameter '%s'
must be an integer",
+ clickHouseType,
+ typeFull,
+ indexName,
+ parameter);
+ int value;
+ try {
+ value = Integer.parseInt(parameter);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Invalid text-index metadata '%s' for index '%s': tokenizer
parameter '%s' is outside the supported integer range"
+ .formatted(typeFull, indexName, parameter),
+ e);
+ }
+
+ if (value == 0) {
+ return Map.of(TOKENIZER, TEXT_INDEX_TOKENS);
+ }
+ Preconditions.checkArgument(
+ value >= 1 && value <= 8,
+ "Invalid text-index metadata '%s' for index '%s': ngram_size %s is
outside the supported range [1, 8]",
+ typeFull,
+ indexName,
+ value);
+ return Map.of(TOKENIZER, TEXT_INDEX_NGRAMS, NGRAM_SIZE,
String.valueOf(value));
+ }
+
+ private static String requireTextNgramSize(String value, String typeFull,
String indexName) {
+ String normalizedValue = stripTextParameterQuotes(value);
+ Preconditions.checkArgument(
+ StringUtils.isNumeric(normalizedValue),
+ "Invalid text-index metadata '%s' for index '%s': ngram_size '%s' must
be an integer",
+ typeFull,
+ indexName,
+ normalizedValue);
+ int ngramSize;
+ try {
+ ngramSize = Integer.parseInt(normalizedValue);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Invalid text-index metadata '%s' for index '%s': ngram_size '%s' is
outside the supported integer range"
+ .formatted(typeFull, indexName, normalizedValue),
+ e);
+ }
+ Preconditions.checkArgument(
+ ngramSize >= 1 && ngramSize <= 8,
+ "Invalid text-index metadata '%s' for index '%s': ngram_size %s is
outside the supported range [1, 8]",
+ typeFull,
+ indexName,
+ ngramSize);
+ return String.valueOf(ngramSize);
+ }
+
+ private static List<String> splitTextIndexParameters(
+ String rawParameters, String typeFull, String indexName) {
+ List<String> parameters = new ArrayList<>();
+ StringBuilder current = new StringBuilder();
+ int parenthesesDepth = 0;
+ int bracketsDepth = 0;
+ char quote = 0;
+ boolean escaped = false;
+ for (int i = 0; i < rawParameters.length(); i++) {
+ char character = rawParameters.charAt(i);
+ if (quote != 0) {
+ current.append(character);
+ if (escaped) {
+ escaped = false;
+ } else if (character == '\\') {
+ escaped = true;
+ } else if (character == quote) {
+ if (i + 1 < rawParameters.length() && rawParameters.charAt(i + 1) ==
quote) {
+ current.append(rawParameters.charAt(++i));
+ } else {
+ quote = 0;
+ }
+ }
+ continue;
+ }
+ if (character == '\'' || character == '"') {
+ quote = character;
+ current.append(character);
+ } else if (character == '(') {
+ parenthesesDepth++;
+ current.append(character);
+ } else if (character == ')') {
+ parenthesesDepth--;
+ Preconditions.checkArgument(
+ parenthesesDepth >= 0,
+ "Invalid text-index metadata '%s' for index '%s'",
+ typeFull,
+ indexName);
+ current.append(character);
+ } else if (character == '[') {
+ bracketsDepth++;
+ current.append(character);
+ } else if (character == ']') {
+ bracketsDepth--;
+ Preconditions.checkArgument(
+ bracketsDepth >= 0,
+ "Invalid text-index metadata '%s' for index '%s'",
+ typeFull,
+ indexName);
+ current.append(character);
+ } else if (character == ',' && parenthesesDepth == 0 && bracketsDepth ==
0) {
+ parameters.add(current.toString().trim());
+ current.setLength(0);
+ } else {
+ current.append(character);
+ }
+ }
+ Preconditions.checkArgument(
+ quote == 0 && parenthesesDepth == 0 && bracketsDepth == 0 && !escaped,
+ "Invalid text-index metadata '%s' for index '%s'",
+ typeFull,
+ indexName);
+ if (current.length() > 0 || !rawParameters.isEmpty()) {
+ parameters.add(current.toString().trim());
+ }
+ return parameters;
+ }
+
+ private static String stripTextParameterQuotes(String value) {
+ String normalizedValue = StringUtils.trimToEmpty(value);
+ if (normalizedValue.length() >= 2
+ && ((normalizedValue.startsWith("'") && normalizedValue.endsWith("'"))
+ || (normalizedValue.startsWith("\"") &&
normalizedValue.endsWith("\"")))) {
+ normalizedValue = normalizedValue.substring(1, normalizedValue.length()
- 1);
+ }
+ return normalizedValue.replace("\\'", "'").replace("''", "'");
+ }
+
+ private static boolean isClickHouseTextIndexTypeName(String typeName) {
+ return StringUtils.equalsAnyIgnoreCase(
+ StringUtils.trimToEmpty(typeName),
+ DATA_SKIPPING_TEXT,
+ DATA_SKIPPING_GIN,
+ DATA_SKIPPING_FULL_TEXT,
+ DATA_SKIPPING_INVERTED);
+ }
+
+ private static boolean isLegacyTextIndexTypeName(String typeName) {
+ return StringUtils.equalsAnyIgnoreCase(
+ StringUtils.trimToEmpty(typeName),
+ DATA_SKIPPING_GIN,
+ DATA_SKIPPING_FULL_TEXT,
+ DATA_SKIPPING_INVERTED);
+ }
+
+ private static boolean isClickHouseTextIndexType(String rawType) {
+ if (StringUtils.isBlank(rawType)) {
+ return false;
+ }
+ int paramsStart = rawType.indexOf('(');
+ String typeName = paramsStart < 0 ? rawType.trim() : rawType.substring(0,
paramsStart).trim();
+ return isClickHouseTextIndexTypeName(typeName);
+ }
+
private static Map<String, String> parseLegacyAnnUsearchProperties(
Index.IndexType indexType, String typeFull, String indexName) {
String expectedType = legacyAnnUsearchTypeName(indexType);
@@ -2725,12 +3022,12 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
/**
* Maps a ClickHouse data skipping index type string to the corresponding
Gravitino {@link
* Index.IndexType}. Returns {@code DATA_SKIPPING_MINMAX} for blank/null
input (ClickHouse
- * default). Also handles parameterized formats such as {@code set(N)},
{@code annoy(N)}, {@code
- * usearch(distanceFunction)}, and {@code vector_similarity(...)} that some
ClickHouse versions
- * may return from {@code system.data_skipping_indices}.
+ * default). Also handles parameterized formats returned from {@code
+ * system.data_skipping_indices}, including text, legacy Annoy/USearch, and
vector similarity
+ * indexes.
*
- * @param rawType the index type string from ClickHouse metadata (e.g.
"minmax", "bloom_filter",
- * "set", "set(0)", or "vector_similarity")
+ * @param rawType the index type string from ClickHouse metadata (e.g.
"minmax", "set(0)",
+ * "inverted(0)", "text(tokenizer = ngrams(3))", or "vector_similarity")
* @return the corresponding Gravitino IndexType
* @throws IllegalArgumentException if the type is not supported
*/
@@ -2751,6 +3048,11 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
return Index.IndexType.DATA_SKIPPING_NGRAMBFV1;
case DATA_SKIPPING_TOKENBFV1:
return Index.IndexType.DATA_SKIPPING_TOKENBFV1;
+ case DATA_SKIPPING_TEXT:
+ case DATA_SKIPPING_GIN:
+ case DATA_SKIPPING_FULL_TEXT:
+ case DATA_SKIPPING_INVERTED:
+ return Index.IndexType.DATA_SKIPPING_TEXT;
case DATA_SKIPPING_ANNOY:
return Index.IndexType.DATA_SKIPPING_ANNOY;
case DATA_SKIPPING_USEARCH:
@@ -2758,8 +3060,8 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
case DATA_SKIPPING_VECTOR_SIMILARITY:
return Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY;
default:
- // ClickHouse may return type with parameters in some versions. Match
on prefix to handle
- // both bare and parameterized formats.
+ // ClickHouse may return types with parameters in some versions. Match
the parameterized
+ // format for data skipping indexes whose raw type is not covered by
an exact case above.
if (rawType.startsWith(DATA_SKIPPING_SET + "(")) {
return Index.IndexType.DATA_SKIPPING_SET;
}
@@ -2769,6 +3071,9 @@ public class ClickHouseTableOperations extends
JdbcTableOperations {
if (rawType.startsWith(DATA_SKIPPING_TOKENBFV1 + "(")) {
return Index.IndexType.DATA_SKIPPING_TOKENBFV1;
}
+ if (isClickHouseTextIndexType(rawType)) {
+ return Index.IndexType.DATA_SKIPPING_TEXT;
+ }
if (isParameterizedLegacyAnnUsearchType(rawType, DATA_SKIPPING_ANNOY))
{
return Index.IndexType.DATA_SKIPPING_ANNOY;
}
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
index 8d7444acb8..fdcb6f1c44 100644
---
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/integration/test/CatalogClickHouseIT.java
@@ -3986,6 +3986,46 @@ public class CatalogClickHouseIT extends BaseIT {
"SHOW CREATE TABLE should contain Enum16 definition: " + createSql);
}
+ @Test
+ @Tag("gravitino-docker-test")
+ void testLoadNativeTextDataSkippingIndexes() {
+ String tableName = GravitinoITUtils.genRandomName("ch_text_idx_");
+ NameIdentifier tableIdentifier = NameIdentifier.of(schemaName, tableName);
+
+ // ClickHouse 24.8 requires the experimental text-index setting for this
table creation.
+ clickhouseService.executeQuery(
+ String.format(
+ "CREATE TABLE `%s`.`%s` (\n"
+ + " `id` UInt64,\n"
+ + " `token_text` String,\n"
+ + " `ngram_text` String,\n"
+ + " INDEX `idx_tokens` `token_text` TYPE text(tokenizer =
'default') GRANULARITY 1,\n"
+ + " INDEX `idx_ngrams` `ngram_text` TYPE text(tokenizer =
'ngram', ngram_size = 3) GRANULARITY 1\n"
+ + ") ENGINE = MergeTree ORDER BY `id` COMMENT 'text index IT' "
+ + "SETTINGS allow_experimental_full_text_index = 1",
+ schemaName, tableName));
+
+ Table loaded = catalog.asTableCatalog().loadTable(tableIdentifier);
+ Index loadedTokens =
+ Arrays.stream(loaded.index())
+ .filter(index -> Objects.equals(index.name(), "idx_tokens"))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_TEXT,
loadedTokens.type());
+ Assertions.assertArrayEquals(new String[][] {{"token_text"}},
loadedTokens.fieldNames());
+ Assertions.assertEquals(Map.of("tokenizer", "tokens"),
loadedTokens.properties());
+
+ Index loadedNgrams =
+ Arrays.stream(loaded.index())
+ .filter(index -> Objects.equals(index.name(), "idx_ngrams"))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_TEXT,
loadedNgrams.type());
+ Assertions.assertArrayEquals(new String[][] {{"ngram_text"}},
loadedNgrams.fieldNames());
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "3"),
loadedNgrams.properties());
+ }
+
@Test
@Tag("gravitino-docker-test")
void testCreateAndLoadTableWithNgrambfAndTokenbfIndexes() {
diff --git
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
index f06dd53bd3..5eb19b1702 100644
---
a/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
+++
b/catalogs-contrib/catalog-jdbc-clickhouse/src/test/java/org/apache/gravitino/catalog/clickhouse/operations/TestClickHouseTableOperationsUnit.java
@@ -1851,6 +1851,175 @@ public class TestClickHouseTableOperationsUnit {
indexes.get(0).properties());
}
+ @Test
+ void testGetClickHouseTextIndexTypeAliases() {
+ ExposedClickHouseTableOperations ops = newOps();
+
+ for (String type :
+ List.of(
+ "text",
+ "gin",
+ "full_text",
+ "inverted",
+ "inverted(0)",
+ "full_text(3)",
+ "text(tokenizer = ngrams(3))")) {
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_TEXT,
ops.getClickHouseIndexType(type));
+ }
+ }
+
+ @Test
+ void testParseTextIndexProperties() {
+ Assertions.assertEquals(
+ Map.of("tokenizer", "tokens"),
+
ClickHouseTableOperations.parseTextIndexPropertiesForQuery("inverted(0)",
"idx_tokens"));
+ Assertions.assertEquals(
+ Map.of("tokenizer", "tokens"),
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery("gin(0)",
"idx_tokens"));
+ Assertions.assertEquals(
+ Map.of("tokenizer", "tokens"),
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery(
+ "text(tokenizer = 'default')", "idx_tokens"));
+ Assertions.assertEquals(
+ Map.of("tokenizer", "tokens"),
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery(
+ "text(tokenizer = splitByNonAlpha)", "idx_tokens"));
+
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "2"),
+
ClickHouseTableOperations.parseTextIndexPropertiesForQuery("full_text(2)",
"idx_ngrams"));
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "5"),
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery("gin(5)",
"idx_ngrams"));
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "8"),
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery(
+ "text(tokenizer = ngrams(8))", "idx_ngrams"));
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "1"),
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery(
+ "text(tokenizer = ngrams(1))", "idx_ngrams"));
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "1"),
+
ClickHouseTableOperations.parseTextIndexPropertiesForQuery("inverted(1)",
"idx_ngrams"));
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "4"),
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery(
+ "text(tokenizer = 'ngram', ngram_size = 4, "
+ + "preprocessor = lower(concat(body, ', ')))",
+ "idx_ngrams"));
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams"),
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery(
+ "text(tokenizer = ngrams)", "idx_ngrams_incomplete"));
+ Assertions.assertTrue(
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery(
+ "text(tokenizer = sparseGrams(3, 100))", "idx_unsupported")
+ .isEmpty());
+ Assertions.assertTrue(
+ ClickHouseTableOperations.parseTextIndexPropertiesForQuery("inverted",
"idx_legacy_bare")
+ .isEmpty());
+ }
+
+ @Test
+ void testParseTextIndexPropertiesRejectsMalformedNgramSize() {
+ for (String typeFull :
+ List.of(
+ "text(tokenizer = ngrams(0))",
+ "text(tokenizer = 'ngram', ngram_size = 9)",
+ "text(tokenizer = ngrams(abc))")) {
+ IllegalArgumentException exception =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+
ClickHouseTableOperations.parseTextIndexPropertiesForQuery(typeFull,
"idx_bad"));
+ Assertions.assertTrue(exception.getMessage().contains("idx_bad"));
+ }
+ }
+
+ @Test
+ void testGetIndexesReadsTextIndexPropertiesFromTypeFull() throws Exception {
+ ExposedClickHouseTableOperations ops = newOps();
+
+ PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+ ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+ PreparedStatement secondaryStmt = Mockito.mock(PreparedStatement.class);
+ ResultSet secondaryRs = Mockito.mock(ResultSet.class);
+
+ Mockito.when(primaryKeyRs.next()).thenReturn(false);
+ Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+ Mockito.when(secondaryRs.next()).thenReturn(true, true, true, true, false);
+ Mockito.when(secondaryStmt.executeQuery()).thenReturn(secondaryRs);
+ Mockito.when(secondaryRs.getString("name"))
+ .thenReturn(
+ "idx_inverted_tokens", "idx_full_text_ngrams", "idx_text_tokens",
"idx_text_ngrams");
+ Mockito.when(secondaryRs.getString("type")).thenReturn("inverted",
"full_text", "text", "text");
+ Mockito.when(secondaryRs.getString("type_full"))
+ .thenReturn(
+ "inverted(0)",
+ "full_text(3)",
+ "text(tokenizer = 'default')",
+ "text(tokenizer = 'ngram', ngram_size = 4, "
+ + "preprocessor = lower(concat(body, ', ')))");
+ Mockito.when(secondaryRs.getString("expr")).thenReturn("body", "body",
"body", "body");
+ Mockito.when(secondaryRs.getLong("granularity")).thenReturn(1L, 1L, 1L,
1L);
+
+ Connection connection = Mockito.mock(Connection.class);
+ Mockito.when(connection.prepareStatement(Mockito.anyString()))
+ .thenReturn(primaryKeyStmt)
+ .thenReturn(secondaryStmt);
+
+ List<Index> indexes = ops.callGetIndexes(connection, "db", "tbl");
+
+ Assertions.assertEquals(4, indexes.size());
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_TEXT,
indexes.get(0).type());
+ Assertions.assertArrayEquals(new String[][] {{"body"}},
indexes.get(0).fieldNames());
+ Assertions.assertEquals(Map.of("tokenizer", "tokens"),
indexes.get(0).properties());
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "3"),
indexes.get(1).properties());
+ Assertions.assertEquals(Map.of("tokenizer", "tokens"),
indexes.get(2).properties());
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "4"),
indexes.get(3).properties());
+ }
+
+ @Test
+ void testGetIndexesFallsBackAndReadsLegacyTextMetadata() throws Exception {
+ ExposedClickHouseTableOperations ops = newOps();
+
+ PreparedStatement primaryKeyStmt = Mockito.mock(PreparedStatement.class);
+ ResultSet primaryKeyRs = Mockito.mock(ResultSet.class);
+ PreparedStatement modernSecondaryStmt =
Mockito.mock(PreparedStatement.class);
+ PreparedStatement legacySecondaryStmt =
Mockito.mock(PreparedStatement.class);
+ ResultSet legacySecondaryRs = Mockito.mock(ResultSet.class);
+
+ Mockito.when(primaryKeyRs.next()).thenReturn(false);
+ Mockito.when(primaryKeyStmt.executeQuery()).thenReturn(primaryKeyRs);
+ Mockito.when(modernSecondaryStmt.executeQuery())
+ .thenThrow(new SQLException("Unknown identifier 'type_full'"));
+
Mockito.when(legacySecondaryStmt.executeQuery()).thenReturn(legacySecondaryRs);
+ Mockito.when(legacySecondaryRs.next()).thenReturn(true, true, false);
+ Mockito.when(legacySecondaryRs.getString("name"))
+ .thenReturn("idx_legacy_ngrams", "idx_legacy_bare");
+
Mockito.when(legacySecondaryRs.getString("type")).thenReturn("inverted(3)",
"inverted");
+ Mockito.when(legacySecondaryRs.getString("expr")).thenReturn("body",
"body");
+ Mockito.when(legacySecondaryRs.getLong("granularity")).thenReturn(1L, 1L);
+
+ Connection connection = Mockito.mock(Connection.class);
+ Mockito.when(connection.prepareStatement(Mockito.anyString()))
+ .thenReturn(primaryKeyStmt)
+ .thenReturn(modernSecondaryStmt)
+ .thenReturn(legacySecondaryStmt);
+
+ List<Index> indexes = ops.callGetIndexes(connection, "db", "tbl");
+
+ Assertions.assertEquals(2, indexes.size());
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_TEXT,
indexes.get(0).type());
+ Assertions.assertEquals(
+ Map.of("tokenizer", "ngrams", "ngram_size", "3"),
indexes.get(0).properties());
+ Assertions.assertEquals(Index.IndexType.DATA_SKIPPING_TEXT,
indexes.get(1).type());
+ Assertions.assertTrue(indexes.get(1).properties().isEmpty());
+ }
+
@Test
void testGetIndexesPreservesLegacyAnnUsearchMetadata() throws Exception {
ExposedClickHouseTableOperations ops = newOps();
diff --git a/clients/client-python/gravitino/api/rel/indexes/index.py
b/clients/client-python/gravitino/api/rel/indexes/index.py
index 2803423490..995c72be4e 100644
--- a/clients/client-python/gravitino/api/rel/indexes/index.py
+++ b/clients/client-python/gravitino/api/rel/indexes/index.py
@@ -24,7 +24,8 @@ from typing import List, Optional
class Index(ABC):
"""The Index interface defines methods for implementing table index
columns.
- Catalogs may provide key and connector-specific index types.
+ Catalogs may provide generic key and connector-specific index types;
supported operations
+ depend on the catalog.
"""
@unique
@@ -67,6 +68,9 @@ class Index(ABC):
DATA_SKIPPING_USEARCH = "DATA_SKIPPING_USEARCH"
"""Legacy ClickHouse USearch index metadata. This type is
metadata-only."""
+ DATA_SKIPPING_TEXT = "DATA_SKIPPING_TEXT"
+ """ClickHouse full-text data-skipping index loaded from native table
metadata."""
+
DATA_SKIPPING_VECTOR_SIMILARITY = "DATA_SKIPPING_VECTOR_SIMILARITY"
"""ClickHouse vector similarity data skipping index."""
@@ -101,5 +105,5 @@ class Index(ABC):
pass # pragma: no cover
def properties(self) -> dict[str, str]:
- """Returns the index properties, or an empty map when none are
defined."""
+ """Returns optional index properties, empty when none are defined."""
return {}
diff --git a/clients/client-python/tests/unittests/dto/rel/test_index_serdes.py
b/clients/client-python/tests/unittests/dto/rel/test_index_serdes.py
index 263270ecd4..b78c11eecb 100644
--- a/clients/client-python/tests/unittests/dto/rel/test_index_serdes.py
+++ b/clients/client-python/tests/unittests/dto/rel/test_index_serdes.py
@@ -132,6 +132,39 @@ class TestIndexSerdes(unittest.TestCase):
serialized_dict = json.loads(mock_data_class.to_json())
self.assertDictEqual(json_dict, serialized_dict)
+ def test_text_index_properties_round_trip(self):
+ index_json = {
+ "indexType": "DATA_SKIPPING_TEXT",
+ "name": "idx_text",
+ "fieldNames": [["body"]],
+ "properties": {"tokenizer": "ngrams", "ngram_size": "3"},
+ }
+
+ index = IndexSerdes.deserialize(index_json)
+ self.assertIs(index.type(), Index.IndexType.DATA_SKIPPING_TEXT)
+ self.assertEqual(index.properties(), index_json["properties"])
+ self.assertEqual(IndexSerdes.serialize(index), index_json)
+
+ def test_legacy_index_without_properties(self):
+ class LegacyIndex(Index):
+ def type(self):
+ return Index.IndexType.PRIMARY_KEY
+
+ def name(self):
+ return "PRIMARY"
+
+ def field_names(self):
+ return [["id"]]
+
+ self.assertEqual(
+ IndexSerdes.serialize(LegacyIndex()),
+ {
+ "indexType": "PRIMARY_KEY",
+ "name": "PRIMARY",
+ "fieldNames": [["id"]],
+ },
+ )
+
def test_index_serdes_legacy_clickhouse_metadata(self):
annoy_properties = {
"annoy_trees": "100",
diff --git a/clients/client-python/tests/unittests/dto/rel/test_table_dto.py
b/clients/client-python/tests/unittests/dto/rel/test_table_dto.py
index bd78ee1cf5..7cd6da2b1c 100644
--- a/clients/client-python/tests/unittests/dto/rel/test_table_dto.py
+++ b/clients/client-python/tests/unittests/dto/rel/test_table_dto.py
@@ -35,6 +35,7 @@ from gravitino.dto.rel.partitioning.identity_partitioning_dto
import (
from gravitino.dto.rel.partitioning.partitioning import SingleFieldPartitioning
from gravitino.dto.rel.sort_order_dto import SortOrderDTO
from gravitino.dto.rel.table_dto import TableDTO
+from gravitino.dto.responses.table_response import TableResponse
from gravitino.exceptions.base import IllegalArgumentException
from tests.unittests.fixtures.table_fixtures import TABLE_DTO_JSON_STRING
@@ -66,6 +67,29 @@ class TestTableDTO(unittest.TestCase):
self.assertDictEqual(dto.properties(), {"format": "ORC"})
+ def test_table_response_with_text_index(self):
+ table_json = json.loads(self.complete_json)
+ table_json["indexes"] = [
+ {
+ "indexType": "DATA_SKIPPING_TEXT",
+ "name": "idx_text",
+ "fieldNames": [["name"]],
+ "properties": {"tokenizer": "ngrams", "ngram_size": "3"},
+ }
+ ]
+
+ response = TableResponse.from_json(
+ json.dumps({"code": 0, "table": table_json}), infer_missing=True
+ )
+ response.validate()
+ index = response.table().index()[0]
+ self.assertIs(index.type(), Index.IndexType.DATA_SKIPPING_TEXT)
+ self.assertEqual(index.properties(), {"tokenizer": "ngrams",
"ngram_size": "3"})
+ self.assertEqual(
+ json.loads(response.table().to_json())["indexes"][0]["properties"],
+ index.properties(),
+ )
+
def test_table_dto_deserialize_required_fields_success(self):
json_string = """
{
diff --git a/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
b/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
index dd9dca0026..b653e67a4f 100644
--- a/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
+++ b/common/src/test/java/org/apache/gravitino/json/TestJsonUtils.java
@@ -526,6 +526,17 @@ public class TestJsonUtils {
objectMapper.readValue(expected, IndexDTO.class),
objectMapper.readValue(jsonValue, IndexDTO.class));
+ Index textIndex =
+ IndexDTO.builder()
+ .withIndexType(Index.IndexType.DATA_SKIPPING_TEXT)
+ .withName("idx_text")
+ .withFieldNames(new String[][] {{"body"}})
+ .withProperties(Map.of("tokenizer", "ngrams", "ngram_size", "3"))
+ .build();
+ String textJson = JsonUtils.objectMapper().writeValueAsString(textIndex);
+
Assertions.assertTrue(textJson.contains("\"indexType\":\"DATA_SKIPPING_TEXT\""));
+ Assertions.assertEquals(textIndex, objectMapper.readValue(textJson,
IndexDTO.class));
+
Index vectorIndex =
IndexDTO.builder()
.withIndexType(Index.IndexType.DATA_SKIPPING_VECTOR_SIMILARITY)
diff --git a/docs/jdbc-clickhouse-catalog.md b/docs/jdbc-clickhouse-catalog.md
index 4b6e64db22..9806b08b5e 100644
--- a/docs/jdbc-clickhouse-catalog.md
+++ b/docs/jdbc-clickhouse-catalog.md
@@ -297,6 +297,7 @@ The `engine_parameters` property applies to
`ReplacingMergeTree`, `SummingMergeT
- `DATA_SKIPPING_SET` (default `GRANULARITY 1`, plus configurable `set(N)`
max values)
- `DATA_SKIPPING_NGRAMBFV1` (`GRANULARITY` customizable via
`Index.properties()`, default 1; requires `ngram_size`, `bloom_filter_size`,
`hash_functions`, `random_seed` in `Index.properties()`)
- `DATA_SKIPPING_TOKENBFV1` (`GRANULARITY` customizable via
`Index.properties()`, default 1; requires `bloom_filter_size`,
`hash_functions`, `random_seed` in `Index.properties()`)
+ - `DATA_SKIPPING_TEXT` (native ClickHouse text indexes are recognized when
loading a table; `tokenizer` is exposed as `tokens` or `ngrams`, with
`ngram_size` from 1 to 8 for `ngrams`; creating or altering text indexes
through Gravitino is not supported yet)
- `DATA_SKIPPING_VECTOR_SIMILARITY` (requires ClickHouse 25.8+; default
`GRANULARITY 100000000`; requires `type=hnsw`, `distance_function`, and
`dimensions` in `Index.properties()`)
`DATA_SKIPPING_VECTOR_SIMILARITY` supports `distance_function` values
`L2Distance` and `cosineDistance` on ClickHouse 25.8+. Optional properties are
`quantization` (`f64`, `f32`, `f16`, `bf16`, `i8`, or `b1`; default `bf16`),
`hnsw_max_connections_per_layer` (default 32), and
`hnsw_candidate_list_size_for_construction` (default 128). A value of 0 for
either HNSW numeric property requests the ClickHouse default. Custom
`granularity` must be a positive integer. See the [ClickHouse vector [...]
diff --git a/docs/open-api/indexes.yaml b/docs/open-api/indexes.yaml
index 894761b776..ba594994df 100644
--- a/docs/open-api/indexes.yaml
+++ b/docs/open-api/indexes.yaml
@@ -42,6 +42,7 @@ components:
- "data_skipping_tokenbfv1"
- "data_skipping_annoy"
- "data_skipping_usearch"
+ - "data_skipping_text"
- "data_skipping_vector_similarity"
name:
type: string