From dd51f10da8a81f4f04c0f43f2b5362f9490a4e77 Mon Sep 17 00:00:00 2001 From: "linzhi.wzw" Date: Fri, 12 Jun 2026 18:22:21 +0800 Subject: [PATCH] feat: add support for Alibaba Cloud ADB-PG Nova --- README.md | 39 ++ pyproject.toml | 1 + tests/test_adbpg.py | 210 ++++++++++ vectordb_bench/backend/clients/__init__.py | 16 + .../backend/clients/adbpg/__init__.py | 0 vectordb_bench/backend/clients/adbpg/adbpg.py | 373 ++++++++++++++++++ vectordb_bench/backend/clients/adbpg/cli.py | 205 ++++++++++ .../backend/clients/adbpg/config.py | 137 +++++++ vectordb_bench/cli/vectordbbench.py | 2 + .../frontend/config/dbCaseConfigs.py | 120 ++++++ vectordb_bench/frontend/config/styles.py | 1 + vectordb_bench/models.py | 11 +- 12 files changed, 1113 insertions(+), 2 deletions(-) create mode 100644 tests/test_adbpg.py create mode 100644 vectordb_bench/backend/clients/adbpg/__init__.py create mode 100644 vectordb_bench/backend/clients/adbpg/adbpg.py create mode 100644 vectordb_bench/backend/clients/adbpg/cli.py create mode 100644 vectordb_bench/backend/clients/adbpg/config.py diff --git a/README.md b/README.md index 685e37a47..9c4413d51 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ All the database client supported | zvec | `pip install vectordb-bench[zvec]` | | endee | `pip install vectordb-bench[endee]` | | lindorm | `pip install vectordb-bench[lindorm]` | +| adbpg | `pip install vectordb-bench[adbpg]` | ### Run @@ -550,6 +551,44 @@ To list the options for Lindorm, execute `vectordbbench lindormhnsw --help`, The --ef-search INTEGER hnsw ef-search [required] ``` +### Run ADBPG (Aliyun AnalyticDB for PostgreSQL) from command line + +ADBPG Nova uses the fastann/Nova vector index engine with `USING ann` syntax. + +**Example: Run novamr index benchmark (BioASQ 1M, 1024-dim)** + +```shell +vectordbbench adbpgnova --case-type Performance1024D1M --k 10 \ +--host --port 5432 --db-name postgres \ +--user-name --password \ +--algorithm novamr --hnsw-m 48 --ef-construction 600 \ +--ef-search 130 --max-scan-points 5000 --quantize-rescore-amp 2.0 +``` + +**Example: Run from config file** + +```shell +vectordbbench adbpgnova --config-file adbpg_bioasq1m_novamr.yml +``` + +To list the options for ADBPG, execute `vectordbbench adbpgnova --help`. The following are some ADBPG-specific command-line options. + +```text + --user-name TEXT Db username [required] + --password TEXT Postgres database password [$POSTGRES_PASSWORD] + --host TEXT Db host [required] + --port INTEGER Postgres database port [default: 5432] + --db-name TEXT Db name [required] + --algorithm TEXT algorithm [default: novamr] + --hnsw-m INTEGER hnsw_m [default: 16] + --ef-construction INTEGER ef_construction [default: 200] + --ef-search INTEGER ef_search [default: 100] + --max-scan-points INTEGER max scan points [default: 2000] + --quantize-rescore-amp FLOAT fastann.quantize_rescore_amp [default: 1.0] + --nova-adaptive-gamma FLOAT fastann.nova_adaptive_gamma [default: 0.0] + --auto-reduction/--no-auto-reduction Index WITH auto_reduction=on [default: False] +``` + ### Run PolarDB from command line PolarDB supports index types: faiss_hnsw_flat, faiss_hnsw_pq, and faiss_hnsw_sq. diff --git a/pyproject.toml b/pyproject.toml index 3bbba8ac0..40ec32b0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ endee = [ "endee==0.1.10" ] lindorm = [ "opensearch-py" ] seekdb = [ "mysql-connector-python" ] pinot = [ "requests" ] +adbpg = [ "psycopg", "psycopg-binary", "pgvector" ] [project.urls] Repository = "https://github.com/zilliztech/VectorDBBench" diff --git a/tests/test_adbpg.py b/tests/test_adbpg.py new file mode 100644 index 000000000..465acf762 --- /dev/null +++ b/tests/test_adbpg.py @@ -0,0 +1,210 @@ +"""Unit tests for the ADB-PG Nova client config layer. + +These tests do not require a live database — they only exercise: + - AdbpgConfig defaults and connection-string assembly + - AdbpgIndexConfig.index_param() WITH-clause options (incl. raw auto_reduction) + - AdbpgIndexConfig.session_param() fastann GUC emission + - TestResult.read_file() round-trip when password is absent in saved JSON + (regression for the result-loading failure caused by polymorphic + serialization stripping subclass fields from DBConfig) + +Usage: + pytest tests/test_adbpg.py -v +""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +import pytest +from pydantic import SecretStr + +from vectordb_bench.backend.clients import DB +from vectordb_bench.backend.clients.adbpg.config import AdbpgConfig, AdbpgIndexConfig +from vectordb_bench.backend.clients.api import MetricType +from vectordb_bench.models import TestResult + +if TYPE_CHECKING: + from pathlib import Path + + +def make_index_config(**overrides) -> AdbpgIndexConfig: + base = { + "metric_type": MetricType.COSINE, + "hnsw_m": 32, + "ef_search": 100, + "ef_construction": 200, + "nlist": 1024, + "algorithm": "novamr", + "rabitq_bits": 7, + "quantize_rescore_amp": 1.0, + "nova_adaptive_gamma": 0.0, + "max_scan_points": 2000, + "index_scan_mode": "snapshot", + "auto_reduction": False, + "nprobe": 5, + } + base.update(overrides) + return AdbpgIndexConfig(**base) + + +class TestAdbpgConfig: + def test_defaults_allow_construction_without_password(self): + # Regression: result JSON only contains DBConfig parent fields + # (db_label/version/note) because of pydantic polymorphic serialization. + # AdbpgConfig must therefore be constructible from that minimal dict. + cfg = AdbpgConfig(db_label="", version="", note="") + assert cfg.host == "localhost" + assert cfg.port == 5432 + assert cfg.db_name == "postgres" + assert cfg.password.get_secret_value() == "" + + def test_to_dict_carries_utility_session_option(self): + cfg = AdbpgConfig( + user_name=SecretStr("u"), + password=SecretStr("pw"), + host="h.example.com", + port=5432, + db_name="postgres", + ) + d = cfg.to_dict() + assert d["table_name"] == "vector" + cc = d["connect_config"] + assert cc["host"] == "h.example.com" + assert cc["user"] == "u" + assert cc["password"] == "pw" # noqa: S105 + assert cc["dbname"] == "postgres" + assert cc["options"] == "-c gp_session_role=utility" + + +class TestAdbpgIndexConfigBuild: + def test_parse_metric(self): + assert make_index_config(metric_type=MetricType.L2).parse_metric() == "l2" + assert make_index_config(metric_type=MetricType.COSINE).parse_metric() == "cosine" + assert make_index_config(metric_type=MetricType.IP).parse_metric() == "ip" + + def test_parse_metric_unsupported_raises(self): + with pytest.raises(ValueError, match="Metric type"): + make_index_config(metric_type=None).parse_metric() + + def test_index_param_options_default(self): + params = make_index_config().index_param() + names = {opt["option_name"]: opt for opt in params["index_creation_with_options"]} + assert names["algorithm"]["val"] == "novamr" + assert names["hnsw_m"]["val"] == 32 + assert names["hnsw_ef_construction"]["val"] == 200 + assert names["nlist"]["val"] == 1024 + assert names["rabitq_bits"]["val"] == 7 + assert names["max_key_len"]["val"] == 1 + # auto_reduction is omitted when False + assert "auto_reduction" not in names + + def test_index_param_auto_reduction_emits_raw(self): + params = make_index_config(auto_reduction=True).index_param() + opt = next(o for o in params["index_creation_with_options"] if o["option_name"] == "auto_reduction") + # `raw=True` so the value is rendered as a bare SQL identifier (`on`) + # rather than a quoted literal. + assert opt["val"] == "on" + assert opt.get("raw") is True + + def test_index_param_pca_dim_omitted_when_none(self): + params = make_index_config(pca_dim=None).index_param() + names = {opt["option_name"] for opt in params["index_creation_with_options"]} + assert "pca_dim" not in names + + def test_index_param_pca_dim_emitted_when_set(self): + params = make_index_config(pca_dim=448).index_param() + opt = next(o for o in params["index_creation_with_options"] if o["option_name"] == "pca_dim") + assert opt["val"] == 448 + + +class TestAdbpgIndexConfigSession: + def test_session_param_emits_all_search_gucs(self): + cfg = make_index_config( + quantize_rescore_amp=0.6, + nova_adaptive_gamma=0.0, + ef_search=50, + max_scan_points=16000, + index_scan_mode="snapshot", + nprobe=64, + ) + opts = cfg.session_param()["session_options"] + emitted = {o["parameter"]["setting_name"]: o["parameter"]["val"] for o in opts} + assert emitted["fastann.quantize_rescore_amp"] == "0.6" + assert emitted["fastann.nova_adaptive_gamma"] == "0.0" + assert emitted["fastann.hnsw_ef_search"] == "50" + assert emitted["fastann.hnsw_max_scan_points"] == "16000" + assert emitted["fastann.index_scan_mode"] == "snapshot" + # novad-specific GUC is always emitted (no-op for HNSW algorithms) + assert emitted["fastann.nova_nprobe"] == "64" + + def test_session_param_emits_zero_values(self): + # Forcing 0 / 0.0 must still produce a SET command — callers rely on + # being able to pin a GUC to zero. + cfg = make_index_config(quantize_rescore_amp=0.0, nova_adaptive_gamma=0.0, nprobe=0) + opts = cfg.session_param()["session_options"] + emitted = {o["parameter"]["setting_name"]: o["parameter"]["val"] for o in opts} + assert emitted["fastann.quantize_rescore_amp"] == "0.0" + assert emitted["fastann.nova_adaptive_gamma"] == "0.0" + assert emitted["fastann.nova_nprobe"] == "0" + + +class TestResultRoundTrip: + def test_read_file_with_minimal_db_config(self, tmp_path: Path): + """Saved result JSON keeps only DBConfig parent fields for db_config. + + TestResult.read_file must still rehydrate the AdbpgConfig instance + without raising a Field-required pydantic ValidationError. + """ + result_dir = tmp_path / "AnalyticDB for PostgreSQL" + result_dir.mkdir() + result_file = result_dir / "result_test_run.json" + payload = { + "run_id": "round-trip", + "task_label": "round-trip", + "results": [ + { + "metrics": { + "max_load_count": 0, + "insert_duration": 0.0, + "optimize_duration": 0.0, + "load_duration": 0.0, + "qps": 1.0, + "serial_latency_p99": 0.0, + "serial_latency_p95": 0.0, + "recall": 1.0, + "ndcg": 1.0, + "conc_num_list": [], + "conc_qps_list": [], + "conc_latency_p99_list": [], + "conc_latency_p95_list": [], + "conc_latency_avg_list": [], + }, + "task_config": { + "db": DB.Adbpg.value, + "db_config": {"db_label": "", "version": "", "note": ""}, + "db_case_config": { + "metric_type": "COSINE", + "algorithm": "novamr", + "hnsw_m": 16, + "ef_search": 100, + "ef_construction": 200, + "nlist": 1024, + }, + "case_config": {"case_id": 5, "custom_case": {}, "k": 10}, + "stages": ["search_serial"], + "load_concurrency": 0, + }, + "label": ":)", + } + ], + "timestamp": 0.0, + } + result_file.write_text(json.dumps(payload)) + + tr = TestResult.read_file(result_file, trans_unit=False) + assert len(tr.results) == 1 + rehydrated = tr.results[0].task_config.db_config + assert isinstance(rehydrated, AdbpgConfig) + assert rehydrated.host == "localhost" # came from default diff --git a/vectordb_bench/backend/clients/__init__.py b/vectordb_bench/backend/clients/__init__.py index 4be8d0424..2965e3f4c 100644 --- a/vectordb_bench/backend/clients/__init__.py +++ b/vectordb_bench/backend/clients/__init__.py @@ -63,6 +63,7 @@ class DB(Enum): PolarDB = "PolarDB" Pinot = "Pinot" SeekDB = "SeekDB" + Adbpg = "AnalyticDB for PostgreSQL" @property def init_cls(self) -> type[VectorDB]: # noqa: PLR0911, PLR0912, C901, PLR0915 @@ -269,6 +270,11 @@ def init_cls(self) -> type[VectorDB]: # noqa: PLR0911, PLR0912, C901, PLR0915 return SeekDB + if self == DB.Adbpg: + from .adbpg.adbpg import Adbpg + + return Adbpg + msg = f"Unknown DB: {self.name}" raise ValueError(msg) @@ -477,6 +483,11 @@ def config_cls(self) -> type[DBConfig]: # noqa: PLR0911, PLR0912, C901, PLR0915 return SeekDBConfig + if self == DB.Adbpg: + from .adbpg.config import AdbpgConfig + + return AdbpgConfig + msg = f"Unknown DB: {self.name}" raise ValueError(msg) @@ -667,6 +678,11 @@ def case_config_cls( # noqa: C901, PLR0911, PLR0912, PLR0915 return _seekdb_case_config.get(index_type) + if self == DB.Adbpg: + from .adbpg.config import AdbpgIndexConfig + + return AdbpgIndexConfig + # DB.Pinecone, DB.Redis return EmptyDBCaseConfig diff --git a/vectordb_bench/backend/clients/adbpg/__init__.py b/vectordb_bench/backend/clients/adbpg/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vectordb_bench/backend/clients/adbpg/adbpg.py b/vectordb_bench/backend/clients/adbpg/adbpg.py new file mode 100644 index 000000000..6df59dfeb --- /dev/null +++ b/vectordb_bench/backend/clients/adbpg/adbpg.py @@ -0,0 +1,373 @@ +"""Wrapper around the Aliyun ADBPG (AnalyticDB for PostgreSQL) vector database.""" + +import logging +from collections.abc import Generator, Sequence +from contextlib import contextmanager +from typing import Any + +import numpy as np +import psycopg +from pgvector.psycopg import register_vector +from psycopg import Connection, Cursor, sql + +from vectordb_bench.backend.filter import Filter, FilterOp + +from ..api import VectorDB +from .config import AdbpgConfigDict, AdbpgIndexConfig + +log = logging.getLogger(__name__) + + +class Adbpg(VectorDB): + """ADBPG vector database client, using psycopg.""" + + # psycopg Cursor is not thread-safe and the COPY protocol cannot be + # interleaved on a shared connection. Match PgVector/VectorChord and + # let ConcurrentInsertRunner clamp max_workers=1. + thread_safe: bool = False + + supported_filter_types: list[FilterOp] = [ + FilterOp.NonFilter, + FilterOp.NumGE, + FilterOp.StrEqual, + ] + + conn: psycopg.Connection[Any] | None = None + cursor: psycopg.Cursor[Any] | None = None + + _search: sql.Composed + + def __init__( + self, + dim: int, + db_config: AdbpgConfigDict, + db_case_config: AdbpgIndexConfig, + drop_old: bool = False, + with_scalar_labels: bool = False, + **kwargs, + ): + self.name = "Adbpg" + self.case_config = db_case_config + # Allow the framework layer (task_runner) to inject a case-specific table + # name via the `collection_name` kwarg (see Doris for the same pattern). + override_name = kwargs.get("collection_name") + self.table_name = override_name if override_name else db_config["table_name"] + self.connect_config = db_config["connect_config"] + self.dim = dim + self.with_scalar_labels = with_scalar_labels + + self._primary_field = "id" + self._vector_field = "embedding" + self._scalar_label_field = "label" + # Index name derives from the table name + algorithm, e.g. vector_1024d_10m_novam_index. + self._index_name = f"{self.table_name}_{self.case_config.algorithm}_index" + + self.where_clause = "" + + # construct basic units + self.conn, self.cursor = self._create_connection(**self.connect_config) + + log.info(f"{self.name} config values: {self.connect_config}\n{self.case_config}") + if not any( + ( + self.case_config.create_index_before_load, + self.case_config.create_index_after_load, + ), + ): + msg = ( + f"{self.name} config must create an index using create_index_before_load or create_index_after_load" + f"{self.name} config values: {self.connect_config}\n{self.case_config}" + ) + log.error(msg) + raise RuntimeError(msg) + + if drop_old: + self._drop_index() + self._drop_table() + self._create_table(dim) + if self.case_config.create_index_before_load: + self._create_index() + + self.cursor.close() + self.conn.close() + self.cursor = None + self.conn = None + + @staticmethod + def _create_connection(**kwargs) -> tuple[Connection, Cursor]: + conn = psycopg.connect(**kwargs) + register_vector(conn) + conn.autocommit = False + cursor = conn.cursor() + + assert conn is not None, "Connection is not initialized" + assert cursor is not None, "Cursor is not initialized" + + return conn, cursor + + def _generate_search_query(self) -> sql.Composed: + search_param = self.case_config.search_param() + distance_operator = { + "l2": "<->", + "ip": "<#>", + "cosine": "<=>", + }.get(search_param["metric"], "<->") + + where_clause = sql.SQL(self.where_clause) if self.where_clause else sql.SQL("") + + return sql.Composed( + [ + sql.SQL( + "SELECT {primary_field} FROM public.{table_name} {where_clause} ORDER BY {vector_field} ", + ).format( + table_name=sql.Identifier(self.table_name), + primary_field=sql.Identifier(self._primary_field), + where_clause=where_clause, + vector_field=sql.Identifier(self._vector_field), + ), + sql.SQL(distance_operator), + sql.SQL(" {search_vector}::vector({dim}) LIMIT %s::int").format( + search_vector=sql.Placeholder(), + dim=self.dim, + ), + ], + ) + + @contextmanager + def init(self) -> Generator[None, None, None]: + """Open a session, apply GUCs, yield, then close.""" + self.conn, self.cursor = self._create_connection(**self.connect_config) + + session_options: Sequence[dict[str, Any]] = self.case_config.session_param()["session_options"] + + if len(session_options) > 0: + for setting in session_options: + command = sql.SQL("SET {setting_name} = {val};").format( + setting_name=sql.Identifier(setting["parameter"]["setting_name"]), + val=sql.Identifier(str(setting["parameter"]["val"])), + ) + log.debug(command.as_string(self.cursor)) + self.cursor.execute(command) + self.conn.commit() + + try: + yield + finally: + self.cursor.close() + self.conn.close() + self.cursor = None + self.conn = None + + def _drop_table(self): + assert self.conn is not None, "Connection is not initialized" + assert self.cursor is not None, "Cursor is not initialized" + log.info(f"{self.name} client drop table : {self.table_name}") + + self.cursor.execute( + sql.SQL("DROP TABLE IF EXISTS public.{table_name}").format( + table_name=sql.Identifier(self.table_name), + ), + ) + self.conn.commit() + + def optimize(self, data_size: int | None = None): + self._post_insert() + + def _post_insert(self): + log.info(f"{self.name} post insert before optimize") + if self.case_config.create_index_after_load: + self._drop_index() + self._create_index() + + def _drop_index(self): + assert self.conn is not None, "Connection is not initialized" + assert self.cursor is not None, "Cursor is not initialized" + log.info(f"{self.name} client drop index : {self._index_name}") + + drop_index_sql = sql.SQL("DROP INDEX IF EXISTS {index_name}").format( + index_name=sql.Identifier(self._index_name), + ) + log.debug(drop_index_sql.as_string(self.cursor)) + self.cursor.execute(drop_index_sql) + self.conn.commit() + + def _set_parallel_index_build_param(self): + assert self.conn is not None, "Connection is not initialized" + assert self.cursor is not None, "Cursor is not initialized" + + index_param = self.case_config.index_param() + + if index_param["build_parallel_processes"] is not None: + self.cursor.execute( + sql.SQL("SET fastann.build_parallel_processes TO {};").format( + index_param["build_parallel_processes"], + ), + ) + self.conn.commit() + + results = self.cursor.execute(sql.SQL("SHOW fastann.build_parallel_processes;")).fetchall() + log.info(f"{self.name} parallel index creation parameters: {results}") + + def _create_index(self): + assert self.conn is not None, "Connection is not initialized" + assert self.cursor is not None, "Cursor is not initialized" + log.info(f"{self.name} client create index : {self._index_name}") + + index_param = self.case_config.index_param() + self._set_parallel_index_build_param() + + # Pre-build GUC: raise optimizer level before creating the ANN index. + self.cursor.execute(sql.SQL("SET fastann.nova_build_optimize_level = 3;")) + self.conn.commit() + + options = [] + options.append(sql.SQL("dim = {dim}").format(dim=sql.Literal(self.dim))) + options.append( + sql.SQL("distancemeasure = {measure}").format( + measure=sql.Identifier(index_param["metric"]), + ), + ) + + for option in index_param["index_creation_with_options"]: + if option["val"] is not None: + # When `raw` is set, emit the value as a bare SQL token + # (e.g. auto_reduction=on) instead of a quoted literal. + rendered_val = sql.SQL(str(option["val"])) if option.get("raw") else sql.Literal(option["val"]) + options.append( + sql.SQL("{option_name} = {val}").format( + option_name=sql.Identifier(option["option_name"]), + val=rendered_val, + ), + ) + + with_clause = sql.SQL("WITH ({});").format(sql.SQL(", ").join(options)) if options else sql.Composed(()) + + # Covering index: always INCLUDE the primary field (e.g. id). + index_create_sql = sql.SQL( + """ + CREATE INDEX IF NOT EXISTS {index_name} ON public.{table_name} + USING ann ({vector_field}) INCLUDE ({primary_field}) + """, + ).format( + index_name=sql.Identifier(self._index_name), + table_name=sql.Identifier(self.table_name), + vector_field=sql.Identifier(self._vector_field), + primary_field=sql.Identifier(self._primary_field), + ) + + full_sql = (index_create_sql + with_clause).join(" ") + log.debug(full_sql.as_string(self.cursor)) + self.cursor.execute(full_sql) + self.conn.commit() + + def _create_table(self, dim: int): + assert self.conn is not None, "Connection is not initialized" + assert self.cursor is not None, "Cursor is not initialized" + + try: + log.info(f"{self.name} client create table : {self.table_name}") + + if self.with_scalar_labels: + self.cursor.execute( + sql.SQL( + """ + CREATE TABLE IF NOT EXISTS public.{table_name} + ({primary_field} BIGINT PRIMARY KEY, embedding vector({dim}), {label_field} VARCHAR(64)); + """, + ).format( + table_name=sql.Identifier(self.table_name), + primary_field=sql.Identifier(self._primary_field), + dim=dim, + label_field=sql.Identifier(self._scalar_label_field), + ), + ) + else: + self.cursor.execute( + sql.SQL( + """ + CREATE TABLE IF NOT EXISTS public.{table_name} + ({primary_field} BIGINT PRIMARY KEY, embedding vector({dim})); + """, + ).format( + table_name=sql.Identifier(self.table_name), + primary_field=sql.Identifier(self._primary_field), + dim=dim, + ), + ) + + self.cursor.execute( + sql.SQL( + "ALTER TABLE public.{table_name} ALTER COLUMN embedding SET STORAGE PLAIN;", + ).format(table_name=sql.Identifier(self.table_name)), + ) + self.conn.commit() + except Exception as e: + log.warning(f"Failed to create adbpg table: {self.table_name} error: {e}") + raise e from None + + def insert_embeddings( + self, + embeddings: list[list[float]], + metadata: list[int], + labels_data: list[str] | None = None, + **kwargs: Any, + ) -> tuple[int, Exception | None]: + assert self.conn is not None, "Connection is not initialized" + assert self.cursor is not None, "Cursor is not initialized" + if self.with_scalar_labels: + assert labels_data is not None, "labels_data should be provided if with_scalar_labels is set to True" + + try: + metadata_arr = np.array(metadata) + embeddings_arr = np.array(embeddings) + + with self.cursor.copy( + sql.SQL("COPY public.{table_name} FROM STDIN (FORMAT BINARY)").format( + table_name=sql.Identifier(self.table_name), + ), + ) as copy: + for i, row in enumerate(metadata_arr): + if self.with_scalar_labels: + copy.set_types(["bigint", "vector", "varchar"]) + copy.write_row((row, embeddings_arr[i], labels_data[i])) + else: + copy.set_types(["bigint", "vector"]) + copy.write_row((row, embeddings_arr[i])) + self.conn.commit() + + return len(metadata), None + except Exception as e: + log.warning(f"Failed to insert data into adbpg table ({self.table_name}), error: {e}") + return 0, e + + def prepare_filter(self, filters: Filter): + if filters.type == FilterOp.NonFilter: + self.where_clause = "" + elif filters.type == FilterOp.NumGE: + self.where_clause = f"WHERE {self._primary_field} >= {filters.int_value}" + elif filters.type == FilterOp.StrEqual: + self.where_clause = f"WHERE {self._scalar_label_field} = '{filters.label_value}'" + else: + msg = f"Not support Filter for Adbpg - {filters}" + raise ValueError(msg) + + self._search = self._generate_search_query() + + def search_embedding( + self, + query: list[float], + k: int = 100, + timeout: int | None = None, + **kwargs: Any, + ) -> list[int]: + assert self.conn is not None, "Connection is not initialized" + assert self.cursor is not None, "Cursor is not initialized" + + q = np.asarray(query) + result = self.cursor.execute( + self._search, + (q, k), + prepare=True, + binary=True, + ) + return [int(i[0]) for i in result.fetchall()] diff --git a/vectordb_bench/backend/clients/adbpg/cli.py b/vectordb_bench/backend/clients/adbpg/cli.py new file mode 100644 index 000000000..e579f9028 --- /dev/null +++ b/vectordb_bench/backend/clients/adbpg/cli.py @@ -0,0 +1,205 @@ +import os +from typing import Annotated, Unpack + +import click +from pydantic import SecretStr + +from vectordb_bench.backend.clients import DB + +from ....cli.cli import ( + CommonTypedDict, + cli, + click_parameter_decorators_from_typed_dict, + get_custom_case_config, + run, +) + + +class AdbpgTypedDict(CommonTypedDict): + user_name: Annotated[ + str, + click.option("--user-name", type=str, help="Db username", required=True), + ] + password: Annotated[ + str, + click.option( + "--password", + type=str, + help="Postgres database password", + default=lambda: os.environ.get("POSTGRES_PASSWORD", ""), + show_default="$POSTGRES_PASSWORD", + ), + ] + host: Annotated[str, click.option("--host", type=str, help="Db host", required=True)] + port: Annotated[ + int, + click.option( + "--port", + type=int, + help="Postgres database port", + default=5432, + show_default=True, + required=False, + ), + ] + db_name: Annotated[str, click.option("--db-name", type=str, help="Db name", required=True)] + hnsw_m: Annotated[ + int, + click.option("--hnsw-m", type=int, help="hnsw_m", default=48, show_default=True, required=False), + ] + ef_search: Annotated[ + int, + click.option("--ef-search", type=int, help="ef_search", default=150, show_default=True, required=False), + ] + ef_construction: Annotated[ + int, + click.option( + "--ef-construction", + type=int, + help="ef_construction", + default=600, + show_default=True, + required=False, + ), + ] + nlist: Annotated[ + int, + click.option("--nlist", type=int, help="nlist", default=1024, show_default=True, required=False), + ] + rabitq_bits: Annotated[ + int, + click.option("--rabitq-bits", type=int, help="rabitq_bits", default=7, show_default=True, required=False), + ] + quantize_rescore_amp: Annotated[ + float, + click.option( + "--quantize-rescore-amp", + type=float, + help="fastann.quantize_rescore_amp", + default=0.0, + show_default=True, + required=False, + ), + ] + nova_adaptive_gamma: Annotated[ + float, + click.option( + "--nova-adaptive-gamma", + type=float, + help="fastann.nova_adaptive_gamma", + default=0.0, + show_default=True, + required=False, + ), + ] + auto_reduction: Annotated[ + bool, + click.option( + "--auto-reduction/--no-auto-reduction", + "auto_reduction", + type=bool, + help="Index WITH auto_reduction=on when enabled", + default=False, + show_default=True, + required=False, + ), + ] + max_scan_points: Annotated[ + int, + click.option( + "--max-scan-points", + type=int, + help="max_scan_points", + default=20000, + show_default=True, + required=False, + ), + ] + index_scan_mode: Annotated[ + str, + click.option( + "--index-scan-mode", + type=str, + help="fastann.index_scan_mode", + default="snapshot", + show_default=True, + required=False, + ), + ] + algorithm: Annotated[ + str, + click.option( + "--algorithm", + type=str, + help="algorithm", + default="novamr", + show_default=True, + required=False, + ), + ] + build_parallel_processes: Annotated[ + int, + click.option( + "--build-parallel-processes", + type=int, + help="Sets the maximum process to build index", + required=False, + ), + ] + pca_dim: Annotated[ + int | None, + click.option( + "--pca-dim", + type=int, + help="PCA dimension for index dimensionality reduction", + default=None, + show_default=True, + required=False, + ), + ] + nprobe: Annotated[ + int, + click.option( + "--nprobe", + type=int, + help="fastann.nova_nprobe (novad search)", + default=5, + show_default=True, + required=False, + ), + ] + + +@cli.command() +@click_parameter_decorators_from_typed_dict(AdbpgTypedDict) +def AdbpgNova(**parameters: Unpack[AdbpgTypedDict]): + from .config import AdbpgConfig, AdbpgIndexConfig + + parameters["custom_case"] = get_custom_case_config(parameters) + run( + db=DB.Adbpg, + db_config=AdbpgConfig( + user_name=SecretStr(parameters["user_name"]), + password=SecretStr(parameters["password"]), + host=parameters["host"], + port=parameters["port"], + db_name=parameters["db_name"], + ), + db_case_config=AdbpgIndexConfig( + hnsw_m=parameters["hnsw_m"], + ef_search=parameters["ef_search"], + ef_construction=parameters["ef_construction"], + nlist=parameters["nlist"], + algorithm=parameters["algorithm"], + build_parallel_processes=parameters["build_parallel_processes"], + rabitq_bits=parameters["rabitq_bits"], + quantize_rescore_amp=parameters["quantize_rescore_amp"], + nova_adaptive_gamma=parameters["nova_adaptive_gamma"], + auto_reduction=parameters["auto_reduction"], + pca_dim=parameters["pca_dim"], + max_scan_points=parameters["max_scan_points"], + index_scan_mode=parameters["index_scan_mode"], + nprobe=parameters["nprobe"], + ), + **parameters, + ) diff --git a/vectordb_bench/backend/clients/adbpg/config.py b/vectordb_bench/backend/clients/adbpg/config.py new file mode 100644 index 000000000..cc69085bc --- /dev/null +++ b/vectordb_bench/backend/clients/adbpg/config.py @@ -0,0 +1,137 @@ +from collections.abc import Mapping, Sequence +from typing import Any, TypedDict + +from pydantic import BaseModel, SecretStr + +from ..api import DBCaseConfig, DBConfig, MetricType + + +class AdbpgSessionCommands(TypedDict): + session_options: Sequence[dict[str, Any]] + + +class AdbpgConfigDict(TypedDict): + """These keys will be directly used as kwargs in psycopg connection string, + so the names must match exactly psycopg API.""" + + user: str + password: str + host: str + port: int + dbname: str + + +class AdbpgConfig(DBConfig): + user_name: SecretStr = SecretStr("tester") + password: SecretStr = SecretStr("") + host: str = "localhost" + port: int = 5432 + db_name: str = "postgres" + + def to_dict(self) -> dict: + user_str = self.user_name.get_secret_value() if isinstance(self.user_name, SecretStr) else self.user_name + pwd_str = self.password.get_secret_value() + return { + "table_name": "vector", + "connect_config": { + "host": self.host, + "port": self.port, + "dbname": self.db_name, + "user": user_str, + "password": pwd_str, + "options": "-c gp_session_role=utility", + }, + } + + +class AdbpgIndexConfig(BaseModel, DBCaseConfig): + metric_type: MetricType | None = None + create_index_before_load: bool = False + create_index_after_load: bool = True + + # ADB PG specific parameters + hnsw_m: int = 48 + ef_search: int = 150 + ef_construction: int = 600 + nlist: int = 1024 + algorithm: str = "novamr" + build_parallel_processes: int | None = None + # rabitq quantization params + rabitq_bits: int = 7 + quantize_rescore_amp: float = 0.0 + nova_adaptive_gamma: float = 0.0 + max_scan_points: int = 20000 + index_scan_mode: str = "snapshot" + auto_reduction: bool = False + pca_dim: int | None = None + # novad-specific search param (no-op for novamr/HNSW algorithms) + nprobe: int = 5 + + def parse_metric(self) -> str: + if self.metric_type == MetricType.L2: + return "l2" + if self.metric_type == MetricType.COSINE: + return "cosine" + if self.metric_type == MetricType.IP: + return "ip" + msg = f"Metric type {self.metric_type} is not supported!" + raise ValueError(msg) + + @staticmethod + def _build_forced_set_options(set_mapping: Mapping[str, Any]) -> Sequence[dict[str, Any]]: + """Always emit SET commands regardless of value (including 0 / 0.0).""" + return [ + { + "parameter": { + "setting_name": name, + "val": str(value), + }, + } + for name, value in set_mapping.items() + ] + + def index_param(self) -> dict: + with_options = [ + {"option_name": "algorithm", "val": self.algorithm}, + {"option_name": "hnsw_m", "val": self.hnsw_m}, + {"option_name": "hnsw_ef_construction", "val": self.ef_construction}, + {"option_name": "nlist", "val": self.nlist}, + {"option_name": "rabitq_bits", "val": self.rabitq_bits}, + # Covering index key length. + {"option_name": "max_key_len", "val": 1}, + ] + # Optional: auto_reduction=on — only include when True. + # Uses raw=True so the value 'on' is emitted as a bare identifier + # instead of a quoted string literal. + if self.auto_reduction: + with_options.append({"option_name": "auto_reduction", "val": "on", "raw": True}) + if self.pca_dim is not None: + with_options.append({"option_name": "pca_dim", "val": self.pca_dim}) + + return { + "metric": self.parse_metric(), + "build_parallel_processes": self.build_parallel_processes, + "create_index_before_load": self.create_index_before_load, + "create_index_after_load": self.create_index_after_load, + "index_creation_with_options": with_options, + } + + def search_param(self) -> dict: + return { + "metric": self.parse_metric(), + } + + def session_param(self) -> AdbpgSessionCommands: + # All CLI-driven search GUCs are always sent, regardless of value, + # so that callers can explicitly tune any parameter — including to 0. + session_parameters = { + "fastann.quantize_rescore_amp": self.quantize_rescore_amp, + "fastann.nova_adaptive_gamma": self.nova_adaptive_gamma, + "fastann.hnsw_ef_search": self.ef_search, + "fastann.hnsw_max_scan_points": self.max_scan_points, + "fastann.index_scan_mode": self.index_scan_mode, + "fastann.nova_nprobe": self.nprobe, + "optimizer": "off", + "elog_process_parameters": "off", + } + return {"session_options": self._build_forced_set_options(session_parameters)} diff --git a/vectordb_bench/cli/vectordbbench.py b/vectordb_bench/cli/vectordbbench.py index eca3dbc52..15adbdd42 100644 --- a/vectordb_bench/cli/vectordbbench.py +++ b/vectordb_bench/cli/vectordbbench.py @@ -1,3 +1,4 @@ +from ..backend.clients.adbpg.cli import AdbpgNova from ..backend.clients.alisql.cli import AliSQLHNSW from ..backend.clients.alloydb.cli import AlloyDBScaNN from ..backend.clients.aws_opensearch.cli import AWSOpenSearch @@ -48,6 +49,7 @@ from .batch_cli import BatchCli from .cli import cli +cli.add_command(AdbpgNova) cli.add_command(PgVectorHNSW) cli.add_command(PgVectoRSHNSW) cli.add_command(PgVectoRSIVFFlat) diff --git a/vectordb_bench/frontend/config/dbCaseConfigs.py b/vectordb_bench/frontend/config/dbCaseConfigs.py index d15c4e7ee..706c845f9 100644 --- a/vectordb_bench/frontend/config/dbCaseConfigs.py +++ b/vectordb_bench/frontend/config/dbCaseConfigs.py @@ -2970,6 +2970,122 @@ class FilterType(Enum): CaseConfigParamInput_SQType_PolarDB, ] +# ADBPG (Aliyun AnalyticDB for PostgreSQL) configs +CaseConfigParamInput_Algorithm_Adbpg = CaseConfigInput( + label=CaseConfigParamType.algorithm, + inputHelp="Select Nova algorithm variant", + inputType=InputType.Option, + inputConfig={ + "options": ["novamr", "novam"], + }, +) + +CaseConfigParamInput_HnswM_Adbpg = CaseConfigInput( + label=CaseConfigParamType.hnsw_m, + inputType=InputType.Number, + inputConfig={ + "min": 1, + "max": 256, + "value": 16, + }, + inputHelp="HNSW M parameter", +) + +CaseConfigParamInput_EFConstruction_Adbpg = CaseConfigInput( + label=CaseConfigParamType.ef_construction, + inputType=InputType.Number, + inputConfig={ + "min": 1, + "max": 2000, + "value": 200, + }, + inputHelp="HNSW ef_construction", +) + +CaseConfigParamInput_RabitqBits_Adbpg = CaseConfigInput( + label=CaseConfigParamType.rabitq_bits, + inputType=InputType.Number, + inputConfig={ + "min": 1, + "max": 8, + "value": 7, + }, + inputHelp="RaBitQ quantization bits", +) + +CaseConfigParamInput_AutoReduction_Adbpg = CaseConfigInput( + label=CaseConfigParamType.auto_reduction, + inputType=InputType.Bool, + inputConfig={"value": False}, + inputHelp="Enable auto_reduction=on for index build", +) + +CaseConfigParamInput_EFSearch_Adbpg = CaseConfigInput( + label=CaseConfigParamType.ef_search, + inputType=InputType.Number, + inputConfig={ + "min": 1, + "max": 2000, + "value": 100, + }, + inputHelp="fastann.hnsw_ef_search", +) + +CaseConfigParamInput_MaxScanPoints_Adbpg = CaseConfigInput( + label=CaseConfigParamType.max_scan_points, + inputType=InputType.Number, + inputConfig={ + "min": 1, + "max": 100000, + "value": 2000, + }, + inputHelp="fastann.hnsw_max_scan_points", +) + +CaseConfigParamInput_QuantizeRescoreAmp_Adbpg = CaseConfigInput( + label=CaseConfigParamType.quantize_rescore_amp, + inputType=InputType.Float, + inputConfig={ + "min": 0.0, + "max": 100.0, + "value": 1.0, + "step": 0.1, + }, + inputHelp="fastann.quantize_rescore_amp", +) + +CaseConfigParamInput_NovaAdaptiveGamma_Adbpg = CaseConfigInput( + label=CaseConfigParamType.nova_adaptive_gamma, + inputType=InputType.Float, + inputConfig={ + "min": 0.0, + "max": 1.0, + "value": 0.0, + "step": 0.05, + }, + inputHelp="fastann.nova_adaptive_gamma", +) + +AdbpgLoadConfig = [ + CaseConfigParamInput_Algorithm_Adbpg, + CaseConfigParamInput_HnswM_Adbpg, + CaseConfigParamInput_EFConstruction_Adbpg, + CaseConfigParamInput_RabitqBits_Adbpg, + CaseConfigParamInput_AutoReduction_Adbpg, +] + +AdbpgPerformanceConfig = [ + CaseConfigParamInput_Algorithm_Adbpg, + CaseConfigParamInput_HnswM_Adbpg, + CaseConfigParamInput_EFConstruction_Adbpg, + CaseConfigParamInput_RabitqBits_Adbpg, + CaseConfigParamInput_AutoReduction_Adbpg, + CaseConfigParamInput_EFSearch_Adbpg, + CaseConfigParamInput_MaxScanPoints_Adbpg, + CaseConfigParamInput_QuantizeRescoreAmp_Adbpg, + CaseConfigParamInput_NovaAdaptiveGamma_Adbpg, +] + # Map DB to config CASE_CONFIG_MAP = { DB.Milvus: { @@ -3064,6 +3180,10 @@ class FilterType(Enum): CaseLabel.Load: PolarDBConfig, CaseLabel.Performance: PolarDBConfig, }, + DB.Adbpg: { + CaseLabel.Load: AdbpgLoadConfig, + CaseLabel.Performance: AdbpgPerformanceConfig, + }, } diff --git a/vectordb_bench/frontend/config/styles.py b/vectordb_bench/frontend/config/styles.py index 268a2cd7d..4b162e9ed 100644 --- a/vectordb_bench/frontend/config/styles.py +++ b/vectordb_bench/frontend/config/styles.py @@ -75,6 +75,7 @@ def getPatternShape(i): DB.Endee: "data:image/svg+xml,%3c?xml%20version=%271.0%27%20encoding=%27UTF-8%27?%3e%3csvg%20id=%27Layer_1%27%20xmlns=%27http://www.w3.org/2000/svg%27%20version=%271.1%27%20viewBox=%270%200%20600%20600%27%3e%3c!--%20Generator:%20Adobe%20Illustrator%2030.0.0,%20SVG%20Export%20Plug-In%20.%20SVG%20Version:%202.1.1%20Build%20123)%20--%3e%3cdefs%3e%3cstyle%3e%20.st0%20{%20fill:%20%233266a4;%20}%20%3c/style%3e%3c/defs%3e%3cpath%20class=%27st0%27%20d=%27M106.22,490.02H10.36l-.04-184.85c11.19-163.31,232.1-211.74,306.42-61.94,23.96,48.3,15.59,99.83,17,152.01h61.62c15.25,0,42.7-13.75,54.75-23.2,66.11-51.86,57.28-158.2-16.28-198.49-9.65-5.28-33.16-14.19-43.74-14.19h-154.31v-91.09c0-.87,2.55-1.86,3.63-1.63,104.05,4.23,201.15-21.64,284.48,55.84,119.18,110.8,69.12,325.47-91.33,362.6-28.65,6.63-85.47,7.76-115.02,4.8-19.71-1.97-43.29-16.57-55.97-31.45-37.98-44.56-20.77-98.07-24.7-151.16-5.18-69.99-100-85.31-125.9-20.47-1.16,2.92-4.76,13.88-4.76,16.3v186.91Z%27/%3e%3c/svg%3e", DB.Lindorm: "data:image/png;base64,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", DB.PolarDB: "data:image/svg+xml;base64,PHN2ZyB0PSIxNzczNTU4MDAyMzQ5IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDMwNTEgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE3MzgiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMCAwaDQ3Ny40NzA3MmMyNjMuMjgwNjQgMCA0NzYuNzIzMiAyMTMuNDMyMzIgNDc2LjcyMzIgNDc2LjcyMzJ2NjguMDg1NzZjMCAyNjMuMjgwNjQtMjEzLjQ0MjU2IDQ3Ni43MjMyLTQ3Ni43MjMyIDQ3Ni43MjMySDBWMHoiIGZpbGw9IiNGRTY5MDIiIHAtaWQ9IjE3MzkiPjwvcGF0aD48cGF0aCBkPSJNMzY0LjkwMjQgMTAyNGMyNC43ODA4LTEzMC4yMzIzMiA2Ni44MzY0OC0yMzIuNzk2MTYgMTI2LjE3NzI4LTMwNy42NzEwNEM1OTkuODQ4OTYgNTc5LjA1MTUyIDcwNi41NiA1MTAuMDQ0MTYgNzE1LjAwOCA0OTkuMTU5MDRjMTguNjQ3MDQtMjQuMDQzNTIgOS4wNDE5Mi01MC4wODM4NCAxNC4yMDI4OC02MC45NDg0OHMzNS4zMjgtMzEuMTkxMDQgNDEuMDAwOTYtNDkuMDQ5NmM1LjY3Mjk2LTE3LjgzODA4IDUuNjcyOTYtOTEuMjA3NjgtMjIuODk2NjQtMTA1LjQwMDMyLTE5LjA0NjQtOS40NzItNjcuNzY4MzItMi4zNTUyLTE0Ni4xODYyNCAyMS4zNTA0bC0xMjkuMDU0NzItMzEuNDA2MDhjLTk1Ljg3NzEyLTEwLjQ4NTc2LTE1Ni44MTUzNi0xMC40ODU3Ni0xODIuNzg0IDAtMjUuOTg5MTIgMTAuNDc1NTItMTIyLjQxOTIgNzMuOTYzNTItMjg5LjI5MDI0IDE5MC40NjRWMTAyNGgzNjQuOTAyNHoiIGZpbGw9IiNGRUZGRkEiIHAtaWQ9IjE3NDAiPjwvcGF0aD48cGF0aCBkPSJNMzM5LjY3MTA0IDM2Mi4xODg4YzM1LjE3NDQtMjkuNzQ3MiA1Ni4wMTI4LTQ0LjYxNTY4IDYyLjUyNTQ0LTQ0LjYxNTY4IDExLjQ3OTA0IDAgMTkuMjIwNDggNS43MjQxNiAyNS40NjY4OCAxMy41ODg0OGE0MjkuMDU2IDQyOS4wNTYgMCAwIDEgMTQuMjMzNiAxOS41ODkxMiA1LjE4MTQ0IDUuMTgxNDQgMCAwIDEtNC4wNDQ4IDguMTUxMDRjLTI2LjM1Nzc2IDAuOTYyNTYtNDUuOTY3MzYgMi4zMjQ0OC01OC44MTg1NiA0LjA3NTUyLTkuMjg3NjggMS4yNjk3Ni0yMC44MDc2OCAzLjk3MzEyLTM0LjU3MDI0IDguMTEwMDh2MC4wMTAyNGE1LjE4MTQ0IDUuMTgxNDQgMCAwIDEtNC44MTI4LTguOTI5Mjh6TTYyMS41MTY4IDMzMS4yMjMwNGMzLjUwMjA4LTE4LjczOTIgOTUuOTQ4OC01NC4zNDM2OCAxMTUuNTg5MTItNDAuNDI3NTIgMTkuNjQwMzIgMTMuOTI2NCAxMC40MTQwOCA0MC40Mjc1Mi0xMi45NjM4NCA2OS40Mzc0NC0zMC41NjY0IDMzLjI4LTEwNi4xMTcxMi0xMC4yODA5Ni0xMDIuNjI1MjgtMjkuMDA5OTJ6IiBmaWxsPSIjRkU2OTAyIiBwLWlkPSIxNzQxIj48L3BhdGg+PHBhdGggZD0iTTExOS42MzM5MiA0NjMuOTg0NjRsLTQ4LjQ4NjQtNDYuNTkyYy0yNi43ODc4NC0yNS43MjI4OC0yNS45MTc0NC02OS44NDcwNCAxLjkzNTM2LTk4LjUzOTUyIDI3Ljg1MjgtMjguNzAyNzIgNzIuMTQwOC0zMS4wOTg4OCA5OC45MTg0LTUuMzc2bDQ4LjUwNjg4IDQ2LjU5MiIgZmlsbD0iI0ZFRkZGQSIgcC1pZD0iMTc0MiI+PC9wYXRoPjxwYXRoIGQ9Ik02OS4zOTY0OCAzMTUuMjM4NGMyOS43ODgxNi0zMC43MDk3NiA3Ny4zMjIyNC0zMy4zMTA3MiAxMDYuMjA5MjgtNS41Mjk2bDQ4LjQ1NTY4IDQ2LjU5Mi03LjE4ODQ4IDcuNDc1Mi00OC40NTU2OC00Ni41OTJjLTI0LjY0NzY4LTIzLjY5NTM2LTY1LjY1ODg4LTIxLjQ1MjgtOTEuNTc2MzIgNS4yNzM2LTI1LjkwNzIgMjYuNzE2MTYtMjYuNzI2NCA2Ny41NjM1Mi0yLjA5OTIgOTEuMjM4NGw0OC40NTU2OCA0Ni41OTItNy4xODg0OCA3LjQ3NTItNDguNDU1NjgtNDYuNTkyYy0yOC44OTcyOC0yNy43ODExMi0yNy45NTUyLTc1LjIxMjggMS44NDMyLTEwNS45MzI4eiIgZmlsbD0iI0ZCNkQwMSIgcC1pZD0iMTc0MyI+PC9wYXRoPjxwYXRoIGQ9Ik00NDQuNzMzNDQgMjk0LjA3MjMyYTYyLjIyODQ4IDU2Ljc1MDA4IDAgMSAwIDEyNC40NTY5NiAwIDYyLjIyODQ4IDU2Ljc1MDA4IDAgMSAwLTEyNC40NTY5NiAwWiIgZmlsbD0iI0ZFRkZGQSIgcC1pZD0iMTc0NCI+PC9wYXRoPjxwYXRoIGQ9Ik0xOTEuMzY1MTIgNjEwLjg1Njk2YzAgOS44OTE4NCA2MC44MjU2IDU1LjI5NiAxMDAuNzIwNjQgNjIuOTU1NTIgMzkuODg0OCA3LjY2OTc2IDg2LjI3Mi0yNi4yNzU4NCA4Ni4yNzItMzIuODI5NDQgMC02LjU1MzYtNDcuODIwOCAxMC4zNDI0LTg2LjI3MiA0LjE0NzItMzguNDUxMi02LjE5NTItMTAwLjcyMDY0LTQ0LjE3NTM2LTEwMC43MjA2NC0zNC4yNzMyOHpNNzI3LjM3NzkyIDQ1NC4yODczNmMtMC4wNjE0NCAxMi42MjU5Mi0wLjE5NDU2IDI5LjE3Mzc2LTEyLjM2OTkyIDQ0Ljg3MTY4LTguNDM3NzYgMTAuODg1MTItMTE1LjE1OTA0IDc5Ljg5MjQ4LTIyMy45MjgzMiAyMTcuMTY5OTItNTkuMzQwOCA3NC44NzQ4OC0xMDEuMzk2NDggMTc3LjQzODcyLTEyNi4xNzcyOCAzMDcuNjcxMDRINzIuMzA0NjRjMTExLjYxNi0xNzQuMjc0NTYgMjIxLjMzNzYtMzA3LjA5NzYgMzI5LjE0NDMyLTM5OC40OTk4NEM1MjUuMjA5NiA1MjAuNjAxNiA2MzUuMjU4ODggNDYzLjM3MDI0IDczMS42Mjc1MiA0NTMuODQ3MDR6IiBmaWxsPSIjRkVEQkJCIiBwLWlkPSIxNzQ1Ij48L3BhdGg+PHBhdGggZD0iTTExNTMuODYzNjggMzMwLjM0MjR2MzYwLjk5MDcyaDU1LjM5ODRWNTUwLjc3ODg4aDk0LjAxMzQ0Yzg2LjkwNjg4IDAgMTMwLjYxMTItMzYuOTA0OTYgMTMwLjYxMTItMTEwLjcyNTEyIDAtNzMuMzE4NC00My4xOTIzMi0xMDkuNzIxNi0xMjkuNTg3Mi0xMDkuNzIxNmgtMTUwLjQyNTZ6IG01NS4zOTg0IDQ3LjAxMTg0aDkwLjQ2MDE2YzI2LjkzMTIgMCA0Ni43NTU4NCA1LjA1ODU2IDU5LjQ2MzY4IDE1LjE3NTY4IDEyLjY5NzYgOS4wOTMxMiAxOS4zMDI0IDI1LjI3MjMyIDE5LjMwMjQgNDcuNTEzNiAwIDIyLjI1MTUyLTYuNjA0OCAzOC40MzA3Mi0xOC44MDA2NCA0OC41Mzc2LTEyLjY5NzYgMTAuMTE3MTItMzIuNTIyMjQgMTUuMTc1NjgtNTkuOTY1NDQgMTUuMTc1NjhoLTkwLjQ2MDE2VjM3Ny4zNTQyNHpNMTYwMS4wODU0NCA0MjIuODYwOGMtMzkuNjI4OCAwLTcxLjY0OTI4IDEzLjE0ODE2LTk1LjUzOTIgMzkuNDM0MjQtMjMuODg5OTIgMjUuNzg0MzItMzUuNTczNzYgNTguNjU0NzItMzUuNTczNzYgOTguNjAwOTYgMCAzOS40MjQgMTEuNjgzODQgNzIuMjk0NCAzNS4wNjE3NiA5Ny41NzY5NiAyNC40MDE5MiAyNi4yOTYzMiA1Ni40MjI0IDM5LjkzNiA5Ni4wNTEyIDM5LjkzNiAzOS42MzkwNCAwIDcxLjY1OTUyLTEzLjYzOTY4IDk2LjA1MTItMzkuOTM2IDIzLjM3NzkyLTI1LjI4MjU2IDM1LjA3Mi01OC4xNDI3MiAzNS4wNzItOTcuNTg3MiAwLTM5LjkzNi0xMi4xOTU4NC03Mi44MDY0LTM1LjU3Mzc2LTk4LjU5MDcyLTIzLjg4OTkyLTI2LjI4NjA4LTU1LjkxMDQtMzkuNDM0MjQtOTUuNTM5Mi0zOS40MzQyNHogbTAgNDMuOTkxMDRjMjQuOTAzNjggMCA0NC4yMTYzMiA5LjYwNTEyIDU4LjQ0OTkyIDI5LjMxNzEyIDEyLjE4NTYgMTYuNjkxMiAxOC4yODg2NCAzOC40MzA3MiAxOC4yODg2NCA2NC43MTY4IDAgMjUuNzk0NTYtNi4wOTI4IDQ3LjAyMjA4LTE4LjI4ODY0IDY0LjIxNTA0LTE0LjIzMzYgMTkuMjIwNDgtMzMuNTQ2MjQgMjkuMzI3MzYtNTguNDQ5OTIgMjkuMzI3MzYtMjQuOTAzNjggMC00NC4yMDYwOC0xMC4xMDY4OC01Ny45Mjc2OC0yOS4zMjczNi0xMi4yMDYwOC0xNi42OTEyLTE3Ljc4Njg4LTM3LjkxODcyLTE3Ljc4Njg4LTY0LjIwNDggMC0yNi4yOTYzMiA1LjU4MDgtNDguMDM1ODQgMTcuNzg2ODgtNjQuNzE2OCAxMy43MjE2LTE5LjcyMjI0IDMzLjAyNC0yOS4zMjczNiA1Ny45Mjc2OC0yOS4zMjczNnpNMTc4OS42MzQ1NiAzMjMuMjU2MzJ2MzY4LjA3NjhoNTMuODYyNFYzMjMuMjU2MzJ6TTIwMjkuMDA0OCA0MjIuODYwOGMtMzIuNTMyNDggMC01OC45NTE2OCA1LjU2MDMyLTc4LjI2NDMyIDE3LjY5NDcyLTIyLjM2NDE2IDEzLjE0ODE2LTM2LjU5Nzc2IDM0LjM4NTkyLTQyLjE4ODggNjIuNjk5NTJsNTMuMzcwODggNC41NDY1NmMzLjA0MTI4LTE0LjY2MzY4IDEwLjY3MDA4LTI1LjI4MjU2IDIyLjg2NTkyLTMyLjM1ODQgMTAuMTY4MzItNi4wNjIwOCAyMy44ODk5Mi05LjEwMzM2IDQwLjY1MjgtOS4xMDMzNiAzOS42MzkwNCAwIDU5LjQ2MzY4IDE4LjIwNjcyIDU5LjQ2MzY4IDU0LjYwOTkydjEwLjYxODg4bC01OC45NTE2OCAxLjUxNTUyYy0zOC42MjUyOCAxLjAxMzc2LTY5LjEyIDguNjAxNi05MC40NjAxNiAyMy43NTY4LTIzLjM3NzkyIDE1LjY3NzQ0LTM1LjA3MiAzOC40MzA3Mi0zNS4wNzIgNjcuNzU4MDggMCAyMS43Mzk1MiA4LjEzMDU2IDM5LjQzNDI0IDI0LjkwMzY4IDUzLjA4NDE2IDE1LjI1NzYgMTMuNjQ5OTIgMzYuNTk3NzYgMjAuNzM2IDY0LjA0MDk2IDIwLjczNiAyMy4zNzc5MiAwIDQzLjcwNDMyLTQuNTU2OCA2MC45NzkyLTEyLjY0NjRhMTA5LjMxMiAxMDkuMzEyIDAgMCAwIDM4LjExMzI4LTMxLjM0NDY0djM2LjkwNDk2aDQ5LjgwNzM2VjUyNC40OTI4YzAtMzEuODU2NjQtOC4xMzA1Ni01Ni4xMjU0NC0yMy44Nzk2OC03Mi44MDY0LTE4LjI5ODg4LTE5LjIyMDQ4LTQ2Ljc1NTg0LTI4LjgyNTYtODUuMzgxMTItMjguODI1NnogbTU1LjkwMDE2IDE0Ny42NDAzMnYxNS4xNjU0NGMwIDIwLjIyNC04LjY0MjU2IDM3LjQxNjk2LTI0LjkwMzY4IDUxLjA2Njg4LTE2LjI2MTEyIDEzLjY0OTkyLTM1LjU3Mzc2IDIwLjcyNTc2LTU4LjQzOTY4IDIwLjcyNTc2LTEzLjcyMTYgMC0yNC45MDM2OC0zLjUzMjgtMzMuMDM0MjQtMTAuMTA2ODgtOC42NDI1Ni02LjU3NDA4LTEyLjcwNzg0LTE0LjY2MzY4LTEyLjcwNzg0LTI0Ljc4MDggMC0zMi4zNTg0IDI0LjM5MTY4LTQ5LjU0MTEyIDczLjY4NzA0LTUwLjU1NDg4bDU1LjM5ODQtMS41MTU1MnpNMjMyMS43MzU2OCA0MjIuODYwOGMtMTYuMjcxMzYgMC0zMC40OTQ3MiA0LjU0NjU2LTQyLjcwMDggMTQuNjYzNjgtMTAuMTU4MDggNy4wNzU4NC0xOC44MDA2NCAxNy42OTQ3Mi0yNS4zOTUyIDMxLjg0NjR2LTM5LjQyNGgtNTMuODgyODh2MjYxLjM4NjI0aDUzLjg3MjY0VjU1Mi44MDY0YzAtMjIuNzUzMjggNi42MDQ4LTQxLjQ3MiAyMC4zMjY0LTU1LjYyMzY4IDEyLjcwNzg0LTEzLjE0ODE2IDI3LjQ0MzItMTkuNzEyIDQzLjcwNDMyLTE5LjcxMiAxMi4yMDYwOCAwIDI0LjkwMzY4IDEuNTE1NTIgMzguMTIzNTIgNS41NjAzMnYtNTMuNTk2MTZjLTkuMTU0NTYtNC41NDY1Ni0yMC44Mzg0LTYuNTc0MDgtMzQuMDQ4LTYuNTc0MDh6TTIzOTMuODk2OTYgMzMwLjM0MjR2MzYwLjk5MDcyaDEzMS4xMTI5NmM1OC40NDk5MiAwIDEwMi42NjYyNC0xNi4xNzkyIDEzMy4xNTA3Mi00OC41Mzc2IDI4Ljk3OTItMzEuMzM0NCA0My43MTQ1Ni03NS4zMzU2OCA0My43MTQ1Ni0xMzEuOTYyODggMC01Ny4xMjg5Ni0xNC4yMzM2LTEwMS4xMi00Mi43MDA4LTEzMS40NTA4OC0zMC40ODQ0OC0zMi44NzA0LTc0LjcwMDgtNDkuMDQ5Ni0xMzMuMTQwNDgtNDkuMDQ5NmgtMTMyLjEzNjk2eiBtNTUuMzk4NCA0Ny4wMTE4NGg2Ni41NzAyNGM0NS43NDIwOCAwIDc5LjI3ODA4IDEwLjYxODg4IDEwMC42Mjg0OCAzMi4zNTg0IDIwLjMyNjQgMjEuMjM3NzYgMzAuOTk2NDggNTQuNjA5OTIgMzAuOTk2NDggMTAxLjEyIDAgNDUuNTA2NTYtMTAuNjcwMDggNzguODc4NzItMzEuNTA4NDggMTAwLjYxODI0LTIxLjM0MDE2IDIxLjczOTUyLTU1LjM5ODQgMzIuODcwNC0xMDEuMTMwMjQgMzIuODcwNGgtNjUuNTU2NDh2LTI2Ni45NTY4ek0yNzU4LjI4NzM2IDMzMC4zNDI0djM2MC45OTA3MmgxNjUuNjcyOTZjMzguNjI1MjggMCA2OC42MDgtNy4wNjU2IDg5Ljk0ODE2LTIxLjIyNzUyIDI0LjkwMzY4LTE3LjIwMzIgMzcuNjExNTItNDMuNDg5MjggMzcuNjExNTItNzkuODkyNDggMC0yNC4yNjg4LTYuMTAzMDQtNDMuOTgwOC0xOC4yOTg4OC01OC42NDQ0OC0xMi4xODU2LTE0LjY2MzY4LTMwLjQ4NDQ4LTI0LjI2ODgtNTQuMzc0NC0yOC44MjU2IDE4LjI5ODg4LTYuNTc0MDggMzIuMDIwNDgtMTYuNjgwOTYgNDIuMTg4OC0yOS44MjkxMiAxMC4xNTgwOC0xNC4xNTE2OCAxNS4yMzcxMi0zMS4zNDQ2NCAxNS4yMzcxMi01MS41Njg2NCAwLTI3LjgxMTg0LTkuNjU2MzItNDkuNTUxMzYtMjguOTY4OTYtNjUuNzMwNTYtMjAuMzI2NC0xNy4xOTI5Ni00Ny43Njk2LTI1LjI4MjU2LTgzLjM1MzYtMjUuMjgyNTZoLTE2NS42NjI3MnogbTU1LjM5ODQgNDUuNDk2MzJoOTYuNTUyOTZjMjQuMzkxNjggMCA0Mi42OTA1NiA0LjA0NDggNTMuODYyNCAxMi42NDY0IDExLjE5MjMyIDguMDg5NiAxNi43NzMxMiAyMS4yMjc1MiAxNi43NzMxMiAzOS40MjQgMCAxOS4yMjA0OC01LjU4MDggMzMuMzgyNC0xNi43NjI4OCA0Mi40NzU1Mi0xMS4xODIwOCA4LjYwMTYtMjkuNDgwOTYgMTMuMTQ4MTYtNTQuODg2NCAxMy4xNDgxNmgtOTUuNTM5MlYzNzUuODM4NzJ6IG0wIDE1Mi42OTg4OGgxMDQuMTcxNTJjMjYuNDI5NDQgMCA0Ni4yNTQwOCA0LjU0NjU2IDU4Ljk1MTY4IDE0LjE1MTY4IDEyLjcwNzg0IDkuNjA1MTIgMTkuMzEyNjQgMjUuMjgyNTYgMTkuMzEyNjQgNDYuNTIwMzIgMCAyMC43MjU3Ni04LjYzMjMyIDM1Ljg5MTItMjQuOTAzNjggNDUuNDk2MzItMTMuMjA5NiA3LjA4NjA4LTMxLjUwODQ4IDExLjEzMDg4LTU0Ljg4NjQgMTEuMTMwODhoLTEwMi42NTZWNTI4LjUzNzZ6IiBmaWxsPSIjMTExMTExIiBwLWlkPSIxNzQ2Ij48L3BhdGg+PC9zdmc+", + DB.Adbpg: "data:image/png;base64,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", } # RedisCloud color: #0D6EFD diff --git a/vectordb_bench/models.py b/vectordb_bench/models.py index a7e7c09f1..e900877c6 100644 --- a/vectordb_bench/models.py +++ b/vectordb_bench/models.py @@ -173,6 +173,15 @@ class CaseConfigParamType(Enum): exbits = "exbits" number_of_regions = "number_of_regions" + # ADBPG parameters + hnsw_m = "hnsw_m" + algorithm = "algorithm" + rabitq_bits = "rabitq_bits" + quantize_rescore_amp = "quantize_rescore_amp" + nova_adaptive_gamma = "nova_adaptive_gamma" + max_scan_points = "max_scan_points" + auto_reduction = "auto_reduction" + class CustomizedCase(BaseModel): pass @@ -346,9 +355,7 @@ def read_file(cls, full_path: pathlib.Path, trans_unit: bool = False) -> Self: task_config = case_result.get("task_config") case_config = task_config.get("case_config") db = DB(task_config.get("db")) - task_config["db_config"] = db.config_cls(**task_config["db_config"]) - # Safely instantiate DBCaseConfig (fallback to EmptyDBCaseConfig on None) raw_case_cfg = task_config.get("db_case_config") or {} index_value = raw_case_cfg.get("index", None)