| name | filtered-search |
| description | Use when user needs vector search with scalar field filtering. Triggers on: filtered search, filter by category, metadata filter, faceted search, conditional search, attribute filtering, search with constraints. |
Filtered Search
Vector semantic search combined with scalar field filtering — find semantically relevant results that also match specific criteria.
When to Activate
Activate this skill when:
- User needs to constrain search results by attributes (category, price, date, etc.)
- User mentions "filter by", "only show", "within range", "where category is"
- User has structured metadata alongside text content
- User wants faceted search like e-commerce product filtering
Do NOT activate when:
- User only needs pure semantic search → use
semantic-search
- User needs keyword + semantic fusion → use
hybrid-search
- User has no metadata fields to filter on
Interactive Flow
Step 1: Identify Filter Fields
"What attributes do you need to filter by?"
| Common Filter Types | Examples |
|---|
| Category/Type | category = "electronics", status = "active" |
| Numeric Range | price between 100-500, rating >= 4.0 |
| Date/Time | created_at > "2024-01-01", within last 7 days |
| Tags/Arrays | tags contains "new", skills includes "python" |
| Boolean | in_stock = true, is_verified = true |
Which filter types do you need? (can select multiple)
Step 2: Understand Filter Cardinality
"For each filter field, how many unique values exist?"
| Cardinality | Example | Index Strategy |
|---|
| Low (< 100) | category, status | TRIE index |
| Medium (100-10K) | brand, city | INVERTED index |
| High (> 10K) | user_id, timestamp | STL_SORT or no index |
This affects index design and query performance.
Step 3: Confirm Schema Design
"Based on your requirements, here's the proposed schema:
schema.add_field('category', DataType.VARCHAR, max_length=256)
schema.add_field('price', DataType.FLOAT)
schema.add_field('tags', DataType.ARRAY, element_type=DataType.VARCHAR)
Proceed? (yes / adjust [what])"
Core Concepts
Mental Model: Department Store
Think of filtered search as a department store with sections:
- Pure semantic search = "Find me something comfortable" (searches everywhere)
- Filtered search = "Find me something comfortable in the shoe department, under $100"
┌─────────────────────────────────────────────────────────┐
│ Filtered Search │
│ │
│ Query: "comfortable work shoes" │
│ Filters: category="shoes", price<=100 │
│ │
│ ┌─────────────────────────────┐ │
│ │ Step 1: Filter First │ │
│ │ │ │
│ │ Full Collection (1M items) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ category="shoes" (50K) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ price <= 100 (10K) │ │
│ └──────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Step 2: Vector Search │ │
│ │ on 10K filtered items │ │
│ │ │ │
│ │ → Semantically match │ │
│ │ "comfortable work shoes" │ │
│ └──────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ Top 10 relevant results │
└─────────────────────────────────────────────────────────┘
Filter vs Post-Filter
| Approach | When Applied | Performance |
|---|
| Pre-filter | Before vector search | ✅ Efficient (searches fewer vectors) |
| Post-filter | After vector search | ⚠️ May miss results if limit is small |
Milvus uses pre-filtering by default — filters are applied before ANN search.
Why Filtered Search
| Scenario | Without Filtering | With Filtering |
|---|
| E-commerce: "laptop under $1000" | Returns $2000 laptops too | Only budget options |
| Job search: "Python developer in NYC" | Returns SF jobs too | Location-specific |
| Content: "AI news from this week" | Returns old articles | Recent only |
When NOT to Use Filtering
- Filter reduces results too much: If filter leaves < 100 items, vector search adds little value
- Filter is the only criteria: Just use scalar query, no need for vectors
- Dynamic filters change frequently: Consider separate indexes
Implementation
from pymilvus import MilvusClient, DataType
from sentence_transformers import SentenceTransformer
class FilteredSearch:
def __init__(self, uri: str = "./milvus.db"):
self.client = MilvusClient(uri=uri)
self.model = SentenceTransformer('BAAI/bge-large-en-v1.5')
self.collection_name = "filtered_search"
self._init_collection()
def _init_collection(self):
if self.client.has_collection(self.collection_name):
return
schema = self.client.create_schema()
schema.add_field("id", DataType.INT64, is_primary=True, auto_id=True)
schema.add_field("text", DataType.VARCHAR, max_length=65535)
schema.add_field("category", DataType.VARCHAR, max_length=256)
schema.add_field("price", DataType.FLOAT)
schema.add_field("tags", DataType.ARRAY, element_type=DataType.VARCHAR, max_capacity=20, max_length=64)
schema.add_field("created_at", DataType.INT64)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=1024)
index_params = self.client.prepare_index_params()
index_params.add_index(field_name=, index_type=, metric_type=)
index_params.add_index(field_name=, index_type=)
index_params.add_index(field_name=, index_type=)
.client.create_collection(
collection_name=.collection_name,
schema=schema,
index_params=index_params
)
():
texts = [item[] item items]
embeddings = .model.encode(texts).tolist()
item, emb (items, embeddings):
item[] = emb
.client.insert(collection_name=.collection_name, data=items)
():
embedding = .model.encode(query).tolist()
expr_parts = []
filters:
filters:
expr_parts.append()
filters:
expr_parts.append()
filters:
expr_parts.append()
filters:
tag filters[]:
expr_parts.append()
filters:
expr_parts.append()
filter_expr = .join(expr_parts) expr_parts
results = .client.search(
collection_name=.collection_name,
data=[embedding],
=filter_expr,
limit=limit,
output_fields=[, , , ]
)
[{: hit[][],
: hit[][],
: hit[][],
: hit[]} hit results[]]
search = FilteredSearch()
search.add([
{: , : , : , : [, ], : },
{: , : , : , : [, ], : },
{: , : , : , : [], : },
])
results = search.search(
query=,
filters={: , : , : }
)
Filter Expression Syntax
'price == 100'
'price != 100'
'price > 100'
'price >= 100'
'price < 100'
'price <= 100'
'category == "electronics"'
'title like "iPhone%"'
'title like "%Pro%"'
'category in ["phones", "laptops", "tablets"]'
'array_contains(tags, "new")'
'array_contains_all(tags, ["a", "b"])'
'array_contains_any(tags, ["a", "b"])'
'price >= 100 and price <= 1000'
'category == "phones" or category == "tablets"'
'not (price > 1000)'
'category == "phones" and price >= 500 and array_contains(tags, "5G")'
Index Strategy Guide
| Field Type | Cardinality | Index Type | Use Case |
|---|
| VARCHAR | Low (< 100) | TRIE | Category, status, type |
| VARCHAR | High | INVERTED | Tags, keywords |
| INT/FLOAT | Any | STL_SORT | Numeric ranges |
| ARRAY | Any | INVERTED | Array contains |
| BOOL | 2 | None needed | Boolean flags |
Index Creation Example
index_params = self.client.prepare_index_params()
index_params.add_index("embedding", index_type="AUTOINDEX", metric_type="COSINE")
index_params.add_index("category", index_type="TRIE")
index_params.add_index("price", index_type="STL_SORT")
index_params.add_index("tags", index_type="INVERTED")
Common Pitfalls
❌ Pitfall 1: Over-Filtering
Problem: Filter returns 0 results
Why: Filter conditions too restrictive
Fix: Check filter cardinality before searching
count = client.query(
collection_name="products",
filter='category == "rare_category" and price < 10',
output_fields=["count(*)"]
)
❌ Pitfall 2: Missing Scalar Index
Problem: Filtered search is slow
Why: No index on frequently filtered field
Fix: Add appropriate index type
index_params.add_index("category", index_type="TRIE")
❌ Pitfall 3: Wrong Index Type
Problem: Index doesn't improve performance
Why: Using TRIE for high-cardinality field
Fix: Match index type to cardinality
- Low cardinality → TRIE
- High cardinality → INVERTED
- Numeric range → STL_SORT
❌ Pitfall 4: SQL Injection in Filters
Problem: User input directly in filter expression
Why: Security vulnerability
Fix: Validate and sanitize user input
filter_expr = f'category == "{user_input}"'
VALID_CATEGORIES = ["phones", "laptops", "tablets"]
if user_input in VALID_CATEGORIES:
filter_expr = f'category == "{user_input}"'
When to Level Up
| Need | Upgrade To |
|---|
| Keyword matching + filters | hybrid-search with filters |
| Multiple text fields | multi-vector-search with filters |
| Complex multi-hop queries | agentic-rag with tool-based filtering |
References
- Filter expression syntax:
references/filter-optimization.md
- Index configuration:
core:indexing
- Vertical guides:
verticals/