Skip to content

Commit a471138

Browse files
committed
Add batch support to the async client
1 parent 2ac251f commit a471138

4 files changed

Lines changed: 406 additions & 18 deletions

File tree

‎client/python/USAGE.md‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ Example available in:
4141

4242
### Async Client Support
4343

44-
For async applications, use `AsyncVortexDB`. It mirrors the synchronous client API and uses `grpc.aio` under the hood.
44+
For async applications, use `AsyncVortexDB`. It mirrors the synchronous client API and uses `grpc.aio` under the hood, including full support for `batch_insert` and `batch_search`.
4545

46-
Example available in:
47-
```examples/async_usage.py```
46+
Examples available in:
47+
```examples/async_usage.py``` & ```examples/async_batch_usage.py```
4848

4949
```python
5050
async with AsyncVortexDB(
@@ -56,11 +56,12 @@ async with AsyncVortexDB(
5656
payload=Payload.text("hello async vortex"),
5757
)
5858
```
59+
5960
### Batch Insertion and Search Support
6061

61-
The client now supports batch insertion and batch search queries.
62+
Both `VortexDB` and `AsyncVortexDB` support batch insertion and batch search queries.
6263
Methods of usage and examples available in:
63-
```examples/batch_insert_usage.py``` & ```examples/search_query_usage.py```
64+
```examples/batch_insert_usage.py``` & ```examples/search_query_usage.py``` & ```examples/async_batch_usage.py```
6465

6566
---
6667

@@ -76,8 +77,10 @@ Async client class for I/O-heavy applications. It has the same constructor and m
7677

7778
```
7879
await db.insert(...)
80+
await db.batch_insert(...)
7981
await db.get(...)
8082
await db.search(...)
83+
await db.batch_search(...)
8184
await db.delete(...)
8285
await db.close()
8386
```
@@ -371,4 +374,4 @@ python -m grpc_tools.protoc \
371374

372375
After running this:
373376
- `vector_db_pb2_grpc.py` and `vector_db_pb2.py` will be updated
374-
- No other client code should need changes
377+
- No other client code should need changes
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import asyncio
2+
3+
from vortexdb import AsyncVortexDB
4+
from vortexdb import DenseVector, Payload, Similarity, SearchQuery, to_dense_vectors
5+
6+
7+
async def main():
8+
async with AsyncVortexDB(
9+
grpc_url="localhost:50051",
10+
api_key="my-secret-password",
11+
) as db:
12+
13+
raw_vectors = [
14+
[0.1, 0.2, 0.3],
15+
[0.4, 0.5, 0.6],
16+
[0.7, 0.8, 0.9],
17+
]
18+
vectors = to_dense_vectors(raw_vectors)
19+
20+
p1 = Payload.text("hello world")
21+
p2 = Payload.image("/img/a.png")
22+
p3 = Payload.text("foo bar")
23+
24+
items = [
25+
(vectors[0], p1),
26+
(vectors[1], p2),
27+
(vectors[2], p3),
28+
]
29+
30+
# Batch Insert
31+
point_ids = await db.batch_insert(items=items)
32+
print("Inserted ids:\n", point_ids)
33+
34+
q = SearchQuery(
35+
vector=vectors[0],
36+
similarity=Similarity.COSINE,
37+
limit=3,
38+
)
39+
res = await db.search(query=q)
40+
print("\nSingle SearchQuery:\n", res)
41+
42+
# List of SearchQuery
43+
queries = [
44+
SearchQuery(vectors[0], Similarity.HAMMING, 3),
45+
SearchQuery(vectors[1], Similarity.EUCLIDEAN, 2),
46+
q,
47+
]
48+
res = await db.batch_search(queries=queries)
49+
print("\nBatch SearchQuery:\n", res)
50+
51+
# List of vectors with global Similarity and Limit
52+
res = await db.batch_search(
53+
queries=vectors,
54+
similarity=Similarity.COSINE,
55+
limit=3,
56+
)
57+
print("\nList of DenseVectors:\n", res)
58+
59+
# List of tuple (DenseVector, Similarity) with global Limit
60+
queries = [
61+
(vectors[0], Similarity.COSINE),
62+
(vectors[1], Similarity.MANHATTAN),
63+
]
64+
res = await db.batch_search(
65+
queries=queries,
66+
limit=3,
67+
)
68+
print("\nList of (DenseVector, Similarity):\n", res)
69+
70+
# List of tuple (DenseVector, Limit) with global Similarity
71+
queries = [
72+
(vectors[0], 2),
73+
(vectors[1], 4),
74+
]
75+
res = await db.batch_search(
76+
queries=queries,
77+
similarity=Similarity.COSINE,
78+
)
79+
print("\nList of (DenseVector, Limit):\n", res)
80+
81+
for pid in point_ids:
82+
await db.delete(point_id=pid)
83+
84+
85+
if __name__ == "__main__":
86+
asyncio.run(main())

‎client/python/tests/test_async_client.py‎

Lines changed: 214 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from vortexdb.async_client import AsyncVortexDB
77
from vortexdb.async_connection import AsyncGRPCConnection
8-
from vortexdb.models import ContentType, DenseVector, Payload, Point, Similarity
8+
from vortexdb.models import ContentType, DenseVector, Payload, Point, Similarity, SearchQuery
99

1010

1111
@pytest.fixture
@@ -58,6 +58,59 @@ async def run():
5858
asyncio.run(run())
5959

6060

61+
# Batch Insert
62+
63+
def test_async_batch_insert_success(client, mock_connection):
64+
async def run():
65+
response = Mock()
66+
response.ids = [
67+
Mock(id=Mock(value="p1")),
68+
Mock(id=Mock(value="p2")),
69+
]
70+
mock_connection.call.return_value = response
71+
72+
items = [
73+
(DenseVector([1, 2, 3]), Payload.text("a")),
74+
(DenseVector([4, 5, 6]), Payload.text("b")),
75+
]
76+
result = await client.batch_insert(items=items)
77+
assert result == ["p1", "p2"]
78+
79+
asyncio.run(run())
80+
81+
82+
def test_async_batch_insert_invalid_items_type(client):
83+
async def run():
84+
with pytest.raises(TypeError):
85+
await client.batch_insert(items="not-a-list")
86+
87+
asyncio.run(run())
88+
89+
90+
def test_async_batch_insert_invalid_tuple_structure(client):
91+
async def run():
92+
items = [
93+
(DenseVector([1, 2, 3]),), # only one element
94+
]
95+
with pytest.raises(TypeError):
96+
await client.batch_insert(items=items)
97+
98+
asyncio.run(run())
99+
100+
101+
def test_async_batch_insert_invalid_vector(client):
102+
async def run():
103+
items = [
104+
([1, 2, 3], Payload.text("a")), # not DenseVector
105+
]
106+
with pytest.raises(TypeError):
107+
await client.batch_insert(items=items)
108+
109+
asyncio.run(run())
110+
111+
112+
# Get
113+
61114
def test_async_get_point_success(client, mock_connection):
62115
async def run():
63116
proto_point = Mock()
@@ -88,6 +141,8 @@ async def run():
88141
asyncio.run(run())
89142

90143

144+
# Delete
145+
91146
def test_async_delete_success(client, mock_connection):
92147
async def run():
93148
mock_connection.call.return_value = None
@@ -99,6 +154,8 @@ async def run():
99154
asyncio.run(run())
100155

101156

157+
# Search
158+
102159
def test_async_search_success(client, mock_connection):
103160
async def run():
104161
mock_connection.call.return_value = Mock(
@@ -119,6 +176,39 @@ async def run():
119176
asyncio.run(run())
120177

121178

179+
def test_async_search_with_query_object(client, mock_connection):
180+
async def run():
181+
mock_connection.call.return_value = Mock(
182+
result_point_ids=[
183+
Mock(id=Mock(value="p1")),
184+
]
185+
)
186+
187+
q = SearchQuery(DenseVector([1, 2, 3]), Similarity.COSINE, 2)
188+
results = await client.search(query=q)
189+
190+
assert results == ["p1"]
191+
192+
asyncio.run(run())
193+
194+
195+
def test_async_search_accepts_ef(client, mock_connection):
196+
async def run():
197+
mock_connection.call.return_value = Mock(result_point_ids=[])
198+
199+
await client.search(
200+
vector=DenseVector([1, 2, 3]),
201+
similarity=Similarity.COSINE,
202+
limit=2,
203+
ef=128,
204+
)
205+
206+
request = mock_connection.call.call_args.args[1]
207+
assert request.ef == 128
208+
209+
asyncio.run(run())
210+
211+
122212
def test_async_search_invalid_vector(client):
123213
async def run():
124214
with pytest.raises(TypeError):
@@ -131,6 +221,128 @@ async def run():
131221
asyncio.run(run())
132222

133223

224+
# Batch Search
225+
226+
def test_async_batch_search_full_tuple(client, mock_connection):
227+
async def run():
228+
mock_connection.call.return_value = Mock(
229+
results=[
230+
Mock(result_point_ids=[Mock(id=Mock(value="p1"))]),
231+
Mock(result_point_ids=[Mock(id=Mock(value="p2"))]),
232+
]
233+
)
234+
queries = [
235+
(DenseVector([1, 2, 3]), Similarity.COSINE, 2),
236+
(DenseVector([4, 5, 6]), Similarity.EUCLIDEAN, 1),
237+
]
238+
result = await client.batch_search(queries=queries)
239+
assert result == [["p1"], ["p2"]]
240+
241+
asyncio.run(run())
242+
243+
244+
def test_async_batch_search_searchquery_objects(client, mock_connection):
245+
async def run():
246+
mock_connection.call.return_value = Mock(
247+
results=[
248+
Mock(result_point_ids=[Mock(id=Mock(value="p1"))]),
249+
]
250+
)
251+
queries = [
252+
SearchQuery(DenseVector([1, 2, 3]), Similarity.COSINE, 2),
253+
]
254+
result = await client.batch_search(queries=queries)
255+
assert result == [["p1"]]
256+
257+
asyncio.run(run())
258+
259+
260+
def test_async_batch_search_vectors_with_global_params(client, mock_connection):
261+
async def run():
262+
mock_connection.call.return_value = Mock(
263+
results=[
264+
Mock(result_point_ids=[Mock(id=Mock(value="p1"))]),
265+
]
266+
)
267+
queries = [DenseVector([1, 2, 3])]
268+
result = await client.batch_search(
269+
queries=queries,
270+
similarity=Similarity.MANHATTAN,
271+
limit=2,
272+
)
273+
assert result == [["p1"]]
274+
275+
asyncio.run(run())
276+
277+
278+
def test_async_batch_search_vector_similarity_with_global_limit(client, mock_connection):
279+
async def run():
280+
mock_connection.call.return_value = Mock(
281+
results=[
282+
Mock(result_point_ids=[Mock(id=Mock(value="p1"))]),
283+
]
284+
)
285+
queries = [
286+
(DenseVector([1, 2, 3]), Similarity.COSINE),
287+
]
288+
result = await client.batch_search(
289+
queries=queries,
290+
limit=2,
291+
)
292+
assert result == [["p1"]]
293+
294+
asyncio.run(run())
295+
296+
297+
def test_async_batch_search_accepts_ef(client, mock_connection):
298+
async def run():
299+
mock_connection.call.return_value = Mock(results=[])
300+
301+
await client.batch_search(
302+
queries=[
303+
(DenseVector([1, 2, 3]), Similarity.COSINE, 2),
304+
(DenseVector([4, 5, 6]), Similarity.COSINE, 1),
305+
],
306+
ef=256,
307+
)
308+
309+
request = mock_connection.call.call_args.args[1]
310+
assert [query.ef for query in request.queries] == [256, 256]
311+
312+
asyncio.run(run())
313+
314+
315+
def test_async_batch_search_missing_globals_for_vector(client):
316+
async def run():
317+
queries = [DenseVector([1, 2, 3])]
318+
with pytest.raises(ValueError):
319+
await client.batch_search(queries=queries)
320+
321+
asyncio.run(run())
322+
323+
324+
def test_async_batch_search_missing_limit(client):
325+
async def run():
326+
queries = [
327+
(DenseVector([1, 2, 3]), Similarity.COSINE),
328+
]
329+
with pytest.raises(ValueError):
330+
await client.batch_search(queries=queries)
331+
332+
asyncio.run(run())
333+
334+
335+
def test_async_batch_search_invalid_format(client):
336+
async def run():
337+
queries = ["invalid"]
338+
with pytest.raises(TypeError):
339+
await client.batch_search(queries=queries)
340+
341+
asyncio.run(run())
342+
343+
344+
# Close / Context Manager
345+
134346
def test_async_close_closes_connection(client, mock_connection):
135347
async def run():
136348
await client.close()
@@ -155,4 +367,4 @@ async def run():
155367

156368
conn.close.assert_awaited_once()
157369

158-
asyncio.run(run())
370+
asyncio.run(run())

0 commit comments

Comments
 (0)