Filter Expressions
Every example on this page uses a collection with , , , , a nullable , a array, an JSON field and a 4-dimension vector, kept short so the rows fit. stands for a list of floats from your embedding model. The same filter string works in , , and the iterators.
| Task | Filter |
|---|---|
| Equals (SQL ) | |
| Not equal (SQL ) | |
| Greater than, less than | |
| Between two values (SQL ) | |
| Range in one expression (server only, not Milvus Lite) | |
| Any of several values (SQL ) | |
| None of several values (SQL ) | |
| Primary key in a list | |
| Starts with (SQL ) | |
| Contains | |
| Ends with | |
| Exactly one unknown character | |
| Does not match (SQL ) | |
| Both conditions (SQL , also ) | |
| Either condition (SQL , also ) | |
| Negate a condition (SQL , also ) | |
| Group conditions with parentheses | |
| Value is missing (SQL ) | |
| Value is present (SQL ) | |
| Arithmetic on a field | |
| Modulo, e.g. even IDs (server only, not Milvus Lite) | |
| Every entity (needs a in ) | or |
Strings take double or single quotes. Keywords work in upper case too (, , , ). needs a field created with . Arithmetic works on one side of the comparison only; fails because applies to constants.
Pass the filter as a Python string. Triple quotes save escaping when the filter has quotes of both kinds:
client.query(
collection_name="products",
filter='''category in ["shoes", "bags"] and not (name like "%Kids%")''',
output_fields=["name", "price"],
)
Example: in-stock shoes under 100 that are not part of the Runner line.
from pymilvus import MilvusClient
client = MilvusClient("./shop.db")
client.load_collection("products")
rows = client.query(
collection_name="products",
filter='category == "shoes" and price < 100 and stock is not null and not (name like "%Runner%")',
output_fields=["name", "price", "stock"],
limit=20,
)
for row in rows:
print(row["name"], row["price"], row["stock"])
In 1bench: the Milvus GUI has a visual filter builder. Pick a field and an operator (, , , , , ), join the conditions with AND or OR, and preview the JSON request it sends before you run it.
JSON Fields
Index into a JSON field with . The same syntax reads dynamic fields, the extra keys Milvus stores when .
| Task | Filter |
|---|---|
| Key equals a value | |
| Key not equal (on a server, rows missing the key match too) | |
| Number in a key | |
| Pattern on a string key | |
| Key in a list (server only, not Milvus Lite) | |
| Element of an array inside JSON | |
| JSON array contains a value | |
| JSON array contains any of these | |
| JSON array contains all of these | |
| Key exists | |
| Key is missing or null | |
| Dynamic field (key not in the schema) | |
| Dynamic field, explicit form |
Example: store mixed attributes per product, then find red items that come in size 42.
client.upsert("products", [
{"id": 1, "name": "Trail Runner 3", "category": "shoes", "price": 89.0, "stock": 12,
"tags": ["running", "trail"],
"attrs": {"color": "red", "sizes": [40, 41, 42], "brand": "Acme"},
"description": "Waterproof trail running shoe with a grippy sole",
"embedding": [0.1, 0.2, 0.3, 0.4], "supplier": "north"},
])
client.query(
collection_name="products",
filter='attrs["color"] == "red" and json_contains(attrs["sizes"], 42)',
output_fields=["name", "attrs"],
)
Array Fields
An ARRAY field holds values of one type, up to elements. Use the functions on it; the ones are for arrays inside JSON.
| Task | Filter |
|---|---|
| Array contains a value | |
| Array does not contain a value | |
| Contains any of these (overlap) | |
| Contains all of these | |
| Empty array | |
| At least two elements | |
| Element at a position (0-based) |
Example: everything tagged for travel that is also on sale.
client.query(
collection_name="products",
filter='array_contains_all(tags, ["travel", "sale"])',
output_fields=["name", "tags"],
)
Search with Filter
finds the nearest vectors and takes the same string. Results come back as one list of hits per query vector.
| Task | Code |
|---|---|
| Top 5 nearest vectors | |
| Choose the vector field | |
| Filter while searching (SQL ) | |
| Return these fields with each hit | |
| Skip the first hits (SQL ) | |
| Several query vectors in one call | |
| HNSW recall versus speed | |
| IVF clusters to probe | |
| Range search, COSINE score between 0.8 and 1.0 | |
| One hit per category | |
| Search one partition | |
| Filter with parameters (server only, not Milvus Lite) | |
| Page through a large result |
The full call, and how to read a hit:
res = client.search(
collection_name="products",
data=[query_vec],
anns_field="embedding",
filter='category == "shoes" and price < 100',
limit=5,
output_fields=["name", "price"],
search_params={"metric_type": "COSINE", "params": {"ef": 64}},
)
for hit in res[0]:
print(hit["id"], round(hit["distance"], 3), hit["entity"]["name"])
For COSINE and IP, is a similarity score and higher is closer. For L2 it is a distance and lower is closer, so a range search flips: is the lower bound and the upper one.
Example: the five closest shoes under 100, keeping only strong matches.
res = client.search(
collection_name="products",
data=[query_vec],
anns_field="embedding",
filter='category == "shoes" and price < 100',
limit=5,
output_fields=["name", "price"],
search_params={"params": {"radius": 0.8, "range_filter": 1.0}},
)
print([(h["entity"]["name"], round(h["distance"], 3)) for h in res[0]])
In 1bench: rows in the Milvus GUI have a Find similar button that runs a vector search with that entity's own vector and leaves the entity itself out of the results.
Query and Get
returns entities that match a filter, with no vector ranking. fetches by primary key.
| Task | Code |
|---|---|
| Rows matching a filter (SQL ) | |
| Page with SQL / | |
| Every field, dynamic keys included | |
| Count rows (SQL ) | |
| Count with a filter | |
| All rows, no filter | |
| Get by primary key | |
| Get one row with chosen fields | |
| Query by primary keys | |
| Only rows in one partition | |
| See writes made a moment ago | |
| Iterate a whole collection |
returns one row: and cannot be mixed with other output fields. A query result is a list of dicts. in , and in , max out at 16,384 per call on a server.
total = client.query("products", filter='category == "shoes"', output_fields=["count(*)"])
print(total[0]["count(*)"])
Example: export every product priced over 20 in batches of 1,000.
it = client.query_iterator(
collection_name="products",
batch_size=1000,
filter="price > 20",
output_fields=["name", "price", "category"],
)
while True:
batch = it.next()
if not batch:
it.close()
break
for row in batch:
print(row["id"], row["name"], row["price"])
Insert, Upsert and Delete
| Task | Code |
|---|---|
| Insert rows (SQL ) | |
| Insert into a partition | |
| Insert or replace by primary key | |
| Change some fields only (SQL ) | |
| Delete by primary key | Destructive |
| Delete by filter (SQL ) | Destructive |
| Seal growing segments to storage |
is a list of dicts with every schema field, except nullable fields, fields with a and an primary key. A plain replaces the whole entity, so a missing field fails with "Insert missed an field". keeps the fields you leave out.
Example: add a product, cut its price, then remove it.
client.insert("products", [{
"id": 6, "name": "Trail Runner 4", "category": "shoes", "price": 99.0, "stock": 3,
"tags": ["running"], "attrs": {"color": "red"},
"description": "New trail shoe", "embedding": [0.1, 0.2, 0.3, 0.5],
}])
client.upsert("products", [{"id": 6, "price": 79.0}], partial_update=True)
print(client.query("products", filter="id == 6", output_fields=["name", "price"], consistency_level="Strong"))
client.delete("products", ids=[6])
Collections and Schema
| Task | Code |
|---|---|
| Quick setup: , and dynamic fields | |
| Start a schema | |
| Primary key | |
| Generated primary key | |
| String field | |
| Float field | |
| Nullable field | |
| Field with a default | |
| Array field | |
| JSON field | |
| Dense vector field | |
| List collections | |
| Check one exists | |
| Fields, primary key, dynamic flag | |
| Load into memory before search or query | |
| Load state | |
| Free the memory | |
| Rename | |
| Create and list partitions | , |
| Alias you can repoint later | |
| Add a field to an existing collection (Milvus 2.6+, not Milvus Lite) | |
| Drop a collection | Destructive |
Quick setup creates an INT64 , a field, a COSINE index, dynamic fields, and loads the collection. With a custom schema, pass to and it loads too; without them you create the index and load it yourself.
Example: create the collection used on this page.
from pymilvus import MilvusClient, DataType
client = MilvusClient("./shop.db")
schema = MilvusClient.create_schema(auto_id=False, enable_dynamic_field=True)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("name", DataType.VARCHAR, max_length=200)
schema.add_field("category", DataType.VARCHAR, max_length=50)
schema.add_field("price", DataType.FLOAT)
schema.add_field("stock", DataType.INT64, nullable=True)
schema.add_field("tags", DataType.ARRAY, element_type=DataType.VARCHAR, max_capacity=10, max_length=30)
schema.add_field("attrs", DataType.JSON)
schema.add_field("description", DataType.VARCHAR, max_length=1000)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=4)
index_params = client.prepare_index_params()
index_params.add_index(field_name="embedding", index_type="AUTOINDEX", metric_type="COSINE")
client.create_collection("products", schema=schema, index_params=index_params)
print(client.get_load_state("products"))
In 1bench: the Milvus GUI shows each collection's schema view with its indexes (type, metric and build params), partitions, load state, dynamic field setting and partition key, without a and call per collection.
Indexes
| Task | Code |
|---|---|
| Start an index spec | |
| Let Milvus pick the index | |
| HNSW graph index | |
| IVF_FLAT cluster index | |
| Exact brute-force search | |
| Scalar index for frequent filters | |
| Index one JSON key (Milvus 2.6+, not Milvus Lite) | |
| Build the indexes | |
| List and describe | , |
| Drop an index (release first) |
| Metric | Use it for | Closer means |
|---|---|---|
| Text and image embeddings, angle only | Higher score, 1.0 is identical | |
| Normalized embeddings, or models trained for dot product | Higher score | |
| Euclidean distance | Lower distance, 0 is identical | |
| Sparse vectors from a BM25 function | Higher score |
The metric is fixed when you build the index. On a Milvus server, a search that passes a different fails with "metric type not match"; Milvus Lite 3.2.1 accepted it in our test and returned L2 distances. Leave out of and the index metric applies.
Example: add an INVERTED index on to a loaded collection, then confirm it.
client.release_collection("products")
index_params = client.prepare_index_params()
index_params.add_index(field_name="category", index_type="INVERTED")
client.create_index("products", index_params)
client.load_collection("products")
print(client.list_indexes("products"))
print(client.describe_index("products", index_name="embedding"))
Connections and Databases
| Task | Code |
|---|---|
| Install the client | |
| Install with Milvus Lite (macOS, Linux) | |
| Milvus Lite, data in a local file | |
| Local Milvus server | |
| Server with authentication | |
| Zilliz Cloud | |
| Connect to a database | |
| Default timeout for every call, in seconds | |
| Timeout for one call | |
| Server version | |
| List databases | |
| Create a database | |
| Switch database | |
| Drop a database (must be empty) | Destructive |
| Async client | |
| Close the connection |
The default database is , and the default server port is 19530. A Milvus Lite file path must end in .
Example: search from asyncio with AsyncMilvusClient.
import asyncio
from pymilvus import AsyncMilvusClient
async def main():
client = AsyncMilvusClient(uri="http://localhost:19530", token="root:Milvus")
await client.load_collection("products")
res = await client.search(
"products", data=[query_vec], anns_field="embedding",
limit=3, output_fields=["name"],
)
print([h["entity"]["name"] for h in res[0]])
await client.close()
asyncio.run(main())
Full-Text Search (BM25)
A BM25 function turns an analyzed text field into a sparse vector when you insert, so you search with plain text and never compute those vectors yourself.
| Task | Code |
|---|---|
| Text field that gets tokenized | |
| Also allow exact keyword filters | on the same field |
| Sparse field the function writes to | |
| The BM25 function | |
| Index for it | |
| Search with plain text | |
| Keyword filter, any of the terms | |
| Words next to each other, in order (Milvus 2.6+, not Milvus Lite) | |
| See how text gets tokenized (server only, not Milvus Lite) | |
| Merge vector and keyword results | |
| Weight one side more |
These lines replace the plain field in the schema above, and the function has to be in the schema before you create the collection. Do not send the field when you insert; the function fills it.
from pymilvus import Function, FunctionType
schema.add_field("description", DataType.VARCHAR, max_length=1000, enable_analyzer=True, enable_match=True)
schema.add_field("sparse", DataType.SPARSE_FLOAT_VECTOR)
schema.add_function(Function(
name="bm25",
function_type=FunctionType.BM25,
input_field_names=["description"],
output_field_names=["sparse"],
))
index_params.add_index(field_name="sparse", index_type="SPARSE_INVERTED_INDEX", metric_type="BM25")
Example: hybrid search for "waterproof", ranking by meaning and by keyword at once.
from pymilvus import AnnSearchRequest, RRFRanker
dense_req = AnnSearchRequest(data=[query_vec], anns_field="embedding", param={}, limit=10)
bm25_req = AnnSearchRequest(data=["waterproof"], anns_field="sparse", param={}, limit=10)
res = client.hybrid_search(
collection_name="products",
reqs=[dense_req, bm25_req],
ranker=RRFRanker(60),
limit=5,
output_fields=["name"],
)
print([h["entity"]["name"] for h in res[0]])
ORM to MilvusClient
Older tutorials use the ORM API (, , ). It still works in PyMilvus 3.0, but MilvusClient is the current interface.
| ORM call | MilvusClient call |
|---|---|
| / | / |
Gotchas
- A collection must be loaded before , or . In our test, Milvus Lite came back with collections released after the process restarted, so call at startup.
- A Milvus server defaults to Bounded consistency, so a right after or can miss the new rows or return the old values. Pass when you need to read your own writes. Milvus Lite read them at once in our test.
- does not check for an existing primary key. On Milvus 2.6.15, inserting 6 twice left two entities and returned 2, so use when a row might already exist.
- cannot be undone, and a broad filter such as removes every entity. Run the same filter through with first.
- Milvus Lite 3.2.1 parses filters with its own parser. Chained ranges (), , templates and on a JSON key all work on Milvus 2.6.15 but failed on Lite, and on a JSON key skips rows without the key on Lite but matches them on the server. Test filters against the engine you deploy.
- has no dependable sort order. PyMilvus 3.0 sends , but both Milvus 2.6.15 and Milvus Lite 3.2.1 ignored it in our test, so sort the rows in Python.