vd 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vd-0.2.1 → vd-0.2.2}/.gitignore +3 -0
- {vd-0.2.1 → vd-0.2.2}/PKG-INFO +3 -1
- {vd-0.2.1 → vd-0.2.2}/pyproject.toml +2 -2
- vd-0.2.2/tests/README.md +60 -0
- vd-0.2.2/tests/conftest.py +218 -0
- vd-0.2.2/tests/docker-compose.yml +82 -0
- {vd-0.2.1 → vd-0.2.2}/tests/test_core.py +9 -4
- {vd-0.2.1 → vd-0.2.2}/vd/backends/elasticsearch.py +59 -47
- {vd-0.2.1 → vd-0.2.2}/vd/backends/milvus.py +6 -2
- {vd-0.2.1 → vd-0.2.2}/vd/backends/mongodb.py +170 -188
- {vd-0.2.1 → vd-0.2.2}/vd/backends/pgvector.py +20 -12
- {vd-0.2.1 → vd-0.2.2}/vd/backends/sqlite_vec.py +41 -20
- {vd-0.2.1 → vd-0.2.2}/vd/backends/weaviate.py +6 -4
- vd-0.2.1/tests/conftest.py +0 -62
- {vd-0.2.1 → vd-0.2.2}/.claude/CLAUDE.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/.claude/skills/vd-add-backend/SKILL.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/.claude/skills/vd-backend-choose/SKILL.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/.claude/skills/vd-ingest/SKILL.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/.claude/skills/vd-ops/SKILL.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/.claude/skills/vd-quickstart/SKILL.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/.claude/skills/vd-search/SKILL.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/.gitattributes +0 -0
- {vd-0.2.1 → vd-0.2.2}/.github/workflows/ci.yml +0 -0
- {vd-0.2.1 → vd-0.2.2}/LICENSE +0 -0
- {vd-0.2.1 → vd-0.2.2}/README.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/docs/11 -- VectorDB Selection & Setup Guide -- Provider Profiles, Decision Framework, and Installation Playbooks.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/docs/notes_for_possible_improvements.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/docs/vd_design_notes.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/docs/vd_key_takeaways.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/docs/vd_specification.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/docs/vd_use_cases.md +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/examples/demo_backend_discovery.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/examples/demo_comparison.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/examples/demo_config.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/examples/demo_utilities.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/misc/examples/example_usage.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/tests/__init__.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/tests/test_contract.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/tests/test_filters.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/tests/test_providers.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/tests/test_time_indexed.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/__init__.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/analytics.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/__init__.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/_helpers.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/chroma.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/duckdb.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/faiss.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/lancedb.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/memory.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/pinecone.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/qdrant.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/redis.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/backends/turbopuffer.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/base.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/cli.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/config.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/data/providers.yaml +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/filters.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/health.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/io.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/migration.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/providers.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/requirements.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/search.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/text.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/time_indexed.py +0 -0
- {vd-0.2.1 → vd-0.2.2}/vd/util.py +0 -0
{vd-0.2.1 → vd-0.2.2}/.gitignore
RENAMED
{vd-0.2.1 → vd-0.2.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vd
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A facade over vector databases — one interface, ~15 backends
|
|
5
5
|
Project-URL: Homepage, https://github.com/i2mint/vd
|
|
6
6
|
License: mit
|
|
@@ -14,6 +14,7 @@ Requires-Dist: duckdb; extra == 'all-backends'
|
|
|
14
14
|
Requires-Dist: elasticsearch; extra == 'all-backends'
|
|
15
15
|
Requires-Dist: faiss-cpu; extra == 'all-backends'
|
|
16
16
|
Requires-Dist: lancedb; extra == 'all-backends'
|
|
17
|
+
Requires-Dist: milvus-lite; (platform_system != 'Windows') and extra == 'all-backends'
|
|
17
18
|
Requires-Dist: numpy; extra == 'all-backends'
|
|
18
19
|
Requires-Dist: pgvector; extra == 'all-backends'
|
|
19
20
|
Requires-Dist: pinecone; extra == 'all-backends'
|
|
@@ -63,6 +64,7 @@ Requires-Dist: numpy; extra == 'faiss'
|
|
|
63
64
|
Provides-Extra: lancedb
|
|
64
65
|
Requires-Dist: lancedb; extra == 'lancedb'
|
|
65
66
|
Provides-Extra: milvus
|
|
67
|
+
Requires-Dist: milvus-lite; (platform_system != 'Windows') and extra == 'milvus'
|
|
66
68
|
Requires-Dist: pymilvus; extra == 'milvus'
|
|
67
69
|
Provides-Extra: mongodb
|
|
68
70
|
Requires-Dist: pymongo; extra == 'mongodb'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vd"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "A facade over vector databases — one interface, ~15 backends"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -34,7 +34,7 @@ duckdb = ["duckdb"]
|
|
|
34
34
|
pgvector = ["pgvector", "psycopg[binary]"]
|
|
35
35
|
pinecone = ["pinecone"]
|
|
36
36
|
weaviate = ["weaviate-client"]
|
|
37
|
-
milvus = ["pymilvus"]
|
|
37
|
+
milvus = ["pymilvus", "milvus-lite ; platform_system != 'Windows'"]
|
|
38
38
|
redis = ["redis", "numpy"]
|
|
39
39
|
elasticsearch = ["elasticsearch"]
|
|
40
40
|
mongodb = ["pymongo"]
|
vd-0.2.2/tests/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# vd test suite
|
|
2
|
+
|
|
3
|
+
The headline file is `test_core.py` — the **facade contract suite**. Every test
|
|
4
|
+
there takes the `client` fixture, which is parametrized over every backend, so a
|
|
5
|
+
single test asserts the same behaviour across all of them. That is how `vd`
|
|
6
|
+
proves one uniform interface over many vector databases.
|
|
7
|
+
|
|
8
|
+
## Backends the suite sweeps over
|
|
9
|
+
|
|
10
|
+
`conftest.py` defines two groups:
|
|
11
|
+
|
|
12
|
+
- **Embedded backends** — `memory`, `chroma`, `faiss`, `duckdb`, `lancedb`,
|
|
13
|
+
`qdrant`, `sqlite_vec`, `milvus`. No server needed; each test gets a fresh
|
|
14
|
+
client. `sqlite_vec` is skipped on a Python whose `sqlite3` lacks
|
|
15
|
+
loadable-extension support; `milvus` runs against the embedded **Milvus Lite**
|
|
16
|
+
engine and is skipped if `milvus-lite` is not installed.
|
|
17
|
+
- **Server backends** — `pgvector`, `redis`, `elasticsearch`, `weaviate`,
|
|
18
|
+
`mongodb`. Each needs a running container. A backend whose port is not open is
|
|
19
|
+
**skipped**, so the suite stays green in a plain CI environment.
|
|
20
|
+
|
|
21
|
+
## Running against the server backends
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Start every server backend (first run pulls images — a few minutes).
|
|
25
|
+
docker compose -f tests/docker-compose.yml up -d
|
|
26
|
+
|
|
27
|
+
# Wait until all five report (healthy), then run the suite.
|
|
28
|
+
docker compose -f tests/docker-compose.yml ps
|
|
29
|
+
|
|
30
|
+
pytest tests/
|
|
31
|
+
|
|
32
|
+
# Tear down and wipe volumes when done.
|
|
33
|
+
docker compose -f tests/docker-compose.yml down -v
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run one backend at a time with `-k`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pytest tests/test_core.py -k pgvector
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Connection settings
|
|
43
|
+
|
|
44
|
+
The server backends use these defaults; override with environment variables:
|
|
45
|
+
|
|
46
|
+
| Backend | Env var | Default |
|
|
47
|
+
|-----------------|-----------------------|------------------------------------------------------|
|
|
48
|
+
| `pgvector` | `VD_PGVECTOR_DSN` | `postgresql://vd:vd@localhost:5432/vd` |
|
|
49
|
+
| `redis` | `VD_REDIS_HOST` / `VD_REDIS_PORT` | `localhost` / `6379` |
|
|
50
|
+
| `elasticsearch` | `VD_ELASTICSEARCH_URL`| `http://localhost:9200` |
|
|
51
|
+
| `weaviate` | `VD_WEAVIATE_HOST` | `localhost` |
|
|
52
|
+
| `mongodb` | `VD_MONGODB_URI` | `mongodb://localhost:27018/?directConnection=true` |
|
|
53
|
+
|
|
54
|
+
`mongodb` maps to host port **27018** (not 27017) to avoid colliding with a
|
|
55
|
+
developer's native `mongod`. It uses the `mongodb-atlas-local` image because
|
|
56
|
+
`$vectorSearch` requires an Atlas-capable deployment.
|
|
57
|
+
|
|
58
|
+
`milvus` needs no container — it is verified against the embedded Milvus Lite
|
|
59
|
+
engine, which exercises the same adapter code path as a Milvus server (only the
|
|
60
|
+
client constructor differs).
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pytest fixtures shared across the vd test suite.
|
|
3
|
+
|
|
4
|
+
The headline fixture is :func:`client` — parametrized over every backend vd
|
|
5
|
+
can reach in the current environment. A test that takes ``client`` runs once
|
|
6
|
+
per backend, which is how the suite proves the facade contract holds uniformly.
|
|
7
|
+
|
|
8
|
+
Two kinds of backend run:
|
|
9
|
+
|
|
10
|
+
- **Embedded backends** (``memory``, ``chroma``, ``faiss``, ``duckdb``,
|
|
11
|
+
``lancedb``, ``qdrant``, ``sqlite_vec``, ``milvus``) need no server. Each
|
|
12
|
+
test gets a fresh client. ``sqlite_vec`` is skipped on a Python whose
|
|
13
|
+
``sqlite3`` lacks loadable-extension support; ``milvus`` runs against the
|
|
14
|
+
embedded Milvus Lite engine and is skipped if ``milvus-lite`` is absent.
|
|
15
|
+
- **Server backends** (``pgvector``, ``redis``, ``elasticsearch``,
|
|
16
|
+
``weaviate``, ``mongodb``) need a running container — see
|
|
17
|
+
``tests/docker-compose.yml``. Each is TCP-probed and **skipped** when its
|
|
18
|
+
container is down, so the suite stays green in a plain CI environment.
|
|
19
|
+
|
|
20
|
+
Connection settings for the server backends are environment-overridable
|
|
21
|
+
(``VD_PGVECTOR_DSN``, ``VD_REDIS_HOST``/``VD_REDIS_PORT``,
|
|
22
|
+
``VD_ELASTICSEARCH_URL``, ``VD_WEAVIATE_HOST``, ``VD_MONGODB_URI``).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import importlib.util
|
|
27
|
+
import os
|
|
28
|
+
import socket
|
|
29
|
+
import sqlite3
|
|
30
|
+
|
|
31
|
+
import pytest
|
|
32
|
+
|
|
33
|
+
import vd
|
|
34
|
+
|
|
35
|
+
#: Embedding dimension used by the test embedder.
|
|
36
|
+
EMBED_DIM = 16
|
|
37
|
+
|
|
38
|
+
#: Backends that need no server. Each test gets a fresh client.
|
|
39
|
+
EMBEDDED_BACKENDS = [
|
|
40
|
+
"memory",
|
|
41
|
+
"chroma",
|
|
42
|
+
"faiss",
|
|
43
|
+
"duckdb",
|
|
44
|
+
"lancedb",
|
|
45
|
+
"qdrant",
|
|
46
|
+
"sqlite_vec",
|
|
47
|
+
"milvus", # verified against the embedded Milvus Lite engine
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
#: Server backends — each needs a container (``tests/docker-compose.yml``).
|
|
51
|
+
#: ``probe`` is TCP-probed; the backend is skipped when the port is closed.
|
|
52
|
+
#: ``connect_kwargs`` builds the :func:`vd.connect` arguments (env-overridable).
|
|
53
|
+
SERVER_BACKENDS = {
|
|
54
|
+
"pgvector": {
|
|
55
|
+
"probe": ("localhost", 5432),
|
|
56
|
+
"connect_kwargs": lambda: {
|
|
57
|
+
"dsn": os.environ.get(
|
|
58
|
+
"VD_PGVECTOR_DSN", "postgresql://vd:vd@localhost:5432/vd"
|
|
59
|
+
)
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"redis": {
|
|
63
|
+
"probe": ("localhost", 6379),
|
|
64
|
+
"connect_kwargs": lambda: {
|
|
65
|
+
"host": os.environ.get("VD_REDIS_HOST", "localhost"),
|
|
66
|
+
"port": int(os.environ.get("VD_REDIS_PORT", "6379")),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"elasticsearch": {
|
|
70
|
+
"probe": ("localhost", 9200),
|
|
71
|
+
"connect_kwargs": lambda: {
|
|
72
|
+
"url": os.environ.get("VD_ELASTICSEARCH_URL", "http://localhost:9200")
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"weaviate": {
|
|
76
|
+
"probe": ("localhost", 8080),
|
|
77
|
+
"connect_kwargs": lambda: {
|
|
78
|
+
"host": os.environ.get("VD_WEAVIATE_HOST", "localhost")
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"mongodb": {
|
|
82
|
+
# Host port 27018 — see tests/docker-compose.yml (avoids colliding
|
|
83
|
+
# with a developer's native mongod on the default 27017).
|
|
84
|
+
"probe": ("localhost", 27018),
|
|
85
|
+
"connect_kwargs": lambda: {
|
|
86
|
+
"uri": os.environ.get(
|
|
87
|
+
"VD_MONGODB_URI", "mongodb://localhost:27018/?directConnection=true"
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#: Every backend the parametrized ``client`` fixture sweeps over.
|
|
94
|
+
ALL_BACKENDS = EMBEDDED_BACKENDS + list(SERVER_BACKENDS)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --------------------------------------------------------------------------- #
|
|
98
|
+
# Availability probes
|
|
99
|
+
# --------------------------------------------------------------------------- #
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _tcp_open(host: str, port: int, timeout: float = 0.5) -> bool:
|
|
103
|
+
"""Return ``True`` if a TCP connection to ``host:port`` succeeds."""
|
|
104
|
+
try:
|
|
105
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
106
|
+
return True
|
|
107
|
+
except OSError:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _sqlite_ext_supported() -> bool:
|
|
112
|
+
"""Return ``True`` if this Python's sqlite3 supports loadable extensions."""
|
|
113
|
+
try:
|
|
114
|
+
conn = sqlite3.connect(":memory:")
|
|
115
|
+
ok = hasattr(conn, "enable_load_extension")
|
|
116
|
+
conn.close()
|
|
117
|
+
return ok
|
|
118
|
+
except Exception:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _unavailable_reason(name: str) -> str | None:
|
|
123
|
+
"""Return a skip reason for backend ``name``, or ``None`` if it can run."""
|
|
124
|
+
if name == "sqlite_vec" and not _sqlite_ext_supported():
|
|
125
|
+
return "sqlite3 was built without loadable-extension support"
|
|
126
|
+
if name == "milvus" and importlib.util.find_spec("milvus_lite") is None:
|
|
127
|
+
return "milvus-lite not installed (embedded Milvus engine unavailable)"
|
|
128
|
+
if name in SERVER_BACKENDS:
|
|
129
|
+
host, port = SERVER_BACKENDS[name]["probe"]
|
|
130
|
+
if not _tcp_open(host, port):
|
|
131
|
+
return (
|
|
132
|
+
f"{name!r} server unreachable at {host}:{port} "
|
|
133
|
+
f"— start it with tests/docker-compose.yml"
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _connect_kwargs(name: str) -> dict:
|
|
139
|
+
"""Return the :func:`vd.connect` kwargs for backend ``name``."""
|
|
140
|
+
if name in SERVER_BACKENDS:
|
|
141
|
+
return SERVER_BACKENDS[name]["connect_kwargs"]()
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _drop_all_collections(client) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Delete every collection on ``client`` — best-effort.
|
|
148
|
+
|
|
149
|
+
Server backends keep state across runs; embedded backends are fresh each
|
|
150
|
+
time. Dropping everything before and after each test makes a run against a
|
|
151
|
+
live server idempotent (a re-run does not trip "already exists").
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
names = list(client.list_collections())
|
|
155
|
+
except Exception:
|
|
156
|
+
return
|
|
157
|
+
for name in names:
|
|
158
|
+
try:
|
|
159
|
+
client.delete_collection(name)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --------------------------------------------------------------------------- #
|
|
165
|
+
# Test embedder
|
|
166
|
+
# --------------------------------------------------------------------------- #
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def make_embedder():
|
|
170
|
+
"""Return a deterministic ``text -> 16-dim vector`` embedder for tests."""
|
|
171
|
+
|
|
172
|
+
def embed(text: str) -> list[float]:
|
|
173
|
+
digest = hashlib.md5(text.encode()).digest()
|
|
174
|
+
vector = [(b / 128.0) - 1.0 for b in digest]
|
|
175
|
+
while len(vector) < EMBED_DIM:
|
|
176
|
+
vector += vector
|
|
177
|
+
return vector[:EMBED_DIM]
|
|
178
|
+
|
|
179
|
+
return embed
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.fixture
|
|
183
|
+
def embedder():
|
|
184
|
+
"""A deterministic test embedder."""
|
|
185
|
+
return make_embedder()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# --------------------------------------------------------------------------- #
|
|
189
|
+
# Backend fixtures
|
|
190
|
+
# --------------------------------------------------------------------------- #
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.fixture(params=ALL_BACKENDS)
|
|
194
|
+
def backend_name(request):
|
|
195
|
+
"""Each reachable backend, one at a time; unreachable ones are skipped."""
|
|
196
|
+
name = request.param
|
|
197
|
+
if name not in vd.list_backends():
|
|
198
|
+
pytest.skip(f"backend {name!r} is not installed")
|
|
199
|
+
reason = _unavailable_reason(name)
|
|
200
|
+
if reason:
|
|
201
|
+
pytest.skip(reason)
|
|
202
|
+
return name
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.fixture
|
|
206
|
+
def client(backend_name, embedder):
|
|
207
|
+
"""A fresh, connected client for each backend (with an embedder)."""
|
|
208
|
+
connection = vd.connect(
|
|
209
|
+
backend_name, embedder=embedder, **_connect_kwargs(backend_name)
|
|
210
|
+
)
|
|
211
|
+
_drop_all_collections(connection)
|
|
212
|
+
yield connection
|
|
213
|
+
_drop_all_collections(connection)
|
|
214
|
+
if hasattr(connection, "close"):
|
|
215
|
+
try:
|
|
216
|
+
connection.close()
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Server backends for vd's live-verification test suite.
|
|
2
|
+
#
|
|
3
|
+
# Brings up one container per server-backed vd backend. The pytest suite
|
|
4
|
+
# (see conftest.py) TCP-probes each port and runs the contract tests against
|
|
5
|
+
# whatever is reachable; backends whose container is down are skipped, so the
|
|
6
|
+
# suite stays green in a plain CI environment.
|
|
7
|
+
#
|
|
8
|
+
# docker compose -f tests/docker-compose.yml up -d # start
|
|
9
|
+
# pytest tests/ # run the suite
|
|
10
|
+
# docker compose -f tests/docker-compose.yml down -v # stop + wipe
|
|
11
|
+
#
|
|
12
|
+
# milvus is intentionally absent: the milvus adapter is verified against the
|
|
13
|
+
# embedded Milvus Lite engine (no server), which exercises the same code path.
|
|
14
|
+
|
|
15
|
+
services:
|
|
16
|
+
pgvector:
|
|
17
|
+
image: pgvector/pgvector:pg17
|
|
18
|
+
environment:
|
|
19
|
+
POSTGRES_USER: vd
|
|
20
|
+
POSTGRES_PASSWORD: vd
|
|
21
|
+
POSTGRES_DB: vd
|
|
22
|
+
ports:
|
|
23
|
+
- "5432:5432"
|
|
24
|
+
healthcheck:
|
|
25
|
+
test: ["CMD-SHELL", "pg_isready -U vd"]
|
|
26
|
+
interval: 3s
|
|
27
|
+
timeout: 5s
|
|
28
|
+
retries: 20
|
|
29
|
+
|
|
30
|
+
redis:
|
|
31
|
+
image: redis/redis-stack-server:latest
|
|
32
|
+
ports:
|
|
33
|
+
- "6379:6379"
|
|
34
|
+
healthcheck:
|
|
35
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
36
|
+
interval: 3s
|
|
37
|
+
timeout: 5s
|
|
38
|
+
retries: 20
|
|
39
|
+
|
|
40
|
+
elasticsearch:
|
|
41
|
+
image: docker.elastic.co/elasticsearch/elasticsearch:9.0.3
|
|
42
|
+
environment:
|
|
43
|
+
discovery.type: single-node
|
|
44
|
+
xpack.security.enabled: "false"
|
|
45
|
+
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
|
|
46
|
+
ports:
|
|
47
|
+
- "9200:9200"
|
|
48
|
+
healthcheck:
|
|
49
|
+
test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
|
|
50
|
+
interval: 5s
|
|
51
|
+
timeout: 5s
|
|
52
|
+
retries: 30
|
|
53
|
+
|
|
54
|
+
weaviate:
|
|
55
|
+
image: cr.weaviate.io/semitechnologies/weaviate:1.28.4
|
|
56
|
+
command: ["--host", "0.0.0.0", "--port", "8080", "--scheme", "http"]
|
|
57
|
+
environment:
|
|
58
|
+
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true"
|
|
59
|
+
PERSISTENCE_DATA_PATH: "/var/lib/weaviate"
|
|
60
|
+
DEFAULT_VECTORIZER_MODULE: "none"
|
|
61
|
+
ENABLE_MODULES: ""
|
|
62
|
+
QUERY_DEFAULTS_LIMIT: "25"
|
|
63
|
+
ports:
|
|
64
|
+
- "8080:8080"
|
|
65
|
+
- "50051:50051"
|
|
66
|
+
healthcheck:
|
|
67
|
+
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/v1/.well-known/ready || exit 1"]
|
|
68
|
+
interval: 5s
|
|
69
|
+
timeout: 5s
|
|
70
|
+
retries: 30
|
|
71
|
+
|
|
72
|
+
mongodb:
|
|
73
|
+
image: mongodb/mongodb-atlas-local:latest
|
|
74
|
+
# Host port 27018 (not 27017) to avoid colliding with a developer's
|
|
75
|
+
# native mongod. The container still listens on 27017 internally.
|
|
76
|
+
ports:
|
|
77
|
+
- "27018:27017"
|
|
78
|
+
healthcheck:
|
|
79
|
+
test: ["CMD-SHELL", "mongosh --quiet --eval 'db.runCommand({ ping: 1 })' || exit 1"]
|
|
80
|
+
interval: 5s
|
|
81
|
+
timeout: 5s
|
|
82
|
+
retries: 30
|
|
@@ -166,7 +166,9 @@ def test_search_with_comparison_filter(client):
|
|
|
166
166
|
col = client.create_collection("search_cmp")
|
|
167
167
|
for i in range(6):
|
|
168
168
|
v = [0.0] * 16
|
|
169
|
-
|
|
169
|
+
# (i + 1), not i — a zero-magnitude vector has undefined cosine
|
|
170
|
+
# similarity and some backends (Elasticsearch) reject it outright.
|
|
171
|
+
v[0] = (i + 1) / 6
|
|
170
172
|
col[str(i)] = _doc(str(i), v, n=i)
|
|
171
173
|
hits = list(col.search([0.5] * 16, limit=10, filter={"n": {"$gte": 3}}))
|
|
172
174
|
assert sorted(int(h["id"]) for h in hits) == [3, 4, 5]
|
|
@@ -176,7 +178,8 @@ def test_search_with_in_filter(client):
|
|
|
176
178
|
col = client.create_collection("search_in")
|
|
177
179
|
for i in range(5):
|
|
178
180
|
v = [0.0] * 16
|
|
179
|
-
|
|
181
|
+
# (i + 1), not i — avoid a zero-magnitude vector (see comment above).
|
|
182
|
+
v[0] = (i + 1) / 5
|
|
180
183
|
col[str(i)] = _doc(str(i), v, n=i)
|
|
181
184
|
hits = list(col.search([0.5] * 16, limit=10, filter={"n": {"$in": [1, 3]}}))
|
|
182
185
|
assert sorted(int(h["id"]) for h in hits) == [1, 3]
|
|
@@ -229,6 +232,8 @@ def test_upsert_is_idempotent(client):
|
|
|
229
232
|
|
|
230
233
|
def test_dimension_mismatch_raises(client):
|
|
231
234
|
col = client.create_collection("dimcheck")
|
|
232
|
-
|
|
235
|
+
# A non-zero vector — a zero-magnitude one has undefined cosine similarity
|
|
236
|
+
# and some backends (Elasticsearch) reject it outright.
|
|
237
|
+
col["ok"] = Document(id="ok", text="t", vector=[1.0] * 16)
|
|
233
238
|
with pytest.raises(ValueError):
|
|
234
|
-
col["bad"] = Document(id="bad", text="t", vector=[
|
|
239
|
+
col["bad"] = Document(id="bad", text="t", vector=[1.0] * 8)
|
|
@@ -195,16 +195,28 @@ class ElasticsearchCollection(AbstractCollection):
|
|
|
195
195
|
|
|
196
196
|
def _ensure_index(self) -> None:
|
|
197
197
|
"""
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
The
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
Ensure the ``embedding`` dense_vector field is mapped before a write.
|
|
199
|
+
|
|
200
|
+
The index itself is created eagerly by
|
|
201
|
+
:meth:`ElasticsearchClient.create_collection`, so an empty collection
|
|
202
|
+
is still visible to ``list_collections`` / ``in`` / ``get_collection``.
|
|
203
|
+
The ``dense_vector`` field, however, needs the vector dimension — not
|
|
204
|
+
known until the first vector arrives — so it is added here, lazily, via
|
|
205
|
+
``put_mapping``. ``self.dimension`` is guaranteed set by then because
|
|
206
|
+
:meth:`~vd.base.AbstractCollection._vet_vector` runs before ``_write``.
|
|
203
207
|
"""
|
|
208
|
+
full_mapping = _build_mapping(self.dimension, self.metric)
|
|
204
209
|
if not _index_exists(self._es, self.name):
|
|
205
|
-
|
|
210
|
+
# Index missing (collection created outside the adapter, or the
|
|
211
|
+
# index was dropped) — create it whole.
|
|
212
|
+
self._es.indices.create(index=self.name, mappings=full_mapping)
|
|
213
|
+
return
|
|
214
|
+
mapping = self._es.indices.get_mapping(index=self.name)
|
|
215
|
+
properties = mapping[self.name]["mappings"].get("properties", {})
|
|
216
|
+
if "embedding" not in properties:
|
|
217
|
+
self._es.indices.put_mapping(
|
|
206
218
|
index=self.name,
|
|
207
|
-
|
|
219
|
+
properties={"embedding": full_mapping["properties"]["embedding"]},
|
|
208
220
|
)
|
|
209
221
|
|
|
210
222
|
# ----- raw primitives --------------------------------------------------- #
|
|
@@ -276,37 +288,26 @@ class ElasticsearchCollection(AbstractCollection):
|
|
|
276
288
|
"""
|
|
277
289
|
Iterate all document ids in the index.
|
|
278
290
|
|
|
279
|
-
Uses
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
``
|
|
283
|
-
|
|
291
|
+
Uses the scroll-based ``helpers.scan`` to walk every document.
|
|
292
|
+
Elasticsearch disallows sorting (fielddata access) on the ``_id``
|
|
293
|
+
field, which rules out an ``_id``-keyed ``search_after`` pagination;
|
|
294
|
+
``scan`` needs no sort. Ids are materialised into a list (as the
|
|
295
|
+
previous ``search_after`` implementation also did).
|
|
284
296
|
"""
|
|
285
297
|
if not _index_exists(self._es, self.name):
|
|
286
298
|
return iter(())
|
|
299
|
+
from elasticsearch.helpers import scan
|
|
287
300
|
|
|
288
|
-
ids
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
body["search_after"] = search_after
|
|
299
|
-
|
|
300
|
-
response = self._es.search(index=self.name, body=body)
|
|
301
|
-
hits = response["hits"]["hits"]
|
|
302
|
-
if not hits:
|
|
303
|
-
break
|
|
304
|
-
for hit in hits:
|
|
305
|
-
ids.append(hit["_id"])
|
|
306
|
-
search_after = hits[-1]["sort"]
|
|
307
|
-
if len(hits) < _KEYS_PAGE_SIZE:
|
|
308
|
-
break
|
|
309
|
-
|
|
301
|
+
ids = [
|
|
302
|
+
hit["_id"]
|
|
303
|
+
for hit in scan(
|
|
304
|
+
self._es,
|
|
305
|
+
index=self.name,
|
|
306
|
+
query={"query": {"match_all": {}}},
|
|
307
|
+
_source=False,
|
|
308
|
+
size=_KEYS_PAGE_SIZE,
|
|
309
|
+
)
|
|
310
|
+
]
|
|
310
311
|
return iter(ids)
|
|
311
312
|
|
|
312
313
|
def _count(self) -> int:
|
|
@@ -441,8 +442,12 @@ class ElasticsearchClient(AbstractClient):
|
|
|
441
442
|
"""
|
|
442
443
|
Create a new collection (Elasticsearch index).
|
|
443
444
|
|
|
444
|
-
The index is created **
|
|
445
|
-
|
|
445
|
+
The index is created **eagerly** so the collection is immediately
|
|
446
|
+
visible to :meth:`list_collections`, ``in``, and :meth:`get_collection`
|
|
447
|
+
even before its first write. The ``embedding`` ``dense_vector`` field
|
|
448
|
+
is added with a full mapping now when ``dimension`` is supplied, or
|
|
449
|
+
**lazily** on the first write otherwise (Elasticsearch needs the vector
|
|
450
|
+
dimension to map a ``dense_vector`` field).
|
|
446
451
|
|
|
447
452
|
Parameters
|
|
448
453
|
----------
|
|
@@ -450,8 +455,8 @@ class ElasticsearchClient(AbstractClient):
|
|
|
450
455
|
Index name. **Must be lowercase** — Elasticsearch rejects names
|
|
451
456
|
with uppercase letters.
|
|
452
457
|
dimension : int, optional
|
|
453
|
-
Vector dimension.
|
|
454
|
-
deferred until the first write.
|
|
458
|
+
Vector dimension. When given, the ``dense_vector`` field is mapped
|
|
459
|
+
now; otherwise it is deferred until the first write.
|
|
455
460
|
metric : str
|
|
456
461
|
Distance metric: ``"cosine"``, ``"dot"``, or ``"l2"``.
|
|
457
462
|
**index_config
|
|
@@ -472,16 +477,23 @@ class ElasticsearchClient(AbstractClient):
|
|
|
472
477
|
dimension=dimension,
|
|
473
478
|
metric=metric,
|
|
474
479
|
)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
"
|
|
480
|
+
# Always create the index now (an empty collection must still be
|
|
481
|
+
# listable). The dense_vector field needs the dimension, so it is only
|
|
482
|
+
# mapped here when known — otherwise _ensure_index adds it on first write.
|
|
483
|
+
mappings: dict[str, Any] = {
|
|
484
|
+
"properties": {
|
|
485
|
+
"text": {"type": "text"},
|
|
486
|
+
"metadata": {"type": "object", "enabled": False},
|
|
481
487
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
488
|
+
}
|
|
489
|
+
if dimension is not None:
|
|
490
|
+
mappings["properties"]["embedding"] = _build_mapping(dimension, metric)[
|
|
491
|
+
"properties"
|
|
492
|
+
]["embedding"]
|
|
493
|
+
create_kwargs: dict[str, Any] = {"index": name, "mappings": mappings}
|
|
494
|
+
if index_config:
|
|
495
|
+
create_kwargs["settings"] = index_config
|
|
496
|
+
self._client.indices.create(**create_kwargs)
|
|
485
497
|
return col
|
|
486
498
|
|
|
487
499
|
def get_collection(self, name: str) -> ElasticsearchCollection:
|
|
@@ -45,6 +45,7 @@ Requires: ``pip install pymilvus``
|
|
|
45
45
|
|
|
46
46
|
from __future__ import annotations
|
|
47
47
|
|
|
48
|
+
import os
|
|
48
49
|
import tempfile
|
|
49
50
|
from typing import Any, Callable, Iterable, Iterator, Optional
|
|
50
51
|
|
|
@@ -514,8 +515,11 @@ class MilvusBackendClient(AbstractClient):
|
|
|
514
515
|
elif uri is not None:
|
|
515
516
|
self._client = MilvusClient(uri=uri, token=token)
|
|
516
517
|
else:
|
|
517
|
-
# Milvus Lite:
|
|
518
|
-
|
|
518
|
+
# Milvus Lite: an embedded, server-less engine. Hand it a fresh
|
|
519
|
+
# ".db" path inside a new temp directory. milvus-lite >=3.0 treats
|
|
520
|
+
# the path as a data directory it creates itself, so it must not
|
|
521
|
+
# already exist as a file (which ``tempfile.mkstemp`` would create).
|
|
522
|
+
tmp_path = os.path.join(tempfile.mkdtemp(prefix="vd_milvus_"), "milvus.db")
|
|
519
523
|
self._client = MilvusClient(tmp_path)
|
|
520
524
|
|
|
521
525
|
#: Track metrics for collections created via this client so they survive
|