diff --git a/README.md b/README.md index bd7568da1..64701f353 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ All the database client supported | awsopensearch | `pip install vectordb-bench[opensearch]` | | aliyun_opensearch | `pip install vectordb-bench[aliyun_opensearch]` | | mongodb | `pip install vectordb-bench[mongodb]` | +| astradb | `pip install vectordb-bench[astradb]` | | tidb | `pip install vectordb-bench[tidb]` | | vespa | `pip install vectordb-bench[vespa]` | | oceanbase | `pip install vectordb-bench[oceanbase]` | diff --git a/pyproject.toml b/pyproject.toml index 63c585c55..281f8d543 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,7 @@ chromadb = [ "chromadb" ] opensearch = [ "opensearch-py" ] aliyun_opensearch = [ "alibabacloud_ha3engine_vector" ] mongodb = [ "pymongo" ] +astradb = [ "astrapy" ] mariadb = [ "mariadb" ] tidb = [ "PyMySQL" ] cockroachdb = [ "psycopg[binary,pool]", "pgvector" ] diff --git a/vectordb_bench/backend/clients/__init__.py b/vectordb_bench/backend/clients/__init__.py index d69c54504..6a3b52d9f 100644 --- a/vectordb_bench/backend/clients/__init__.py +++ b/vectordb_bench/backend/clients/__init__.py @@ -44,6 +44,7 @@ class DB(Enum): Test = "test" AliyunOpenSearch = "AliyunOpenSearch" MongoDB = "MongoDB" + AstraDB = "AstraDB" TiDB = "TiDB" CockroachDB = "CockroachDB" Clickhouse = "Clickhouse" @@ -165,6 +166,11 @@ def init_cls(self) -> type[VectorDB]: # noqa: PLR0911, PLR0912, C901, PLR0915 return MongoDB + if self == DB.AstraDB: + from .astradb.astradb import AstraDB + + return AstraDB + if self == DB.OceanBase: from .oceanbase.oceanbase import OceanBase @@ -339,6 +345,11 @@ def config_cls(self) -> type[DBConfig]: # noqa: PLR0911, PLR0912, C901, PLR0915 return MongoDBConfig + if self == DB.AstraDB: + from .astradb.config import AstraDBConfig + + return AstraDBConfig + if self == DB.OceanBase: from .oceanbase.config import OceanBaseConfig @@ -494,6 +505,11 @@ def case_config_cls( # noqa: C901, PLR0911, PLR0912, PLR0915 return MongoDBIndexConfig + if self == DB.AstraDB: + from .astradb.config import AstraDBIndexConfig + + return AstraDBIndexConfig + if self == DB.OceanBase: from .oceanbase.config import _oceanbase_case_config diff --git a/vectordb_bench/backend/clients/astradb/astradb.py b/vectordb_bench/backend/clients/astradb/astradb.py new file mode 100644 index 000000000..df7eb0049 --- /dev/null +++ b/vectordb_bench/backend/clients/astradb/astradb.py @@ -0,0 +1,169 @@ +import logging +import time +from contextlib import contextmanager + +from astrapy import DataAPIClient +from astrapy.constants import VectorMetric +from astrapy.info import CollectionDefinition + +from ..api import VectorDB +from .config import AstraDBIndexConfig + +log = logging.getLogger(__name__) + + +class AstraDBError(Exception): + """Custom exception class for AstraDB client errors.""" + + +class AstraDB(VectorDB): + def __init__( + self, + dim: int, + db_config: dict, + db_case_config: AstraDBIndexConfig, + collection_name: str = "vdb_bench_collection", + id_field: str = "id", + vector_field: str = "vector", + drop_old: bool = False, + **kwargs, + ): + self.dim = dim + self.db_config = db_config + self.case_config = db_case_config + self.collection_name = collection_name + self.id_field = id_field + self.vector_field = vector_field + self.drop_old = drop_old + + # Get index parameters + index_params = self.case_config.index_param() + log.info(f"index params: {index_params}") + self.index_params = index_params + + # Initialize client - will be properly set in init() + self.client = None + self.db = None + self.collection = None + + # Initialize and drop collection if needed + temp_client = DataAPIClient(self.db_config["token"]) + temp_db = temp_client.get_database( + api_endpoint=self.db_config["api_endpoint"], + keyspace=self.db_config["namespace"] + ) + + if self.drop_old: + try: + temp_db.drop_collection(self.collection_name) + log.info(f"AstraDB client dropped old collection: {self.collection_name}") + except Exception: + log.info(f"Collection {self.collection_name} does not exist, skipping drop") + + @contextmanager + def init(self): + """Initialize AstraDB client and cleanup when done""" + try: + self.client = DataAPIClient(self.db_config["token"]) + self.db = self.client.get_database( + api_endpoint=self.db_config["api_endpoint"], + keyspace=self.db_config["namespace"] + ) + + # Create or get collection with vector configuration + metric_str = self.case_config.parse_metric() + + # Map metric string to VectorMetric constant + metric_map = { + "euclidean": VectorMetric.EUCLIDEAN, + "dot_product": VectorMetric.DOT_PRODUCT, + "cosine": VectorMetric.COSINE, + } + metric = metric_map.get(metric_str, VectorMetric.COSINE) + + # Create collection with new API + # Note: check_exists is no longer needed - API handles conflicts automatically + self.collection = self.db.create_collection( + name=self.collection_name, + definition=( + CollectionDefinition.builder() + .set_vector_dimension(self.dim) + .set_vector_metric(metric) + .build() + ), + ) + log.info(f"Created/accessed collection: {self.collection_name} with metric: {metric_str}") + + yield + finally: + if self.client is not None: + self.client = None + self.db = None + self.collection = None + + def need_normalize_cosine(self) -> bool: + return False + + def insert_embeddings( + self, + embeddings: list[list[float]], + metadata: list[int], + **kwargs, + ) -> (int, Exception | None): + """Insert embeddings into AstraDB""" + + # Prepare documents in bulk + documents = [ + { + "_id": str(id_), + "$vector": embedding, + } + for id_, embedding in zip(metadata, embeddings, strict=False) + ] + + # Insert documents in batches + try: + result = self.collection.insert_many(documents, ordered=False) + return len(result.inserted_ids), None + except Exception as e: + log.exception("Error inserting embeddings") + return 0, e + + def search_embedding( + self, + query: list[float], + k: int = 100, + filters: dict | None = None, + **kwargs, + ) -> list[int]: + """Search for similar vectors""" + + # Build filter if specified + search_filter = None + if filters: + log.info(f"Applying filter: {filters}") + search_filter = { + self.id_field: {"$gte": filters["id"]}, + } + + # Perform vector search + try: + results = self.collection.find( + filter=search_filter, + sort={"$vector": query}, + limit=k, + include_similarity=True, + ) + + # Extract IDs from results + return [int(doc["_id"]) for doc in results] + except Exception: + log.exception("Error searching embeddings") + return [] + + def optimize(self, data_size: int | None = None) -> None: + """AstraDB vector indexes are automatically managed""" + log.info("optimize for search - AstraDB manages indexes automatically") + + def ready_to_load(self) -> None: + """AstraDB is always ready to load""" diff --git a/vectordb_bench/backend/clients/astradb/cli.py b/vectordb_bench/backend/clients/astradb/cli.py new file mode 100644 index 000000000..01ea6a31f --- /dev/null +++ b/vectordb_bench/backend/clients/astradb/cli.py @@ -0,0 +1,86 @@ +from typing import Annotated, TypedDict, Unpack + +import click +from pydantic import SecretStr + +from ....cli.cli import ( + CommonTypedDict, + cli, + click_parameter_decorators_from_typed_dict, + run, +) +from .. import DB +from ..api import MetricType +from .config import AstraDBIndexConfig + + +class AstraDBTypedDict(TypedDict): + api_endpoint: Annotated[ + str, + click.option( + "--api-endpoint", + type=str, + help="AstraDB API endpoint (e.g., https://-.apps.astra.datastax.com)", + required=True, + ), + ] + token: Annotated[ + str, + click.option( + "--token", + type=str, + help="AstraDB authentication token", + required=True, + ), + ] + namespace: Annotated[ + str, + click.option( + "--namespace", + type=str, + help="AstraDB namespace (keyspace)", + default="default_keyspace", + show_default=True, + ), + ] + metric: Annotated[ + str, + click.option( + "--metric", + type=click.Choice(["cosine", "euclidean", "dot_product"], case_sensitive=False), + help="Distance metric for vector similarity", + default="cosine", + show_default=True, + ), + ] + + +class AstraDBIndexTypedDict(CommonTypedDict, AstraDBTypedDict): ... + + +@cli.command() +@click_parameter_decorators_from_typed_dict(AstraDBIndexTypedDict) +def AstraDB(**parameters: Unpack[AstraDBIndexTypedDict]): + from .config import AstraDBConfig + + # Convert metric string to MetricType enum + metric_map = { + "cosine": MetricType.COSINE, + "euclidean": MetricType.L2, + "dot_product": MetricType.IP, + } + metric_type = metric_map.get(parameters["metric"].lower(), MetricType.COSINE) + + run( + db=DB.AstraDB, + db_config=AstraDBConfig( + db_label=parameters["db_label"], + api_endpoint=parameters["api_endpoint"], + token=SecretStr(parameters["token"]), + namespace=parameters["namespace"], + ), + db_case_config=AstraDBIndexConfig( + metric_type=metric_type, + ), + **parameters, + ) diff --git a/vectordb_bench/backend/clients/astradb/config.py b/vectordb_bench/backend/clients/astradb/config.py new file mode 100644 index 000000000..01efa6dd9 --- /dev/null +++ b/vectordb_bench/backend/clients/astradb/config.py @@ -0,0 +1,38 @@ +from enum import Enum + +from pydantic import BaseModel, SecretStr + +from ..api import DBCaseConfig, DBConfig, IndexType, MetricType + + +class AstraDBConfig(DBConfig, BaseModel): + api_endpoint: str = "https://-.apps.astra.datastax.com" + token: SecretStr = "" + namespace: str = "default_keyspace" + + def to_dict(self) -> dict: + return { + "api_endpoint": self.api_endpoint, + "token": self.token.get_secret_value(), + "namespace": self.namespace, + } + + +class AstraDBIndexConfig(BaseModel, DBCaseConfig): + index: IndexType = IndexType.HNSW # AstraDB uses vector search + metric_type: MetricType = MetricType.COSINE + + def parse_metric(self) -> str: + if self.metric_type == MetricType.L2: + return "euclidean" + if self.metric_type == MetricType.IP: + return "dot_product" + return "cosine" # Default to cosine similarity + + def index_param(self) -> dict: + return { + "metric": self.parse_metric(), + } + + def search_param(self) -> dict: + return {} diff --git a/vectordb_bench/cli/vectordbbench.py b/vectordb_bench/cli/vectordbbench.py index 76e9534f9..4cd0a2b5e 100644 --- a/vectordb_bench/cli/vectordbbench.py +++ b/vectordb_bench/cli/vectordbbench.py @@ -1,5 +1,6 @@ from ..backend.clients.alisql.cli import AliSQLHNSW from ..backend.clients.alloydb.cli import AlloyDBScaNN +from ..backend.clients.astradb.cli import AstraDB from ..backend.clients.aws_opensearch.cli import AWSOpenSearch from ..backend.clients.chroma.cli import Chroma from ..backend.clients.clickhouse.cli import Clickhouse @@ -50,6 +51,7 @@ cli.add_command(PgVectorScaleDiskAnn) cli.add_command(PgDiskAnn) cli.add_command(AlloyDBScaNN) +cli.add_command(AstraDB) cli.add_command(OceanBaseHNSW) cli.add_command(OceanBaseIVF) cli.add_command(MariaDBHNSW) diff --git a/vectordb_bench/frontend/config/dbCaseConfigs.py b/vectordb_bench/frontend/config/dbCaseConfigs.py index 6a32e5ff1..c46ba0cef 100644 --- a/vectordb_bench/frontend/config/dbCaseConfigs.py +++ b/vectordb_bench/frontend/config/dbCaseConfigs.py @@ -2373,6 +2373,9 @@ class CaseConfigInput(BaseModel): CaseConfigParamInput_MongoDBNumCandidatesRatio, ] +AstraDBLoadingConfig = [] +AstraDBPerformanceConfig = [] + CockroachDBLoadingConfig = [ CaseConfigParamInput_IndexType_CockroachDB, CaseConfigParamInput_MinPartitionSize_CockroachDB, @@ -2691,6 +2694,10 @@ class CaseConfigInput(BaseModel): CaseLabel.Load: MongoDBLoadingConfig, CaseLabel.Performance: MongoDBPerformanceConfig, }, + DB.AstraDB: { + CaseLabel.Load: AstraDBLoadingConfig, + CaseLabel.Performance: AstraDBPerformanceConfig, + }, DB.MariaDB: { CaseLabel.Load: MariaDBLoadingConfig, CaseLabel.Performance: MariaDBPerformanceConfig, diff --git a/vectordb_bench/frontend/config/styles.py b/vectordb_bench/frontend/config/styles.py index bce4561fd..8cd0f3c9f 100644 --- a/vectordb_bench/frontend/config/styles.py +++ b/vectordb_bench/frontend/config/styles.py @@ -60,6 +60,7 @@ def getPatternShape(i): DB.AliyunOpenSearch: "data:image/png;base64,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", DB.AWSOpenSearch: "https://assets.zilliz.com/opensearch_1eee37584e.jpeg", DB.OSSOpenSearch: "https://images.seeklogo.com/logo-png/50/1/opensearch-icon-logo-png_seeklogo-500356.png", + DB.AstraDB: "data:image/png;base64,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", DB.MongoDB: "data:image/png;base64,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", DB.TiDB: "https://img2.pingcap.com/forms/3/d/3d7fd5f9767323d6f037795704211ac44b4923d6.png", DB.Clickhouse: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAO4AAADUCAMAAACs0e/bAAAANlBMVEX/////zAD/yQD/AAD//fX/8Mf/9uD/ygD/6a7/0C3/CAj/rq7/0QD/hQz/67T/zhv/fwD/za9VhqZUAAABG0lEQVR4nO3auQ3DQBRDQa1un7L7b1YVUJkB4++8nMHkHFpq24fUPKbRuMTRmkdrHC15NMfRVbi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLi4uLj/xn0ft1RF7h5HrSR3w8XFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXF/Rm3rytKZ0ejq3BxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXGLcZ+pVpL7Sn2+Fbn3KfXAxcXFxcXFxcXFxcXFxcXFxcXFxcXFxe2ZewKjx49mqHXf2AAAAABJRU5ErkJggg==",