From 686c7497a14ed1f5c6014fab0dc7ac9d9208a931 Mon Sep 17 00:00:00 2001 From: kanichen Date: Thu, 25 Sep 2025 20:27:03 +0800 Subject: [PATCH 1/8] add tencent es --- install/requirements_py3.11.txt | 2 +- vectordb_bench/backend/clients/__init__.py | 11 +++ .../backend/clients/elastic_cloud/config.py | 2 +- .../clients/tencent_elasticsearch/config.py | 86 +++++++++++++++++++ .../tencent_elasticsearch.py | 26 ++++++ 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 vectordb_bench/backend/clients/tencent_elasticsearch/config.py create mode 100644 vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py diff --git a/install/requirements_py3.11.txt b/install/requirements_py3.11.txt index 0ae328a6f..6b051fc47 100644 --- a/install/requirements_py3.11.txt +++ b/install/requirements_py3.11.txt @@ -3,7 +3,7 @@ grpcio-tools==1.53.0 qdrant-client pinecone-client weaviate-client -elasticsearch +elasticsearch==8.16.0 pgvector pgvecto_rs[psycopg3]>=0.2.1 sqlalchemy diff --git a/vectordb_bench/backend/clients/__init__.py b/vectordb_bench/backend/clients/__init__.py index 79a6f964a..e16d06818 100644 --- a/vectordb_bench/backend/clients/__init__.py +++ b/vectordb_bench/backend/clients/__init__.py @@ -51,6 +51,7 @@ class DB(Enum): OceanBase = "OceanBase" S3Vectors = "S3Vectors" Hologres = "Alibaba Cloud Hologres" + TencentElasticsearch = "TencentElasticsearch" @property def init_cls(self) -> type[VectorDB]: # noqa: PLR0911, PLR0912, C901, PLR0915 @@ -199,6 +200,11 @@ def init_cls(self) -> type[VectorDB]: # noqa: PLR0911, PLR0912, C901, PLR0915 from .hologres.hologres import Hologres return Hologres + + if self == DB.TencentElasticsearch: + from .tencent_elasticsearch.tencent_elasticsearch import TencentElasticsearch + + return TencentElasticsearch msg = f"Unknown DB: {self.name}" raise ValueError(msg) @@ -476,6 +482,11 @@ def case_config_cls( # noqa: C901, PLR0911, PLR0912 from .hologres.config import HologresIndexConfig return HologresIndexConfig + + if self == DB.TencentElasticsearch: + from .elastic_cloud.config import ElasticCloudIndexConfig + + return ElasticCloudIndexConfig # DB.Pinecone, DB.Chroma, DB.Redis return EmptyDBCaseConfig diff --git a/vectordb_bench/backend/clients/elastic_cloud/config.py b/vectordb_bench/backend/clients/elastic_cloud/config.py index 4d9ec32d4..e198c02d9 100644 --- a/vectordb_bench/backend/clients/elastic_cloud/config.py +++ b/vectordb_bench/backend/clients/elastic_cloud/config.py @@ -56,7 +56,7 @@ def __hash__(self) -> int: self.number_of_replicas, self.use_routing, self.efConstruction, - self.M, + self.M,2 ) ) diff --git a/vectordb_bench/backend/clients/tencent_elasticsearch/config.py b/vectordb_bench/backend/clients/tencent_elasticsearch/config.py new file mode 100644 index 000000000..4d9ec32d4 --- /dev/null +++ b/vectordb_bench/backend/clients/tencent_elasticsearch/config.py @@ -0,0 +1,86 @@ +from enum import Enum + +from pydantic import BaseModel, SecretStr + +from ..api import DBCaseConfig, DBConfig, IndexType, MetricType + + +class ElasticCloudConfig(DBConfig, BaseModel): + cloud_id: SecretStr + password: SecretStr + + def to_dict(self) -> dict: + return { + "cloud_id": self.cloud_id.get_secret_value(), + "basic_auth": ("elastic", self.password.get_secret_value()), + } + + +class ESElementType(str, Enum): + float = "float" # 4 byte + byte = "byte" # 1 byte, -128 to 127 + + +class ElasticCloudIndexConfig(BaseModel, DBCaseConfig): + element_type: ESElementType = ESElementType.float + index: IndexType = IndexType.ES_HNSW + number_of_shards: int = 1 + number_of_replicas: int = 0 + refresh_interval: str = "30s" + merge_max_thread_count: int = 8 + use_rescore: bool = False + oversample_ratio: float = 2.0 + use_routing: bool = False + use_force_merge: bool = True + + metric_type: MetricType | None = None + efConstruction: int | None = None + M: int | None = None + num_candidates: int | None = None + + def __eq__(self, obj: any): + return ( + self.index == obj.index + and self.number_of_shards == obj.number_of_shards + and self.number_of_replicas == obj.number_of_replicas + and self.use_routing == obj.use_routing + and self.efConstruction == obj.efConstruction + and self.M == obj.M + ) + + def __hash__(self) -> int: + return hash( + ( + self.index, + self.number_of_shards, + self.number_of_replicas, + self.use_routing, + self.efConstruction, + self.M, + ) + ) + + def parse_metric(self) -> str: + if self.metric_type == MetricType.L2: + return "l2_norm" + if self.metric_type == MetricType.IP: + return "dot_product" + return "cosine" + + def index_param(self) -> dict: + return { + "type": "dense_vector", + "index": True, + "element_type": self.element_type.value, + "similarity": self.parse_metric(), + "index_options": { + "type": self.index.value, + "m": self.M, + "ef_construction": self.efConstruction, + }, + } + + def search_param(self) -> dict: + return { + "num_candidates": self.num_candidates, + } diff --git a/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py b/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py new file mode 100644 index 000000000..8a2a81e16 --- /dev/null +++ b/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py @@ -0,0 +1,26 @@ +from ..elastic_cloud.config import ElasticCloudIndexConfig +from ..elastic_cloud.elastic_cloud import ElasticCloud + + +class TencentElasticsearch(ElasticCloud): + def __init__( + self, + dim: int, + db_config: dict, + db_case_config: ElasticCloudIndexConfig, + indice: str = "vdb_bench_indice", # must be lowercase + id_col_name: str = "id", + vector_col_name: str = "vector", + drop_old: bool = False, + **kwargs, + ): + super().__init__( + dim=dim, + db_config=db_config, + db_case_config=db_case_config, + indice=indice, + id_col_name=id_col_name, + vector_col_name=vector_col_name, + drop_old=drop_old, + **kwargs, + ) \ No newline at end of file From 7aede6de88dc380e3607d7c3f4e2a5feff45dbaf Mon Sep 17 00:00:00 2001 From: kanichen Date: Fri, 26 Sep 2025 14:09:26 +0800 Subject: [PATCH 2/8] support tencent es --- vectordb_bench/backend/clients/__init__.py | 11 +- vectordb_bench/backend/clients/api.py | 1 + .../clients/tencent_elasticsearch/config.py | 30 ++- .../tencent_elasticsearch.py | 250 +++++++++++++++++- .../frontend/config/dbCaseConfigs.py | 53 ++++ 5 files changed, 322 insertions(+), 23 deletions(-) diff --git a/vectordb_bench/backend/clients/__init__.py b/vectordb_bench/backend/clients/__init__.py index e16d06818..2d4b29eab 100644 --- a/vectordb_bench/backend/clients/__init__.py +++ b/vectordb_bench/backend/clients/__init__.py @@ -356,6 +356,11 @@ def config_cls(self) -> type[DBConfig]: # noqa: PLR0911, PLR0912, C901, PLR0915 from .hologres.config import HologresConfig return HologresConfig + + if self == DB.TencentElasticsearch: + from .tencent_elasticsearch.config import TencentElasticsearchConfig + + return TencentElasticsearchConfig msg = f"Unknown DB: {self.name}" raise ValueError(msg) @@ -484,9 +489,9 @@ def case_config_cls( # noqa: C901, PLR0911, PLR0912 return HologresIndexConfig if self == DB.TencentElasticsearch: - from .elastic_cloud.config import ElasticCloudIndexConfig - - return ElasticCloudIndexConfig + from .tencent_elasticsearch.config import TencentElasticsearchIndexConfig + + return TencentElasticsearchIndexConfig # DB.Pinecone, DB.Chroma, DB.Redis return EmptyDBCaseConfig diff --git a/vectordb_bench/backend/clients/api.py b/vectordb_bench/backend/clients/api.py index fdc445618..605e85ac0 100644 --- a/vectordb_bench/backend/clients/api.py +++ b/vectordb_bench/backend/clients/api.py @@ -34,6 +34,7 @@ class IndexType(str, Enum): ES_HNSW_INT8 = "int8_hnsw" ES_HNSW_INT4 = "int4_hnsw" ES_HNSW_BBQ = "bbq_hnsw" + TES_VSEARCH = "vsearch" ES_IVFFlat = "ivfflat" GPU_IVF_FLAT = "GPU_IVF_FLAT" GPU_BRUTE_FORCE = "GPU_BRUTE_FORCE" diff --git a/vectordb_bench/backend/clients/tencent_elasticsearch/config.py b/vectordb_bench/backend/clients/tencent_elasticsearch/config.py index 4d9ec32d4..9fe8db505 100644 --- a/vectordb_bench/backend/clients/tencent_elasticsearch/config.py +++ b/vectordb_bench/backend/clients/tencent_elasticsearch/config.py @@ -5,14 +5,18 @@ from ..api import DBCaseConfig, DBConfig, IndexType, MetricType -class ElasticCloudConfig(DBConfig, BaseModel): - cloud_id: SecretStr +class TencentElasticsearchConfig(DBConfig, BaseModel): + #: Protocol in use to connect to the node + scheme: str = "http" + host: str = "" + port: int = 9200 + user: str = "elastic" password: SecretStr def to_dict(self) -> dict: return { - "cloud_id": self.cloud_id.get_secret_value(), - "basic_auth": ("elastic", self.password.get_secret_value()), + "hosts": [{"scheme": self.scheme, "host": self.host, "port": self.port}], + "basic_auth": (self.user, self.password.get_secret_value()), } @@ -21,7 +25,7 @@ class ESElementType(str, Enum): byte = "byte" # 1 byte, -128 to 127 -class ElasticCloudIndexConfig(BaseModel, DBCaseConfig): +class TencentElasticsearchIndexConfig(BaseModel, DBCaseConfig): element_type: ESElementType = ESElementType.float index: IndexType = IndexType.ES_HNSW number_of_shards: int = 1 @@ -56,7 +60,7 @@ def __hash__(self) -> int: self.number_of_replicas, self.use_routing, self.efConstruction, - self.M, + self.M,2 ) ) @@ -68,6 +72,20 @@ def parse_metric(self) -> str: return "cosine" def index_param(self) -> dict: + if self.index == IndexType.TES_VSEARCH: + print(f"Tencent Elasticsearch use index type: {self.index}") + return { + "type": "dense_vector", + "index": True, + "element_type": self.element_type.value, + "similarity": self.parse_metric(), + "index_options": { + "type": self.index.value, + "index": "hnsw", + "m": self.M, + "ef_construction": self.efConstruction, + }, + } return { "type": "dense_vector", "index": True, diff --git a/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py b/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py index 8a2a81e16..8efe1ab78 100644 --- a/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py +++ b/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py @@ -1,26 +1,248 @@ -from ..elastic_cloud.config import ElasticCloudIndexConfig -from ..elastic_cloud.elastic_cloud import ElasticCloud +import logging +import time +from collections.abc import Iterable +from contextlib import contextmanager +from elasticsearch.helpers import bulk + +from vectordb_bench.backend.filter import Filter, FilterOp + +from ..api import VectorDB +from .config import TencentElasticsearchIndexConfig + +for logger in ("elasticsearch", "elastic_transport"): + logging.getLogger(logger).setLevel(logging.WARNING) + +log = logging.getLogger(__name__) + + +SECONDS_WAITING_FOR_FORCE_MERGE_API_CALL_SEC = 30 + + +class TencentElasticsearch(VectorDB): + supported_filter_types: list[FilterOp] = [ + FilterOp.NonFilter, + FilterOp.NumGE, + FilterOp.StrEqual, + ] -class TencentElasticsearch(ElasticCloud): def __init__( self, dim: int, db_config: dict, - db_case_config: ElasticCloudIndexConfig, + db_case_config: TencentElasticsearchIndexConfig, indice: str = "vdb_bench_indice", # must be lowercase id_col_name: str = "id", + label_col_name: str = "label", vector_col_name: str = "vector", drop_old: bool = False, + with_scalar_labels: bool = False, **kwargs, ): - super().__init__( - dim=dim, - db_config=db_config, - db_case_config=db_case_config, - indice=indice, - id_col_name=id_col_name, - vector_col_name=vector_col_name, - drop_old=drop_old, - **kwargs, - ) \ No newline at end of file + self.dim = dim + self.db_config = db_config + self.case_config = db_case_config + self.indice = indice + self.id_col_name = id_col_name + self.label_col_name = label_col_name + self.vector_col_name = vector_col_name + self.with_scalar_labels = with_scalar_labels + + from elasticsearch import Elasticsearch + + client = Elasticsearch(**self.db_config) + + if drop_old: + log.info(f"Elasticsearch client drop_old indices: {self.indice}") + is_existed_res = client.indices.exists(index=self.indice) + if is_existed_res.raw: + client.indices.delete(index=self.indice) + self._create_indice(client) + + @contextmanager + def init(self) -> None: + """connect to elasticsearch""" + from elasticsearch import Elasticsearch + + self.client = Elasticsearch(**self.db_config, request_timeout=180) + + yield + self.client = None + del self.client + + def _create_indice(self, client: any) -> None: + mappings = { + "_source": {"excludes": [self.vector_col_name]}, + "properties": { + self.id_col_name: {"type": "integer", "store": True}, + self.vector_col_name: { + "dims": self.dim, + **self.case_config.index_param(), + }, + }, + } + settings = { + "index": { + "number_of_shards": self.case_config.number_of_shards, + "number_of_replicas": self.case_config.number_of_replicas, + "refresh_interval": self.case_config.refresh_interval, + "merge.scheduler.max_thread_count": self.case_config.merge_max_thread_count, + } + } + + try: + client.indices.create(index=self.indice, mappings=mappings, settings=settings) + except Exception as e: + log.warning(f"Failed to create indice: {self.indice} error: {e!s}") + raise e from None + + def insert_embeddings( + self, + embeddings: Iterable[list[float]], + metadata: list[int], + labels_data: list[str] | None = None, + **kwargs, + ) -> tuple[int, Exception]: + """Insert the embeddings to the elasticsearch.""" + assert self.client is not None, "should self.init() first" + + insert_data = ( + [ + ( + { + "_index": self.indice, + "_source": { + self.id_col_name: metadata[i], + self.label_col_name: labels_data[i], + self.vector_col_name: embeddings[i], + }, + "_routing": labels_data[i], + } + if self.case_config.use_routing + else { + "_index": self.indice, + "_source": { + self.id_col_name: metadata[i], + self.label_col_name: labels_data[i], + self.vector_col_name: embeddings[i], + }, + } + ) + for i in range(len(embeddings)) + ] + if self.with_scalar_labels + else [ + { + "_index": self.indice, + "_source": { + self.id_col_name: metadata[i], + self.vector_col_name: embeddings[i], + }, + } + for i in range(len(embeddings)) + ] + ) + try: + bulk_insert_res = bulk(self.client, insert_data) + return (bulk_insert_res[0], None) + except Exception as e: + log.warning(f"Failed to insert data: {self.indice} error: {e!s}") + return (0, e) + + def prepare_filter(self, filters: Filter): + self.routing_key = None + if filters.type == FilterOp.NonFilter: + self.filter = [] + elif filters.type == FilterOp.NumGE: + self.filter = {"range": {self.id_col_name: {"gt": filters.int_value}}} + elif filters.type == FilterOp.StrEqual: + self.filter = {"term": {self.label_col_name: filters.label_value}} + if self.case_config.use_routing: + self.routing_key = filters.label_value + else: + msg = f"Not support Filter for Milvus - {filters}" + raise ValueError(msg) + + def search_embedding( + self, + query: list[float], + k: int = 100, + **kwargs, + ) -> list[int]: + """Get k most similar embeddings to query vector. + + Args: + query(list[float]): query embedding to look up documents similar to. + k(int): Number of most similar embeddings to return. Defaults to 100. + + Returns: + list[tuple[int, float]]: list of k most similar embeddings in (id, score) tuple to the query embedding. + """ + assert self.client is not None, "should self.init() first" + + if self.case_config.use_rescore: + oversample_k = int(k * self.case_config.oversample_ratio) + oversample_num_candidates = int(self.case_config.num_candidates * self.case_config.oversample_ratio) + knn = { + "field": self.vector_col_name, + "k": oversample_k, + "num_candidates": oversample_num_candidates, + "filter": self.filter, + "query_vector": query, + } + rescore = { + "window_size": oversample_k, + "query": { + "rescore_query": { + "script_score": { + "query": {"match_all": {}}, + "script": { + "source": f"cosineSimilarity(params.queryVector, '{self.vector_col_name}')", + "params": {"queryVector": query}, + }, + } + }, + "query_weight": 0, + "rescore_query_weight": 1, + }, + } + else: + knn = { + "field": self.vector_col_name, + "k": k, + "num_candidates": self.case_config.num_candidates, + "filter": self.filter, + "query_vector": query, + } + rescore = None + size = k + + res = self.client.search( + index=self.indice, + knn=knn, + routing=self.routing_key, + rescore=rescore, + size=size, + _source=False, + docvalue_fields=[self.id_col_name], + stored_fields="_none_", + filter_path=[f"hits.hits.fields.{self.id_col_name}"], + ) + return [h["fields"][self.id_col_name][0] for h in res["hits"]["hits"]] + + def optimize(self, data_size: int | None = None): + """optimize will be called between insertion and search in performance cases.""" + assert self.client is not None, "should self.init() first" + self.client.indices.refresh(index=self.indice) + if self.case_config.use_force_merge: + force_merge_task_id = self.client.indices.forcemerge( + index=self.indice, + max_num_segments=1, + wait_for_completion=False, + )["task"] + log.info(f"Elasticsearch force merge task id: {force_merge_task_id}") + while True: + time.sleep(SECONDS_WAITING_FOR_FORCE_MERGE_API_CALL_SEC) + task_status = self.client.tasks.get(task_id=force_merge_task_id) + if task_status["completed"]: + return diff --git a/vectordb_bench/frontend/config/dbCaseConfigs.py b/vectordb_bench/frontend/config/dbCaseConfigs.py index 778e19e48..c9bdea264 100644 --- a/vectordb_bench/frontend/config/dbCaseConfigs.py +++ b/vectordb_bench/frontend/config/dbCaseConfigs.py @@ -1492,6 +1492,44 @@ class CaseConfigInput(BaseModel): }, ) +CaseConfigParamInput_IndexType_TES = CaseConfigInput( + label=CaseConfigParamType.IndexType, + inputHelp="hnsw or vsearch", + inputType=InputType.Text, + inputConfig={ + "value": "hnsw", + }, +) + +CaseConfigParamInput_EFConstruction_TES = CaseConfigInput( + label=CaseConfigParamType.EFConstruction, + inputType=InputType.Number, + inputConfig={ + "min": 8, + "max": 512, + "value": 360, + }, +) + +CaseConfigParamInput_M_TES = CaseConfigInput( + label=CaseConfigParamType.M, + inputType=InputType.Number, + inputConfig={ + "min": 4, + "max": 64, + "value": 30, + }, +) +CaseConfigParamInput_NumCandidates_TES = CaseConfigInput( + label=CaseConfigParamType.numCandidates, + inputType=InputType.Number, + inputConfig={ + "min": 1, + "max": 10000, + "value": 100, + }, +) + CaseConfigParamInput_IndexType_MariaDB = CaseConfigInput( label=CaseConfigParamType.IndexType, inputHelp="Select Index Type", @@ -1922,6 +1960,17 @@ class CaseConfigInput(BaseModel): CaseConfigParamInput_NumCandidates_AliES, ] +TencentElasticsearchLoadingConfig = [ + CaseConfigParamInput_EFConstruction_TES, + CaseConfigParamInput_M_TES, + CaseConfigParamInput_IndexType_TES, +] +TencentElasticsearchPerformanceConfig = [ + CaseConfigParamInput_EFConstruction_TES, + CaseConfigParamInput_M_AliES, + CaseConfigParamInput_NumCandidates_TES, +] + MongoDBLoadingConfig = [ CaseConfigParamInput_MongoDBQuantizationType, ] @@ -2171,6 +2220,10 @@ class CaseConfigInput(BaseModel): CaseLabel.Load: LanceDBLoadConfig, CaseLabel.Performance: LanceDBPerformanceConfig, }, + DB.TencentElasticsearch: { + CaseLabel.Load: TencentElasticsearchLoadingConfig, + CaseLabel.Performance: TencentElasticsearchPerformanceConfig, + }, } From ba5d44fd11f623e716da903521d860740df8aeb4 Mon Sep 17 00:00:00 2001 From: kanichen Date: Mon, 13 Oct 2025 15:49:18 +0800 Subject: [PATCH 3/8] Add support for TencentElasticsearch --- vectordb_bench/backend/clients/__init__.py | 8 +- .../clients/tencent_elasticsearch/cli.py | 96 +++++++++++++++++++ .../clients/tencent_elasticsearch/config.py | 22 +---- .../tencent_elasticsearch.py | 4 +- vectordb_bench/cli/vectordbbench.py | 2 + .../frontend/config/dbCaseConfigs.py | 4 +- 6 files changed, 112 insertions(+), 24 deletions(-) create mode 100644 vectordb_bench/backend/clients/tencent_elasticsearch/cli.py diff --git a/vectordb_bench/backend/clients/__init__.py b/vectordb_bench/backend/clients/__init__.py index 2d4b29eab..7ddb04383 100644 --- a/vectordb_bench/backend/clients/__init__.py +++ b/vectordb_bench/backend/clients/__init__.py @@ -200,7 +200,7 @@ def init_cls(self) -> type[VectorDB]: # noqa: PLR0911, PLR0912, C901, PLR0915 from .hologres.hologres import Hologres return Hologres - + if self == DB.TencentElasticsearch: from .tencent_elasticsearch.tencent_elasticsearch import TencentElasticsearch @@ -356,7 +356,7 @@ def config_cls(self) -> type[DBConfig]: # noqa: PLR0911, PLR0912, C901, PLR0915 from .hologres.config import HologresConfig return HologresConfig - + if self == DB.TencentElasticsearch: from .tencent_elasticsearch.config import TencentElasticsearchConfig @@ -487,10 +487,10 @@ def case_config_cls( # noqa: C901, PLR0911, PLR0912 from .hologres.config import HologresIndexConfig return HologresIndexConfig - + if self == DB.TencentElasticsearch: from .tencent_elasticsearch.config import TencentElasticsearchIndexConfig - + return TencentElasticsearchIndexConfig # DB.Pinecone, DB.Chroma, DB.Redis diff --git a/vectordb_bench/backend/clients/tencent_elasticsearch/cli.py b/vectordb_bench/backend/clients/tencent_elasticsearch/cli.py new file mode 100644 index 000000000..40d78e02c --- /dev/null +++ b/vectordb_bench/backend/clients/tencent_elasticsearch/cli.py @@ -0,0 +1,96 @@ +import os +from typing import Annotated, Unpack + +import click +from pydantic import SecretStr + +from vectordb_bench.backend.clients import DB +from vectordb_bench.cli.cli import ( + CommonTypedDict, + cli, + click_parameter_decorators_from_typed_dict, + run, +) + + +class TencentElasticsearchTypedDict(CommonTypedDict): + scheme: Annotated[ + str, + click.option( + "--scheme", + type=str, + help="Protocol in use to connect to the node", + default="http", + show_default=True, + ), + ] + host: Annotated[ + str, + click.option("--host", type=str, help="shot connection string", required=True), + ] + port: Annotated[ + int, + click.option("--port", type=int, help="Port to connect to", default=9200, show_default=True), + ] + user: Annotated[ + str, + click.option("--user", type=str, help="Db username", required=True), + ] + password: Annotated[ + str, + click.option( + "--password", + type=str, + help="TencentElasticsearch password", + default=lambda: os.environ.get("TES_PASSWORD", ""), + show_default="$TES_PASSWORD", + ), + ] + m: Annotated[ + int, + click.option("--m", type=int, help="HNSW M parameter", default=16, show_default=True), + ] + ef_construction: Annotated[ + int, + click.option( + "--ef_construction", + type=int, + help="HNSW efConstruction parameter", + default=200, + show_default=True, + ), + ] + num_candidates: Annotated[ + int, + click.option( + "--num_candidates", + type=int, + help="Number of candidates to consider during searching", + default=200, + show_default=True, + ), + ] + + +@cli.command() +@click_parameter_decorators_from_typed_dict(TencentElasticsearchTypedDict) +def TencentElasticsearch(**parameters: Unpack[TencentElasticsearchTypedDict]): + from .config import TencentElasticsearchConfig, TencentElasticsearchIndexConfig + + run( + db=DB.TencentElasticsearch, + db_config=TencentElasticsearchConfig( + db_label=parameters["db_label"], + scheme=parameters["scheme"], + host=parameters["host"], + port=parameters["port"], + user=parameters["user"], + password=SecretStr(parameters["password"]), + ), + db_case_config=TencentElasticsearchIndexConfig( + M=parameters["m"], + efConstruction=parameters["ef_construction"], + num_candidates=parameters["num_candidates"], + ), + **parameters, + ) diff --git a/vectordb_bench/backend/clients/tencent_elasticsearch/config.py b/vectordb_bench/backend/clients/tencent_elasticsearch/config.py index 9fe8db505..53a42c058 100644 --- a/vectordb_bench/backend/clients/tencent_elasticsearch/config.py +++ b/vectordb_bench/backend/clients/tencent_elasticsearch/config.py @@ -27,10 +27,10 @@ class ESElementType(str, Enum): class TencentElasticsearchIndexConfig(BaseModel, DBCaseConfig): element_type: ESElementType = ESElementType.float - index: IndexType = IndexType.ES_HNSW + index: IndexType = IndexType.TES_VSEARCH number_of_shards: int = 1 number_of_replicas: int = 0 - refresh_interval: str = "30s" + refresh_interval: str = "3s" merge_max_thread_count: int = 8 use_rescore: bool = False oversample_ratio: float = 2.0 @@ -60,7 +60,8 @@ def __hash__(self) -> int: self.number_of_replicas, self.use_routing, self.efConstruction, - self.M,2 + self.M, + 2, ) ) @@ -72,20 +73,6 @@ def parse_metric(self) -> str: return "cosine" def index_param(self) -> dict: - if self.index == IndexType.TES_VSEARCH: - print(f"Tencent Elasticsearch use index type: {self.index}") - return { - "type": "dense_vector", - "index": True, - "element_type": self.element_type.value, - "similarity": self.parse_metric(), - "index_options": { - "type": self.index.value, - "index": "hnsw", - "m": self.M, - "ef_construction": self.efConstruction, - }, - } return { "type": "dense_vector", "index": True, @@ -93,6 +80,7 @@ def index_param(self) -> dict: "similarity": self.parse_metric(), "index_options": { "type": self.index.value, + "index": "hnsw", "m": self.M, "ef_construction": self.efConstruction, }, diff --git a/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py b/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py index 8efe1ab78..63f02f67b 100644 --- a/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py +++ b/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py @@ -64,7 +64,7 @@ def init(self) -> None: """connect to elasticsearch""" from elasticsearch import Elasticsearch - self.client = Elasticsearch(**self.db_config, request_timeout=180) + self.client = Elasticsearch(**self.db_config, request_timeout=1800) yield self.client = None @@ -81,6 +81,7 @@ def _create_indice(self, client: any) -> None: }, }, } + settings = { "index": { "number_of_shards": self.case_config.number_of_shards, @@ -234,6 +235,7 @@ def optimize(self, data_size: int | None = None): """optimize will be called between insertion and search in performance cases.""" assert self.client is not None, "should self.init() first" self.client.indices.refresh(index=self.indice) + time.sleep(SECONDS_WAITING_FOR_FORCE_MERGE_API_CALL_SEC) if self.case_config.use_force_merge: force_merge_task_id = self.client.indices.forcemerge( index=self.indice, diff --git a/vectordb_bench/cli/vectordbbench.py b/vectordb_bench/cli/vectordbbench.py index 83dab74f6..3d8309fa6 100644 --- a/vectordb_bench/cli/vectordbbench.py +++ b/vectordb_bench/cli/vectordbbench.py @@ -16,6 +16,7 @@ from ..backend.clients.qdrant_local.cli import QdrantLocal from ..backend.clients.redis.cli import Redis from ..backend.clients.s3_vectors.cli import S3Vectors +from ..backend.clients.tencent_elasticsearch.cli import TencentElasticsearch from ..backend.clients.test.cli import Test from ..backend.clients.tidb.cli import TiDB from ..backend.clients.vespa.cli import Vespa @@ -50,6 +51,7 @@ cli.add_command(QdrantLocal) cli.add_command(BatchCli) cli.add_command(S3Vectors) +cli.add_command(TencentElasticsearch) if __name__ == "__main__": diff --git a/vectordb_bench/frontend/config/dbCaseConfigs.py b/vectordb_bench/frontend/config/dbCaseConfigs.py index c9bdea264..02c4f7bf2 100644 --- a/vectordb_bench/frontend/config/dbCaseConfigs.py +++ b/vectordb_bench/frontend/config/dbCaseConfigs.py @@ -1967,7 +1967,7 @@ class CaseConfigInput(BaseModel): ] TencentElasticsearchPerformanceConfig = [ CaseConfigParamInput_EFConstruction_TES, - CaseConfigParamInput_M_AliES, + CaseConfigParamInput_M_TES, CaseConfigParamInput_NumCandidates_TES, ] @@ -2220,7 +2220,7 @@ class CaseConfigInput(BaseModel): CaseLabel.Load: LanceDBLoadConfig, CaseLabel.Performance: LanceDBPerformanceConfig, }, - DB.TencentElasticsearch: { + DB.TencentElasticsearch: { CaseLabel.Load: TencentElasticsearchLoadingConfig, CaseLabel.Performance: TencentElasticsearchPerformanceConfig, }, From 32ab47d05609477c55c1dd4e5b05da2d479a0118 Mon Sep 17 00:00:00 2001 From: morning-color Date: Thu, 30 Oct 2025 15:16:59 +0800 Subject: [PATCH 4/8] Fix argument order in Elasticsearch config --- vectordb_bench/backend/clients/elastic_cloud/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vectordb_bench/backend/clients/elastic_cloud/config.py b/vectordb_bench/backend/clients/elastic_cloud/config.py index e198c02d9..13826a61e 100644 --- a/vectordb_bench/backend/clients/elastic_cloud/config.py +++ b/vectordb_bench/backend/clients/elastic_cloud/config.py @@ -56,7 +56,8 @@ def __hash__(self) -> int: self.number_of_replicas, self.use_routing, self.efConstruction, - self.M,2 + self.M, + 2, ) ) From 85f14326a298a461f4335171c10bf6b764622f3c Mon Sep 17 00:00:00 2001 From: morning-color Date: Thu, 30 Oct 2025 15:26:22 +0800 Subject: [PATCH 5/8] Add Tencent ES installation command to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20a19ccbe..aae9632ce 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ All the database client supported | vespa | `pip install vectordb-bench[vespa]` | | oceanbase | `pip install vectordb-bench[oceanbase]` | | hologres | `pip install vectordb-bench[hologres]` | - +| tencent_es | `pip install vectordb-bench[hologres]` | ### Run ``` shell From 33a17be65ed19c3fa8fd32036166ff4977a4110b Mon Sep 17 00:00:00 2001 From: morning-color Date: Thu, 30 Oct 2025 15:26:48 +0800 Subject: [PATCH 6/8] Fix tencent_es installation command in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aae9632ce..920d6c1ec 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ All the database client supported | vespa | `pip install vectordb-bench[vespa]` | | oceanbase | `pip install vectordb-bench[oceanbase]` | | hologres | `pip install vectordb-bench[hologres]` | -| tencent_es | `pip install vectordb-bench[hologres]` | +| tencent_es | `pip install vectordb-bench[tencent_es]` | ### Run ``` shell From 35d77a2b1f71e697538649f036b83f4a0e58b541 Mon Sep 17 00:00:00 2001 From: morningchen Date: Wed, 12 Nov 2025 15:04:42 +0800 Subject: [PATCH 7/8] Extends TencentElasticsearch client from ElasticCloud --- .../tencent_elasticsearch.py | 201 +----------------- 1 file changed, 2 insertions(+), 199 deletions(-) diff --git a/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py b/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py index 63f02f67b..887797212 100644 --- a/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py +++ b/vectordb_bench/backend/clients/tencent_elasticsearch/tencent_elasticsearch.py @@ -1,13 +1,10 @@ import logging import time -from collections.abc import Iterable from contextlib import contextmanager -from elasticsearch.helpers import bulk - from vectordb_bench.backend.filter import Filter, FilterOp -from ..api import VectorDB +from ..elastic_cloud.elastic_cloud import ElasticCloud from .config import TencentElasticsearchIndexConfig for logger in ("elasticsearch", "elastic_transport"): @@ -19,46 +16,13 @@ SECONDS_WAITING_FOR_FORCE_MERGE_API_CALL_SEC = 30 -class TencentElasticsearch(VectorDB): +class TencentElasticsearch(ElasticCloud): supported_filter_types: list[FilterOp] = [ FilterOp.NonFilter, FilterOp.NumGE, FilterOp.StrEqual, ] - def __init__( - self, - dim: int, - db_config: dict, - db_case_config: TencentElasticsearchIndexConfig, - indice: str = "vdb_bench_indice", # must be lowercase - id_col_name: str = "id", - label_col_name: str = "label", - vector_col_name: str = "vector", - drop_old: bool = False, - with_scalar_labels: bool = False, - **kwargs, - ): - self.dim = dim - self.db_config = db_config - self.case_config = db_case_config - self.indice = indice - self.id_col_name = id_col_name - self.label_col_name = label_col_name - self.vector_col_name = vector_col_name - self.with_scalar_labels = with_scalar_labels - - from elasticsearch import Elasticsearch - - client = Elasticsearch(**self.db_config) - - if drop_old: - log.info(f"Elasticsearch client drop_old indices: {self.indice}") - is_existed_res = client.indices.exists(index=self.indice) - if is_existed_res.raw: - client.indices.delete(index=self.indice) - self._create_indice(client) - @contextmanager def init(self) -> None: """connect to elasticsearch""" @@ -70,167 +34,6 @@ def init(self) -> None: self.client = None del self.client - def _create_indice(self, client: any) -> None: - mappings = { - "_source": {"excludes": [self.vector_col_name]}, - "properties": { - self.id_col_name: {"type": "integer", "store": True}, - self.vector_col_name: { - "dims": self.dim, - **self.case_config.index_param(), - }, - }, - } - - settings = { - "index": { - "number_of_shards": self.case_config.number_of_shards, - "number_of_replicas": self.case_config.number_of_replicas, - "refresh_interval": self.case_config.refresh_interval, - "merge.scheduler.max_thread_count": self.case_config.merge_max_thread_count, - } - } - - try: - client.indices.create(index=self.indice, mappings=mappings, settings=settings) - except Exception as e: - log.warning(f"Failed to create indice: {self.indice} error: {e!s}") - raise e from None - - def insert_embeddings( - self, - embeddings: Iterable[list[float]], - metadata: list[int], - labels_data: list[str] | None = None, - **kwargs, - ) -> tuple[int, Exception]: - """Insert the embeddings to the elasticsearch.""" - assert self.client is not None, "should self.init() first" - - insert_data = ( - [ - ( - { - "_index": self.indice, - "_source": { - self.id_col_name: metadata[i], - self.label_col_name: labels_data[i], - self.vector_col_name: embeddings[i], - }, - "_routing": labels_data[i], - } - if self.case_config.use_routing - else { - "_index": self.indice, - "_source": { - self.id_col_name: metadata[i], - self.label_col_name: labels_data[i], - self.vector_col_name: embeddings[i], - }, - } - ) - for i in range(len(embeddings)) - ] - if self.with_scalar_labels - else [ - { - "_index": self.indice, - "_source": { - self.id_col_name: metadata[i], - self.vector_col_name: embeddings[i], - }, - } - for i in range(len(embeddings)) - ] - ) - try: - bulk_insert_res = bulk(self.client, insert_data) - return (bulk_insert_res[0], None) - except Exception as e: - log.warning(f"Failed to insert data: {self.indice} error: {e!s}") - return (0, e) - - def prepare_filter(self, filters: Filter): - self.routing_key = None - if filters.type == FilterOp.NonFilter: - self.filter = [] - elif filters.type == FilterOp.NumGE: - self.filter = {"range": {self.id_col_name: {"gt": filters.int_value}}} - elif filters.type == FilterOp.StrEqual: - self.filter = {"term": {self.label_col_name: filters.label_value}} - if self.case_config.use_routing: - self.routing_key = filters.label_value - else: - msg = f"Not support Filter for Milvus - {filters}" - raise ValueError(msg) - - def search_embedding( - self, - query: list[float], - k: int = 100, - **kwargs, - ) -> list[int]: - """Get k most similar embeddings to query vector. - - Args: - query(list[float]): query embedding to look up documents similar to. - k(int): Number of most similar embeddings to return. Defaults to 100. - - Returns: - list[tuple[int, float]]: list of k most similar embeddings in (id, score) tuple to the query embedding. - """ - assert self.client is not None, "should self.init() first" - - if self.case_config.use_rescore: - oversample_k = int(k * self.case_config.oversample_ratio) - oversample_num_candidates = int(self.case_config.num_candidates * self.case_config.oversample_ratio) - knn = { - "field": self.vector_col_name, - "k": oversample_k, - "num_candidates": oversample_num_candidates, - "filter": self.filter, - "query_vector": query, - } - rescore = { - "window_size": oversample_k, - "query": { - "rescore_query": { - "script_score": { - "query": {"match_all": {}}, - "script": { - "source": f"cosineSimilarity(params.queryVector, '{self.vector_col_name}')", - "params": {"queryVector": query}, - }, - } - }, - "query_weight": 0, - "rescore_query_weight": 1, - }, - } - else: - knn = { - "field": self.vector_col_name, - "k": k, - "num_candidates": self.case_config.num_candidates, - "filter": self.filter, - "query_vector": query, - } - rescore = None - size = k - - res = self.client.search( - index=self.indice, - knn=knn, - routing=self.routing_key, - rescore=rescore, - size=size, - _source=False, - docvalue_fields=[self.id_col_name], - stored_fields="_none_", - filter_path=[f"hits.hits.fields.{self.id_col_name}"], - ) - return [h["fields"][self.id_col_name][0] for h in res["hits"]["hits"]] - def optimize(self, data_size: int | None = None): """optimize will be called between insertion and search in performance cases.""" assert self.client is not None, "should self.init() first" From a30b5586b8f98428de83f6d0b875aa9f1a77f218 Mon Sep 17 00:00:00 2001 From: morning-color Date: Fri, 28 Nov 2025 11:05:52 +0800 Subject: [PATCH 8/8] Add Tencent Elasticsearch results --- .gitignore | 1 - ...51128_2025112810_tencentelasticsearch.json | 135 ++++++++++++++++++ vectordb_bench/results/dbPrices.json | 3 + 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 vectordb_bench/results/TencentElasticsearch/result_20251128_2025112810_tencentelasticsearch.json diff --git a/.gitignore b/.gitignore index aa3a72f44..8437b6797 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,4 @@ build/ venv/ .venv/ .idea/ -results/ logs/ \ No newline at end of file diff --git a/vectordb_bench/results/TencentElasticsearch/result_20251128_2025112810_tencentelasticsearch.json b/vectordb_bench/results/TencentElasticsearch/result_20251128_2025112810_tencentelasticsearch.json new file mode 100644 index 000000000..e44869231 --- /dev/null +++ b/vectordb_bench/results/TencentElasticsearch/result_20251128_2025112810_tencentelasticsearch.json @@ -0,0 +1,135 @@ +{ + "run_id": "2b7731aedcf24574a76987efc93e01d5", + "task_label": "2025112810", + "results": [ + { + "metrics": { + "max_load_count": 0, + "insert_duration": 0, + "optimize_duration": 0, + "load_duration": 0, + "qps": 328.772, + "serial_latency_p99": 0.0184, + "serial_latency_p95": 0.0169, + "recall": 0.9983, + "ndcg": 0.9984, + "conc_num_list": [ + 1, + 5, + 10, + 20, + 30, + 40, + 60, + 80 + ], + "conc_qps_list": [ + 74.0598, + 247.3597, + 323.0974, + 326.6714, + 325.9711, + 326.109, + 327.0808, + 328.772 + ], + "conc_latency_p99_list": [ + 0.017595845824107528, + 0.028741827234625816, + 0.04373327549546957, + 0.08262120254337789, + 0.11297819921746849, + 0.14499322351068258, + 0.22070514821447432, + 0.323815074507147 + ], + "conc_latency_p95_list": [ + 0.01655156835913658, + 0.026233703596517444, + 0.04003582429140806, + 0.07535540163516997, + 0.10579421008005738, + 0.13709212997928263, + 0.21008139979094267, + 0.3113258379977196 + ], + "conc_latency_avg_list": [ + 0.013500699638887824, + 0.020207179967241768, + 0.030934518305094215, + 0.06117603413692736, + 0.0919196840900463, + 0.12243950031881758, + 0.1829535317926977, + 0.24257953358352213 + ], + "st_ideal_insert_duration": 0, + "st_search_stage_list": [], + "st_search_time_list": [], + "st_max_qps_list_list": [], + "st_recall_list": [], + "st_ndcg_list": [], + "st_serial_latency_p99_list": [], + "st_serial_latency_p95_list": [], + "st_conc_failed_rate_list": [] + }, + "task_config": { + "db": "TencentElasticsearch", + "db_config": { + "db_label": "sa5-1node", + "version": "", + "note": "", + "scheme": "http", + "host": "10.0.228.48", + "port": 9200, + "user": "elastic", + "password": "**********" + }, + "db_case_config": { + "element_type": "float", + "index": "vsearch", + "number_of_shards": 1, + "number_of_replicas": 0, + "refresh_interval": "-1", + "merge_max_thread_count": 8, + "use_rescore": false, + "oversample_ratio": 2, + "use_routing": false, + "use_force_merge": true, + "metric_type": "COSINE", + "efConstruction": 200, + "M": 16, + "num_candidates": 4000 + }, + "case_config": { + "case_id": 11, + "custom_case": null, + "k": 100, + "concurrency_search_config": { + "num_concurrency": [ + 1, + 5, + 10, + 20, + 30, + 40, + 60, + 80 + ], + "concurrency_duration": 30, + "concurrency_timeout": 3600 + } + }, + "stages": [ + "drop_old", + "load", + "search_serial", + "search_concurrent" + ] + }, + "label": ":)" + } + ], + "file_fmt": "result_{}_{}_{}.json", + "timestamp": 1764259200 +} \ No newline at end of file diff --git a/vectordb_bench/results/dbPrices.json b/vectordb_bench/results/dbPrices.json index b9f1b51a8..239d9f5cc 100644 --- a/vectordb_bench/results/dbPrices.json +++ b/vectordb_bench/results/dbPrices.json @@ -36,5 +36,8 @@ "OpenSearch": { "16c128g": 1.418, "16c128g-force_merge": 1.418 + }, + "TencentElasticsearch": { + "sa5-1node": 0.95 } } \ No newline at end of file