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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ duckdb = ["duckdb"]
pgvector = ["pgvector", "psycopg[binary]"]
pinecone = ["pinecone"]
weaviate = ["weaviate-client"]
milvus = ["pymilvus"]
milvus = ["pymilvus", "milvus-lite ; platform_system != 'Windows'"]
redis = ["redis", "numpy"]
elasticsearch = ["elasticsearch"]
mongodb = ["pymongo"]
Expand Down
60 changes: 60 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# vd test suite

The headline file is `test_core.py` — the **facade contract suite**. Every test
there takes the `client` fixture, which is parametrized over every backend, so a
single test asserts the same behaviour across all of them. That is how `vd`
proves one uniform interface over many vector databases.

## Backends the suite sweeps over

`conftest.py` defines two groups:

- **Embedded backends** — `memory`, `chroma`, `faiss`, `duckdb`, `lancedb`,
`qdrant`, `sqlite_vec`, `milvus`. No server needed; each test gets a fresh
client. `sqlite_vec` is skipped on a Python whose `sqlite3` lacks
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
**skipped**, so the suite stays green in a plain CI environment.

## 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

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

pytest tests/

# Tear down and wipe volumes when done.
docker compose -f tests/docker-compose.yml down -v
```

Run one backend at a time with `-k`:

```bash
pytest tests/test_core.py -k pgvector
```

## Connection settings

The server backends use these defaults; override with environment variables:

| Backend | Env var | Default |
|-----------------|-----------------------|------------------------------------------------------|
| `pgvector` | `VD_PGVECTOR_DSN` | `postgresql://vd:vd@localhost:5432/vd` |
| `redis` | `VD_REDIS_HOST` / `VD_REDIS_PORT` | `localhost` / `6379` |
| `elasticsearch` | `VD_ELASTICSEARCH_URL`| `http://localhost:9200` |
| `weaviate` | `VD_WEAVIATE_HOST` | `localhost` |
| `mongodb` | `VD_MONGODB_URI` | `mongodb://localhost:27018/?directConnection=true` |

`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.

`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).
180 changes: 168 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
"""
Pytest fixtures shared across the vd test suite.

The headline fixture is :func:`client` — parametrized over every backend that
can actually run in a plain CI environment (no servers, no cloud accounts).
A test that takes ``client`` runs once per backend, which is how the test
suite proves the facade contract holds uniformly.
The headline fixture is :func:`client` — parametrized over every backend vd
can reach in the current environment. A test that takes ``client`` runs once
per backend, which is how the suite proves the facade contract holds uniformly.

Two kinds of backend run:

- **Embedded backends** (``memory``, ``chroma``, ``faiss``, ``duckdb``,
``lancedb``, ``qdrant``, ``sqlite_vec``, ``milvus``) need no server. Each
test gets a fresh client. ``sqlite_vec`` is skipped on a Python whose
``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
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``).
"""

import hashlib
import importlib.util
import os
import socket
import sqlite3

import pytest

Expand All @@ -16,10 +35,135 @@
#: Embedding dimension used by the test embedder.
EMBED_DIM = 16

#: Backends exercised end-to-end here: embedded / pip-only, no server needed.
#: (sqlite_vec is intentionally excluded — some Python builds ship a sqlite3
#: with extension loading disabled, which the backend needs.)
TESTABLE_BACKENDS = ["memory", "chroma", "faiss", "duckdb", "lancedb", "qdrant"]
#: Backends that need no server. Each test gets a fresh client.
EMBEDDED_BACKENDS = [
"memory",
"chroma",
"faiss",
"duckdb",
"lancedb",
"qdrant",
"sqlite_vec",
"milvus", # verified against the embedded Milvus Lite engine
]

#: Server backends — each needs a container (``tests/docker-compose.yml``).
#: ``probe`` is TCP-probed; the backend is skipped when the port is closed.
#: ``connect_kwargs`` builds the :func:`vd.connect` arguments (env-overridable).
SERVER_BACKENDS = {
"pgvector": {
"probe": ("localhost", 5432),
"connect_kwargs": lambda: {
"dsn": os.environ.get(
"VD_PGVECTOR_DSN", "postgresql://vd:vd@localhost:5432/vd"
)
},
},
"redis": {
"probe": ("localhost", 6379),
"connect_kwargs": lambda: {
"host": os.environ.get("VD_REDIS_HOST", "localhost"),
"port": int(os.environ.get("VD_REDIS_PORT", "6379")),
},
},
"elasticsearch": {
"probe": ("localhost", 9200),
"connect_kwargs": lambda: {
"url": os.environ.get("VD_ELASTICSEARCH_URL", "http://localhost:9200")
},
},
"weaviate": {
"probe": ("localhost", 8080),
"connect_kwargs": lambda: {
"host": os.environ.get("VD_WEAVIATE_HOST", "localhost")
},
},
"mongodb": {
# Host port 27018 — see tests/docker-compose.yml (avoids colliding
# with a developer's native mongod on the default 27017).
"probe": ("localhost", 27018),
"connect_kwargs": lambda: {
"uri": os.environ.get(
"VD_MONGODB_URI", "mongodb://localhost:27018/?directConnection=true"
)
},
},
}

#: Every backend the parametrized ``client`` fixture sweeps over.
ALL_BACKENDS = EMBEDDED_BACKENDS + list(SERVER_BACKENDS)


# --------------------------------------------------------------------------- #
# Availability probes
# --------------------------------------------------------------------------- #


def _tcp_open(host: str, port: int, timeout: float = 0.5) -> bool:
"""Return ``True`` if a TCP connection to ``host:port`` succeeds."""
try:
with socket.create_connection((host, port), timeout=timeout):
return True
except OSError:
return False


def _sqlite_ext_supported() -> bool:
"""Return ``True`` if this Python's sqlite3 supports loadable extensions."""
try:
conn = sqlite3.connect(":memory:")
ok = hasattr(conn, "enable_load_extension")
conn.close()
return ok
except Exception:
return False


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 in SERVER_BACKENDS:
host, port = SERVER_BACKENDS[name]["probe"]
if 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 _connect_kwargs(name: str) -> dict:
"""Return the :func:`vd.connect` kwargs for backend ``name``."""
if name in SERVER_BACKENDS:
return SERVER_BACKENDS[name]["connect_kwargs"]()
return {}


def _drop_all_collections(client) -> None:
"""
Delete every collection on ``client`` — best-effort.

Server backends keep state across runs; embedded backends are fresh each
time. Dropping everything before and after each test makes a run against a
live server idempotent (a re-run does not trip "already exists").
"""
try:
names = list(client.list_collections())
except Exception:
return
for name in names:
try:
client.delete_collection(name)
except Exception:
pass


# --------------------------------------------------------------------------- #
# Test embedder
# --------------------------------------------------------------------------- #


def make_embedder():
Expand All @@ -41,20 +185,32 @@ def embedder():
return make_embedder()


@pytest.fixture(params=TESTABLE_BACKENDS)
# --------------------------------------------------------------------------- #
# Backend fixtures
# --------------------------------------------------------------------------- #


@pytest.fixture(params=ALL_BACKENDS)
def backend_name(request):
"""Each installed testable backend, one at a time."""
"""Each reachable backend, one at a time; unreachable ones are skipped."""
name = request.param
if name not in vd.list_backends():
pytest.skip(f"backend {name!r} is not installed")
reason = _unavailable_reason(name)
if reason:
pytest.skip(reason)
return name


@pytest.fixture
def client(backend_name, embedder):
"""A fresh, connected client for each testable backend (with an embedder)."""
connection = vd.connect(backend_name, embedder=embedder)
"""A fresh, connected client for each backend (with an embedder)."""
connection = vd.connect(
backend_name, embedder=embedder, **_connect_kwargs(backend_name)
)
_drop_all_collections(connection)
yield connection
_drop_all_collections(connection)
if hasattr(connection, "close"):
try:
connection.close()
Expand Down
82 changes: 82 additions & 0 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Server backends for vd's live-verification test suite.
#
# Brings up one container per server-backed vd backend. The pytest suite
# (see conftest.py) TCP-probes each port and runs the contract tests against
# whatever is reachable; backends whose container is down are skipped, so the
# suite stays green in a plain CI environment.
#
# docker compose -f tests/docker-compose.yml up -d # start
# pytest tests/ # run the suite
# docker compose -f tests/docker-compose.yml down -v # stop + wipe
#
# milvus is intentionally absent: the milvus adapter is verified against the
# embedded Milvus Lite engine (no server), which exercises the same code path.

services:
pgvector:
image: pgvector/pgvector:pg17
environment:
POSTGRES_USER: vd
POSTGRES_PASSWORD: vd
POSTGRES_DB: vd
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U vd"]
interval: 3s
timeout: 5s
retries: 20

redis:
image: redis/redis-stack-server:latest
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 5s
retries: 20

elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:9.0.3
environment:
discovery.type: single-node
xpack.security.enabled: "false"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
ports:
- "9200:9200"
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
interval: 5s
timeout: 5s
retries: 30

weaviate:
image: cr.weaviate.io/semitechnologies/weaviate:1.28.4
command: ["--host", "0.0.0.0", "--port", "8080", "--scheme", "http"]
environment:
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true"
PERSISTENCE_DATA_PATH: "/var/lib/weaviate"
DEFAULT_VECTORIZER_MODULE: "none"
ENABLE_MODULES: ""
QUERY_DEFAULTS_LIMIT: "25"
ports:
- "8080:8080"
- "50051:50051"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/v1/.well-known/ready || exit 1"]
interval: 5s
timeout: 5s
retries: 30

mongodb:
image: 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:
- "27018:27017"
healthcheck:
test: ["CMD-SHELL", "mongosh --quiet --eval 'db.runCommand({ ping: 1 })' || exit 1"]
interval: 5s
timeout: 5s
retries: 30
13 changes: 9 additions & 4 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ def test_search_with_comparison_filter(client):
col = client.create_collection("search_cmp")
for i in range(6):
v = [0.0] * 16
v[0] = i / 6
# (i + 1), not i — a zero-magnitude vector has undefined cosine
# similarity and some backends (Elasticsearch) reject it outright.
v[0] = (i + 1) / 6
col[str(i)] = _doc(str(i), v, n=i)
hits = list(col.search([0.5] * 16, limit=10, filter={"n": {"$gte": 3}}))
assert sorted(int(h["id"]) for h in hits) == [3, 4, 5]
Expand All @@ -176,7 +178,8 @@ def test_search_with_in_filter(client):
col = client.create_collection("search_in")
for i in range(5):
v = [0.0] * 16
v[0] = i / 5
# (i + 1), not i — avoid a zero-magnitude vector (see comment above).
v[0] = (i + 1) / 5
col[str(i)] = _doc(str(i), v, n=i)
hits = list(col.search([0.5] * 16, limit=10, filter={"n": {"$in": [1, 3]}}))
assert sorted(int(h["id"]) for h in hits) == [1, 3]
Expand Down Expand Up @@ -229,6 +232,8 @@ def test_upsert_is_idempotent(client):

def test_dimension_mismatch_raises(client):
col = client.create_collection("dimcheck")
col["ok"] = Document(id="ok", text="t", vector=[0.0] * 16)
# A non-zero vector — a zero-magnitude one has undefined cosine similarity
# and some backends (Elasticsearch) reject it outright.
col["ok"] = Document(id="ok", text="t", vector=[1.0] * 16)
with pytest.raises(ValueError):
col["bad"] = Document(id="bad", text="t", vector=[0.0] * 8)
col["bad"] = Document(id="bad", text="t", vector=[1.0] * 8)
Loading
Loading