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.
Files changed (32) hide show
  1. vector_inspector/__init__.py +3 -0
  2. vector_inspector/__main__.py +4 -0
  3. vector_inspector/core/__init__.py +1 -0
  4. vector_inspector/core/connections/__init__.py +7 -0
  5. vector_inspector/core/connections/base_connection.py +233 -0
  6. vector_inspector/core/connections/chroma_connection.py +384 -0
  7. vector_inspector/core/connections/qdrant_connection.py +723 -0
  8. vector_inspector/core/connections/template_connection.py +346 -0
  9. vector_inspector/main.py +21 -0
  10. vector_inspector/services/__init__.py +1 -0
  11. vector_inspector/services/backup_restore_service.py +286 -0
  12. vector_inspector/services/filter_service.py +72 -0
  13. vector_inspector/services/import_export_service.py +287 -0
  14. vector_inspector/services/settings_service.py +60 -0
  15. vector_inspector/services/visualization_service.py +116 -0
  16. vector_inspector/ui/__init__.py +1 -0
  17. vector_inspector/ui/components/__init__.py +1 -0
  18. vector_inspector/ui/components/backup_restore_dialog.py +350 -0
  19. vector_inspector/ui/components/filter_builder.py +370 -0
  20. vector_inspector/ui/components/item_dialog.py +118 -0
  21. vector_inspector/ui/components/loading_dialog.py +30 -0
  22. vector_inspector/ui/main_window.py +288 -0
  23. vector_inspector/ui/views/__init__.py +1 -0
  24. vector_inspector/ui/views/collection_browser.py +112 -0
  25. vector_inspector/ui/views/connection_view.py +423 -0
  26. vector_inspector/ui/views/metadata_view.py +555 -0
  27. vector_inspector/ui/views/search_view.py +268 -0
  28. vector_inspector/ui/views/visualization_view.py +245 -0
  29. vector_inspector-0.2.0.dist-info/METADATA +382 -0
  30. vector_inspector-0.2.0.dist-info/RECORD +32 -0
  31. vector_inspector-0.2.0.dist-info/WHEEL +4 -0
  32. 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
+ ]