vecspan 0.1.0__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.
vecspan-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: vecspan
3
+ Version: 0.1.0
4
+ Summary: One Python API for Milvus, Qdrant, and other vector databases.
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Provides-Extra: milvus
8
+ Requires-Dist: pymilvus>=2.5.0; extra == "milvus"
9
+ Provides-Extra: qdrant
10
+ Requires-Dist: qdrant-client>=1.12.0; extra == "qdrant"
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8; extra == "dev"
13
+
14
+ # vecspan
15
+
16
+ vecspan 是一个同步 Python 客户端。业务代码只依赖一套集合、写入、过滤和检索接口,Milvus 和 Qdrant 放在适配器后面。向量由调用方传入,这个库不负责生成 embedding。
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ pip install vecspan
22
+ pip install "vecspan[milvus]"
23
+ pip install "vecspan[qdrant]"
24
+ ```
25
+
26
+ 只装其中一个可选依赖时,另一个后端不会被导入。
27
+
28
+ ## 连接
29
+
30
+ ```python
31
+ from vecspan import VectorClient
32
+
33
+ client = VectorClient.connect("memory://")
34
+ client = VectorClient.connect("milvus://localhost:19530")
35
+ client = VectorClient.connect("milvus://user:password@localhost:19530/default")
36
+ client = VectorClient.connect("qdrant://localhost:6333", api_key="...")
37
+ client = VectorClient.connect("qdrant+https://example.com", api_key="...")
38
+ ```
39
+
40
+ `memory://` 把数据放在当前进程里,用来做单测和本地开发。第三方后端可以用 `register_backend(scheme)` 注册,工厂签名是 `factory(parsed_url, **kwargs)`。
41
+
42
+ ## 集合和写入
43
+
44
+ 主键在创建时选定,之后不再改变。`int` 的可移植范围是 `0 .. 2^63-1`。`uuid` 只接受 UUID,因为 Qdrant 的 point id 不能是任意字符串。
45
+
46
+ ```python
47
+ from vecspan import (
48
+ CollectionSpec,
49
+ Distance,
50
+ Filter,
51
+ PayloadField,
52
+ PayloadType,
53
+ Point,
54
+ VectorField,
55
+ )
56
+
57
+ client.create_collection(CollectionSpec(
58
+ name="docs",
59
+ id_type="uuid",
60
+ vectors=[VectorField("embedding", size=768, distance=Distance.COSINE)],
61
+ payload_fields=[
62
+ PayloadField("source", PayloadType.KEYWORD, indexed=True),
63
+ PayloadField("year", PayloadType.INT, indexed=True),
64
+ ],
65
+ ))
66
+
67
+ client.upsert("docs", [Point(
68
+ id="550e8400-e29b-41d4-a716-446655440000",
69
+ vectors={"embedding": [0.1] * 768},
70
+ payload={"source": "wiki", "year": 2020},
71
+ )])
72
+ ```
73
+
74
+ 一个 point 可以带多个命名向量。写入分成四个语义:
75
+
76
+ | 方法 | 语义 |
77
+ | --- | --- |
78
+ | `upsert(name, points)` | 按 id 新增,或整条替换向量和 payload |
79
+ | `update_payload(name, ids, payload)` | 合并 payload,未提到的字段保留 |
80
+ | `update_vectors(name, ids, vectors)` | 只替换给出的向量字段 |
81
+ | `delete(name, ids=...)` 或 `delete(name, filter=...)` | 按 id 或按过滤条件删除 |
82
+
83
+ `update_payload` 在 Qdrant 上是合并写。Milvus 在客户端支持 `partial_update` 时走局部更新,否则先读出整行再写回。后一种做法在读和写之间有并发窗口。
84
+
85
+ 未在 `payload_fields` 里声明的字段仍然可以写入。声明过的字段会按类型检查;标了 `indexed=True` 的字段会建标量索引。
86
+
87
+ ## 读取
88
+
89
+ ```python
90
+ client.get("docs", [point_id])
91
+ client.count("docs", filter=Filter.eq("source", "wiki"))
92
+ page = client.scroll("docs", limit=100)
93
+ page = client.scroll("docs", limit=100, offset=page.next_offset)
94
+
95
+ query = (Filter.eq("source", "wiki") & Filter.gte("year", 2020)) | Filter.is_null("lang")
96
+ hits = client.search("docs", vector=query_vector, filter=query, limit=10)
97
+ ```
98
+
99
+ `scroll` 的 `offset` 只对同一次连接里的同一个集合有意义。过滤条件支持 `eq`、`gt` / `gte` / `lt` / `lte`、`in_`、`is_null`,以及 `&`、`|`、`~`。
100
+
101
+ 集合有多个向量字段时,`search` 必须传 `vector_name`。只有一个向量字段时可以省略。
102
+
103
+ `SearchHit.score` 是后端自己的分数,不会在库之间换算。Cosine 和 Dot 越大越近,L2 越小越近。memory 和 Qdrant 的 L2 是欧氏距离,Milvus 的 L2 是平方欧氏距离,数值不要跨库比较。
104
+
105
+ HNSW 参数 `m` 和 `ef_construct` 写在 `VectorField` 上,默认 `16` 和 `200`。检索时可以用 `backend_options` 覆盖搜索宽度:
106
+
107
+ ```python
108
+ client.search("docs", vector=query_vector, backend_options={"ef": 128})
109
+ ```
110
+
111
+ `hnsw_ef` 和 `ef` 是同一个参数。Milvus 另外接受 `nprobe`。
112
+
113
+ ## 范围
114
+
115
+ v1 包含:
116
+
117
+ - 集合的创建、删除、是否存在、描述、列表
118
+ - `upsert`、`update_payload`、`update_vectors`、`delete`
119
+ - `get`、`count`、`scroll`、`search`
120
+ - 过滤表达式,以及默认的 HNSW 索引
121
+ - `MemoryBackend`,以及同一套契约测试
122
+
123
+ v1 不包含稀疏向量、混合检索、分区、别名、多副本和权限。客户端是同步的。
124
+
125
+ ## 测试
126
+
127
+ ```bash
128
+ pip install pytest
129
+ pytest
130
+ ```
131
+
132
+ 连上真实数据库时再跑集成测试:
133
+
134
+ ```bash
135
+ VECSPAN_MILVUS_URL=milvus://localhost:19530 pytest -m integration
136
+ VECSPAN_QDRANT_URL=qdrant://localhost:6333 pytest -m integration
137
+ ```
@@ -0,0 +1,124 @@
1
+ # vecspan
2
+
3
+ vecspan 是一个同步 Python 客户端。业务代码只依赖一套集合、写入、过滤和检索接口,Milvus 和 Qdrant 放在适配器后面。向量由调用方传入,这个库不负责生成 embedding。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install vecspan
9
+ pip install "vecspan[milvus]"
10
+ pip install "vecspan[qdrant]"
11
+ ```
12
+
13
+ 只装其中一个可选依赖时,另一个后端不会被导入。
14
+
15
+ ## 连接
16
+
17
+ ```python
18
+ from vecspan import VectorClient
19
+
20
+ client = VectorClient.connect("memory://")
21
+ client = VectorClient.connect("milvus://localhost:19530")
22
+ client = VectorClient.connect("milvus://user:password@localhost:19530/default")
23
+ client = VectorClient.connect("qdrant://localhost:6333", api_key="...")
24
+ client = VectorClient.connect("qdrant+https://example.com", api_key="...")
25
+ ```
26
+
27
+ `memory://` 把数据放在当前进程里,用来做单测和本地开发。第三方后端可以用 `register_backend(scheme)` 注册,工厂签名是 `factory(parsed_url, **kwargs)`。
28
+
29
+ ## 集合和写入
30
+
31
+ 主键在创建时选定,之后不再改变。`int` 的可移植范围是 `0 .. 2^63-1`。`uuid` 只接受 UUID,因为 Qdrant 的 point id 不能是任意字符串。
32
+
33
+ ```python
34
+ from vecspan import (
35
+ CollectionSpec,
36
+ Distance,
37
+ Filter,
38
+ PayloadField,
39
+ PayloadType,
40
+ Point,
41
+ VectorField,
42
+ )
43
+
44
+ client.create_collection(CollectionSpec(
45
+ name="docs",
46
+ id_type="uuid",
47
+ vectors=[VectorField("embedding", size=768, distance=Distance.COSINE)],
48
+ payload_fields=[
49
+ PayloadField("source", PayloadType.KEYWORD, indexed=True),
50
+ PayloadField("year", PayloadType.INT, indexed=True),
51
+ ],
52
+ ))
53
+
54
+ client.upsert("docs", [Point(
55
+ id="550e8400-e29b-41d4-a716-446655440000",
56
+ vectors={"embedding": [0.1] * 768},
57
+ payload={"source": "wiki", "year": 2020},
58
+ )])
59
+ ```
60
+
61
+ 一个 point 可以带多个命名向量。写入分成四个语义:
62
+
63
+ | 方法 | 语义 |
64
+ | --- | --- |
65
+ | `upsert(name, points)` | 按 id 新增,或整条替换向量和 payload |
66
+ | `update_payload(name, ids, payload)` | 合并 payload,未提到的字段保留 |
67
+ | `update_vectors(name, ids, vectors)` | 只替换给出的向量字段 |
68
+ | `delete(name, ids=...)` 或 `delete(name, filter=...)` | 按 id 或按过滤条件删除 |
69
+
70
+ `update_payload` 在 Qdrant 上是合并写。Milvus 在客户端支持 `partial_update` 时走局部更新,否则先读出整行再写回。后一种做法在读和写之间有并发窗口。
71
+
72
+ 未在 `payload_fields` 里声明的字段仍然可以写入。声明过的字段会按类型检查;标了 `indexed=True` 的字段会建标量索引。
73
+
74
+ ## 读取
75
+
76
+ ```python
77
+ client.get("docs", [point_id])
78
+ client.count("docs", filter=Filter.eq("source", "wiki"))
79
+ page = client.scroll("docs", limit=100)
80
+ page = client.scroll("docs", limit=100, offset=page.next_offset)
81
+
82
+ query = (Filter.eq("source", "wiki") & Filter.gte("year", 2020)) | Filter.is_null("lang")
83
+ hits = client.search("docs", vector=query_vector, filter=query, limit=10)
84
+ ```
85
+
86
+ `scroll` 的 `offset` 只对同一次连接里的同一个集合有意义。过滤条件支持 `eq`、`gt` / `gte` / `lt` / `lte`、`in_`、`is_null`,以及 `&`、`|`、`~`。
87
+
88
+ 集合有多个向量字段时,`search` 必须传 `vector_name`。只有一个向量字段时可以省略。
89
+
90
+ `SearchHit.score` 是后端自己的分数,不会在库之间换算。Cosine 和 Dot 越大越近,L2 越小越近。memory 和 Qdrant 的 L2 是欧氏距离,Milvus 的 L2 是平方欧氏距离,数值不要跨库比较。
91
+
92
+ HNSW 参数 `m` 和 `ef_construct` 写在 `VectorField` 上,默认 `16` 和 `200`。检索时可以用 `backend_options` 覆盖搜索宽度:
93
+
94
+ ```python
95
+ client.search("docs", vector=query_vector, backend_options={"ef": 128})
96
+ ```
97
+
98
+ `hnsw_ef` 和 `ef` 是同一个参数。Milvus 另外接受 `nprobe`。
99
+
100
+ ## 范围
101
+
102
+ v1 包含:
103
+
104
+ - 集合的创建、删除、是否存在、描述、列表
105
+ - `upsert`、`update_payload`、`update_vectors`、`delete`
106
+ - `get`、`count`、`scroll`、`search`
107
+ - 过滤表达式,以及默认的 HNSW 索引
108
+ - `MemoryBackend`,以及同一套契约测试
109
+
110
+ v1 不包含稀疏向量、混合检索、分区、别名、多副本和权限。客户端是同步的。
111
+
112
+ ## 测试
113
+
114
+ ```bash
115
+ pip install pytest
116
+ pytest
117
+ ```
118
+
119
+ 连上真实数据库时再跑集成测试:
120
+
121
+ ```bash
122
+ VECSPAN_MILVUS_URL=milvus://localhost:19530 pytest -m integration
123
+ VECSPAN_QDRANT_URL=qdrant://localhost:6333 pytest -m integration
124
+ ```
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "vecspan"
3
+ version = "0.1.0"
4
+ description = "One Python API for Milvus, Qdrant, and other vector databases."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+
8
+ [project.optional-dependencies]
9
+ milvus = ["pymilvus>=2.5.0"]
10
+ qdrant = ["qdrant-client>=1.12.0"]
11
+ dev = ["pytest>=8"]
12
+
13
+ [build-system]
14
+ requires = ["setuptools>=68"]
15
+ build-backend = "setuptools.build_meta"
16
+
17
+ [tool.setuptools.packages.find]
18
+ where = ["src"]
19
+
20
+ [tool.setuptools.package-data]
21
+ vecspan = ["py.typed"]
22
+
23
+ [tool.pytest.ini_options]
24
+ testpaths = ["tests"]
25
+ pythonpath = ["src"]
26
+ markers = [
27
+ "integration: tests that talk to a live Milvus or Qdrant",
28
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,50 @@
1
+ """One Python API for Milvus, Qdrant, and other vector databases."""
2
+
3
+ from vecspan.capabilities import Capability
4
+ from vecspan.client import VectorClient
5
+ from vecspan.errors import (
6
+ BackendError,
7
+ CollectionAlreadyExistsError,
8
+ CollectionNotFoundError,
9
+ UnsupportedOperation,
10
+ ValidationError,
11
+ VecspanError,
12
+ )
13
+ from vecspan.filters import Filter
14
+ from vecspan.registry import register_backend
15
+ from vecspan.types import (
16
+ CollectionSpec,
17
+ Distance,
18
+ IdType,
19
+ PayloadField,
20
+ PayloadType,
21
+ Point,
22
+ ScrollPage,
23
+ SearchHit,
24
+ VectorField,
25
+ )
26
+
27
+ __version__ = "0.1.0"
28
+
29
+ __all__ = [
30
+ "BackendError",
31
+ "Capability",
32
+ "CollectionAlreadyExistsError",
33
+ "CollectionNotFoundError",
34
+ "CollectionSpec",
35
+ "Distance",
36
+ "Filter",
37
+ "IdType",
38
+ "PayloadField",
39
+ "PayloadType",
40
+ "Point",
41
+ "ScrollPage",
42
+ "SearchHit",
43
+ "UnsupportedOperation",
44
+ "ValidationError",
45
+ "VecspanError",
46
+ "VectorClient",
47
+ "VectorField",
48
+ "__version__",
49
+ "register_backend",
50
+ ]
@@ -0,0 +1,6 @@
1
+ """Built-in backends. Milvus and Qdrant stay lazy so importing vecspan is cheap."""
2
+
3
+ from vecspan.backends.base import VectorBackend
4
+ from vecspan.backends.memory import MemoryBackend
5
+
6
+ __all__ = ["MemoryBackend", "VectorBackend"]
@@ -0,0 +1,107 @@
1
+ """Backend interface implemented by Milvus, Qdrant, and the in-memory store."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Sequence
7
+
8
+ from vecspan.capabilities import ALL_CAPABILITIES, Capability
9
+ from vecspan.filters import Filter
10
+ from vecspan.types import CollectionSpec, Point, ScrollPage, SearchHit
11
+
12
+
13
+ class VectorBackend(ABC):
14
+ """Storage operations behind `VectorClient`.
15
+
16
+ `VectorClient` validates arguments and checks capabilities before it calls
17
+ these methods. A backend still raises `VecspanError` for conditions only
18
+ the database can see, such as a missing collection or a missing point id.
19
+ """
20
+
21
+ name = "backend"
22
+ capabilities: frozenset[Capability] = ALL_CAPABILITIES
23
+
24
+ @abstractmethod
25
+ def create_collection(self, spec: CollectionSpec) -> None:
26
+ """Create a collection from `spec`."""
27
+
28
+ @abstractmethod
29
+ def drop_collection(self, name: str) -> None:
30
+ """Drop a collection."""
31
+
32
+ @abstractmethod
33
+ def has_collection(self, name: str) -> bool:
34
+ """Return whether the collection exists."""
35
+
36
+ @abstractmethod
37
+ def describe_collection(self, name: str) -> CollectionSpec:
38
+ """Return the schema stored for a collection."""
39
+
40
+ @abstractmethod
41
+ def list_collections(self) -> list[str]:
42
+ """Return collection names visible to callers."""
43
+
44
+ @abstractmethod
45
+ def upsert(self, name: str, points: Sequence[Point]) -> None:
46
+ """Insert points or replace them completely when the id already exists."""
47
+
48
+ @abstractmethod
49
+ def update_payload(self, name: str, ids: Sequence[int | str], payload: dict) -> None:
50
+ """Merge `payload` into existing points. Missing ids are an error."""
51
+
52
+ @abstractmethod
53
+ def update_vectors(
54
+ self,
55
+ name: str,
56
+ ids: Sequence[int | str],
57
+ vectors: Sequence[dict[str, list[float]]],
58
+ ) -> None:
59
+ """Replace the given vector fields on existing points and leave the rest."""
60
+
61
+ @abstractmethod
62
+ def delete(
63
+ self,
64
+ name: str,
65
+ *,
66
+ ids: Sequence[int | str] | None = None,
67
+ filter: Filter | None = None,
68
+ ) -> None:
69
+ """Delete by id or by filter. Exactly one of the two is set."""
70
+
71
+ @abstractmethod
72
+ def get(self, name: str, ids: Sequence[int | str], *, with_vectors: bool = False) -> list[Point]:
73
+ """Return the points that exist, in the same order as `ids`."""
74
+
75
+ @abstractmethod
76
+ def count(self, name: str, *, filter: Filter | None = None) -> int:
77
+ """Count points, optionally restricted by a filter."""
78
+
79
+ @abstractmethod
80
+ def scroll(
81
+ self,
82
+ name: str,
83
+ *,
84
+ filter: Filter | None = None,
85
+ limit: int = 100,
86
+ offset: int | str | None = None,
87
+ with_vectors: bool = False,
88
+ ) -> ScrollPage:
89
+ """Read a page of points. `offset` comes from a previous page."""
90
+
91
+ @abstractmethod
92
+ def search(
93
+ self,
94
+ name: str,
95
+ vector: Sequence[float],
96
+ *,
97
+ vector_name: str,
98
+ filter: Filter | None = None,
99
+ limit: int = 10,
100
+ with_vectors: bool = False,
101
+ backend_options: dict | None = None,
102
+ ) -> list[SearchHit]:
103
+ """Return the nearest points for one named vector field."""
104
+
105
+ def close(self) -> None:
106
+ """Release the underlying connection. The default does nothing."""
107
+ return None