diff --git a/tests/test_lancedb_client.py b/tests/test_lancedb_client.py new file mode 100644 index 000000000..621225cac --- /dev/null +++ b/tests/test_lancedb_client.py @@ -0,0 +1,243 @@ +"""Unit tests for LanceDB client insert semantics. + +Focus: ConcurrentInsertRunner + thread_safe=True must not duplicate IDs when +a multi-fragment insert commits a prefix and then fails. +""" + +from __future__ import annotations + +from contextlib import contextmanager + +import pytest + +pytest.importorskip("lancedb") +pytest.importorskip("pyarrow") + +from vectordb_bench.backend.clients.api import PartialInsertError # noqa: E402 +from vectordb_bench.backend.clients.lancedb import lancedb as lancedb_module # noqa: E402 +from vectordb_bench.backend.clients.lancedb.config import LanceDBBTreeConfig, LanceDBNoIndexConfig # noqa: E402 +from vectordb_bench.backend.clients.lancedb.lancedb import LanceDB # noqa: E402 +from vectordb_bench.backend.runner.concurrent_runner import ConcurrentInsertRunner # noqa: E402 + + +def _make_client(tmp_path, *, batch_size: int, monkeypatch) -> LanceDB: + monkeypatch.setattr(lancedb_module, "LANCEDB_BATCH_SIZE", batch_size) + return LanceDB( + dim=2, + db_config={"uri": str(tmp_path / "db")}, + db_case_config=LanceDBNoIndexConfig(), + collection_name="bench", + drop_old=True, + ) + + +def test_insert_embeddings_partial_commit_is_non_retryable(tmp_path, monkeypatch): + """If the first fragment commits and a later add fails, report the + committed prefix as PartialInsertError (non-retryable) instead of (0, err). + """ + db = _make_client(tmp_path, batch_size=2, monkeypatch=monkeypatch) + embeddings = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6], [0.7, 0.8]] + metadata = [1, 2, 3, 4] + + with db.init(): + real_add = db.table.add + calls = {"n": 0} + + def flaky_add(batch): + calls["n"] += 1 + if calls["n"] == 1: + return real_add(batch) + raise RuntimeError("simulated fragment commit failure") + + db.table.add = flaky_add + inserted, err = db.insert_embeddings(embeddings, metadata) + + assert inserted == 2 + assert isinstance(err, PartialInsertError) + assert err.non_retryable is True + assert err.inserted_count == 2 + + with db.init(): + assert db.table.count_rows() == 2 + + +def test_insert_embeddings_full_success_returns_all_rows(tmp_path, monkeypatch): + db = _make_client(tmp_path, batch_size=2, monkeypatch=monkeypatch) + embeddings = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]] + metadata = [10, 11, 12] + + with db.init(): + inserted, err = db.insert_embeddings(embeddings, metadata) + assert err is None + assert inserted == 3 + assert db.table.count_rows() == 3 + + +def test_concurrent_insert_exact_row_count_with_thread_safe(tmp_path, monkeypatch): + """Regression: ConcurrentInsertRunner + thread_safe must insert each ID once. + + Uses NUM_PER_BATCH > LANCEDB_BATCH_SIZE so inserts span multiple fragments. + """ + monkeypatch.setattr(lancedb_module, "LANCEDB_BATCH_SIZE", 2) + db = LanceDB( + dim=2, + db_config={"uri": str(tmp_path / "db")}, + db_case_config=LanceDBNoIndexConfig(), + collection_name="bench", + drop_old=True, + ) + assert db.thread_safe is True + + class Data: + train_id_field = "id" + train_vector_field = "vector" + scalar_labels_file_separated = False + + class Dataset: + data = Data() + + def iter_batches(self, batch_size: int): + import pandas as pd + + # Two runner batches of 3 rows each; each batch spans 2 Lance fragments. + rows = [ + {"id": i, "vector": [float(i), float(i) + 0.1]} + for i in range(6) + ] + for start in range(0, len(rows), batch_size): + yield pd.DataFrame(rows[start : start + batch_size]) + + runner = ConcurrentInsertRunner( + db, + Dataset(), + normalize=False, + max_workers=2, + batch_size=3, + ) + count = runner.task() + assert count == 6 + + with db.init(): + assert db.table.count_rows() == 6 + ids = sorted(db.table.to_pandas()["id"].tolist()) + assert ids == [0, 1, 2, 3, 4, 5] + + +def test_concurrent_runner_does_not_retry_lancedb_partial_insert(monkeypatch): + """PartialInsertError from LanceDB must stop the batch retry loop.""" + from vectordb_bench.backend.runner import concurrent_runner as concurrent_runner_module + + class FakeDB: + name = "LanceDB" + thread_safe = True + + def __init__(self): + self.calls = 0 + + @contextmanager + def init(self): + yield + + def insert_embeddings(self, embeddings, metadata, labels_data=None): + self.calls += 1 + # Simulate first fragment (2 rows) committed, second failed. + return 2, PartialInsertError( + "partial fragment commit", + inserted_count=2, + cause=RuntimeError("add failed"), + ) + + monkeypatch.setattr(concurrent_runner_module.time, "sleep", lambda _seconds: None) + + runner = ConcurrentInsertRunner.__new__(ConcurrentInsertRunner) + db = FakeDB() + with pytest.raises(RuntimeError, match="Non-retryable insert failure"): + runner._insert_batch_with_retry( + db, + embeddings=[[0.1, 0.2]] * 4, + metadata=[1, 2, 3, 4], + ) + assert db.calls == 1 + + +def test_optimize_btree_uses_scalar_create_index_api(): + """A btree-only config calls create_index(column, config=BTree()), not the vector API.""" + from unittest.mock import MagicMock + + db = LanceDB.__new__(LanceDB) + db.table_name = "bench" + db._id_field = "id" + db.case_config = LanceDBBTreeConfig() + db.table = MagicMock() + + db.optimize() + + db.table.create_index.assert_called_once() + args, kwargs = db.table.create_index.call_args + assert args == ("id",) + assert kwargs["config"].__class__.__name__ == "BTree" + db.table.optimize.assert_called_once() + + +def test_optimize_keeps_vector_index_and_adds_scalar_btree(): + """IVF_* and BTREE(label) are both created, on the vector column and on label.""" + from unittest.mock import MagicMock + + from vectordb_bench.backend.clients.api import IndexType + from vectordb_bench.backend.clients.lancedb.config import LanceDBIVFFlatConfig + + db = LanceDB.__new__(LanceDB) + db.table_name = "bench" + db._id_field = "id" + db.case_config = LanceDBIVFFlatConfig(scalar_index=IndexType.BTREE, scalar_column="label") + db.table = MagicMock() + + db.optimize() + + assert db.table.create_index.call_count == 2 + vector_call, scalar_call = db.table.create_index.call_args_list + assert vector_call.kwargs["index_type"] == "IVF_FLAT" + assert "config" not in vector_call.kwargs + assert scalar_call.args == ("label",) + assert scalar_call.kwargs["config"].__class__.__name__ == "BTree" + db.table.optimize.assert_called_once() + + +def test_streaming_runner_does_not_retry_lancedb_partial_insert(tmp_path, monkeypatch): + """A committed fragment must not be inserted again by the streaming retry loop. + + The first attempt commits ids [0, 1] and then fails. Retrying the same + batch would insert [0, 1, 2, 3] and leave [0, 1, 0, 1, 2, 3]. + """ + from vectordb_bench.backend.runner import rate_runner as rate_runner_module + from vectordb_bench.backend.runner.rate_runner import RatedMultiThreadingInsertRunner + + monkeypatch.setattr(lancedb_module, "LANCEDB_BATCH_SIZE", 2) + monkeypatch.setattr(rate_runner_module.time, "sleep", lambda _seconds: None) + db = LanceDB( + dim=2, + db_config={"uri": str(tmp_path / "db")}, + db_case_config=LanceDBNoIndexConfig(), + collection_name="bench", + drop_old=True, + ) + embeddings = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6], [0.7, 0.8]] + metadata = [0, 1, 2, 3] + + with db.init(): + real_add = db.table.add + calls = {"n": 0} + + def flaky_add(batch): + calls["n"] += 1 + if calls["n"] == 2: + raise RuntimeError("simulated fragment commit failure") + return real_add(batch) + + db.table.add = flaky_add + runner = RatedMultiThreadingInsertRunner(rate=4, db=db, dataset_iter=iter(()), batch_size=4) + with pytest.raises(RuntimeError, match="Non-retryable insert failure"): + runner.send_insert_task(db, embeddings, metadata) + + assert calls["n"] == 2 + assert sorted(db.table.to_pandas()["id"].tolist()) == [0, 1] diff --git a/tests/test_lancedb_config.py b/tests/test_lancedb_config.py index cf2d6e12f..6e2829825 100644 --- a/tests/test_lancedb_config.py +++ b/tests/test_lancedb_config.py @@ -1,27 +1,65 @@ """Offline unit tests for LanceDB config objects. These tests don't require a running LanceDB instance. They freeze the -contract for the three IVF-family indexes (IVF_PQ / IVF_HNSW_SQ / -IVF_HNSW_PQ) so that any future refactor that accidentally diverges their -code paths (e.g. introduces index-type-specific branches) will fail CI. - -Background: IVF_HNSW_SQ / IVF_HNSW_PQ share the same code path as IVF_PQ -and the CLI is wired up for all three. These tests encode that claim. +contract for the IVF-family indexes (IVF_FLAT / IVF_PQ / IVF_SQ / IVF_RQ / +IVF_HNSW_SQ / IVF_HNSW_PQ) and the BTREE scalar index so that any future +refactor that accidentally diverges their code paths (e.g. introduces +index-type-specific branches) will fail CI. + +Background: IVF_FLAT / IVF_SQ / IVF_RQ / IVF_HNSW_SQ / IVF_HNSW_PQ share +the same search path as IVF_PQ and the CLI is wired up for all of them. +A scalar B-tree is optional and independent of the vector index, so IVF_* + +BTREE(id|label) stays expressible. The btree-only config remains for a +table with no ANN index. """ import typing from typing import Annotated, get_type_hints +import pytest + from vectordb_bench.backend.clients.api import IndexType, MetricType from vectordb_bench.backend.clients.lancedb.config import ( LanceDBAutoIndexConfig, + LanceDBBTreeConfig, LanceDBIndexConfig, + LanceDBIVFFlatConfig, LanceDBIVFHNSWPQConfig, LanceDBIVFHNSWSQConfig, + LanceDBIVFRQConfig, + LanceDBIVFSQConfig, LanceDBNoIndexConfig, _lancedb_case_config, + build_lancedb_storage_options, ) +# --------------------------------------------------------------------------- +# storage_options resolution +# --------------------------------------------------------------------------- + + +def test_explicit_empty_storage_options_disable_env_inheritance(monkeypatch): + """``storage_options={}`` must win over GOOSEFS_*/COS env inheritance.""" + monkeypatch.setenv("GOOSEFS_AUTH_TYPE", "simple") + monkeypatch.setenv("GOOSEFS_AUTH_USERNAME", "bench") + assert build_lancedb_storage_options("goosefs://bucket/path", explicit={}) == {} + + +def test_none_storage_options_fall_back_to_env(monkeypatch): + monkeypatch.setenv("GOOSEFS_AUTH_TYPE", "simple") + monkeypatch.delenv("GOOSEFS_AUTH_USERNAME", raising=False) + monkeypatch.delenv("GOOSEFS_WRITE_TYPE", raising=False) + monkeypatch.delenv("GOOSEFS_BLOCK_SIZE", raising=False) + monkeypatch.delenv("GOOSEFS_CHUNK_SIZE", raising=False) + opts = build_lancedb_storage_options("goosefs://bucket/path", explicit=None) + assert opts == {"goosefs_auth_type": "simple"} + + +def test_explicit_storage_options_are_returned_unchanged(): + explicit = {"endpoint": "https://example.invalid"} + assert build_lancedb_storage_options("s3://bucket", explicit=explicit) is explicit + + # --------------------------------------------------------------------------- # Registry mapping # --------------------------------------------------------------------------- @@ -30,16 +68,21 @@ def test_registry_covers_all_lancedb_index_types(): """Every LanceDB-supported IndexType must resolve to a config class.""" required = { + IndexType.IVFFlat, IndexType.IVFPQ, + IndexType.IVF_SQ, + IndexType.IVF_RQ, IndexType.AUTOINDEX, IndexType.IVF_HNSW_SQ, IndexType.IVF_HNSW_PQ, + IndexType.BTREE, IndexType.NONE, } assert required.issubset(_lancedb_case_config.keys()) # HNSW is kept for backwards compatibility and must map to IVF_HNSW_SQ. assert _lancedb_case_config[IndexType.HNSW] is LanceDBIVFHNSWSQConfig + assert _lancedb_case_config[IndexType.BTREE] is LanceDBBTreeConfig # --------------------------------------------------------------------------- @@ -83,6 +126,44 @@ def test_ivfpq_tuned_params_are_forwarded(): assert cfg.search_param() == {"nprobes": 20, "refine_factor": 10} +def test_ivf_flat_sq_rq_params_are_forwarded(): + flat = LanceDBIVFFlatConfig( + metric_type=MetricType.COSINE, + num_partitions=256, + nprobes=20, + refine_factor=10, + ) + assert flat.index_param() == { + "metric": "cosine", + "index_type": "IVF_FLAT", + "sample_rate": 256, + "max_iterations": 50, + "num_partitions": 256, + } + assert "num_bits" not in flat.index_param() + assert flat.search_param() == {"nprobes": 20, "refine_factor": 10} + + sq = LanceDBIVFSQConfig(metric_type=MetricType.COSINE, num_partitions=128) + assert sq.index_param()["index_type"] == "IVF_SQ" + assert "num_bits" not in sq.index_param() + + rq = LanceDBIVFRQConfig( + metric_type=MetricType.COSINE, + num_partitions=64, + nbits=1, + nprobes=8, + ) + assert rq.index_param() == { + "metric": "cosine", + "index_type": "IVF_RQ", + "sample_rate": 256, + "max_iterations": 50, + "num_partitions": 64, + "num_bits": 1, + } + assert rq.search_param() == {"nprobes": 8} + + def test_ivf_hnsw_sq_params_are_forwarded(): cfg = LanceDBIVFHNSWSQConfig( metric_type=MetricType.COSINE, @@ -129,6 +210,49 @@ def test_ivf_hnsw_pq_params_are_forwarded(): } +def test_btree_params_are_forwarded(): + cfg = LanceDBBTreeConfig() + assert cfg.index == IndexType.BTREE + assert cfg.index_param() == {"index_type": "BTREE", "column": "id"} + assert cfg.scalar_btree_column() == "id" + assert cfg.search_param() == {} + assert cfg.model_dump(mode="json")["scalar_index"] == "BTREE" + assert cfg.model_dump(mode="json")["scalar_column"] == "id" + + labeled = LanceDBBTreeConfig(column="label") + assert labeled.index_param() == {"index_type": "BTREE", "column": "label"} + assert labeled.scalar_column == "label" + assert labeled.search_param() == {} + + +def test_scalar_btree_is_independent_of_the_vector_index(): + cfg = LanceDBIVFFlatConfig(scalar_index="BTREE", scalar_column="label") + assert cfg.index == IndexType.IVFFlat + assert cfg.scalar_btree_column() == "label" + assert cfg.index_param()["index_type"] == "IVF_FLAT" + assert "column" not in cfg.index_param() + recorded = cfg.model_dump(mode="json") + assert recorded["index"] == "IVF_FLAT" + assert recorded["scalar_index"] == "BTREE" + assert recorded["scalar_column"] == "label" + + bare = LanceDBNoIndexConfig(scalar_index=IndexType.BTREE, scalar_column="id") + assert bare.index == IndexType.NONE + assert bare.index_param() == {} + assert bare.scalar_btree_column() == "id" + + unset = LanceDBIVFSQConfig() + assert unset.scalar_btree_column() is None + assert unset.model_dump(mode="json")["scalar_index"] == "NONE" + + +def test_scalar_index_rejects_unknown_values(): + with pytest.raises(ValueError, match="NONE or BTREE"): + LanceDBIVFFlatConfig(scalar_index="IVF_FLAT") + with pytest.raises(ValueError, match="id or label"): + LanceDBIVFFlatConfig(scalar_index="BTREE", scalar_column="vector") + + # --------------------------------------------------------------------------- # Code-path unification # --------------------------------------------------------------------------- @@ -142,10 +266,14 @@ def test_ivf_family_shares_search_knobs(): allowed = {"nprobes", "ef", "refine_factor"} ivfpq = LanceDBIndexConfig(nprobes=10, refine_factor=5) + flat = LanceDBIVFFlatConfig(nprobes=10, refine_factor=5) + ivfsq = LanceDBIVFSQConfig(nprobes=10, refine_factor=5) + ivfrq = LanceDBIVFRQConfig(nprobes=10, refine_factor=5) sq = LanceDBIVFHNSWSQConfig(nprobes=10, ef=64, refine_factor=5) pq = LanceDBIVFHNSWPQConfig(nprobes=10, ef=64, refine_factor=5) + btree = LanceDBBTreeConfig() - for cfg in (ivfpq, sq, pq): + for cfg in (ivfpq, flat, ivfsq, ivfrq, sq, pq, btree): assert set(cfg.search_param().keys()).issubset(allowed) @@ -218,13 +346,43 @@ def test_cli_typed_dicts_define_all_expected_commands(): # TypedDicts that drive CLI options assert { "LanceDBTypedDict", + "LanceDBScalarTypedDict", + "LanceDBWithScalarTypedDict", + "LanceDBIVFTypedDict", "LanceDBIVFPQTypedDict", + "LanceDBIVFRQTypedDict", "LanceDBIVFHNSWSQTypedDict", "LanceDBIVFHNSWPQTypedDict", + "LanceDBBTreeTypedDict", }.issubset(class_names) + def _bases(cls_name: str) -> set[str]: + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == cls_name: + return {base.id for base in node.bases if isinstance(base, ast.Name)} + return set() + + for vector_command in ( + "LanceDBIVFTypedDict", + "LanceDBIVFPQTypedDict", + "LanceDBIVFHNSWSQTypedDict", + "LanceDBIVFHNSWPQTypedDict", + ): + assert "LanceDBScalarTypedDict" in _bases(vector_command) + assert "LanceDBScalarTypedDict" in _bases("LanceDBWithScalarTypedDict") + # Command functions registered via @cli.command() - assert {"LanceDB", "LanceDBAutoIndex", "LanceDBIVFPQ", "LanceDBIVFHNSWSQ", "LanceDBIVFHNSWPQ"}.issubset(func_names) + assert { + "LanceDB", + "LanceDBAutoIndex", + "LanceDBIVFFlat", + "LanceDBIVFPQ", + "LanceDBIVFSQ", + "LanceDBIVFRQ", + "LanceDBIVFHNSWSQ", + "LanceDBIVFHNSWPQ", + "LanceDBBTree", + }.issubset(func_names) def test_cli_typeddict_ivfpq_has_search_knobs(): @@ -248,6 +406,18 @@ def _fields_of(cls_name: str) -> set[str]: ivfpq = _fields_of("LanceDBIVFPQTypedDict") assert {"nprobes", "refine_factor", "num_partitions", "num_sub_vectors", "nbits"}.issubset(ivfpq) + ivf = _fields_of("LanceDBIVFTypedDict") + assert {"nprobes", "refine_factor", "num_partitions"}.issubset(ivf) + assert "num_sub_vectors" not in ivf + assert "nbits" not in ivf + + rq = _fields_of("LanceDBIVFRQTypedDict") + assert "nbits" in rq + assert "num_sub_vectors" not in rq + + btree = _fields_of("LanceDBBTreeTypedDict") + assert btree == {"column"} + def test_cli_typeddict_hnsw_variants_are_superset_of_ivfpq(): """Both HNSW TypedDicts must have nprobes + refine_factor + graph knobs.""" diff --git a/tests/test_rate_runner_thread_safety.py b/tests/test_rate_runner_thread_safety.py index 27e56d506..d4ebceb94 100644 --- a/tests/test_rate_runner_thread_safety.py +++ b/tests/test_rate_runner_thread_safety.py @@ -8,7 +8,9 @@ from contextlib import contextmanager from copy import deepcopy -from vectordb_bench.backend.clients.api import VectorDB +import pytest + +from vectordb_bench.backend.clients.api import PartialInsertError, VectorDB from vectordb_bench.backend.runner.rate_runner import RatedMultiThreadingInsertRunner @@ -75,6 +77,51 @@ def test_non_thread_safe_client_routes_through_copy(): assert db.init_calls == 0 +def test_rate_runner_does_not_retry_non_retryable_insert(monkeypatch): + """Streaming insert must stop after a partial commit instead of replaying the batch.""" + + class PartialDB(FakeDB): + def __init__(self): + super().__init__(thread_safe=True) + self.calls = 0 + + def insert_embeddings(self, embeddings, metadata, **kwargs): + self.calls += 1 + return 2, PartialInsertError( + "partial fragment commit", + inserted_count=2, + cause=RuntimeError("add failed"), + ) + + from vectordb_bench.backend.runner import rate_runner as rate_runner_module + + monkeypatch.setattr(rate_runner_module.time, "sleep", lambda _seconds: None) + db = PartialDB() + with pytest.raises(RuntimeError, match="Non-retryable insert failure after 2 inserted rows"): + _runner(db).send_insert_task(db, [[0.1], [0.2], [0.3], [0.4]], [0, 1, 2, 3]) + assert db.calls == 1 + + +def test_rate_runner_still_retries_retryable_insert_errors(monkeypatch): + class FlakyDB(FakeDB): + def __init__(self): + super().__init__(thread_safe=True) + self.calls = 0 + + def insert_embeddings(self, embeddings, metadata, **kwargs): + self.calls += 1 + if self.calls == 1: + return 0, RuntimeError("transient") + return len(embeddings), None + + from vectordb_bench.backend.runner import rate_runner as rate_runner_module + + monkeypatch.setattr(rate_runner_module.time, "sleep", lambda _seconds: None) + db = FlakyDB() + _runner(db).send_insert_task(db, [[0.1], [0.2]], [0, 1]) + assert db.calls == 2 + + def test_default_copy_for_thread_is_a_distinct_deep_copy(): db = FakeDB(thread_safe=False) copy = db.copy_for_thread() diff --git a/vectordb_bench/backend/clients/api.py b/vectordb_bench/backend/clients/api.py index e1c74e9df..d2d131124 100644 --- a/vectordb_bench/backend/clients/api.py +++ b/vectordb_bench/backend/clients/api.py @@ -32,6 +32,8 @@ class IndexType(StrEnum): IVFPQ = "IVF_PQ" IVFBQ = "IVF_BQ" IVFSQ8 = "IVF_SQ8" + IVF_SQ = "IVF_SQ" + IVF_RQ = "IVF_RQ" IVF_RABITQ = "IVF_RABITQ" Flat = "FLAT" AUTOINDEX = "AUTOINDEX" @@ -58,6 +60,7 @@ class IndexType(StrEnum): Hologres_Graph = "Graph" IVF_HNSW_SQ = "IVF_HNSW_SQ" IVF_HNSW_PQ = "IVF_HNSW_PQ" + BTREE = "BTREE" NONE = "NONE" diff --git a/vectordb_bench/backend/clients/lancedb/cli.py b/vectordb_bench/backend/clients/lancedb/cli.py index 494c28a2b..e4b630191 100644 --- a/vectordb_bench/backend/clients/lancedb/cli.py +++ b/vectordb_bench/backend/clients/lancedb/cli.py @@ -1,5 +1,4 @@ -import os -from typing import Annotated, Unpack +from typing import Annotated, TypedDict, Unpack import click from pydantic import SecretStr @@ -61,114 +60,85 @@ class LanceDBTypedDict(CommonTypedDict): ] -def _build_storage_options(**parameters) -> dict[str, str] | None: - """Build storage_options based on URI scheme. - - Supports: - - cos:// / s3:// → Tencent COS credentials - - goosefs:// → GooseFS authentication options - """ - uri = parameters.get("uri", "") - - # --- GooseFS storage options --- - if uri.startswith("goosefs://"): - return _build_goosefs_storage_options(**parameters) - - # --- COS / S3 storage options --- - if uri.startswith(("cos://", "s3://")): - return _build_cos_storage_options(**parameters) - - return None +class LanceDBScalarTypedDict(TypedDict): + scalar_index: Annotated[ + str, + click.option( + "--scalar-index", + type=click.Choice(["NONE", "BTREE"]), + default="NONE", + help="Optional scalar B-tree built in addition to the vector index", + show_default=True, + ), + ] + scalar_column: Annotated[ + str, + click.option( + "--scalar-column", + type=click.Choice(["id", "label"]), + default="id", + help="Column for the scalar B-tree. Label-filter cases predicate on label; range filters predicate on id", + show_default=True, + ), + ] -def _build_cos_storage_options(**parameters) -> dict[str, str] | None: - """Build storage_options for COS/S3 if credentials are provided.""" - secret_id = parameters.get("cos_secret_id") or os.environ.get("COS_SECRET_ID") - secret_key = parameters.get("cos_secret_key") or os.environ.get("COS_SECRET_KEY") - endpoint = parameters.get("cos_endpoint") or os.environ.get("COS_ENDPOINT") - region = parameters.get("cos_region") or os.environ.get("TENCENTCLOUD_REGION") +class LanceDBWithScalarTypedDict(LanceDBTypedDict, LanceDBScalarTypedDict): + pass - if not (secret_id and secret_key): - return None - storage_options = { - "aws_access_key_id": secret_id, - "aws_secret_access_key": secret_key, - } - if endpoint: - storage_options["endpoint"] = endpoint - if region: - storage_options["region"] = region - - return storage_options - - -def _build_goosefs_storage_options(**_parameters: str) -> dict[str, str] | None: - """Build storage_options for GooseFS. - - Recognized environment variables: - - GOOSEFS_AUTH_TYPE → goosefs_auth_type (simple / nosasl) - - GOOSEFS_AUTH_USERNAME → goosefs_auth_username - - GOOSEFS_WRITE_TYPE → goosefs_write_type (CACHE_THROUGH etc.) - - GOOSEFS_BLOCK_SIZE → goosefs_block_size - - GOOSEFS_CHUNK_SIZE → goosefs_chunk_size - """ - storage_options: dict[str, str] = {} - - _goosefs_env_keys = { - "GOOSEFS_AUTH_TYPE": "goosefs_auth_type", - "GOOSEFS_AUTH_USERNAME": "goosefs_auth_username", - "GOOSEFS_WRITE_TYPE": "goosefs_write_type", - "GOOSEFS_BLOCK_SIZE": "goosefs_block_size", - "GOOSEFS_CHUNK_SIZE": "goosefs_chunk_size", +def _scalar_case_kwargs(parameters: dict) -> dict: + return { + "scalar_index": parameters.get("scalar_index", "NONE"), + "scalar_column": parameters.get("scalar_column", "id"), } - for env_key, opt_key in _goosefs_env_keys.items(): - value = os.environ.get(env_key) - if value: - storage_options[opt_key] = value - - return storage_options if storage_options else None - def _build_db_config(**parameters): - from .config import LanceDBConfig + from .config import LanceDBConfig, build_lancedb_storage_options + uri = parameters["uri"] return LanceDBConfig( db_label=parameters["db_label"], - uri=parameters["uri"], + uri=uri, token=SecretStr(parameters["token"]) if parameters.get("token") else None, - storage_options=_build_storage_options(**parameters), + storage_options=build_lancedb_storage_options( + uri, + cos_secret_id=parameters.get("cos_secret_id"), + cos_secret_key=parameters.get("cos_secret_key"), + cos_endpoint=parameters.get("cos_endpoint"), + cos_region=parameters.get("cos_region"), + ), ) @cli.command() -@click_parameter_decorators_from_typed_dict(LanceDBTypedDict) -def LanceDB(**parameters: Unpack[LanceDBTypedDict]): +@click_parameter_decorators_from_typed_dict(LanceDBWithScalarTypedDict) +def LanceDB(**parameters: Unpack[LanceDBWithScalarTypedDict]): from .config import LanceDBNoIndexConfig run( db=DB.LanceDB, db_config=_build_db_config(**parameters), - db_case_config=LanceDBNoIndexConfig(), + db_case_config=LanceDBNoIndexConfig(**_scalar_case_kwargs(parameters)), **parameters, ) @cli.command() -@click_parameter_decorators_from_typed_dict(LanceDBTypedDict) -def LanceDBAutoIndex(**parameters: Unpack[LanceDBTypedDict]): +@click_parameter_decorators_from_typed_dict(LanceDBWithScalarTypedDict) +def LanceDBAutoIndex(**parameters: Unpack[LanceDBWithScalarTypedDict]): from .config import LanceDBAutoIndexConfig run( db=DB.LanceDB, db_config=_build_db_config(**parameters), - db_case_config=LanceDBAutoIndexConfig(), + db_case_config=LanceDBAutoIndexConfig(**_scalar_case_kwargs(parameters)), **parameters, ) -class LanceDBIVFPQTypedDict(CommonTypedDict, LanceDBTypedDict): +class LanceDBIVFPQTypedDict(CommonTypedDict, LanceDBTypedDict, LanceDBScalarTypedDict): num_partitions: Annotated[ int, click.option( @@ -236,12 +206,114 @@ def LanceDBIVFPQ(**parameters: Unpack[LanceDBIVFPQTypedDict]): nbits=parameters["nbits"], nprobes=parameters["nprobes"], refine_factor=parameters["refine_factor"], + **_scalar_case_kwargs(parameters), ), **parameters, ) -class LanceDBIVFHNSWSQTypedDict(CommonTypedDict, LanceDBTypedDict): +class LanceDBIVFTypedDict(CommonTypedDict, LanceDBTypedDict, LanceDBScalarTypedDict): + num_partitions: Annotated[ + int, + click.option( + "--num-partitions", + type=int, + default=0, + help="Number of IVF partitions, 0 = use LanceDB default", + show_default=True, + ), + ] + nprobes: Annotated[ + int, + click.option( + "--nprobes", + type=int, + default=0, + help="Number of probes for IVF search, 0 = use LanceDB default", + show_default=True, + ), + ] + refine_factor: Annotated[ + int, + click.option( + "--refine-factor", + type=int, + default=0, + help="Refine factor for better recall, 0 = disabled", + show_default=True, + ), + ] + + +@cli.command() +@click_parameter_decorators_from_typed_dict(LanceDBIVFTypedDict) +def LanceDBIVFFlat(**parameters: Unpack[LanceDBIVFTypedDict]): + from .config import LanceDBIVFFlatConfig + + run( + db=DB.LanceDB, + db_config=_build_db_config(**parameters), + db_case_config=LanceDBIVFFlatConfig( + num_partitions=parameters["num_partitions"], + nprobes=parameters["nprobes"], + refine_factor=parameters["refine_factor"], + **_scalar_case_kwargs(parameters), + ), + **parameters, + ) + + +@cli.command() +@click_parameter_decorators_from_typed_dict(LanceDBIVFTypedDict) +def LanceDBIVFSQ(**parameters: Unpack[LanceDBIVFTypedDict]): + from .config import LanceDBIVFSQConfig + + run( + db=DB.LanceDB, + db_config=_build_db_config(**parameters), + db_case_config=LanceDBIVFSQConfig( + num_partitions=parameters["num_partitions"], + nprobes=parameters["nprobes"], + refine_factor=parameters["refine_factor"], + **_scalar_case_kwargs(parameters), + ), + **parameters, + ) + + +class LanceDBIVFRQTypedDict(LanceDBIVFTypedDict): + nbits: Annotated[ + int, + click.option( + "--nbits", + type=int, + default=1, + help="RabitQ bits per dimension (Lance default: 1)", + show_default=True, + ), + ] + + +@cli.command() +@click_parameter_decorators_from_typed_dict(LanceDBIVFRQTypedDict) +def LanceDBIVFRQ(**parameters: Unpack[LanceDBIVFRQTypedDict]): + from .config import LanceDBIVFRQConfig + + run( + db=DB.LanceDB, + db_config=_build_db_config(**parameters), + db_case_config=LanceDBIVFRQConfig( + num_partitions=parameters["num_partitions"], + nbits=parameters["nbits"], + nprobes=parameters["nprobes"], + refine_factor=parameters["refine_factor"], + **_scalar_case_kwargs(parameters), + ), + **parameters, + ) + + +class LanceDBIVFHNSWSQTypedDict(CommonTypedDict, LanceDBTypedDict, LanceDBScalarTypedDict): num_partitions: Annotated[ int, click.option( @@ -307,12 +379,13 @@ def LanceDBIVFHNSWSQ(**parameters: Unpack[LanceDBIVFHNSWSQTypedDict]): ef=parameters["ef"], nprobes=parameters["nprobes"], refine_factor=parameters["refine_factor"], + **_scalar_case_kwargs(parameters), ), **parameters, ) -class LanceDBIVFHNSWPQTypedDict(CommonTypedDict, LanceDBTypedDict): +class LanceDBIVFHNSWPQTypedDict(CommonTypedDict, LanceDBTypedDict, LanceDBScalarTypedDict): num_partitions: Annotated[ int, click.option( @@ -389,6 +462,37 @@ def LanceDBIVFHNSWPQ(**parameters: Unpack[LanceDBIVFHNSWPQTypedDict]): ef=parameters["ef"], nprobes=parameters["nprobes"], refine_factor=parameters["refine_factor"], + **_scalar_case_kwargs(parameters), + ), + **parameters, + ) + + +class LanceDBBTreeTypedDict(CommonTypedDict, LanceDBTypedDict): + column: Annotated[ + str, + click.option( + "--column", + type=click.Choice(["id", "label"]), + default="id", + help="Scalar column for a B-tree with no vector index. Label-filter cases use label", + show_default=True, + ), + ] + + +@cli.command() +@click_parameter_decorators_from_typed_dict(LanceDBBTreeTypedDict) +def LanceDBBTree(**parameters: Unpack[LanceDBBTreeTypedDict]): + from .config import LanceDBBTreeConfig + + run( + db=DB.LanceDB, + db_config=_build_db_config(**parameters), + db_case_config=LanceDBBTreeConfig( + column=parameters["column"], + scalar_index=IndexType.BTREE, + scalar_column=parameters["column"], ), **parameters, ) diff --git a/vectordb_bench/backend/clients/lancedb/config.py b/vectordb_bench/backend/clients/lancedb/config.py index 8a98c36f3..56745ce99 100644 --- a/vectordb_bench/backend/clients/lancedb/config.py +++ b/vectordb_bench/backend/clients/lancedb/config.py @@ -1,7 +1,82 @@ -from pydantic import BaseModel, SecretStr +import os +from typing import ClassVar + +from pydantic import BaseModel, SecretStr, field_validator, model_validator from ..api import DBCaseConfig, DBConfig, IndexType, MetricType +_GOOSEFS_ENV_KEYS = { + "GOOSEFS_AUTH_TYPE": "goosefs_auth_type", + "GOOSEFS_AUTH_USERNAME": "goosefs_auth_username", + "GOOSEFS_WRITE_TYPE": "goosefs_write_type", + "GOOSEFS_BLOCK_SIZE": "goosefs_block_size", + "GOOSEFS_CHUNK_SIZE": "goosefs_chunk_size", +} + + +def build_goosefs_storage_options() -> dict[str, str] | None: + """Build storage_options for GooseFS from GOOSEFS_* environment variables. + + Recognized environment variables: + - GOOSEFS_AUTH_TYPE → goosefs_auth_type (simple / nosasl) + - GOOSEFS_AUTH_USERNAME → goosefs_auth_username + - GOOSEFS_WRITE_TYPE → goosefs_write_type (CACHE_THROUGH etc.) + - GOOSEFS_BLOCK_SIZE → goosefs_block_size + - GOOSEFS_CHUNK_SIZE → goosefs_chunk_size + """ + storage_options: dict[str, str] = {} + for env_key, opt_key in _GOOSEFS_ENV_KEYS.items(): + value = os.environ.get(env_key) + if value: + storage_options[opt_key] = value + return storage_options if storage_options else None + + +def build_cos_storage_options(**parameters) -> dict[str, str] | None: + """Build storage_options for COS/S3 if credentials are provided.""" + secret_id = parameters.get("cos_secret_id") or os.environ.get("COS_SECRET_ID") + secret_key = parameters.get("cos_secret_key") or os.environ.get("COS_SECRET_KEY") + endpoint = parameters.get("cos_endpoint") or os.environ.get("COS_ENDPOINT") + region = parameters.get("cos_region") or os.environ.get("TENCENTCLOUD_REGION") + + if not (secret_id and secret_key): + return None + + storage_options = { + "aws_access_key_id": secret_id, + "aws_secret_access_key": secret_key, + } + if endpoint: + storage_options["endpoint"] = endpoint + if region: + storage_options["region"] = region + + return storage_options + + +def build_lancedb_storage_options( + uri: str, + explicit: dict[str, str] | None = None, + **parameters, +) -> dict[str, str] | None: + """Resolve storage_options for a LanceDB URI. + + Explicit options win when provided (including an empty mapping, which + intentionally disables inherited env-derived settings); otherwise options + are derived from the URI scheme (goosefs:// / cos:// / s3://) and + environment variables. + """ + if explicit is not None: + return explicit + + if uri.startswith("goosefs://"): + return build_goosefs_storage_options() + + if uri.startswith(("cos://", "s3://")): + return build_cos_storage_options(**parameters) + + return None + class LanceDBConfig(DBConfig): """LanceDB connection configuration.""" @@ -11,15 +86,60 @@ class LanceDBConfig(DBConfig): token: SecretStr | None = None storage_options: dict[str, str] | None = None + # Optional in the UI form; GooseFS/COS options are filled from env in to_dict(). + _extra_empty_skip: ClassVar[frozenset[str]] = frozenset({"token", "storage_options"}) + + @staticmethod + def ui_hidden_configs() -> list[str]: + return ["storage_options"] + def to_dict(self) -> dict: return { "uri": self.uri, "token": self.token.get_secret_value() if self.token else None, - "storage_options": self.storage_options, + "storage_options": build_lancedb_storage_options(self.uri, self.storage_options), } -class LanceDBIndexConfig(BaseModel, DBCaseConfig): +class LanceDBScalarIndex(BaseModel): + """Optional scalar B-tree, independent of the selected vector index. + + ``scalar_index`` is ``NONE`` or ``BTREE``. ``scalar_column`` is recorded on + the case config so a result can show IVF_* + BTREE(id|label) explicitly. + """ + + scalar_index: IndexType = IndexType.NONE + scalar_column: str = "id" + + @field_validator("scalar_index", mode="before") + @classmethod + def _normalize_scalar_index(cls, value: object) -> IndexType: + if value is None or value == "": + return IndexType.NONE + parsed = value if isinstance(value, IndexType) else IndexType(str(value)) + if parsed not in (IndexType.NONE, IndexType.BTREE): + msg = f"LanceDB scalar index must be NONE or BTREE, got {value}" + raise ValueError(msg) + return parsed + + @field_validator("scalar_column", mode="before") + @classmethod + def _normalize_scalar_column(cls, value: object) -> str: + column = "id" if value is None else str(value).strip() + if column == "": + column = "id" + if column not in {"id", "label"}: + msg = f"LanceDB scalar index column must be id or label, got {value}" + raise ValueError(msg) + return column + + def scalar_btree_column(self) -> str | None: + if self.scalar_index != IndexType.BTREE: + return None + return self.scalar_column + + +class LanceDBIndexConfig(LanceDBScalarIndex, DBCaseConfig): """Default IVF_PQ index configuration.""" index: IndexType = IndexType.IVFPQ @@ -94,7 +214,7 @@ def search_param(self) -> dict: return params -class LanceDBIVFHNSWSQConfig(BaseModel, DBCaseConfig): +class LanceDBIVFHNSWSQConfig(LanceDBScalarIndex, DBCaseConfig): """IVF_HNSW_SQ index — IVF partitioning + HNSW graph + scalar quantization.""" index: IndexType = IndexType.IVF_HNSW_SQ @@ -138,7 +258,118 @@ def search_param(self) -> dict: return params -class LanceDBIVFHNSWPQConfig(BaseModel, DBCaseConfig): +def _parse_lancedb_metric(metric_type: MetricType) -> str: + if metric_type in (MetricType.L2, MetricType.COSINE): + return metric_type.value.lower() + if metric_type in (MetricType.IP, MetricType.DP): + return "dot" + msg = f"Metric type {metric_type} is not supported for LanceDB!" + raise ValueError(msg) + + +class LanceDBIVFFamilyConfig(LanceDBScalarIndex, DBCaseConfig): + """Shared knobs for Lance IVF indexes that are not PQ/HNSW hybrids. + + IVF_FLAT stores raw vectors, IVF_SQ uses scalar quantization, IVF_RQ + uses RabitQ. All three take IVF partitions plus nprobes/refine_factor + at search time. ``nbits`` is only forwarded for IVF_RQ. + """ + + index: IndexType = IndexType.IVFFlat + metric_type: MetricType = MetricType.L2 + num_partitions: int = 0 + sample_rate: int = 256 + max_iterations: int = 50 + nprobes: int = 0 + refine_factor: int = 0 + nbits: int = 0 + + def parse_metric(self) -> str: + return _parse_lancedb_metric(self.metric_type) + + def index_param(self) -> dict: + params = { + "metric": self.parse_metric(), + "index_type": self.index.value, + "sample_rate": self.sample_rate, + "max_iterations": self.max_iterations, + } + if self.num_partitions > 0: + params["num_partitions"] = self.num_partitions + if self.nbits > 0: + params["num_bits"] = self.nbits + return params + + def search_param(self) -> dict: + params = {} + if self.nprobes > 0: + params["nprobes"] = self.nprobes + if self.refine_factor > 0: + params["refine_factor"] = self.refine_factor + return params + + +class LanceDBIVFFlatConfig(LanceDBIVFFamilyConfig): + """IVF_FLAT — IVF partitions over uncompressed vectors.""" + + index: IndexType = IndexType.IVFFlat + + +class LanceDBIVFSQConfig(LanceDBIVFFamilyConfig): + """IVF_SQ — IVF partitions + scalar quantization.""" + + index: IndexType = IndexType.IVF_SQ + + +class LanceDBIVFRQConfig(LanceDBIVFFamilyConfig): + """IVF_RQ — IVF partitions + RabitQ (random rotation + quantization).""" + + index: IndexType = IndexType.IVF_RQ + nbits: int = 1 # Lance default for RabitQ bits-per-dimension + + +class LanceDBBTreeConfig(LanceDBScalarIndex, DBCaseConfig): + """Scalar B-tree with no vector index. + + Prefer a vector index config plus ``scalar_index=BTREE`` when the + benchmark should keep ANN. This class remains for the btree-only CLI + command and for results that selected BTREE as the index type. + """ + + index: IndexType = IndexType.BTREE + metric_type: MetricType = MetricType.L2 + column: str = "id" + scalar_index: IndexType = IndexType.BTREE + + @model_validator(mode="before") + @classmethod + def _sync_btree_column(cls, data: object) -> object: + if not isinstance(data, dict): + return data + column = data.get("column") + scalar_column = data.get("scalar_column") + if column and not scalar_column: + data = {**data, "scalar_column": column} + elif scalar_column and not column: + data = {**data, "column": scalar_column} + elif column and scalar_column and column != scalar_column: + data = {**data, "scalar_column": column} + return data + + def parse_metric(self) -> str: + return _parse_lancedb_metric(self.metric_type) + + def index_param(self) -> dict: + return { + "index_type": "BTREE", + "column": self.column or "id", + } + + def search_param(self) -> dict: + return {} + + +class LanceDBIVFHNSWPQConfig(LanceDBScalarIndex, DBCaseConfig): """IVF_HNSW_PQ index — IVF partitioning + HNSW graph + product quantization.""" index: IndexType = IndexType.IVF_HNSW_PQ @@ -187,9 +418,13 @@ def search_param(self) -> dict: _lancedb_case_config = { IndexType.IVFPQ: LanceDBIndexConfig, + IndexType.IVFFlat: LanceDBIVFFlatConfig, + IndexType.IVF_SQ: LanceDBIVFSQConfig, + IndexType.IVF_RQ: LanceDBIVFRQConfig, IndexType.AUTOINDEX: LanceDBAutoIndexConfig, IndexType.IVF_HNSW_SQ: LanceDBIVFHNSWSQConfig, IndexType.IVF_HNSW_PQ: LanceDBIVFHNSWPQConfig, IndexType.HNSW: LanceDBIVFHNSWSQConfig, # backward compat: HNSW maps to IVF_HNSW_SQ + IndexType.BTREE: LanceDBBTreeConfig, IndexType.NONE: LanceDBNoIndexConfig, } diff --git a/vectordb_bench/backend/clients/lancedb/lancedb.py b/vectordb_bench/backend/clients/lancedb/lancedb.py index 2febc2f3c..4b953acbc 100644 --- a/vectordb_bench/backend/clients/lancedb/lancedb.py +++ b/vectordb_bench/backend/clients/lancedb/lancedb.py @@ -9,7 +9,7 @@ from vectordb_bench.backend.filter import Filter, FilterOp -from ..api import IndexType, VectorDB +from ..api import IndexType, PartialInsertError, VectorDB from .config import LanceDBIndexConfig log = logging.getLogger(__name__) @@ -29,7 +29,12 @@ class LanceDB(VectorDB): FilterOp.NumGE, FilterOp.StrEqual, ] - thread_safe: bool = False + # LanceDB's Python sync wrapper routes every table.add() through a shared + # tokio runtime (LOOP.run), and Lance's Rust core handles concurrent + # manifest commits with optimistic-concurrency retries. So multiple worker + # threads can share one connection. Set to True to let ConcurrentInsertRunner + # use max_workers > 1 for parallel data loading. + thread_safe: bool = True def __init__( self, @@ -133,6 +138,10 @@ def insert_embeddings( **kwargs, ) -> tuple[int, Exception | None]: assert self.table is not None, "Please call self.init() before" + # Track committed rows across multi-fragment adds. A later ``table.add`` + # failure must not report (0, err): ConcurrentInsertRunner would retry + # the whole NUM_PER_BATCH and duplicate already-committed IDs. + inserted = 0 try: log.info( f"LanceDB insert_embeddings called with {len(embeddings)} rows, " @@ -167,10 +176,17 @@ def insert_embeddings( } ) self.table.add(batch_table) + inserted += len(batch_ids) return len(metadata), None except Exception as e: log.warning(f"Failed to insert data into LanceDB table ({self.table_name}), error: {e}") + if inserted > 0: + msg = ( + f"LanceDB insert failed after committing {inserted}/{len(metadata)} rows " + f"into table ({self.table_name}); refusing retry to avoid duplicate IDs" + ) + return inserted, PartialInsertError(msg, inserted_count=inserted, cause=e) return 0, e def prepare_filter(self, filters: Filter): @@ -229,12 +245,20 @@ def search_embedding( def optimize(self, data_size: int | None = None): assert self.table is not None, "Please call self.init() before" - # Build index if configured - if self.case_config.index != IndexType.NONE: + # The vector index and the scalar B-tree are independent. A table can + # have IVF_* (or no ANN index) and also a B-tree on id or label. + if self.case_config.index not in (IndexType.NONE, IndexType.BTREE): index_params = self.case_config.index_param() - log.info(f"LanceDB creating index on table ({self.table_name}), params: {index_params}") + log.info(f"LanceDB creating vector index on table ({self.table_name}), params: {index_params}") self.table.create_index(**index_params) + scalar_column = self.case_config.scalar_btree_column() + if scalar_column: + from lancedb.index import BTree + + log.info(f"LanceDB creating scalar BTree on table ({self.table_name}), column: {scalar_column}") + self.table.create_index(scalar_column, config=BTree()) + # Compact fragments and clean up old versions for better performance. # Prefer the unified ``table.optimize()`` API (lancedb >= 0.10), which # internally handles both compaction and version cleanup without diff --git a/vectordb_bench/backend/runner/rate_runner.py b/vectordb_bench/backend/runner/rate_runner.py index 8aa96a44a..503b3a189 100644 --- a/vectordb_bench/backend/runner/rate_runner.py +++ b/vectordb_bench/backend/runner/rate_runner.py @@ -48,9 +48,14 @@ def __init__( def send_insert_task(self, db: api.VectorDB, emb: list[list[float]], metadata: list[str]): def _insert_embeddings(db: api.VectorDB, emb: list[list[float]], metadata: list[str], retry_idx: int = 0): - _, error = db.insert_embeddings(emb, metadata) + inserted, error = db.insert_embeddings(emb, metadata) if error is not None: log.warning(f"Insert Failed, try_idx={retry_idx}, Exception: {error}") + # A partial commit is already durable. Retrying the same batch + # inserts that prefix a second time. + if getattr(error, "non_retryable", False): + msg = f"Non-retryable insert failure after {inserted} inserted rows: {error}" + raise RuntimeError(msg) from error retry_idx += 1 if retry_idx <= config.MAX_INSERT_RETRY: time.sleep(retry_idx) diff --git a/vectordb_bench/cli/vectordbbench.py b/vectordb_bench/cli/vectordbbench.py index a3649ae84..0c771ccbb 100644 --- a/vectordb_bench/cli/vectordbbench.py +++ b/vectordb_bench/cli/vectordbbench.py @@ -20,9 +20,13 @@ from ..backend.clients.lancedb.cli import ( LanceDB, LanceDBAutoIndex, + LanceDBBTree, + LanceDBIVFFlat, LanceDBIVFHNSWPQ, LanceDBIVFHNSWSQ, LanceDBIVFPQ, + LanceDBIVFRQ, + LanceDBIVFSQ, ) from ..backend.clients.kividb.cli import KiviDB from ..backend.clients.lindorm.cli import LindormHNSW, LindormIVFBQ, LindormIVFPQ @@ -91,7 +95,11 @@ cli.add_command(Vespa) cli.add_command(LanceDB) cli.add_command(LanceDBAutoIndex) +cli.add_command(LanceDBIVFFlat) cli.add_command(LanceDBIVFPQ) +cli.add_command(LanceDBIVFSQ) +cli.add_command(LanceDBIVFRQ) +cli.add_command(LanceDBBTree) cli.add_command(LanceDBIVFHNSWSQ) cli.add_command(LanceDBIVFHNSWPQ) cli.add_command(HologresHGraph) diff --git a/vectordb_bench/frontend/components/run_test/dbConfigSetting.py b/vectordb_bench/frontend/components/run_test/dbConfigSetting.py index 85167fb66..f4df039fb 100644 --- a/vectordb_bench/frontend/components/run_test/dbConfigSetting.py +++ b/vectordb_bench/frontend/components/run_test/dbConfigSetting.py @@ -41,15 +41,22 @@ def dbConfigSettingItem(st, activeDb: DB): dbConfig = {} idx = 0 + ui_hidden = set(getattr(dbConfigClass, "ui_hidden_configs", lambda: [])()) + # db config (unique) for key, property in property_items: + if key in ui_hidden: + continue if key not in dbConfigClass.common_short_configs() and key not in dbConfigClass.common_long_configs(): column = columns[idx % DB_CONFIG_SETTING_COLUMNS] idx += 1 + default_value = property.get("default", "") + if default_value is None: + default_value = "" input_value = column.text_input( key, key=f"{activeDb.name}-{key}", - value=property.get("default", ""), + value=default_value, type="password" if inputIsPassword(key) else "default", placeholder="optional" if key not in required_fields else None, ) diff --git a/vectordb_bench/frontend/config/dbCaseConfigs.py b/vectordb_bench/frontend/config/dbCaseConfigs.py index 9900bb53e..c35eb824c 100644 --- a/vectordb_bench/frontend/config/dbCaseConfigs.py +++ b/vectordb_bench/frontend/config/dbCaseConfigs.py @@ -2972,32 +2972,85 @@ class CaseConfigInput(BaseModel): isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.HNSW.value, ) +_LANCEDB_IVF_INDEXES = { + IndexType.IVFFlat.value, + IndexType.IVFPQ.value, + IndexType.IVF_SQ.value, + IndexType.IVF_RQ.value, + IndexType.IVF_HNSW_SQ.value, + IndexType.IVF_HNSW_PQ.value, + IndexType.HNSW.value, # backward compat alias of IVF_HNSW_SQ +} +_LANCEDB_PQ_INDEXES = { + IndexType.IVFPQ.value, + IndexType.IVF_HNSW_PQ.value, +} +_LANCEDB_IVF_TRAINING_INDEXES = { + IndexType.IVFFlat.value, + IndexType.IVFPQ.value, + IndexType.IVF_SQ.value, + IndexType.IVF_RQ.value, +} +_LANCEDB_HNSW_INDEXES = { + IndexType.IVF_HNSW_SQ.value, + IndexType.IVF_HNSW_PQ.value, + IndexType.HNSW.value, +} + CaseConfigParamInput_IndexType_LanceDB = CaseConfigInput( label=CaseConfigParamType.IndexType, - inputHelp="AUTOINDEX = IVFPQ with default parameters", + inputHelp="AUTOINDEX = IVFPQ with default parameters; IVF_FLAT/SQ/RQ/PQ and IVF_HNSW_SQ/PQ for Lance vector indexes. A scalar B-tree is a separate option and can be combined with any of these", inputType=InputType.Option, inputConfig={ "options": [ IndexType.NONE.value, IndexType.AUTOINDEX.value, + IndexType.IVFFlat.value, IndexType.IVFPQ.value, - IndexType.HNSW.value, + IndexType.IVF_SQ.value, + IndexType.IVF_RQ.value, + IndexType.IVF_HNSW_SQ.value, + IndexType.IVF_HNSW_PQ.value, + ], + }, +) + +CaseConfigParamInput_ScalarIndex_LanceDB = CaseConfigInput( + label=CaseConfigParamType.scalar_index, + displayLabel="Scalar Index", + inputHelp="Optional B-tree built in addition to the vector index. Accelerates id range filters and label equality filters", + inputType=InputType.Option, + inputConfig={ + "options": [ + IndexType.NONE.value, + IndexType.BTREE.value, ], }, ) +CaseConfigParamInput_ScalarColumn_LanceDB = CaseConfigInput( + label=CaseConfigParamType.scalar_column, + displayLabel="Scalar Index Column", + inputHelp="Column for the scalar B-tree. Label-filter cases predicate on label; range filters predicate on id", + inputType=InputType.Option, + inputConfig={ + "options": ["id", "label"], + }, + isDisplayed=lambda config: config.get(CaseConfigParamType.scalar_index, IndexType.NONE.value) + == IndexType.BTREE.value, +) + CaseConfigParamInput_num_partitions_LanceDB = CaseConfigInput( label=CaseConfigParamType.num_partitions, displayLabel="Number of Partitions", - inputHelp="Number of partitions (clusters) for IVF_PQ. Default (when 0): sqrt(num_rows)", + inputHelp="Number of partitions (clusters) for IVF indexes. Default (when 0): sqrt(num_rows)", inputType=InputType.Number, inputConfig={ "min": 0, "max": 10000, "value": 0, }, - isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.IVFPQ.value - or config.get(CaseConfigParamType.IndexType, None) == IndexType.HNSW.value, + isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) in _LANCEDB_IVF_INDEXES, ) CaseConfigParamInput_num_sub_vectors_LanceDB = CaseConfigInput( @@ -3010,8 +3063,7 @@ class CaseConfigInput(BaseModel): "max": 1000, "value": 0, }, - isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.IVFPQ.value - or config.get(CaseConfigParamType.IndexType, None) == IndexType.HNSW.value, + isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) in _LANCEDB_PQ_INDEXES, ) CaseConfigParamInput_num_bits_LanceDB = CaseConfigInput( @@ -3022,8 +3074,18 @@ class CaseConfigInput(BaseModel): inputConfig={ "options": [4, 8], }, - isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.IVFPQ.value - or config.get(CaseConfigParamType.IndexType, None) == IndexType.HNSW.value, + isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.IVFPQ.value, +) + +CaseConfigParamInput_num_bits_LanceDB_RQ = CaseConfigInput( + label=CaseConfigParamType.nbits, + displayLabel="Number of Bits", + inputHelp="RabitQ bits per dimension. Lance default: 1", + inputType=InputType.Option, + inputConfig={ + "options": [1, 2, 4, 8], + }, + isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.IVF_RQ.value, ) CaseConfigParamInput_sample_rate_LanceDB = CaseConfigInput( @@ -3036,8 +3098,7 @@ class CaseConfigInput(BaseModel): "max": 1024, "value": 256, }, - isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.IVFPQ.value - or config.get(CaseConfigParamType.IndexType, None) == IndexType.HNSW.value, + isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) in _LANCEDB_IVF_TRAINING_INDEXES, ) CaseConfigParamInput_max_iterations_LanceDB = CaseConfigInput( @@ -3050,8 +3111,7 @@ class CaseConfigInput(BaseModel): "max": 200, "value": 50, }, - isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.IVFPQ.value - or config.get(CaseConfigParamType.IndexType, None) == IndexType.HNSW.value, + isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) in _LANCEDB_IVF_TRAINING_INDEXES, ) CaseConfigParamInput_m_LanceDB = CaseConfigInput( @@ -3064,7 +3124,7 @@ class CaseConfigInput(BaseModel): "max": 1000, "value": 0, }, - isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.HNSW.value, + isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) in _LANCEDB_HNSW_INDEXES, ) CaseConfigParamInput_ef_construction_LanceDB = CaseConfigInput( @@ -3077,7 +3137,7 @@ class CaseConfigInput(BaseModel): "max": 1000, "value": 0, }, - isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) == IndexType.HNSW.value, + isDisplayed=lambda config: config.get(CaseConfigParamType.IndexType, None) in _LANCEDB_HNSW_INDEXES, ) CaseConfigParamInput_IndexType_Lindorm = CaseConfigInput( @@ -3270,9 +3330,12 @@ class FilterType(Enum): LanceDBLoadConfig = [ CaseConfigParamInput_IndexType_LanceDB, + CaseConfigParamInput_ScalarIndex_LanceDB, + CaseConfigParamInput_ScalarColumn_LanceDB, CaseConfigParamInput_num_partitions_LanceDB, CaseConfigParamInput_num_sub_vectors_LanceDB, CaseConfigParamInput_num_bits_LanceDB, + CaseConfigParamInput_num_bits_LanceDB_RQ, CaseConfigParamInput_sample_rate_LanceDB, CaseConfigParamInput_max_iterations_LanceDB, CaseConfigParamInput_m_LanceDB, diff --git a/vectordb_bench/frontend/config/styles.py b/vectordb_bench/frontend/config/styles.py index eb9186cfc..0a86cd0a4 100644 --- a/vectordb_bench/frontend/config/styles.py +++ b/vectordb_bench/frontend/config/styles.py @@ -79,6 +79,20 @@ def getPatternShape(i): DB.Adbpg: "data:image/png;base64,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", } +# Reuse existing icons for DBs that don't have dedicated assets yet. +DB_TO_ICON.update( + { + DB.TencentElasticsearch: DB_TO_ICON[DB.AliyunElasticsearch], + DB.AliSQL: DB_TO_ICON[DB.MariaDB], + DB.VectorChord: DB_TO_ICON[DB.PgVector], + DB.Pinot: DB_TO_ICON[DB.AlloyDB], + DB.SeekDB: DB_TO_ICON[DB.QdrantCloud], + DB.VolcMySQL: DB_TO_ICON[DB.MemoryDB], + # doris.apache.org logo.svg often fails to load in the browser + DB.Doris: DB_TO_ICON[DB.Clickhouse], + } +) + # RedisCloud color: #0D6EFD # Chroma color: #FFC107 COLOR_MAP = { diff --git a/vectordb_bench/models.py b/vectordb_bench/models.py index f303341b7..89aa0ff18 100644 --- a/vectordb_bench/models.py +++ b/vectordb_bench/models.py @@ -212,6 +212,10 @@ class CaseConfigParamType(Enum): max_scan_points = "max_scan_points" auto_reduction = "auto_reduction" + # LanceDB scalar index, independent of the vector index selection. + scalar_index = "scalar_index" + scalar_column = "scalar_column" + class CustomizedCase(BaseModel): pass