0
# Search Operations
1
2
PyMilvus provides comprehensive search capabilities including vector similarity search, hybrid multi-vector search, scalar filtering, and advanced result handling. This covers single-vector ANN search, multi-vector hybrid search with reranking, and sophisticated result processing.
3
4
## Basic Vector Search
5
6
### Single Vector Search
7
8
```python { .api }
9
from pymilvus import MilvusClient
10
11
client = MilvusClient()
12
13
def search(
14
collection_name: str,
15
data: Union[List[List[float]], List[Dict]],
16
anns_field: str = "vector",
17
search_params: Optional[Dict] = None,
18
limit: int = 10,
19
expr: Optional[str] = None,
20
output_fields: Optional[List[str]] = None,
21
partition_names: Optional[List[str]] = None,
22
round_decimal: int = -1,
23
timeout: Optional[float] = None,
24
consistency_level: Optional[str] = None,
25
**kwargs
26
) -> List[List[Dict[str, Any]]]
27
```
28
29
**Parameters:**
30
- `data`: Query vectors as list of float lists or dict with vector field
31
- `anns_field`: Name of vector field to search against
32
- `search_params`: Algorithm-specific parameters (e.g., {"nprobe": 16})
33
- `limit`: Maximum results per query vector
34
- `expr`: Boolean filter expression
35
- `output_fields`: Fields to include in results
36
- `partition_names`: Target partitions for search
37
- `round_decimal`: Precision for distance values (-1 for no rounding)
38
- `consistency_level`: "Strong", "Eventually", "Bounded", "Session"
39
40
**Examples:**
41
```python
42
# Basic similarity search
43
query_vector = [0.1, 0.2, 0.3] * 256
44
results = client.search(
45
collection_name="documents",
46
data=[query_vector],
47
limit=5,
48
output_fields=["id", "title", "content"]
49
)
50
51
# Search with filtering
52
results = client.search(
53
collection_name="products",
54
data=[embedding],
55
expr="category == 'electronics' and price < 1000",
56
limit=10,
57
output_fields=["id", "name", "price", "description"]
58
)
59
60
# Multi-query search
61
query_vectors = [[0.1] * 768, [0.2] * 768, [0.3] * 768]
62
results = client.search(
63
collection_name="embeddings",
64
data=query_vectors,
65
search_params={"nprobe": 32, "ef": 64},
66
limit=20
67
)
68
69
# Search specific partitions
70
results = client.search(
71
collection_name="time_series",
72
data=[query_vector],
73
partition_names=["2024_q1", "2024_q2"],
74
expr="status == 'active'",
75
limit=15
76
)
77
```
78
79
### Search Parameters by Index Type
80
81
```python { .api }
82
# FLAT index (exact search)
83
flat_params = {} # No additional parameters needed
84
85
# IVF_FLAT index
86
ivf_flat_params = {
87
"nprobe": 16 # Number of clusters to search (1 to nlist)
88
}
89
90
# IVF_PQ index
91
ivf_pq_params = {
92
"nprobe": 32, # Number of clusters to search
93
}
94
95
# HNSW index
96
hnsw_params = {
97
"ef": 64 # Search scope (ef >= limit, higher = more accurate but slower)
98
}
99
100
# Example usage
101
results = client.search(
102
"hnsw_collection",
103
data=[query_vector],
104
search_params=hnsw_params,
105
limit=10
106
)
107
```
108
109
## Hybrid Multi-Vector Search
110
111
### AnnSearchRequest
112
113
```python { .api }
114
from pymilvus import AnnSearchRequest
115
116
def __init__(
117
self,
118
data: List,
119
anns_field: str,
120
param: Dict,
121
limit: int,
122
expr: Optional[str] = None,
123
partition_names: Optional[List[str]] = None,
124
ignore_growing: bool = False
125
)
126
```
127
128
**Parameters:**
129
- `data`: Query vectors for this field
130
- `anns_field`: Vector field name to search
131
- `param`: Search parameters including metric_type and algorithm params
132
- `limit`: Maximum results for this search request
133
- `expr`: Filter expression for this search
134
- `partition_names`: Target partitions
135
- `ignore_growing`: Skip growing segments for consistency
136
137
### Hybrid Search with Reranking
138
139
```python { .api }
140
def hybrid_search(
141
collection_name: str,
142
reqs: List[AnnSearchRequest],
143
ranker: Union[RRFRanker, WeightedRanker],
144
limit: int = 10,
145
partition_names: Optional[List[str]] = None,
146
output_fields: Optional[List[str]] = None,
147
timeout: Optional[float] = None,
148
round_decimal: int = -1,
149
**kwargs
150
) -> List[List[Dict[str, Any]]]
151
```
152
153
**Parameters:**
154
- `reqs`: List of AnnSearchRequest objects for different vector fields
155
- `ranker`: Ranking algorithm to combine results
156
- `limit`: Final number of results after reranking
157
158
### RRF (Reciprocal Rank Fusion) Ranker
159
160
```python { .api }
161
from pymilvus import RRFRanker
162
163
def __init__(self, k: int = 60)
164
```
165
166
**Parameters:**
167
- `k`: RRF parameter controlling rank fusion (default: 60)
168
169
RRF Formula: `score = Σ(1 / (k + rank_i))` for each search result
170
171
### WeightedRanker
172
173
```python { .api }
174
from pymilvus import WeightedRanker
175
176
def __init__(self, *nums, norm_score: bool = True)
177
```
178
179
**Parameters:**
180
- `*nums`: Weight values for each search request (must match number of requests)
181
- `norm_score`: Whether to normalize scores before weighting
182
183
### Hybrid Search Examples
184
185
```python { .api }
186
from pymilvus import AnnSearchRequest, RRFRanker, WeightedRanker
187
188
# Dense + Sparse hybrid search
189
dense_req = AnnSearchRequest(
190
data=dense_vectors, # [[0.1, 0.2, ...]]
191
anns_field="dense_embedding",
192
param={
193
"metric_type": "L2",
194
"params": {"nprobe": 16}
195
},
196
limit=100,
197
expr="status == 'published'"
198
)
199
200
sparse_req = AnnSearchRequest(
201
data=sparse_vectors, # Sparse vectors from BM25/TF-IDF
202
anns_field="sparse_embedding",
203
param={
204
"metric_type": "IP", # Inner Product for sparse
205
"params": {"drop_ratio_build": 0.2}
206
},
207
limit=100,
208
expr="status == 'published'"
209
)
210
211
# RRF hybrid search - good for combining different vector types
212
rrf_results = client.hybrid_search(
213
collection_name="hybrid_documents",
214
reqs=[dense_req, sparse_req],
215
ranker=RRFRanker(k=60),
216
limit=10,
217
output_fields=["id", "title", "content", "score"]
218
)
219
220
# Weighted hybrid search - control contribution of each vector type
221
weighted_results = client.hybrid_search(
222
collection_name="hybrid_documents",
223
reqs=[dense_req, sparse_req],
224
ranker=WeightedRanker(0.7, 0.3, norm_score=True), # 70% dense, 30% sparse
225
limit=10,
226
output_fields=["id", "title", "content"]
227
)
228
229
# Multi-modal search (text + image + audio)
230
text_req = AnnSearchRequest(
231
data=text_embeddings,
232
anns_field="text_vector",
233
param={"metric_type": "COSINE", "params": {"nprobe": 20}},
234
limit=50
235
)
236
237
image_req = AnnSearchRequest(
238
data=image_embeddings,
239
anns_field="image_vector",
240
param={"metric_type": "L2", "params": {"ef": 100}},
241
limit=50
242
)
243
244
audio_req = AnnSearchRequest(
245
data=audio_embeddings,
246
anns_field="audio_vector",
247
param={"metric_type": "IP", "params": {"nprobe": 10}},
248
limit=50
249
)
250
251
multimodal_results = client.hybrid_search(
252
collection_name="multimodal_content",
253
reqs=[text_req, image_req, audio_req],
254
ranker=WeightedRanker(0.5, 0.3, 0.2), # Text dominant
255
limit=15,
256
output_fields=["id", "title", "type", "metadata"]
257
)
258
```
259
260
### Advanced Hybrid Search Patterns
261
262
```python { .api }
263
# Query-time vector generation with different strategies per field
264
def multi_strategy_search(query_text: str, query_image_path: str):
265
# Generate embeddings for different modalities
266
text_dense = text_encoder.encode(query_text)
267
text_sparse = bm25_encoder.encode(query_text)
268
image_vector = image_encoder.encode(query_image_path)
269
270
# Different search strategies
271
requests = [
272
# Semantic text search
273
AnnSearchRequest(
274
data=[text_dense],
275
anns_field="text_dense_vector",
276
param={"metric_type": "COSINE", "params": {"ef": 200}},
277
limit=200,
278
expr="content_type in ['article', 'blog']"
279
),
280
281
# Lexical text search
282
AnnSearchRequest(
283
data=[text_sparse],
284
anns_field="text_sparse_vector",
285
param={"metric_type": "IP"},
286
limit=200,
287
expr="content_type in ['article', 'blog']"
288
),
289
290
# Visual similarity
291
AnnSearchRequest(
292
data=[image_vector],
293
anns_field="image_vector",
294
param={"metric_type": "L2", "params": {"nprobe": 50}},
295
limit=100,
296
expr="content_type in ['image', 'video']"
297
)
298
]
299
300
# Combine with RRF for balanced results
301
return client.hybrid_search(
302
"multimedia_collection",
303
reqs=requests,
304
ranker=RRFRanker(k=100),
305
limit=20,
306
output_fields=["id", "title", "content_type", "url", "metadata"]
307
)
308
```
309
310
## Search Result Handling
311
312
### SearchResult Structure
313
314
```python { .api }
315
# SearchResult contains results for all query vectors
316
from pymilvus.client.search_result import SearchResult, Hits, Hit
317
318
class SearchResult:
319
hits: List[Hits] # One Hits object per query vector
320
distances: List[List[float]] # Nested distances [query][result]
321
ids: List[List] # Nested primary keys [query][result]
322
323
def __len__(self) -> int # Number of queries
324
def __getitem__(self, index: int) -> Hits # Access query results
325
```
326
327
### Hits Object
328
329
```python { .api }
330
class Hits:
331
ids: List # Primary key values for this query
332
distances: List[float] # Distance/similarity scores
333
334
def __len__(self) -> int # Number of results
335
def __getitem__(self, index: int) -> Hit # Access individual result
336
def __iter__(self) -> Iterator[Hit] # Iterate over results
337
```
338
339
### Hit Object
340
341
```python { .api }
342
class Hit:
343
id: Any # Primary key value
344
distance: float # Distance/similarity score
345
score: float # Alias for distance
346
entity: Dict[str, Any] # Returned field values
347
348
def get(self, field: str, default=None) -> Any # Get field with default
349
def to_dict(self) -> Dict[str, Any] # Convert to dictionary
350
```
351
352
### Result Processing Examples
353
354
```python { .api }
355
# Process search results
356
results = client.search(
357
"documents",
358
data=[query_vector],
359
limit=5,
360
output_fields=["id", "title", "content", "score"]
361
)
362
363
# Access first query results (single query)
364
first_query_hits = results[0]
365
print(f"Found {len(first_query_hits)} results")
366
367
# Process individual hits
368
for hit in first_query_hits:
369
print(f"Document ID: {hit.id}")
370
print(f"Similarity Score: {hit.score:.4f}")
371
print(f"Title: {hit.entity.get('title', 'No title')}")
372
print(f"Content: {hit.entity.get('content', '')[:100]}...")
373
print("---")
374
375
# Multi-query result processing
376
multi_results = client.search(
377
"products",
378
data=[vector1, vector2, vector3],
379
limit=10,
380
output_fields=["id", "name", "category", "price"]
381
)
382
383
for query_idx, hits in enumerate(multi_results):
384
print(f"Query {query_idx + 1} results:")
385
for rank, hit in enumerate(hits):
386
product_name = hit.entity.get('name', 'Unknown')
387
price = hit.entity.get('price', 0)
388
print(f" {rank + 1}. {product_name} - ${price:.2f} (score: {hit.score:.3f})")
389
```
390
391
### Advanced Result Analysis
392
393
```python { .api }
394
def analyze_search_results(results: SearchResult) -> Dict[str, Any]:
395
"""Analyze search result quality and distribution"""
396
analysis = {
397
"total_queries": len(results),
398
"query_stats": []
399
}
400
401
for query_idx, hits in enumerate(results):
402
if len(hits) == 0:
403
continue
404
405
scores = [hit.score for hit in hits]
406
query_analysis = {
407
"query_index": query_idx,
408
"result_count": len(hits),
409
"score_stats": {
410
"min": min(scores),
411
"max": max(scores),
412
"avg": sum(scores) / len(scores),
413
"range": max(scores) - min(scores)
414
},
415
"categories": {}
416
}
417
418
# Analyze result categories
419
for hit in hits:
420
category = hit.entity.get('category', 'unknown')
421
query_analysis["categories"][category] = query_analysis["categories"].get(category, 0) + 1
422
423
analysis["query_stats"].append(query_analysis)
424
425
return analysis
426
427
# Use analysis
428
search_results = client.search("products", [query_vector], limit=20, output_fields=["category"])
429
stats = analyze_search_results(search_results)
430
print(f"Search returned results across {len(stats['query_stats'][0]['categories'])} categories")
431
```
432
433
## Paginated Search with Iterators
434
435
### search_iterator
436
437
```python { .api }
438
def search_iterator(
439
collection_name: str,
440
data: Union[List[List[float]], List[Dict]],
441
anns_field: str = "vector",
442
batch_size: int = 1000,
443
limit: Optional[int] = None,
444
search_params: Optional[Dict] = None,
445
expr: Optional[str] = None,
446
output_fields: Optional[List[str]] = None,
447
**kwargs
448
) -> SearchIterator
449
```
450
451
**Parameters:**
452
- `batch_size`: Results per iteration batch
453
- `limit`: Total maximum results across all batches
454
455
```python { .api }
456
# Large-scale similarity search with pagination
457
iterator = client.search_iterator(
458
collection_name="large_embeddings",
459
data=[query_vector],
460
anns_field="embedding",
461
batch_size=1000,
462
limit=10000, # Process up to 10K results
463
output_fields=["id", "metadata", "score"],
464
expr="status == 'active'"
465
)
466
467
# Process results in batches
468
total_processed = 0
469
for batch in iterator:
470
# batch is a list of Hit objects
471
print(f"Processing batch of {len(batch)} results")
472
473
# Process each result in batch
474
for hit in batch:
475
# Custom processing logic
476
if hit.score > 0.8: # High similarity threshold
477
process_high_similarity(hit)
478
479
total_processed += 1
480
481
# Optional: stop early based on conditions
482
if total_processed >= 5000:
483
break
484
485
print(f"Total processed: {total_processed} results")
486
```
487
488
## Query Operations (Scalar Search)
489
490
### Basic Query
491
492
```python { .api }
493
def query(
494
collection_name: str,
495
filter: str,
496
output_fields: Optional[List[str]] = None,
497
partition_names: Optional[List[str]] = None,
498
limit: int = 16384,
499
offset: int = 0,
500
timeout: Optional[float] = None,
501
consistency_level: Optional[str] = None,
502
**kwargs
503
) -> List[Dict[str, Any]]
504
```
505
506
### Query Iterator
507
508
```python { .api }
509
def query_iterator(
510
collection_name: str,
511
filter: str,
512
output_fields: Optional[List[str]] = None,
513
batch_size: int = 1000,
514
limit: Optional[int] = None,
515
**kwargs
516
) -> QueryIterator
517
```
518
519
### Query Expression Syntax
520
521
```python { .api }
522
# Comparison operators
523
"age > 25"
524
"price <= 100.0"
525
"category == 'electronics'"
526
"status != 'inactive'"
527
528
# Logical operators
529
"age > 18 and age < 65"
530
"category == 'books' or category == 'ebooks'"
531
"not (status == 'deleted')"
532
533
# List operations
534
"category in ['electronics', 'computers', 'mobile']"
535
"tag_id not in [1, 2, 3]"
536
537
# JSON field queries
538
"metadata['color'] == 'red'"
539
"metadata['specs']['weight'] < 1.5"
540
"json_contains(metadata['tags'], 'premium')"
541
542
# Array field queries
543
"array_contains(tags, 'new')"
544
"array_contains_all(categories, ['tech', 'gadget'])"
545
"array_contains_any(features, ['bluetooth', 'wifi'])"
546
"array_length(tags) > 2"
547
548
# String operations
549
"title like 'Python%'" # Starts with 'Python'
550
"description like '%machine learning%'" # Contains 'machine learning'
551
552
# Complex expressions
553
"(category == 'books' and price < 50) or (category == 'ebooks' and price < 20)"
554
"json_contains(metadata['tags'], 'bestseller') and rating >= 4.5"
555
"array_contains(features, 'wireless') and price between 50 and 200"
556
```
557
558
### Query Examples
559
560
```python { .api }
561
# Basic filtering
562
products = client.query(
563
"products",
564
filter="category == 'electronics' and price < 500",
565
output_fields=["id", "name", "price", "rating"],
566
limit=50
567
)
568
569
# Complex JSON queries
570
documents = client.query(
571
"documents",
572
filter="metadata['author'] == 'Smith' and metadata['year'] >= 2020",
573
output_fields=["id", "title", "metadata"],
574
offset=100,
575
limit=25
576
)
577
578
# Array field filtering
579
articles = client.query(
580
"articles",
581
filter="array_contains_all(tags, ['AI', 'machine-learning']) and status == 'published'",
582
output_fields=["id", "title", "tags", "publish_date"]
583
)
584
585
# Paginated query processing
586
iterator = client.query_iterator(
587
"large_dataset",
588
filter="created_at > 1640995200", # After 2022-01-01
589
output_fields=["id", "data", "timestamp"],
590
batch_size=2000
591
)
592
593
for batch in iterator:
594
# Process each batch
595
process_data_batch(batch)
596
```
597
598
## Performance Optimization
599
600
### Search Parameter Tuning
601
602
```python { .api }
603
# HNSW index optimization
604
def optimize_hnsw_search(collection_name: str, query_vectors: List[List[float]], target_recall: float = 0.95):
605
"""Optimize HNSW search parameters for target recall"""
606
607
# Start with conservative parameters
608
base_ef = max(50, len(query_vectors[0])) # ef >= dimension recommended
609
610
# Test different ef values
611
ef_values = [base_ef, base_ef * 2, base_ef * 4]
612
613
best_params = None
614
best_latency = float('inf')
615
616
for ef in ef_values:
617
start_time = time.time()
618
results = client.search(
619
collection_name,
620
data=query_vectors,
621
search_params={"ef": ef},
622
limit=10
623
)
624
latency = time.time() - start_time
625
626
# In practice, measure recall against ground truth
627
recall = measure_recall(results, ground_truth) # Custom function
628
629
if recall >= target_recall and latency < best_latency:
630
best_params = {"ef": ef}
631
best_latency = latency
632
633
return best_params
634
635
# IVF index optimization
636
def optimize_ivf_search(collection_name: str, nlist: int):
637
"""Find optimal nprobe for IVF index"""
638
639
# Rule of thumb: nprobe = sqrt(nlist) to nlist/8
640
nprobe_candidates = [
641
max(1, int(nlist ** 0.5)), # sqrt(nlist)
642
max(1, nlist // 32),
643
max(1, nlist // 16),
644
max(1, nlist // 8)
645
]
646
647
best_nprobe = nprobe_candidates[0]
648
649
for nprobe in nprobe_candidates:
650
# Test search performance
651
start_time = time.time()
652
results = client.search(
653
collection_name,
654
data=[test_vector],
655
search_params={"nprobe": nprobe},
656
limit=100
657
)
658
latency = time.time() - start_time
659
660
print(f"nprobe={nprobe}, latency={latency:.4f}s")
661
662
# Choose based on latency/accuracy tradeoff
663
if latency < target_latency:
664
best_nprobe = nprobe
665
break
666
667
return {"nprobe": best_nprobe}
668
```
669
670
### Batch Search Optimization
671
672
```python { .api }
673
def batch_search_optimize(collection_name: str, query_vectors: List[List[float]], batch_size: int = 100):
674
"""Optimize batch search for large query sets"""
675
676
all_results = []
677
678
# Process queries in batches to optimize memory usage
679
for i in range(0, len(query_vectors), batch_size):
680
batch_vectors = query_vectors[i:i + batch_size]
681
682
batch_results = client.search(
683
collection_name,
684
data=batch_vectors,
685
search_params={"nprobe": 16}, # Adjust based on index
686
limit=10,
687
round_decimal=4 # Reduce precision for network efficiency
688
)
689
690
all_results.extend(batch_results)
691
692
# Optional: progress tracking
693
print(f"Processed {min(i + batch_size, len(query_vectors))}/{len(query_vectors)} queries")
694
695
return all_results
696
```
697
698
PyMilvus search operations provide powerful capabilities for similarity search, hybrid retrieval, and scalar filtering, enabling sophisticated search applications with fine-tuned performance optimization.