Skip to content

Commit 7c908d4

Browse files
committed
Update Python client to support batch insert and search
1 parent b3d492f commit 7c908d4

10 files changed

Lines changed: 566 additions & 20 deletions

File tree

client/python/USAGE.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ The client supports usage as a context manager, which automatically closes the u
3939
Example available in:
4040
```examples/context_manager_usage.py```
4141

42+
### Batch Insertion and Search Support
43+
44+
The client now supports batch insertion and batch search queries.
45+
Methods of usage and examples available in:
46+
```examples/batch_insert_usage.py``` & ```examples/search_query_usage.py```
47+
4248
---
4349

4450
## Client API
@@ -78,6 +84,22 @@ Raises
7884

7985
---
8086

87+
#### **Batch Insert**
88+
89+
Insert multiple vectors with payloads in a single request
90+
```
91+
batch_insert(*, items: list[tuple[DenseVector, Payload]]) -> list[str]
92+
```
93+
94+
Returns
95+
- List of `point_id` (UUID string)
96+
97+
Raises
98+
- `TypeError` if input structure is invalid
99+
- gRPC-mapped errors (see Error Handling)
100+
101+
---
102+
81103
#### **Get**
82104

83105
Fetch a point by its ID
@@ -112,6 +134,32 @@ Raises
112134

113135
---
114136

137+
#### **Batch Search**
138+
139+
Search for nearest neighbours for multiple queries in a single request
140+
```
141+
batch_search(
142+
*,
143+
queries,
144+
similarity: Similarity | None = None,
145+
limit: int | None = None,
146+
) -> list[list[str]]
147+
```
148+
149+
Returns
150+
- `TypeError` for invalid query formats
151+
- `ValueError` if required parameters are missing
152+
153+
Supported Input Formats:
154+
The `queries` parameter is flexible and supports multiple formats:
155+
- List of `SearchQuery` objects
156+
- List of `(DenseVector, Similarity, Limit)` tuples
157+
- List of `(DenseVector, Similarity)` tuples with a global `Limit`
158+
- List of `(DenseVector, Limit)` tuples with a global `Similarity`
159+
- List of `DenseVector` with global `Similarity` and `Limit`
160+
161+
---
162+
115163
#### **Delete**
116164

117165
Delete a point by its ID
@@ -177,6 +225,19 @@ All fields are directly accessible:
177225

178226
---
179227

228+
### `SearchQuery`
229+
230+
```
231+
SearchQuery(
232+
vector: DenseVector,
233+
similarity: Similarity,
234+
limit: int,
235+
)
236+
```
237+
Structured representation of a search request
238+
239+
---
240+
180241
### `Similarity`
181242

182243
Enum representing distance functions:
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from vortexdb import VortexDB
2+
from vortexdb import DenseVector, Payload, to_dense_vectors
3+
4+
5+
def main():
6+
db = VortexDB(
7+
grpc_url="localhost:50051",
8+
api_key="my-secret-password",
9+
)
10+
11+
raw_vectors = [
12+
[0.1, 0.2, 0.3],
13+
[0.4, 0.5, 0.6],
14+
[0.7, 0.8, 0.9],
15+
]
16+
vectors = to_dense_vectors(raw_vectors)
17+
18+
p1 = Payload.text("hello world")
19+
p2 = Payload.image("/img/a.png")
20+
p3 = Payload.text("foo bar")
21+
22+
items = [
23+
(vectors[0], p1),
24+
(vectors[1], p2),
25+
(vectors[2], p3),
26+
]
27+
28+
# Batch Insert
29+
point_ids = db.batch_insert(items=items)
30+
print("Inserted ids:\n", point_ids)
31+
32+
for pid in point_ids:
33+
db.delete(point_id=pid)
34+
35+
db.close()
36+
37+
38+
if __name__ == "__main__":
39+
main()
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from vortexdb import VortexDB
2+
from vortexdb import DenseVector, Similarity, SearchQuery, to_dense_vectors
3+
4+
5+
def main():
6+
db = VortexDB(
7+
grpc_url="localhost:50051",
8+
api_key="my-secret-password",
9+
)
10+
11+
raw_vectors = [
12+
[0.1, 0.2, 0.3],
13+
[0.4, 0.5, 0.6],
14+
[0.7, 0.8, 0.9],
15+
]
16+
vectors = to_dense_vectors(raw_vectors)
17+
18+
q = SearchQuery(
19+
vector=vectors[0],
20+
similarity=Similarity.COSINE,
21+
limit=3,
22+
)
23+
res = db.search(query=q)
24+
print("Single SearchQuery:\n", res)
25+
26+
# List of SearchQuery
27+
queries = [
28+
SearchQuery(vectors[0], Similarity.HAMMING, 3),
29+
SearchQuery(vectors[1], Similarity.EUCLIDEAN, 2),
30+
q,
31+
]
32+
res = db.batch_search(queries=queries)
33+
print("\nBatch SearchQuery:\n", res)
34+
35+
# List of vectors with global Similarity and Limit
36+
res = db.batch_search(
37+
queries=vectors,
38+
similarity=Similarity.COSINE,
39+
limit=3,
40+
)
41+
print("\nList of DenseVectors:\n", res)
42+
43+
# List of tuple (DenseVector, Similarity) with global Limit
44+
queries = [
45+
(vectors[0], Similarity.COSINE),
46+
(vectors[1], Similarity.MANHATTAN),
47+
]
48+
res = db.batch_search(
49+
queries=queries,
50+
limit=3,
51+
)
52+
print("\nList of (DenseVector, Similarity):\n", res)
53+
54+
# List of tuple (DenseVector, Limit) with global Similarity
55+
queries = [
56+
(vectors[0], 2),
57+
(vectors[1], 4),
58+
]
59+
res = db.batch_search(
60+
queries=queries,
61+
similarity=Similarity.COSINE,
62+
)
63+
print("\nList of (DenseVector, Limit):\n", res)
64+
65+
db.close()
66+
67+
68+
if __name__ == "__main__":
69+
main()

client/python/tests/test_client.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from vortexdb.connection import GRPCConnection
66
from vortexdb.models import DenseVector, Payload, Similarity, ContentType, Point
77
from vortexdb.exceptions import InvalidArgumentError
8+
from vortexdb.models import SearchQuery
89

910

1011

@@ -45,7 +46,6 @@ def test_insert_success(client, mock_connection):
4546
assert point_id == "point-123"
4647

4748

48-
4949
def test_insert_rejects_invalid_vector(client):
5050
with pytest.raises(TypeError):
5151
client.insert(
@@ -54,6 +54,41 @@ def test_insert_rejects_invalid_vector(client):
5454
)
5555

5656

57+
# Batch Insert
58+
59+
def test_batch_insert_success(client, mock_connection):
60+
response = Mock()
61+
response.ids = [
62+
Mock(id=Mock(value="p1")),
63+
Mock(id=Mock(value="p2")),
64+
]
65+
mock_connection.call.return_value = response
66+
items = [
67+
(DenseVector([1, 2, 3]), Payload.text("a")),
68+
(DenseVector([4, 5, 6]), Payload.text("b")),
69+
]
70+
result = client.batch_insert(items=items)
71+
assert result == ["p1", "p2"]
72+
73+
def test_batch_insert_invalid_items_type(client):
74+
with pytest.raises(TypeError):
75+
client.batch_insert(items="not-a-list")
76+
77+
def test_batch_insert_invalid_tuple_structure(client):
78+
items = [
79+
(DenseVector([1, 2, 3]),), # only one element
80+
]
81+
with pytest.raises(TypeError):
82+
client.batch_insert(items=items)
83+
84+
def test_batch_insert_invalid_vector(client):
85+
items = [
86+
([1, 2, 3], Payload.text("a")), # not DenseVector
87+
]
88+
with pytest.raises(TypeError):
89+
client.batch_insert(items=items)
90+
91+
5792
# Get
5893

5994
def test_get_point_success(client, mock_connection):
@@ -118,6 +153,80 @@ def test_search_invalid_vector(client):
118153
)
119154

120155

156+
# Batch Search
157+
158+
def test_batch_search_full_tuple(client, mock_connection):
159+
mock_connection.call.return_value = Mock(
160+
results=[
161+
Mock(result_point_ids=[Mock(id=Mock(value="p1"))]),
162+
Mock(result_point_ids=[Mock(id=Mock(value="p2"))]),
163+
]
164+
)
165+
queries = [
166+
(DenseVector([1, 2, 3]), Similarity.COSINE, 2),
167+
(DenseVector([4, 5, 6]), Similarity.EUCLIDEAN, 1),
168+
]
169+
result = client.batch_search(queries=queries)
170+
assert result == [["p1"], ["p2"]]
171+
172+
def test_batch_search_vectors_with_global_params(client, mock_connection):
173+
mock_connection.call.return_value = Mock(
174+
results=[
175+
Mock(result_point_ids=[Mock(id=Mock(value="p1"))]),
176+
]
177+
)
178+
queries = [DenseVector([1, 2, 3])]
179+
result = client.batch_search(
180+
queries=queries,
181+
similarity=Similarity.MANHATTAN,
182+
limit=2,
183+
)
184+
assert result == [["p1"]]
185+
186+
def test_batch_search_vector_similarity_with_global_limit(client, mock_connection):
187+
mock_connection.call.return_value = Mock(
188+
results=[
189+
Mock(result_point_ids=[Mock(id=Mock(value="p1"))]),
190+
]
191+
)
192+
queries = [
193+
(DenseVector([1, 2, 3]), Similarity.COSINE),
194+
]
195+
result = client.batch_search(
196+
queries=queries,
197+
limit=2,
198+
)
199+
assert result == [["p1"]]
200+
201+
def test_batch_search_searchquery_objects(client, mock_connection):
202+
mock_connection.call.return_value = Mock(
203+
results=[
204+
Mock(result_point_ids=[Mock(id=Mock(value="p1"))]),
205+
]
206+
)
207+
queries = [
208+
SearchQuery(DenseVector([1, 2, 3]), Similarity.COSINE, 2),
209+
]
210+
result = client.batch_search(queries=queries)
211+
assert result == [["p1"]]
212+
213+
def test_batch_search_missing_globals_for_vector(client):
214+
queries = [DenseVector([1, 2, 3])]
215+
with pytest.raises(ValueError):
216+
client.batch_search(queries=queries)
217+
218+
def test_batch_search_missing_limit(client):
219+
queries = [
220+
(DenseVector([1, 2, 3]), Similarity.COSINE),
221+
]
222+
with pytest.raises(ValueError):
223+
client.batch_search(queries=queries)
224+
225+
def test_batch_search_invalid_format(client):
226+
queries = ["invalid"]
227+
with pytest.raises(TypeError):
228+
client.batch_search(queries=queries)
229+
121230
# Close
122231

123232
def test_close_closes_connection(client, mock_connection):

client/python/vortexdb/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
Payload,
77
Point,
88
Similarity,
9+
SearchQuery,
10+
to_dense_vectors,
911
)
1012
from vortexdb.exceptions import (
1113
VortexDBError,
@@ -23,6 +25,7 @@
2325
"Payload",
2426
"Point",
2527
"Similarity",
28+
"SearchQuery",
2629
"VortexDBError",
2730
"AuthenticationError",
2831
"NotFoundError",

0 commit comments

Comments
 (0)