Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ backends.
`UnsupportedCapabilityError`, `EmbeddingRequiredError`,
`BackendNotInstalledError`); capability protocols.
- `backends/` — 15 adapters: `memory`, `chroma`, `faiss`, `sqlite_vec`,
`duckdb`, `lancedb`, `qdrant` (all tested here) and `pgvector`, `pinecone`,
`weaviate`, `milvus`, `redis`, `elasticsearch`, `mongodb`, `turbopuffer`
(correct-by-construction; need a server/account to exercise).
`duckdb`, `lancedb`, `qdrant`, `milvus` (embedded; tested whenever their
client is installed, milvus via milvus-lite) and
`pgvector`, `pinecone`, `weaviate`, `redis`, `elasticsearch`, `mongodb`
(tested against local containers, no accounts) and `turbopuffer`
(correct-by-construction: no local emulator exists).
- `providers.py` + `data/providers.yaml` — the ~21-provider registry +
`recommend_backend` decision framework. `requirements.py` —
`check_requirements` / `setup_guide` / `install_backend`.
Expand Down Expand Up @@ -83,10 +85,16 @@ too; the root `conftest.py` skips backend modules whose SDK is missing.
Parametrized suites (`tests/test_core.py`, `tests/test_hybrid.py`) run every
reachable backend via the `client` fixture in `tests/conftest.py`: embedded
backends always, server backends only when their port answers. Bring the
servers up with `docker compose -f tests/docker-compose.yml up -d` and install
their clients (`uv pip install -e ".[pgvector,redis,elasticsearch,weaviate,mongodb,milvus]"`).
CI installs only the `test` extra, so backend-specific tests skip there —
run them locally before changing an adapter.
servers up with `DOCKERHUB_MIRROR=mirror.gcr.io docker compose -f tests/docker-compose.yml up -d`
(the mirror avoids Docker Hub's anonymous pull limit) and install their clients
(`uv pip install -e ".[pgvector,redis,elasticsearch,weaviate,mongodb,milvus,pinecone]" "pinecone<10"`).
No server backend needs an account: pinecone runs on Pinecone Local (which
needs the pinecone SDK < 10) and mongodb on MongoDB Atlas Local. Only
turbopuffer has no emulator. See `tests/README.md`.
CI installs the `test` and `ci-backends` extras (qdrant-client, lancedb,
pgvector's client), so the embedded qdrant and LanceDB suites run there;
server backends still skip in CI — run them locally before changing an
adapter.

## 3. Core contracts (the design the refactor should converge on)

Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,15 @@ uv pip install -e ".[test,dev]"
python -m pytest --doctest-modules -o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL'
```

That is exactly what CI runs; package doctests are included. The backend-parametrized suites skip server backends that aren't running. To exercise them, start the containers with `docker compose -f tests/docker-compose.yml up -d` and install their clients with `uv pip install -e ".[pgvector,redis,elasticsearch,weaviate,mongodb,milvus]"`.
That is what CI runs (CI also installs the `ci-backends` extra); package doctests are included. The backend-parametrized suites skip server backends that aren't running. Every server backend except turbopuffer runs locally without an account, Pinecone and MongoDB Atlas included, through their official emulators:

```bash
DOCKERHUB_MIRROR=mirror.gcr.io docker compose -f tests/docker-compose.yml up -d
uv pip install -e ".[test,dev,pgvector,redis,elasticsearch,weaviate,mongodb,milvus,pinecone]" "pinecone<10"
python -m pytest
```

See [`tests/README.md`](tests/README.md) for connection settings and caveats.

**Design rationale.**

Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ dev = [
# suite (tests/test_async.py) has pytest-asyncio.
# hatchling: tests/test_package.py checks the sdist file list (#28).
test = ["pytest>=7.0", "pytest-cov>=4.0", "pytest-asyncio>=0.23", "hatchling"]
# Server-free backend clients CI installs so their tests run instead of
# skipping: qdrant (embedded :memory: + native async), lancedb (embedded, FTS
# hybrid), and pgvector's client for its server-free unit test.
ci-backends = ["qdrant-client", "lancedb", "pgvector", "psycopg[binary]"]
docs = ["sphinx>=6.0", "sphinx-rtd-theme>=1.0"]

[tool.hatch.build.targets.wheel]
Expand Down Expand Up @@ -149,7 +153,7 @@ installer = "uv"
# Install the `test` extra so CI has the async test runner (pytest-asyncio);
# without it tests/test_async.py errors with "async def functions are not
# natively supported" and every async test fails.
extras = "test"
extras = "test,ci-backends"

[tool.wads.ci.testing]
python_versions = ["3.10", "3.12"]
Expand Down
34 changes: 31 additions & 3 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,27 @@ proves one uniform interface over many vector databases.
loadable-extension support; `milvus` runs against the embedded **Milvus Lite**
engine and is skipped if `milvus-lite` is not installed.
- **Server backends** — `pgvector`, `redis`, `elasticsearch`, `weaviate`,
`mongodb`. Each needs a running container. A backend whose port is not open is
`mongodb`, `pinecone`, and `qdrant_server` (the qdrant adapter with `url=`).
Each needs a running container. A backend whose port is not open is
**skipped**, so the suite stays green in a plain CI environment.

None of them needs an account. `pinecone` runs against **Pinecone Local**, the
official in-memory emulator, and `mongodb` against **MongoDB Atlas Local**.
Only `turbopuffer` has no local emulator; its maintainers recommend testing
against production with a throwaway namespace, so it is not in the suite.

## Running against the server backends

```bash
# Start every server backend (first run pulls images — a few minutes).
docker compose -f tests/docker-compose.yml up -d
# DOCKERHUB_MIRROR=mirror.gcr.io avoids Docker Hub's anonymous pull limit.
DOCKERHUB_MIRROR=mirror.gcr.io docker compose -f tests/docker-compose.yml up -d

# Install every server backend's client. Pinecone Local speaks the pre-2026-07
# API, so pin the pinecone SDK below 10 for these tests.
uv pip install -e ".[test,dev,pgvector,redis,elasticsearch,weaviate,mongodb,milvus,pinecone]" "pinecone<10"

# Wait until all five report (healthy), then run the suite.
# Wait until the services are up, then run the suite.
docker compose -f tests/docker-compose.yml ps

pytest tests/
Expand All @@ -50,11 +61,28 @@ The server backends use these defaults; override with environment variables:
| `elasticsearch` | `VD_ELASTICSEARCH_URL`| `http://localhost:9200` |
| `weaviate` | `VD_WEAVIATE_HOST` | `localhost` |
| `mongodb` | `VD_MONGODB_URI` | `mongodb://localhost:27018/?directConnection=true` |
| `pinecone` | `VD_PINECONE_HOST` / `VD_PINECONE_API_KEY` | `http://localhost:5080` / `pclocal` |
| `qdrant_server` | `VD_QDRANT_URL` | `http://localhost:6333` |

Only local servers are used: an override pointing elsewhere is skipped, because
the suite deletes every collection it can see. Set `VD_ALLOW_REMOTE_TESTS=1` to
run against a remote server you are happy to wipe.

`mongodb` maps to host port **27018** (not 27017) to avoid colliding with a
developer's native `mongod`. It uses the `mongodb-atlas-local` image because
`$vectorSearch` requires an Atlas-capable deployment.

If Elasticsearch times out creating indexes, the host disk is probably above
its 90 % high watermark; on a throwaway test node, disable the check:

```bash
curl -X PUT localhost:9200/_cluster/settings -H 'Content-Type: application/json' \
-d '{"persistent":{"cluster.routing.allocation.disk.threshold_enabled":false}}'
```

`tests/test_live_server_regressions.py` holds server-only regression tests
(for example, a MongoDB collection recreated under a dropped name).

`milvus` needs no container — it is verified against the embedded Milvus Lite
engine, which exercises the same adapter code path as a Milvus server (only the
client constructor differs).
106 changes: 98 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
``sqlite3`` lacks loadable-extension support; ``milvus`` runs against the
embedded Milvus Lite engine and is skipped if ``milvus-lite`` is absent.
- **Server backends** (``pgvector``, ``redis``, ``elasticsearch``,
``weaviate``, ``mongodb``) need a running container — see
``tests/docker-compose.yml``. Each is TCP-probed and **skipped** when its
``weaviate``, ``mongodb``, ``pinecone`` via Pinecone Local, and
``qdrant_server``: the qdrant adapter with ``url=``) need a running
container — see ``tests/docker-compose.yml``; none needs an account. Each is TCP-probed and **skipped** when its
container is down, so the suite stays green in a plain CI environment.

Connection settings for the server backends are environment-overridable
(``VD_PGVECTOR_DSN``, ``VD_REDIS_HOST``/``VD_REDIS_PORT``,
``VD_ELASTICSEARCH_URL``, ``VD_WEAVIATE_HOST``, ``VD_MONGODB_URI``).
``VD_ELASTICSEARCH_URL``, ``VD_WEAVIATE_HOST``, ``VD_MONGODB_URI``,
``VD_PINECONE_HOST``/``VD_PINECONE_API_KEY``, ``VD_QDRANT_URL``). An entry may
set ``"backend"`` to test another entry's adapter under a different setup.
"""

import hashlib
Expand Down Expand Up @@ -48,7 +51,10 @@
]

#: Server backends — each needs a container (``tests/docker-compose.yml``).
#: ``probe`` is TCP-probed; the backend is skipped when the port is closed.
#: The server address (from ``connect_kwargs``, falling back to ``probe``) is
#: TCP-probed; the backend is skipped when the port is closed. Only local
#: servers are used unless ``VD_ALLOW_REMOTE_TESTS=1``: the ``client`` fixture
#: deletes every collection it can see, which would wipe a real account.
#: ``connect_kwargs`` builds the :func:`vd.connect` arguments (env-overridable).
SERVER_BACKENDS = {
"pgvector": {
Expand Down Expand Up @@ -78,6 +84,26 @@
"host": os.environ.get("VD_WEAVIATE_HOST", "localhost")
},
},
"pinecone": {
# Pinecone Local, the official in-memory emulator (no account):
# ghcr.io/pinecone-io/pinecone-local. It speaks the pre-2026-07 API, so
# these tests need the pinecone SDK < 10 until the emulator catches up.
"probe": ("localhost", 5080),
"connect_kwargs": lambda: {
"api_key": os.environ.get("VD_PINECONE_API_KEY", "pclocal"),
"host": os.environ.get("VD_PINECONE_HOST", "http://localhost:5080"),
},
},
"qdrant_server": {
# The qdrant adapter against a real Qdrant server (url=), next to the
# embedded "qdrant" entry above: exercises the network client paths.
"backend": "qdrant",
"probe": ("localhost", 6333),
"connect_kwargs": lambda: {
"url": os.environ.get("VD_QDRANT_URL", "http://localhost:6333"),
"check_compatibility": False,
},
},
"mongodb": {
# Host port 27018 — see tests/docker-compose.yml (avoids colliding
# with a developer's native mongod on the default 27017).
Expand Down Expand Up @@ -119,22 +145,86 @@ def _sqlite_ext_supported() -> bool:
return False


_LOCAL_HOSTS = {"localhost", "127.0.0.1", "::1"}


def server_address(name: str) -> tuple:
"""
The ``(host, port)`` a server entry will connect to.

``host`` is ``None`` when it can't be determined (a keyword-style or
multi-host DSN, ...); callers must then treat the target as remote.
"""
import re
from urllib.parse import parse_qs, urlsplit

entry = SERVER_BACKENDS[name]
default_host, default_port = entry["probe"]
kwargs = entry["connect_kwargs"]()
for key in ("url", "dsn", "uri", "host"):
value = kwargs.get(key)
if not isinstance(value, str):
continue
if "://" in value:
parts = urlsplit(value)
if "," in parts.netloc: # multi-host URI
return None, default_port
query_host = parse_qs(parts.query).get("host")
try:
port = parts.port or default_port
except ValueError:
return None, default_port
if query_host:
return None, port # host given in the query string
return parts.hostname or default_host, port
if key == "dsn": # keyword DSN, e.g. "host=db port=5432 dbname=vd"
found = re.search(r"\bhost(?:addr)?\s*=\s*'?([^\s']+)", value)
port = re.search(r"\bport\s*=\s*'?(\d+)", value)
if not found or "," in found.group(1):
return None, default_port
return found.group(1), int(port.group(1)) if port else default_port
if key == "host":
return value, int(kwargs.get("port", default_port))
return default_host, default_port


def _unavailable_reason(name: str) -> str | None:
"""Return a skip reason for backend ``name``, or ``None`` if it can run."""
if name == "sqlite_vec" and not _sqlite_ext_supported():
return "sqlite3 was built without loadable-extension support"
if name == "milvus" and importlib.util.find_spec("milvus_lite") is None:
return "milvus-lite not installed (embedded Milvus engine unavailable)"
if name == "pinecone":
import pinecone

major = str(getattr(pinecone, "__version__", "0")).split(".")[0]
if major.isdigit() and int(major) >= 10:
return (
"Pinecone Local speaks the pre-2026-07 API; the pinecone SDK "
">= 10 cannot drive it. Install 'pinecone<10' to run these."
)
if name in SERVER_BACKENDS:
host, port = SERVER_BACKENDS[name]["probe"]
if not _tcp_open(host, port):
host, port = server_address(name)
if host not in _LOCAL_HOSTS and os.environ.get("VD_ALLOW_REMOTE_TESTS") != "1":
return (
f"{name!r} points at {host or 'a host that could not be parsed'}, "
f"not a local server; the tests "
f"delete every collection they see. Set VD_ALLOW_REMOTE_TESTS=1 "
f"to allow it."
)
if host is None or not _tcp_open(host, port):
return (
f"{name!r} server unreachable at {host}:{port} "
f"— start it with tests/docker-compose.yml"
)
return None


def backend_of(name: str) -> str:
"""The ``vd.connect`` backend for a fixture entry (entries may alias one)."""
return SERVER_BACKENDS.get(name, {}).get("backend", name)


def _connect_kwargs(name: str) -> dict:
"""Return the :func:`vd.connect` kwargs for backend ``name``."""
if name in SERVER_BACKENDS:
Expand Down Expand Up @@ -194,7 +284,7 @@ def embedder():
def backend_name(request):
"""Each reachable backend, one at a time; unreachable ones are skipped."""
name = request.param
if name not in vd.list_backends():
if backend_of(name) not in vd.list_backends():
pytest.skip(f"backend {name!r} is not installed")
reason = _unavailable_reason(name)
if reason:
Expand All @@ -206,7 +296,7 @@ def backend_name(request):
def client(backend_name, embedder):
"""A fresh, connected client for each backend (with an embedder)."""
connection = vd.connect(
backend_name, embedder=embedder, **_connect_kwargs(backend_name)
backend_of(backend_name), embedder=embedder, **_connect_kwargs(backend_name)
)
_drop_all_collections(connection)
yield connection
Expand Down
33 changes: 29 additions & 4 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@
#
# milvus is intentionally absent: the milvus adapter is verified against the
# embedded Milvus Lite engine (no server), which exercises the same code path.
#
# No accounts needed: pinecone runs as Pinecone Local (the official in-memory
# emulator) and mongodb as MongoDB Atlas Local. turbopuffer has no emulator.
#
# Docker Hub rate-limits anonymous pulls. Set DOCKERHUB_MIRROR=mirror.gcr.io
# (Google's public Docker Hub mirror) to pull the Docker Hub images from there:
# DOCKERHUB_MIRROR=mirror.gcr.io docker compose -f tests/docker-compose.yml up -d

services:
pgvector:
image: pgvector/pgvector:pg17
image: ${DOCKERHUB_MIRROR:-docker.io}/pgvector/pgvector:pg17
environment:
POSTGRES_USER: vd
POSTGRES_PASSWORD: vd
Expand All @@ -28,7 +35,7 @@ services:
retries: 20

redis:
image: redis/redis-stack-server:latest
image: ${DOCKERHUB_MIRROR:-docker.io}/redis/redis-stack-server:latest
ports:
- "6379:6379"
healthcheck:
Expand All @@ -52,7 +59,7 @@ services:
retries: 30

weaviate:
image: cr.weaviate.io/semitechnologies/weaviate:1.28.4
image: ${DOCKERHUB_MIRROR:-docker.io}/semitechnologies/weaviate:1.28.4
command: ["--host", "0.0.0.0", "--port", "8080", "--scheme", "http"]
environment:
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true"
Expand All @@ -70,7 +77,7 @@ services:
retries: 30

mongodb:
image: mongodb/mongodb-atlas-local:latest
image: ${DOCKERHUB_MIRROR:-docker.io}/mongodb/mongodb-atlas-local:latest
# Host port 27018 (not 27017) to avoid colliding with a developer's
# native mongod. The container still listens on 27017 internally.
ports:
Expand All @@ -80,3 +87,21 @@ services:
interval: 5s
timeout: 5s
retries: 30

qdrant:
# The qdrant adapter with url= (the "qdrant_server" test entry). Embedded
# qdrant needs no container and is always tested.
image: ${DOCKERHUB_MIRROR:-docker.io}/qdrant/qdrant:latest
ports:
- "6333:6333"
- "6334:6334"

pinecone:
# Pinecone Local: in-memory, no account (api_key="pclocal"). It speaks the
# pre-2026-07 API, so run the pinecone tests with the pinecone SDK < 10.
image: ghcr.io/pinecone-io/pinecone-local:latest
environment:
PORT: "5080"
PINECONE_HOST: localhost
ports:
- "5080-5090:5080-5090"
Loading
Loading