vector-inspector 0.2.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.
- vector_inspector/__init__.py +3 -0
- vector_inspector/__main__.py +4 -0
- vector_inspector/core/__init__.py +1 -0
- vector_inspector/core/connections/__init__.py +7 -0
- vector_inspector/core/connections/base_connection.py +233 -0
- vector_inspector/core/connections/chroma_connection.py +384 -0
- vector_inspector/core/connections/qdrant_connection.py +723 -0
- vector_inspector/core/connections/template_connection.py +346 -0
- vector_inspector/main.py +21 -0
- vector_inspector/services/__init__.py +1 -0
- vector_inspector/services/backup_restore_service.py +286 -0
- vector_inspector/services/filter_service.py +72 -0
- vector_inspector/services/import_export_service.py +287 -0
- vector_inspector/services/settings_service.py +60 -0
- vector_inspector/services/visualization_service.py +116 -0
- vector_inspector/ui/__init__.py +1 -0
- vector_inspector/ui/components/__init__.py +1 -0
- vector_inspector/ui/components/backup_restore_dialog.py +350 -0
- vector_inspector/ui/components/filter_builder.py +370 -0
- vector_inspector/ui/components/item_dialog.py +118 -0
- vector_inspector/ui/components/loading_dialog.py +30 -0
- vector_inspector/ui/main_window.py +288 -0
- vector_inspector/ui/views/__init__.py +1 -0
- vector_inspector/ui/views/collection_browser.py +112 -0
- vector_inspector/ui/views/connection_view.py +423 -0
- vector_inspector/ui/views/metadata_view.py +555 -0
- vector_inspector/ui/views/search_view.py +268 -0
- vector_inspector/ui/views/visualization_view.py +245 -0
- vector_inspector-0.2.0.dist-info/METADATA +382 -0
- vector_inspector-0.2.0.dist-info/RECORD +32 -0
- vector_inspector-0.2.0.dist-info/WHEEL +4 -0
- vector_inspector-0.2.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"""Qdrant connection manager."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
|
+
import uuid
|
|
5
|
+
from qdrant_client import QdrantClient
|
|
6
|
+
from qdrant_client.models import (
|
|
7
|
+
Distance, VectorParams, PointStruct,
|
|
8
|
+
Filter, FieldCondition, MatchValue, MatchText, MatchAny, MatchExcept, Range
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from .base_connection import VectorDBConnection
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QdrantConnection(VectorDBConnection):
|
|
15
|
+
"""Manages connection to Qdrant and provides query interface."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
path: Optional[str] = None,
|
|
20
|
+
url: Optional[str] = None,
|
|
21
|
+
host: Optional[str] = None,
|
|
22
|
+
port: Optional[int] = None,
|
|
23
|
+
api_key: Optional[str] = None,
|
|
24
|
+
prefer_grpc: bool = False
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Initialize Qdrant connection.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
path: Path for local/embedded client
|
|
31
|
+
url: Full URL for remote client (e.g., "http://localhost:6333")
|
|
32
|
+
host: Host for remote client
|
|
33
|
+
port: Port for remote client (default: 6333)
|
|
34
|
+
api_key: API key for authentication (Qdrant Cloud)
|
|
35
|
+
prefer_grpc: Use gRPC instead of REST
|
|
36
|
+
"""
|
|
37
|
+
self.path = path
|
|
38
|
+
self.url = url
|
|
39
|
+
self.host = host
|
|
40
|
+
self.port = port or 6333
|
|
41
|
+
self.api_key = api_key
|
|
42
|
+
self.prefer_grpc = prefer_grpc
|
|
43
|
+
self._client: Optional[QdrantClient] = None
|
|
44
|
+
|
|
45
|
+
def connect(self) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Establish connection to Qdrant.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if connection successful, False otherwise
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
if self.path:
|
|
54
|
+
# Local/embedded mode
|
|
55
|
+
self._client = QdrantClient(path=self.path, check_compatibility=False)
|
|
56
|
+
elif self.url:
|
|
57
|
+
# Full URL provided
|
|
58
|
+
self._client = QdrantClient(
|
|
59
|
+
url=self.url,
|
|
60
|
+
api_key=self.api_key,
|
|
61
|
+
prefer_grpc=self.prefer_grpc,
|
|
62
|
+
check_compatibility=False
|
|
63
|
+
)
|
|
64
|
+
elif self.host:
|
|
65
|
+
# Host and port provided
|
|
66
|
+
self._client = QdrantClient(
|
|
67
|
+
host=self.host,
|
|
68
|
+
port=self.port,
|
|
69
|
+
api_key=self.api_key,
|
|
70
|
+
prefer_grpc=self.prefer_grpc,
|
|
71
|
+
check_compatibility=False
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
# Default to in-memory client
|
|
75
|
+
self._client = QdrantClient(":memory:", check_compatibility=False)
|
|
76
|
+
|
|
77
|
+
# Test connection
|
|
78
|
+
self._client.get_collections()
|
|
79
|
+
return True
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"Connection failed: {e}")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def _to_uuid(self, id_str: str) -> uuid.UUID:
|
|
85
|
+
"""Convert a string ID to a valid UUID.
|
|
86
|
+
|
|
87
|
+
If the string is already a valid UUID, return it.
|
|
88
|
+
Otherwise, generate a deterministic UUID from the string.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
return uuid.UUID(id_str)
|
|
92
|
+
except (ValueError, AttributeError):
|
|
93
|
+
# Generate deterministic UUID from string
|
|
94
|
+
return uuid.uuid5(uuid.NAMESPACE_DNS, id_str)
|
|
95
|
+
|
|
96
|
+
def disconnect(self):
|
|
97
|
+
"""Close connection to Qdrant."""
|
|
98
|
+
if self._client:
|
|
99
|
+
self._client.close()
|
|
100
|
+
self._client = None
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def is_connected(self) -> bool:
|
|
104
|
+
"""Check if connected to Qdrant."""
|
|
105
|
+
return self._client is not None
|
|
106
|
+
|
|
107
|
+
def count_collection(self, name: str) -> int:
|
|
108
|
+
"""Count the number of items in a collection."""
|
|
109
|
+
if not self._client:
|
|
110
|
+
return 0
|
|
111
|
+
try:
|
|
112
|
+
res = self._client.count(collection_name=name)
|
|
113
|
+
return getattr(res, "count", 0) or 0
|
|
114
|
+
except Exception:
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
def get_items(self, name: str, ids: List[str]) -> Dict[str, Any]:
|
|
118
|
+
"""
|
|
119
|
+
Get items by IDs (implementation for compatibility).
|
|
120
|
+
|
|
121
|
+
Note: This is a simplified implementation that retrieves items by scrolling
|
|
122
|
+
and filtering. For production use, consider using get_all_items with filters.
|
|
123
|
+
"""
|
|
124
|
+
if not self._client:
|
|
125
|
+
return {"documents": [], "metadatas": []}
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Retrieve by scrolling and filtering
|
|
129
|
+
all_items = self.get_all_items(name, limit=1000)
|
|
130
|
+
if not all_items:
|
|
131
|
+
return {"documents": [], "metadatas": []}
|
|
132
|
+
|
|
133
|
+
# Filter by requested IDs
|
|
134
|
+
documents = []
|
|
135
|
+
metadatas = []
|
|
136
|
+
for i, item_id in enumerate(all_items.get("ids", [])):
|
|
137
|
+
if item_id in ids:
|
|
138
|
+
documents.append(all_items["documents"][i])
|
|
139
|
+
metadatas.append(all_items["metadatas"][i])
|
|
140
|
+
|
|
141
|
+
return {"documents": documents, "metadatas": metadatas}
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"Failed to get items: {e}")
|
|
144
|
+
return {"documents": [], "metadatas": []}
|
|
145
|
+
|
|
146
|
+
def list_collections(self) -> List[str]:
|
|
147
|
+
"""
|
|
148
|
+
Get list of all collections.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of collection names
|
|
152
|
+
"""
|
|
153
|
+
if not self._client:
|
|
154
|
+
return []
|
|
155
|
+
try:
|
|
156
|
+
collections = self._client.get_collections()
|
|
157
|
+
return [col.name for col in collections.collections]
|
|
158
|
+
except Exception as e:
|
|
159
|
+
print(f"Failed to list collections: {e}")
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
def get_collection_info(self, name: str) -> Optional[Dict[str, Any]]:
|
|
163
|
+
"""
|
|
164
|
+
Get collection metadata and statistics.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
name: Collection name
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Dictionary with collection info
|
|
171
|
+
"""
|
|
172
|
+
if not self._client:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
# Get collection info
|
|
177
|
+
collection_info = self._client.get_collection(name)
|
|
178
|
+
|
|
179
|
+
# Get a sample point to determine metadata fields
|
|
180
|
+
sample = self._client.scroll(
|
|
181
|
+
collection_name=name,
|
|
182
|
+
limit=1,
|
|
183
|
+
with_payload=True,
|
|
184
|
+
with_vectors=False
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
metadata_fields = []
|
|
188
|
+
if sample[0] and len(sample[0]) > 0:
|
|
189
|
+
point = sample[0][0]
|
|
190
|
+
if point.payload:
|
|
191
|
+
# Extract metadata fields, excluding 'document' if present
|
|
192
|
+
metadata_fields = [k for k in point.payload.keys() if k != 'document']
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"name": name,
|
|
196
|
+
"count": collection_info.points_count,
|
|
197
|
+
"metadata_fields": metadata_fields,
|
|
198
|
+
}
|
|
199
|
+
except Exception as e:
|
|
200
|
+
print(f"Failed to get collection info: {e}")
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def _build_qdrant_filter(self, where: Optional[Dict[str, Any]] = None) -> Optional[Filter]:
|
|
204
|
+
"""
|
|
205
|
+
Build Qdrant filter from ChromaDB-style where clause.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
where: ChromaDB-style filter dictionary
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Qdrant Filter object or None
|
|
212
|
+
"""
|
|
213
|
+
if not where:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
must_conditions = []
|
|
218
|
+
must_not_conditions = []
|
|
219
|
+
|
|
220
|
+
for key, value in where.items():
|
|
221
|
+
if isinstance(value, dict):
|
|
222
|
+
# Handle operators like $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains, $not_contains
|
|
223
|
+
for op, val in value.items():
|
|
224
|
+
if op == "$eq":
|
|
225
|
+
must_conditions.append(FieldCondition(key=key, match=MatchValue(value=val)))
|
|
226
|
+
elif op == "$ne":
|
|
227
|
+
# Use must_not for not-equal
|
|
228
|
+
must_not_conditions.append(FieldCondition(key=key, match=MatchValue(value=val)))
|
|
229
|
+
elif op == "$in":
|
|
230
|
+
# Use MatchAny for IN operator (available since v1.1.0)
|
|
231
|
+
must_conditions.append(FieldCondition(key=key, match=MatchAny(any=val)))
|
|
232
|
+
elif op == "$nin":
|
|
233
|
+
# Use MatchExcept for NOT IN operator (available since v1.2.0)
|
|
234
|
+
must_conditions.append(FieldCondition(key=key, match=MatchExcept(**{"except": val})))
|
|
235
|
+
elif op == "$contains":
|
|
236
|
+
# Text matching in Qdrant (uses full-text index if available)
|
|
237
|
+
must_conditions.append(FieldCondition(key=key, match=MatchText(text=str(val))))
|
|
238
|
+
elif op == "$not_contains":
|
|
239
|
+
# Negative text matching using must_not
|
|
240
|
+
must_not_conditions.append(FieldCondition(key=key, match=MatchText(text=str(val))))
|
|
241
|
+
elif op in ["$gt", "$gte", "$lt", "$lte"]:
|
|
242
|
+
range_args = {}
|
|
243
|
+
if op == "$gt":
|
|
244
|
+
range_args["gt"] = val
|
|
245
|
+
elif op == "$gte":
|
|
246
|
+
range_args["gte"] = val
|
|
247
|
+
elif op == "$lt":
|
|
248
|
+
range_args["lt"] = val
|
|
249
|
+
elif op == "$lte":
|
|
250
|
+
range_args["lte"] = val
|
|
251
|
+
must_conditions.append(FieldCondition(key=key, range=Range(**range_args)))
|
|
252
|
+
else:
|
|
253
|
+
# Direct equality match
|
|
254
|
+
must_conditions.append(FieldCondition(key=key, match=MatchValue(value=value)))
|
|
255
|
+
|
|
256
|
+
if must_conditions or must_not_conditions:
|
|
257
|
+
return Filter(must=must_conditions if must_conditions else None,
|
|
258
|
+
must_not=must_not_conditions if must_not_conditions else None)
|
|
259
|
+
return None
|
|
260
|
+
except Exception as e:
|
|
261
|
+
print(f"Failed to build filter: {e}")
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def query_collection(
|
|
265
|
+
self,
|
|
266
|
+
collection_name: str,
|
|
267
|
+
query_texts: Optional[List[str]] = None,
|
|
268
|
+
query_embeddings: Optional[List[List[float]]] = None,
|
|
269
|
+
n_results: int = 10,
|
|
270
|
+
where: Optional[Dict[str, Any]] = None,
|
|
271
|
+
where_document: Optional[Dict[str, Any]] = None,
|
|
272
|
+
) -> Optional[Dict[str, Any]]:
|
|
273
|
+
"""
|
|
274
|
+
Query a collection for similar vectors.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
collection_name: Name of collection to query
|
|
278
|
+
query_texts: Text queries (Qdrant will embed automatically)
|
|
279
|
+
query_embeddings: Direct embedding vectors to search
|
|
280
|
+
n_results: Number of results to return
|
|
281
|
+
where: Metadata filter
|
|
282
|
+
where_document: Document content filter (limited support)
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Query results or None if failed
|
|
286
|
+
"""
|
|
287
|
+
if not self._client:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
if not query_texts and not query_embeddings:
|
|
291
|
+
print("Either query_texts or query_embeddings required")
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# Build filter
|
|
296
|
+
qdrant_filter = self._build_qdrant_filter(where)
|
|
297
|
+
|
|
298
|
+
# Perform search for each query
|
|
299
|
+
all_results = {
|
|
300
|
+
"ids": [],
|
|
301
|
+
"distances": [],
|
|
302
|
+
"documents": [],
|
|
303
|
+
"metadatas": [],
|
|
304
|
+
"embeddings": []
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# Use query_texts if provided (Qdrant handles embedding)
|
|
308
|
+
queries = query_texts if query_texts else []
|
|
309
|
+
|
|
310
|
+
# If embeddings provided instead, use them
|
|
311
|
+
if query_embeddings and not query_texts:
|
|
312
|
+
queries = query_embeddings
|
|
313
|
+
|
|
314
|
+
for query in queries:
|
|
315
|
+
# Embed text queries if needed
|
|
316
|
+
if isinstance(query, str):
|
|
317
|
+
# Generate embeddings for text query
|
|
318
|
+
try:
|
|
319
|
+
from sentence_transformers import SentenceTransformer
|
|
320
|
+
model = SentenceTransformer("all-MiniLM-L6-v2")
|
|
321
|
+
query_vector = model.encode(query).tolist()
|
|
322
|
+
except Exception as e:
|
|
323
|
+
print(f"Failed to embed query text: {e}")
|
|
324
|
+
continue
|
|
325
|
+
else:
|
|
326
|
+
query_vector = query
|
|
327
|
+
|
|
328
|
+
# Use modern query_points API
|
|
329
|
+
try:
|
|
330
|
+
res = self._client.query_points(
|
|
331
|
+
collection_name=collection_name,
|
|
332
|
+
query=query_vector,
|
|
333
|
+
limit=n_results,
|
|
334
|
+
query_filter=qdrant_filter,
|
|
335
|
+
with_payload=True,
|
|
336
|
+
with_vectors=True,
|
|
337
|
+
)
|
|
338
|
+
search_results = getattr(res, "points", res)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
print(f"Query failed: {e}")
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
# Transform results to standard format
|
|
344
|
+
ids = []
|
|
345
|
+
distances = []
|
|
346
|
+
documents = []
|
|
347
|
+
metadatas = []
|
|
348
|
+
embeddings = []
|
|
349
|
+
|
|
350
|
+
for result in search_results:
|
|
351
|
+
ids.append(str(result.id))
|
|
352
|
+
distances.append(result.score)
|
|
353
|
+
|
|
354
|
+
# Extract document and metadata from payload
|
|
355
|
+
payload = result.payload or {}
|
|
356
|
+
documents.append(payload.get('document', ''))
|
|
357
|
+
|
|
358
|
+
# Metadata is everything except 'document'
|
|
359
|
+
metadata = {k: v for k, v in payload.items() if k != 'document'}
|
|
360
|
+
metadatas.append(metadata)
|
|
361
|
+
|
|
362
|
+
# Extract embedding
|
|
363
|
+
embeddings.append(result.vector if result.vector else [])
|
|
364
|
+
|
|
365
|
+
all_results["ids"].append(ids)
|
|
366
|
+
all_results["distances"].append(distances)
|
|
367
|
+
all_results["documents"].append(documents)
|
|
368
|
+
all_results["metadatas"].append(metadatas)
|
|
369
|
+
all_results["embeddings"].append(embeddings)
|
|
370
|
+
|
|
371
|
+
return all_results
|
|
372
|
+
except Exception as e:
|
|
373
|
+
print(f"Query failed: {e}")
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
def get_all_items(
|
|
377
|
+
self,
|
|
378
|
+
collection_name: str,
|
|
379
|
+
limit: Optional[int] = None,
|
|
380
|
+
offset: Optional[int] = None,
|
|
381
|
+
where: Optional[Dict[str, Any]] = None,
|
|
382
|
+
) -> Optional[Dict[str, Any]]:
|
|
383
|
+
"""
|
|
384
|
+
Get all items from a collection.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
collection_name: Name of collection
|
|
388
|
+
limit: Maximum number of items to return
|
|
389
|
+
offset: Number of items to skip
|
|
390
|
+
where: Metadata filter
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Collection items or None if failed
|
|
394
|
+
"""
|
|
395
|
+
if not self._client:
|
|
396
|
+
return None
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
# Build filter
|
|
400
|
+
qdrant_filter = self._build_qdrant_filter(where)
|
|
401
|
+
|
|
402
|
+
# Use scroll to retrieve items
|
|
403
|
+
points, next_offset = self._client.scroll(
|
|
404
|
+
collection_name=collection_name,
|
|
405
|
+
scroll_filter=qdrant_filter,
|
|
406
|
+
limit=limit,
|
|
407
|
+
offset=offset,
|
|
408
|
+
with_payload=True,
|
|
409
|
+
with_vectors=True
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Transform to standard format
|
|
413
|
+
ids = []
|
|
414
|
+
documents = []
|
|
415
|
+
metadatas = []
|
|
416
|
+
embeddings = []
|
|
417
|
+
|
|
418
|
+
for point in points:
|
|
419
|
+
ids.append(str(point.id))
|
|
420
|
+
|
|
421
|
+
payload = point.payload or {}
|
|
422
|
+
documents.append(payload.get('document', ''))
|
|
423
|
+
|
|
424
|
+
# Metadata is everything except 'document'
|
|
425
|
+
metadata = {k: v for k, v in payload.items() if k != 'document'}
|
|
426
|
+
metadatas.append(metadata)
|
|
427
|
+
|
|
428
|
+
# Extract embedding
|
|
429
|
+
if isinstance(point.vector, dict):
|
|
430
|
+
# Named vectors - use the first one
|
|
431
|
+
embeddings.append(list(point.vector.values())[0] if point.vector else [])
|
|
432
|
+
else:
|
|
433
|
+
embeddings.append(point.vector if point.vector else [])
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
"ids": ids,
|
|
437
|
+
"documents": documents,
|
|
438
|
+
"metadatas": metadatas,
|
|
439
|
+
"embeddings": embeddings
|
|
440
|
+
}
|
|
441
|
+
except Exception as e:
|
|
442
|
+
print(f"Failed to get items: {e}")
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
def add_items(
|
|
446
|
+
self,
|
|
447
|
+
collection_name: str,
|
|
448
|
+
documents: List[str],
|
|
449
|
+
metadatas: Optional[List[Dict[str, Any]]] = None,
|
|
450
|
+
ids: Optional[List[str]] = None,
|
|
451
|
+
embeddings: Optional[List[List[float]]] = None,
|
|
452
|
+
) -> bool:
|
|
453
|
+
"""
|
|
454
|
+
Add items to a collection.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
collection_name: Name of collection
|
|
458
|
+
documents: Document texts
|
|
459
|
+
metadatas: Metadata for each document
|
|
460
|
+
ids: IDs for each document (will generate UUIDs if not provided)
|
|
461
|
+
embeddings: Pre-computed embeddings (required for Qdrant)
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
True if successful, False otherwise
|
|
465
|
+
"""
|
|
466
|
+
if not self._client:
|
|
467
|
+
return False
|
|
468
|
+
|
|
469
|
+
if not embeddings:
|
|
470
|
+
print("Embeddings are required for Qdrant")
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
# Generate IDs if not provided
|
|
475
|
+
if not ids:
|
|
476
|
+
ids = [str(uuid.uuid4()) for _ in documents]
|
|
477
|
+
|
|
478
|
+
# Build points
|
|
479
|
+
points = []
|
|
480
|
+
for i, (doc_id, document, embedding) in enumerate(zip(ids, documents, embeddings)):
|
|
481
|
+
# Build payload with document and metadata
|
|
482
|
+
payload = {"document": document}
|
|
483
|
+
if metadatas and i < len(metadatas):
|
|
484
|
+
payload.update(metadatas[i])
|
|
485
|
+
|
|
486
|
+
# Convert string ID to UUID
|
|
487
|
+
point_id = self._to_uuid(doc_id)
|
|
488
|
+
|
|
489
|
+
point = PointStruct(
|
|
490
|
+
id=point_id,
|
|
491
|
+
vector=embedding,
|
|
492
|
+
payload=payload
|
|
493
|
+
)
|
|
494
|
+
points.append(point)
|
|
495
|
+
|
|
496
|
+
# Upsert points
|
|
497
|
+
self._client.upsert(
|
|
498
|
+
collection_name=collection_name,
|
|
499
|
+
points=points
|
|
500
|
+
)
|
|
501
|
+
return True
|
|
502
|
+
except Exception as e:
|
|
503
|
+
print(f"Failed to add items: {e}")
|
|
504
|
+
return False
|
|
505
|
+
|
|
506
|
+
def update_items(
|
|
507
|
+
self,
|
|
508
|
+
collection_name: str,
|
|
509
|
+
ids: List[str],
|
|
510
|
+
documents: Optional[List[str]] = None,
|
|
511
|
+
metadatas: Optional[List[Dict[str, Any]]] = None,
|
|
512
|
+
embeddings: Optional[List[List[float]]] = None,
|
|
513
|
+
) -> bool:
|
|
514
|
+
"""
|
|
515
|
+
Update items in a collection.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
collection_name: Name of collection
|
|
519
|
+
ids: IDs of items to update
|
|
520
|
+
documents: New document texts
|
|
521
|
+
metadatas: New metadata
|
|
522
|
+
embeddings: New embeddings
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
True if successful, False otherwise
|
|
526
|
+
"""
|
|
527
|
+
if not self._client:
|
|
528
|
+
return False
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
# For Qdrant, we need to retrieve existing points, update them, and upsert
|
|
532
|
+
for i, point_id in enumerate(ids):
|
|
533
|
+
# Get existing point
|
|
534
|
+
existing = self._client.retrieve(
|
|
535
|
+
collection_name=collection_name,
|
|
536
|
+
ids=[point_id],
|
|
537
|
+
with_payload=True,
|
|
538
|
+
with_vectors=True
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if not existing:
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
point = existing[0]
|
|
545
|
+
payload = point.payload or {}
|
|
546
|
+
vector = point.vector
|
|
547
|
+
|
|
548
|
+
# Update fields as provided
|
|
549
|
+
if documents and i < len(documents):
|
|
550
|
+
payload['document'] = documents[i]
|
|
551
|
+
|
|
552
|
+
if metadatas and i < len(metadatas):
|
|
553
|
+
# Update metadata, keeping 'document' field
|
|
554
|
+
doc = payload.get('document', '')
|
|
555
|
+
payload = metadatas[i].copy()
|
|
556
|
+
payload['document'] = doc
|
|
557
|
+
|
|
558
|
+
if embeddings and i < len(embeddings):
|
|
559
|
+
vector = embeddings[i]
|
|
560
|
+
|
|
561
|
+
# Upsert updated point
|
|
562
|
+
self._client.upsert(
|
|
563
|
+
collection_name=collection_name,
|
|
564
|
+
points=[PointStruct(id=point_id, vector=vector, payload=payload)]
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
return True
|
|
568
|
+
except Exception as e:
|
|
569
|
+
print(f"Failed to update items: {e}")
|
|
570
|
+
return False
|
|
571
|
+
|
|
572
|
+
def delete_items(
|
|
573
|
+
self,
|
|
574
|
+
collection_name: str,
|
|
575
|
+
ids: Optional[List[str]] = None,
|
|
576
|
+
where: Optional[Dict[str, Any]] = None,
|
|
577
|
+
) -> bool:
|
|
578
|
+
"""
|
|
579
|
+
Delete items from a collection.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
collection_name: Name of collection
|
|
583
|
+
ids: IDs of items to delete
|
|
584
|
+
where: Metadata filter for items to delete
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
True if successful, False otherwise
|
|
588
|
+
"""
|
|
589
|
+
if not self._client:
|
|
590
|
+
return False
|
|
591
|
+
|
|
592
|
+
try:
|
|
593
|
+
if ids:
|
|
594
|
+
# Delete by IDs
|
|
595
|
+
self._client.delete(
|
|
596
|
+
collection_name=collection_name,
|
|
597
|
+
points_selector=ids
|
|
598
|
+
)
|
|
599
|
+
elif where:
|
|
600
|
+
# Delete by filter
|
|
601
|
+
qdrant_filter = self._build_qdrant_filter(where)
|
|
602
|
+
if qdrant_filter:
|
|
603
|
+
self._client.delete(
|
|
604
|
+
collection_name=collection_name,
|
|
605
|
+
points_selector=qdrant_filter
|
|
606
|
+
)
|
|
607
|
+
return True
|
|
608
|
+
except Exception as e:
|
|
609
|
+
print(f"Failed to delete items: {e}")
|
|
610
|
+
return False
|
|
611
|
+
|
|
612
|
+
def delete_collection(self, name: str) -> bool:
|
|
613
|
+
"""
|
|
614
|
+
Delete an entire collection.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
name: Collection name
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
True if successful, False otherwise
|
|
621
|
+
"""
|
|
622
|
+
if not self._client:
|
|
623
|
+
return False
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
self._client.delete_collection(collection_name=name)
|
|
627
|
+
return True
|
|
628
|
+
except Exception as e:
|
|
629
|
+
print(f"Failed to delete collection: {e}")
|
|
630
|
+
return False
|
|
631
|
+
|
|
632
|
+
def create_collection(
|
|
633
|
+
self,
|
|
634
|
+
name: str,
|
|
635
|
+
vector_size: int,
|
|
636
|
+
distance: str = "Cosine"
|
|
637
|
+
) -> bool:
|
|
638
|
+
"""
|
|
639
|
+
Create a new collection.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
name: Collection name
|
|
643
|
+
vector_size: Dimension of vectors
|
|
644
|
+
distance: Distance metric ("Cosine", "Euclid", "Dot")
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
True if successful, False otherwise
|
|
648
|
+
"""
|
|
649
|
+
if not self._client:
|
|
650
|
+
return False
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
# Map distance string to Qdrant Distance enum
|
|
654
|
+
distance_map = {
|
|
655
|
+
"Cosine": Distance.COSINE,
|
|
656
|
+
"Euclid": Distance.EUCLID,
|
|
657
|
+
"Euclidean": Distance.EUCLID,
|
|
658
|
+
"Dot": Distance.DOT,
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
qdrant_distance = distance_map.get(distance, Distance.COSINE)
|
|
662
|
+
|
|
663
|
+
self._client.create_collection(
|
|
664
|
+
collection_name=name,
|
|
665
|
+
vectors_config=VectorParams(
|
|
666
|
+
size=vector_size,
|
|
667
|
+
distance=qdrant_distance
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
return True
|
|
671
|
+
except Exception as e:
|
|
672
|
+
print(f"Failed to create collection: {e}")
|
|
673
|
+
return False
|
|
674
|
+
|
|
675
|
+
def get_connection_info(self) -> Dict[str, Any]:
|
|
676
|
+
"""
|
|
677
|
+
Get information about the current connection.
|
|
678
|
+
|
|
679
|
+
Returns:
|
|
680
|
+
Dictionary with connection details
|
|
681
|
+
"""
|
|
682
|
+
info = {
|
|
683
|
+
"provider": "Qdrant",
|
|
684
|
+
"connected": self.is_connected,
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if self.path:
|
|
688
|
+
info["mode"] = "local"
|
|
689
|
+
info["path"] = self.path
|
|
690
|
+
elif self.url:
|
|
691
|
+
info["mode"] = "remote"
|
|
692
|
+
info["url"] = self.url
|
|
693
|
+
elif self.host:
|
|
694
|
+
info["mode"] = "remote"
|
|
695
|
+
info["host"] = self.host
|
|
696
|
+
info["port"] = self.port
|
|
697
|
+
else:
|
|
698
|
+
info["mode"] = "memory"
|
|
699
|
+
|
|
700
|
+
return info
|
|
701
|
+
|
|
702
|
+
def get_supported_filter_operators(self) -> List[Dict[str, Any]]:
|
|
703
|
+
"""
|
|
704
|
+
Get filter operators supported by Qdrant.
|
|
705
|
+
|
|
706
|
+
Qdrant has richer filtering capabilities than ChromaDB.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
List of operator dictionaries
|
|
710
|
+
"""
|
|
711
|
+
return [
|
|
712
|
+
{"name": "=", "server_side": True},
|
|
713
|
+
{"name": "!=", "server_side": True},
|
|
714
|
+
{"name": ">", "server_side": True},
|
|
715
|
+
{"name": ">=", "server_side": True},
|
|
716
|
+
{"name": "<", "server_side": True},
|
|
717
|
+
{"name": "<=", "server_side": True},
|
|
718
|
+
{"name": "in", "server_side": True},
|
|
719
|
+
{"name": "not in", "server_side": True},
|
|
720
|
+
# Qdrant supports text matching server-side
|
|
721
|
+
{"name": "contains", "server_side": True},
|
|
722
|
+
{"name": "not contains", "server_side": True},
|
|
723
|
+
]
|