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
46 changes: 40 additions & 6 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,24 @@ backends.
- `providers.py` + `data/providers.yaml` — the ~21-provider registry +
`recommend_backend` decision framework. `requirements.py` —
`check_requirements` / `setup_guide` / `install_backend`.
- Modules: `analytics`, `config`, `health`, `io`, `migration`, `search`,
`text`, `time_indexed`, `util`, `cli`. (`compare.py` was folded into
`providers.py`.)
- 5 user-facing skills in `vd/data/skills/`; `vd-add-backend` dev skill in
`.claude/skills/`.
- `asynchronous.py` — async surface: `connect_async`, the universal
`asyncio.to_thread` wrappers, and **native** async bases
(`AsyncAbstractClient` / `AsyncAbstractCollection`) plus a registry
(`register_async_backend`, `list_async_backends`). Native today: `qdrant`
with `url=` (embedded Qdrant's async client blocks the loop, so its factory
returns the wrapper there).
The I/O-free embed / dimension / query policy is shared with the sync base
through `base._CollectionPolicy`.
- Hybrid: `vd.hybrid_search` works everywhere (client-side BM25 + RRF
fallback, `search.BM25Index`); `weaviate`, `elasticsearch`, `redis` and
`lancedb` satisfy `SupportsHybrid` with a native lexical side.
- Modules: `analytics`, `config`, `filters`, `health`, `io`, `migration`,
`search`, `text`, `time_indexed`, `util`, `cli`. (`compare.py` was folded
into `providers.py`.) `providers.install_command` returns
`pip install "vd[<backend>]"` — the `pyproject.toml` extras are the SSOT
for what each adapter needs.
- 7 skills in `vd/data/skills/` (6 user-facing incl. `vd-setup-backend`, plus
the `vd-add-backend` dev skill), each symlinked from `.claude/skills/`.

**Embedding is external.** The core operates on vectors; an `embedder` passed
to `connect` is an optional convenience. `vd` has **no `dol`/`imbed`
Expand All @@ -57,6 +70,24 @@ dependency** — the core is stdlib + `pyyaml` only.
The first consumer, `ef`, was adapted on its `adapt-to-vd-0.2` branch (it
dropped its dummy-embedder workaround); merge that only after vd 0.2 publishes.

### Running the tests

```bash
uv venv .venv && . .venv/bin/activate
uv pip install -e ".[test,dev]" # dev = the embedded backends the suite sweeps
python -m pytest --doctest-modules -o doctest_optionflags='ELLIPSIS IGNORE_EXCEPTION_DETAIL'
```

This is CI's command. `testpaths = ["tests", "vd"]`, so package doctests run
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.

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

### 3.1 The hierarchy: `Client` → `Collection`
Expand Down Expand Up @@ -133,7 +164,10 @@ fallback engine (`reciprocal_rank_fusion` already exists in `search.py`).

## 4. Refactor priorities (gaps to fill)

In rough order:
In rough order. Status as of 2026-09: done — 2, 3, 4, 7; partly done — 1
(the `F(...)` builder is missing), 6 (native async: `qdrant` only; issue #20
tracks the rest), 9 (dimension checks are loud and early; `model_id` is not
stored); open — 5, 8. Native hybrid for the remaining backends is issue #17.

1. **`UnsupportedFilterError`** + per-adapter documented filter subset + a
`_compile_filter` translator per adapter (memory evaluates in Python;
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/vd-setup-backend
277 changes: 160 additions & 117 deletions README.md

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Root pytest configuration: decide which package modules doctest collection skips.

``pytest --doctest-modules`` imports every module under ``vd/``. Each backend
adapter raises :class:`ImportError` at import time when its optional client
SDK is absent (``pip install vd[<backend>]`` installs it), which would abort
collection. Those modules are skipped here, so the rest of the package's
doctests always run and a backend's doctests run whenever its SDK is present.

``misc/`` holds demo scripts and design notes, not tests.
"""

import importlib
import pathlib

_HERE = pathlib.Path(__file__).parent


def _unimportable_backend_modules() -> list[str]:
"""Return the paths of backend modules whose optional SDK is not installed."""
skipped = []
for path in sorted((_HERE / "vd" / "backends").glob("*.py")):
if path.name.startswith("_"):
continue
try:
importlib.import_module(f"vd.backends.{path.stem}")
except ImportError:
skipped.append(str(path.relative_to(_HERE)))
return skipped


collect_ignore = ["misc", *_unimportable_backend_modules()]
24 changes: 21 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ keywords = [
"similarity search",
"RAG",
]
authors = []
authors = [{ name = "Thor Whalen" }]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
Expand Down Expand Up @@ -84,12 +84,28 @@ dev = [
# Lightweight test-runner deps for CI (no backend extras — those live in
# `dev`). Installed in CI via [tool.wads.ci.install].extras so the async
# suite (tests/test_async.py) has pytest-asyncio.
test = ["pytest>=7.0", "pytest-cov>=4.0", "pytest-asyncio>=0.23"]
# hatchling: tests/test_package.py checks the sdist file list (#28).
test = ["pytest>=7.0", "pytest-cov>=4.0", "pytest-asyncio>=0.23", "hatchling"]
docs = ["sphinx>=6.0", "sphinx-rtd-theme>=1.0"]

[tool.hatch.build.targets.wheel]
packages = ["vd"]

[tool.hatch.build.targets.sdist]
# Walk only these roots. `.claude/skills/*` are symlinks into `vd/data/skills/`
# and hatch skips files whose real path it has already seen, so walking
# `.claude` first (even with an `exclude`) dropped `vd/data/skills/` from the
# sdist, and the wheel built from it shipped no skills.
only-include = [
"vd",
"tests",
"conftest.py",
"misc",
"README.md",
"CHANGELOG.md",
"LICENSE",
]

[tool.ruff]
line-length = 88
target-version = "py310"
Expand Down Expand Up @@ -118,7 +134,9 @@ convention = "google"

[tool.pytest.ini_options]
minversion = "6.0"
testpaths = ["tests"]
# `vd` is listed so the package's own doctests run under `--doctest-modules`;
# the root conftest.py skips backend modules whose optional SDK is missing.
testpaths = ["tests", "vd"]
doctest_optionflags = ["NORMALIZE_WHITESPACE", "ELLIPSIS"]
# Make every `async def test_*` run automatically — no per-test decorator
# needed. Used by tests/test_async.py.
Expand Down
3 changes: 1 addition & 2 deletions tests/test_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
- :func:`vd.hybrid_search_async` dispatches correctly through the wrapper;
- the async context-manager protocol works.

Phase 2 follow-ups will add per-backend native async adapters; each backend
will get its own parametrized test entries at that time.
Native async adapters (Phase 2, #20) are tested in ``tests/test_async_native.py``.
"""

import pytest
Expand Down
Loading
Loading