Milvus Filter Expression Cheat Sheet

Milvus is an open-source vector database you usually drive from Python. This sheet covers the filter expression syntax that search, query and delete share, and the PyMilvus MilvusClient calls around it, with one products collection in every example.

Updated Oct 12, 2026Tested against Milvus 2.6.15, Milvus Lite 3.2.1, PyMilvus 3.0.2

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.

TaskFilter
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:

python
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.

python
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 .

TaskFilter
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.

python
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.

TaskFilter
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.

python
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.

TaskCode
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:

python
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.

python
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.

TaskCode
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.

python
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.

python
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

TaskCode
Insert rows (SQL )
Insert into a partition
Insert or replace by primary key
Change some fields only (SQL )
Delete by primary keyDestructive
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.

python
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

TaskCode
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 collectionDestructive

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.

python
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

TaskCode
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)
MetricUse it forCloser means
Text and image embeddings, angle onlyHigher score, 1.0 is identical
Normalized embeddings, or models trained for dot productHigher score
Euclidean distanceLower distance, 0 is identical
Sparse vectors from a BM25 functionHigher 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.

python
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

TaskCode
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.

python
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.

TaskCode
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.

python
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.

python
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 callMilvusClient 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.

Work with Milvus without memorizing it

1bench is a desktop GUI for Milvus. Connect, browse your data and run queries in one window, with the query DSL still there when you want it.

Open Milvus in 1bench

Milvus FAQ

Does Milvus support metadata filtering?
Yes. Every scalar field, JSON key and dynamic field can go in the filter argument of search, query, delete and the iterators. In a vector search Milvus applies the filter during the search, so you still get up to limit hits that all match it. On a large collection, add an INVERTED index on the fields you filter by most often.
How do I connect to Milvus from Python?
Create one MilvusClient and reuse it: MilvusClient("./shop.db") for Milvus Lite, MilvusClient(uri="http://localhost:19530", token="root:Milvus") for a server with auth, or your Zilliz Cloud endpoint with an API key as the token. The error "should create connection first" comes from the older ORM API, where you must call connections.connect() before building a Collection object.
How do I write NOT LIKE in a Milvus filter?
Wrap the like test in not: not (name like "%Runner%"). Writing name not like "%Runner%" is a parse error on both the server and Milvus Lite. like supports % for any run of characters and _ for exactly one, it is case-sensitive, and it only works on VARCHAR fields and JSON string values.
How many rows can a Milvus query return?
On a Milvus server, offset plus limit can be at most 16,384 per call, and an empty filter needs an explicit limit. To read a whole collection, or anything past that window, use query_iterator with a batch_size; it pages by primary key so you never hit the cap.
Does Milvus support BM25 and hybrid search?
Yes, since Milvus 2.5. A BM25 function turns an analyzed VARCHAR field into a sparse vector at insert time, so you search it with plain text. hybrid_search runs that keyword search and a dense vector search together and merges the results with RRFRanker or WeightedRanker. Both ran on Milvus 2.6.15 and on Milvus Lite 3.2.1 in our tests.