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.
@@ -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]
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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
+