asyncpg-vector 0.1.0__py3-none-any.whl
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.
- asyncpg_vector/__init__.py +193 -0
- asyncpg_vector/py.typed +0 -0
- asyncpg_vector-0.1.0.dist-info/METADATA +132 -0
- asyncpg_vector-0.1.0.dist-info/RECORD +8 -0
- asyncpg_vector-0.1.0.dist-info/WHEEL +5 -0
- asyncpg_vector-0.1.0.dist-info/licenses/LICENSE +21 -0
- asyncpg_vector-0.1.0.dist-info/top_level.txt +1 -0
- asyncpg_vector-0.1.0.dist-info/zip-safe +1 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL vector support for asyncpg.
|
|
3
|
+
|
|
4
|
+
Registers data types `vector` and `halfvec` from the PostgreSQL extension `vector` to the asynchronous PostgreSQL
|
|
5
|
+
client `asyncpg`, and marshals vector data to and from PostgreSQL database tables.
|
|
6
|
+
|
|
7
|
+
:see: https://github.com/hunyadi/asyncpg_vector
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import struct
|
|
12
|
+
import sys
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from struct import pack
|
|
16
|
+
from typing import ClassVar
|
|
17
|
+
|
|
18
|
+
import asyncpg
|
|
19
|
+
|
|
20
|
+
if sys.version_info >= (3, 11):
|
|
21
|
+
from typing import Self
|
|
22
|
+
else:
|
|
23
|
+
from typing_extensions import Self
|
|
24
|
+
|
|
25
|
+
if sys.version_info >= (3, 12):
|
|
26
|
+
from typing import override
|
|
27
|
+
else:
|
|
28
|
+
from typing_extensions import override
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
31
|
+
__author__ = "Levente Hunyadi"
|
|
32
|
+
__copyright__ = "Copyright 2025, Levente Hunyadi"
|
|
33
|
+
__license__ = "MIT"
|
|
34
|
+
__maintainer__ = "Levente Hunyadi"
|
|
35
|
+
__status__ = "Production"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BasicVector(ABC):
|
|
39
|
+
"Base class for PostgreSQL vector types."
|
|
40
|
+
|
|
41
|
+
__slots__ = ("_data",)
|
|
42
|
+
|
|
43
|
+
_data: bytes
|
|
44
|
+
|
|
45
|
+
def __init__(self, data: bytes) -> None:
|
|
46
|
+
self._data = data
|
|
47
|
+
|
|
48
|
+
def __eq__(self, value: object) -> bool:
|
|
49
|
+
if not isinstance(value, self.__class__):
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
return self._data == value._data
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
return f"{type(self).__name__}({self._data!r})"
|
|
56
|
+
|
|
57
|
+
def size(self) -> int:
|
|
58
|
+
"Number of vector dimensions."
|
|
59
|
+
|
|
60
|
+
return len(self._data) // self.bytes_per_item()
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def bytes_per_item(cls) -> int:
|
|
65
|
+
"Number of bytes per item in the vector."
|
|
66
|
+
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def to_float_list(self) -> list[float]:
|
|
71
|
+
"Converts the vector to a list of double-precision floating-point values."
|
|
72
|
+
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def from_float_list(cls, vec: Sequence[float]) -> Self:
|
|
78
|
+
"Creates a vector from a list of double-precision floating-point numbers."
|
|
79
|
+
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_float_base64(cls, base64_encoded: str) -> Self:
|
|
84
|
+
"""
|
|
85
|
+
Creates a vector from a base64-encoded string storing a list of double-precision floating-point numbers.
|
|
86
|
+
|
|
87
|
+
This allows lossless transition of a floating-point value between APIs.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
decoded_bytes = base64.b64decode(base64_encoded)
|
|
91
|
+
float_tuple = struct.unpack(f"{len(decoded_bytes) // 4}f", decoded_bytes)
|
|
92
|
+
return cls.from_float_list(float_tuple)
|
|
93
|
+
|
|
94
|
+
def to_database_binary(self) -> bytes:
|
|
95
|
+
"Writes the data into the PostgreSQL data transfer representation."
|
|
96
|
+
|
|
97
|
+
return pack(">HH", self.size(), 0) + self._data
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_database_binary(cls, data: bytes) -> Self:
|
|
101
|
+
"Reads the data from the PostgreSQL data transfer representation."
|
|
102
|
+
|
|
103
|
+
size, _unused = struct.unpack(">HH", data[0:4])
|
|
104
|
+
if len(data) != 4 + cls.bytes_per_item() * size:
|
|
105
|
+
raise ValueError(f"expected size: {cls.bytes_per_item()} * {size}; got {len(data) - 4} bytes")
|
|
106
|
+
return cls(data[4:])
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def _to_database_binary(cls, value: Self | list[float] | None) -> bytes | None:
|
|
110
|
+
"Writes a value or instance into the PostgreSQL data transfer representation."
|
|
111
|
+
|
|
112
|
+
if value is None:
|
|
113
|
+
return value # asyncpg uses `None` for representing SQL `NULL`
|
|
114
|
+
|
|
115
|
+
match value:
|
|
116
|
+
case BasicVector():
|
|
117
|
+
obj = value
|
|
118
|
+
case list():
|
|
119
|
+
if value:
|
|
120
|
+
list_item = value[0]
|
|
121
|
+
if not isinstance(list_item, float):
|
|
122
|
+
raise ValueError(f"unsupported list item type: {type(list_item).__name__}")
|
|
123
|
+
|
|
124
|
+
obj = cls.from_float_list(value)
|
|
125
|
+
else:
|
|
126
|
+
obj = cls(bytes())
|
|
127
|
+
case _:
|
|
128
|
+
raise ValueError(f"unsupported type: {type(value).__name__}")
|
|
129
|
+
|
|
130
|
+
return obj.to_database_binary()
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def _from_database_binary(cls, value: bytes | None) -> Self | None:
|
|
134
|
+
"Creates an instance from the PostgreSQL data transfer representation."
|
|
135
|
+
|
|
136
|
+
if value is None:
|
|
137
|
+
return value
|
|
138
|
+
|
|
139
|
+
return cls.from_database_binary(value)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class HalfVector(BasicVector):
|
|
143
|
+
"Implements the PostgreSQL extension type `halfvec`."
|
|
144
|
+
|
|
145
|
+
type_name: ClassVar[str] = "halfvec"
|
|
146
|
+
cosine_similarity: ClassVar[str] = "halfvec_cosine_ops"
|
|
147
|
+
|
|
148
|
+
@override
|
|
149
|
+
@classmethod
|
|
150
|
+
def bytes_per_item(cls) -> int:
|
|
151
|
+
return 2
|
|
152
|
+
|
|
153
|
+
@override
|
|
154
|
+
def to_float_list(self) -> list[float]:
|
|
155
|
+
return list(struct.unpack(f">{self.size()}e", self._data))
|
|
156
|
+
|
|
157
|
+
@override
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_float_list(cls, vec: Sequence[float]) -> Self:
|
|
160
|
+
return cls(struct.pack(f">{len(vec)}e", *vec))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Vector(BasicVector):
|
|
164
|
+
"Implements the PostgreSQL extension type `vector`."
|
|
165
|
+
|
|
166
|
+
type_name: ClassVar[str] = "vector"
|
|
167
|
+
cosine_similarity: ClassVar[str] = "vector_cosine_ops"
|
|
168
|
+
|
|
169
|
+
@override
|
|
170
|
+
@classmethod
|
|
171
|
+
def bytes_per_item(cls) -> int:
|
|
172
|
+
return 4
|
|
173
|
+
|
|
174
|
+
@override
|
|
175
|
+
def to_float_list(self) -> list[float]:
|
|
176
|
+
return list(struct.unpack(f">{self.size()}f", self._data))
|
|
177
|
+
|
|
178
|
+
@override
|
|
179
|
+
@classmethod
|
|
180
|
+
def from_float_list(cls, vec: Sequence[float]) -> Self:
|
|
181
|
+
return cls(struct.pack(f">{len(vec)}f", *vec))
|
|
182
|
+
|
|
183
|
+
@override
|
|
184
|
+
@classmethod
|
|
185
|
+
def from_float_base64(cls, base64_encoded: str) -> Self:
|
|
186
|
+
return cls(base64.b64decode(base64_encoded))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def register_vector(conn: asyncpg.Connection, schema: str = "public") -> None:
|
|
190
|
+
"Registers `vector` extension types with Python module `asyncpg`."
|
|
191
|
+
|
|
192
|
+
await conn.set_type_codec("vector", schema=schema, encoder=Vector._to_database_binary, decoder=Vector._from_database_binary, format="binary") # pyright: ignore[reportPrivateUsage]
|
|
193
|
+
await conn.set_type_codec("halfvec", schema=schema, encoder=HalfVector._to_database_binary, decoder=HalfVector._from_database_binary, format="binary") # pyright: ignore[reportPrivateUsage]
|
asyncpg_vector/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asyncpg_vector
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PostgreSQL vector support for asyncpg
|
|
5
|
+
Author-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
6
|
+
Maintainer-email: Levente Hunyadi <hunyadi@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/hunyadi/asyncpg_vector
|
|
9
|
+
Project-URL: Source, https://github.com/hunyadi/asyncpg_vector
|
|
10
|
+
Keywords: asyncpg,postgresql-extension,vector,vector-search
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: asyncpg>=0.30
|
|
29
|
+
Requires-Dist: typing-extensions>=4.15; python_version < "3.11"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: asyncpg-stubs>=0.30; extra == "dev"
|
|
32
|
+
Requires-Dist: build>=1.3; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.18; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.14; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# PostgreSQL vector support for asyncpg
|
|
38
|
+
|
|
39
|
+
Adds PostgreSQL [vector](https://github.com/pgvector/pgvector) support for Python.
|
|
40
|
+
|
|
41
|
+
Registers data types `vector` and `halfvec` from the PostgreSQL extension `vector` to the asynchronous PostgreSQL client `asyncpg`, and marshals vector data to and from PostgreSQL database tables.
|
|
42
|
+
|
|
43
|
+
Internally, the data is packed into a Python `bytes` object, with single-precision float vectors stored on 4 bytes per item (for class `Vector`) and half-precision float vectors stored on 2 bytes per item (for class `HalfVector`). Data is (un)packed using `struct` from the standard library as necessary.
|
|
44
|
+
|
|
45
|
+
This module provides functionality similar to [pgvector-python](https://github.com/pgvector/pgvector-python) but imports minimum dependencies (e.g. no dependency on `numpy`).
|
|
46
|
+
|
|
47
|
+
## Setup
|
|
48
|
+
|
|
49
|
+
#### Install the package
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pip install asyncpg_vector
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Initialize
|
|
56
|
+
|
|
57
|
+
Register vector types with your database connection or connection pool:
|
|
58
|
+
|
|
59
|
+
**Connection**:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from asyncpg_vector import register_vector
|
|
63
|
+
|
|
64
|
+
async def main() -> None:
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
await conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
|
68
|
+
await register_vector(conn)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Pool**:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from asyncpg_vector import register_vector
|
|
75
|
+
|
|
76
|
+
async def init_connection(conn: asyncpg.Connection) -> None:
|
|
77
|
+
await conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
|
78
|
+
await register_vector(conn)
|
|
79
|
+
|
|
80
|
+
async def main() -> None:
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
pool = await asyncpg.create_pool(..., init=init_connection)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Perform similarity search
|
|
87
|
+
|
|
88
|
+
First, create a table and an index:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
async def create(conn: asyncpg.Connection) -> None:
|
|
92
|
+
await conn.execute("""
|
|
93
|
+
CREATE TABLE IF NOT EXISTS items
|
|
94
|
+
(
|
|
95
|
+
id bigint NOT NULL GENERATED ALWAYS AS IDENTITY,
|
|
96
|
+
content text NOT NULL,
|
|
97
|
+
embedding halfvec(1536) NOT NULL,
|
|
98
|
+
CONSTRAINT pk_items PRIMARY KEY (id)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE INDEX IF NOT EXISTS embedding_index ON items
|
|
102
|
+
USING hnsw (embedding halfvec_cosine_ops);
|
|
103
|
+
""")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Next, find documents in a knowledge base that match a search phrase using vector similarity with approximate nearest neighbor semantics:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from asyncpg_vector import HalfVector
|
|
110
|
+
|
|
111
|
+
async def search(conn: asyncpg.Connection, phrase: str) -> list[str]:
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
embedding_response = await ai_client.embeddings.create(
|
|
115
|
+
input=phrase,
|
|
116
|
+
model="text-embedding-3-small",
|
|
117
|
+
encoding_format="base64"
|
|
118
|
+
)
|
|
119
|
+
embedding = HalfVector.from_float_base64(embedding_response.data[0].embedding)
|
|
120
|
+
query = """
|
|
121
|
+
SELECT
|
|
122
|
+
id,
|
|
123
|
+
content,
|
|
124
|
+
embedding <=> $1 AS distance
|
|
125
|
+
FROM items
|
|
126
|
+
ORDER BY distance
|
|
127
|
+
LIMIT 5
|
|
128
|
+
"""
|
|
129
|
+
rows = await conn.fetch(query, embedding)
|
|
130
|
+
|
|
131
|
+
...
|
|
132
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
asyncpg_vector/__init__.py,sha256=0Tn14_Kza21A7VerWo8PJxZQYf7RC1KX9tLBAxaEbSA,6117
|
|
2
|
+
asyncpg_vector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
asyncpg_vector-0.1.0.dist-info/licenses/LICENSE,sha256=rx4jD36wX8TyLZaR2HEOJ6TphFPjKUqoCSSYWzwWNRk,1093
|
|
4
|
+
asyncpg_vector-0.1.0.dist-info/METADATA,sha256=e4ALWMZu08sXLT0X4qIayq0LcFJ3LJNsTMYAEUC_L40,4405
|
|
5
|
+
asyncpg_vector-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
asyncpg_vector-0.1.0.dist-info/top_level.txt,sha256=HlvoexcqaYDtjDfX9A6Nibq4_cEfyhj03nAqouJ4Tpg,15
|
|
7
|
+
asyncpg_vector-0.1.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
8
|
+
asyncpg_vector-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Levente Hunyadi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
asyncpg_vector
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|