Skip to main content
This tutorial shows you how to use predicate filters to combine vector similarity search with structured payload conditions in Actian VectorAI DB. Vector search finds the most semantically similar results, but real applications need more than similarity. An e-commerce search for “blue running shoes” should not return red hiking boots just because the embeddings are close. A job matcher should not surface expired postings. A medical system should not return patients from the wrong department. Predicate filters solve this by constraining vector search results with structured conditions on payload fields. Actian VectorAI DB applies filters server-side during the search, so only matching points are considered for ranking — this is more efficient than filtering after retrieval. In this tutorial, you learn how to:
  • Build filters using the Field and FilterBuilder API.
  • Use every filter type: equality, range, datetime, geo, text, array, and null checks.
  • Apply boolean logic: must, should, must_not, and min_should.
  • Compose operators (&, |, and ~) on conditions and builders.
  • Use standalone conditions: has_id, has_vector, is_empty, is_null, and nested.
  • Integrate filters with points.search, points.query, points.count, points.delete, and points.set_payload.

Environment setup

pip install actian-vectorai-client sentence-transformers

Setup: Create a collection and ingest sample data

Step 1: Configure and initialize

import asyncio
from datetime import datetime as dt

from actian_vectorai import (
    AsyncVectorAIClient,
    Distance,
    Field,
    FilterBuilder,
    PointStruct,
    VectorParams,
    has_id,
    has_vector,
    is_empty,
    is_null,
    nested,
)
from actian_vectorai.models.collections import HnswConfigDiff
from sentence_transformers import SentenceTransformer

SERVER     = "localhost:6574"
COLLECTION = "Filter-Tutorial"
EMBED_DIM  = 384

model = SentenceTransformer("all-MiniLM-L6-v2")

def embed_text(text: str) -> list[float]:
    return model.encode(text).tolist()

def embed_texts(texts: list[str]) -> list[list[float]]:
    return model.encode(texts).tolist()

Step 2: Create the collection

async def setup():
    async with AsyncVectorAIClient(url=SERVER) as client:
        await client.collections.get_or_create(
            name=COLLECTION,
            vectors_config=VectorParams(size=EMBED_DIM, distance=Distance.Cosine),
            hnsw_config=HnswConfigDiff(m=16, ef_construct=128),
        )
    print(f"Collection '{COLLECTION}' ready.")

asyncio.run(setup())

Expected output

Collection 'Filter-Tutorial' ready.
The get_or_create call is idempotent — safe to run on every application startup without creating duplicate collections.

Step 3: Ingest sample products

products = [
    {
        "text": "Lightweight blue running shoes with breathable mesh upper and responsive foam cushioning",
        "category": "footwear", "sub_category": "running",
        "brand": "SpeedRunner", "color": "blue",
        "price": 129.99, "rating": 4.5, "in_stock": True,
        "tags": ["running", "lightweight", "breathable"],
        "created_at": "2026-01-15T10:00:00Z",
        "location": {"lat": 40.7128, "lon": -74.0060},
        "reviews": [
            {"author": "Alex", "score": 5, "verified": True},
            {"author": "Sam",  "score": 4, "verified": True},
        ],
        "discontinued": False, "clearance_note": None,
    },
    {
        "text": "Red trail hiking boots with waterproof Gore-Tex lining and Vibram outsole",
        "category": "footwear", "sub_category": "hiking",
        "brand": "TrailMaster", "color": "red",
        "price": 189.99, "rating": 4.8, "in_stock": True,
        "tags": ["hiking", "waterproof", "durable"],
        "created_at": "2025-11-20T08:30:00Z",
        "location": {"lat": 47.6062, "lon": -122.3321},
        "reviews": [{"author": "Jordan", "score": 5, "verified": True}],
        "discontinued": False, "clearance_note": None,
    },
    {
        "text": "Classic white leather sneakers with minimalist design and orthopedic insole",
        "category": "footwear", "sub_category": "casual",
        "brand": "UrbanStep", "color": "white",
        "price": 89.99, "rating": 4.2, "in_stock": True,
        "tags": ["casual", "minimalist", "comfortable"],
        "created_at": "2026-02-01T12:00:00Z",
        "location": {"lat": 34.0522, "lon": -118.2437},
        "reviews": [],
        "discontinued": False, "clearance_note": None,
    },
    {
        "text": "Black formal Oxford dress shoes in full-grain Italian leather with Goodyear welt",
        "category": "footwear", "sub_category": "formal",
        "brand": "ClassicFit", "color": "black",
        "price": 249.99, "rating": 4.9, "in_stock": False,
        "tags": ["formal", "leather", "premium"],
        "created_at": "2025-08-10T14:00:00Z",
        "location": {"lat": 51.5074, "lon": -0.1278},
        "reviews": [
            {"author": "Morgan", "score": 5, "verified": True},
            {"author": "Casey",  "score": 5, "verified": False},
            {"author": "Pat",    "score": 4, "verified": True},
        ],
        "discontinued": True, "clearance_note": "Final sale — limited sizes remaining",
    },
    {
        "text": "Blue and green trail running shoes with aggressive tread pattern and rock plate protection",
        "category": "footwear", "sub_category": "trail_running",
        "brand": "TrailMaster", "color": "blue",
        "price": 159.99, "rating": 4.6, "in_stock": True,
        "tags": ["running", "trail", "protective", "waterproof"],
        "created_at": "2026-03-05T09:15:00Z",
        "location": {"lat": 39.7392, "lon": -104.9903},
        "reviews": [
            {"author": "Riley", "score": 4, "verified": True},
            {"author": "Drew",  "score": 5, "verified": True},
        ],
        "discontinued": False, "clearance_note": None,
    },
]

async def ingest():
    texts   = [p["text"] for p in products]
    vectors = embed_texts(texts)
    points  = [
        PointStruct(id=i, vector=vectors[i], payload=p)
        for i, p in enumerate(products)
    ]
    async with AsyncVectorAIClient(url=SERVER) as client:
        await client.points.upsert(COLLECTION, points=points)
        await client.vde.flush(COLLECTION)
    print(f"Ingested {len(points)} products.")

asyncio.run(ingest())

Expected output

Ingested 5 products.
All 5 products are now stored with their text embeddings and structured payload fields. The helper functions below are used in every filter step that follows.

Helper functions

The following helpers are used throughout this tutorial:
async def search(query: str, filter_obj=None, top_k: int = 5):
    query_vector = embed_text(query)
    async with AsyncVectorAIClient(url=SERVER) as client:
        results = await client.points.search(
            COLLECTION,
            vector=query_vector,
            limit=top_k,
            with_payload=True,
            filter=filter_obj,
        ) or []
    return results

def show(results):
    for r in results:
        p = r.payload
        print(f"  id={r.id}  score={r.score:.4f}  "
              f"{p.get('brand')} {p.get('color')} {p.get('sub_category')} ${p.get('price')}")

Equality filters

Exact match with Field.eq

Field.eq matches a string, integer, or boolean value exactly.
# Restrict results to products where color equals "blue"
f = FilterBuilder().must(Field("color").eq("blue")).build()
results = asyncio.run(search("running shoes", f))
print("=== color == 'blue' ===")
show(results)

Expected output

=== color == 'blue' ===
  id=4  score=0.6590  TrailMaster blue trail_running $159.99
  id=0  score=0.6502  SpeedRunner blue running $129.99
The filter passes only the two blue products — TrailMaster (id=4) and SpeedRunner (id=0). The ClassicFit, UrbanStep, and TrailRunner are excluded before scoring begins, so their scores never appear regardless of vector similarity. Field.eq also works on boolean fields:
# Match products where in_stock is True
f = FilterBuilder().must(Field("in_stock").eq(True)).build()
results = asyncio.run(search("formal leather shoes", f))
print("=== in_stock == True ===")
show(results)

Expected output

=== in_stock == True ===
  id=2  score=0.7296  UrbanStep white casual $89.99
  id=0  score=0.5158  SpeedRunner blue running $129.99
  id=1  score=0.4698  TrailMaster red hiking $189.99
  id=4  score=0.4289  TrailMaster blue trail_running $159.99
Field.eq works on boolean fields without any special syntax — True and False are matched directly. The out-of-stock product (id=1, TrailMaster discontinued=True) is excluded.

Full-text match with Field.text

Field.text performs token-based matching against text-indexed fields. It requires a TextIndexParams payload index on the field and matches documents that contain the given token anywhere in the indexed content.
# Match products whose "text" field contains the token "waterproof"
f = FilterBuilder().must(Field("text").text("waterproof")).build()
results = asyncio.run(search("outdoor shoes", f))
print("=== text contains 'waterproof' ===")
show(results)

Expected output

=== text contains 'waterproof' ===
  id=1  score=0.4001  TrailMaster red hiking $189.99
The trail running shoe (id=4) has “waterproof” in its tags array but not in its text field, so it is excluded by this filter.

IN list with Field.any_of

Field.any_of matches any value in a provided list, equivalent to a SQL IN clause.
# Keep products where color is "blue" or "white"
f = FilterBuilder().must(Field("color").any_of(["blue", "white"])).build()
results = asyncio.run(search("comfortable shoes", f))
print("=== color IN ['blue', 'white'] ===")
show(results)

Expected output

=== color IN ['blue', 'white'] ===
  id=0  score=0.6337  SpeedRunner blue running $129.99
  id=2  score=0.6092  UrbanStep white casual $89.99
  id=4  score=0.5266  TrailMaster blue trail_running $159.99
Three products match — SpeedRunner (blue), UrbanStep (white), and TrailMaster (blue). any_of is equivalent to writing multiple should conditions on the same field but in a single, readable call.

NOT IN list with Field.except_of

Field.except_of excludes any point whose field value matches an entry in the provided list, equivalent to a SQL NOT IN clause.
# Exclude products from "TrailMaster" and "ClassicFit"
f = FilterBuilder().must(Field("brand").except_of(["TrailMaster", "ClassicFit"])).build()
results = asyncio.run(search("shoes", f))
print("=== brand NOT IN ['TrailMaster', 'ClassicFit'] ===")
show(results)

Expected output

=== brand NOT IN ['TrailMaster', 'ClassicFit'] ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
Only UrbanStep (id=2) and SpeedRunner (id=0) pass — the two TrailMaster products and the ClassicFit are excluded. except_of is the complement of any_of: it rejects any document whose field value appears in the list.

Numeric range filters

Single-bound range

# Filter to products where price is strictly greater than 150
f = FilterBuilder().must(Field("price").gt(150.0)).build()
results = asyncio.run(search("shoes", f))
print("=== price > 150 ===")
show(results)

Expected output

=== price > 150 ===
  id=3  score=0.5393  ClassicFit black formal $249.99
  id=4  score=0.4910  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
The same approach works with gte, lt, and lte:
# Filter to products with a rating of 4.5 or higher
f = FilterBuilder().must(Field("rating").gte(4.5)).build()
results = asyncio.run(search("shoes", f))
print("=== rating >= 4.5 ===")
show(results)

Expected output

=== rating >= 4.5 ===
  id=3  score=0.5393  ClassicFit black formal $249.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4910  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
ClassicFit (4.8), SpeedRunner (4.7), and TrailMaster (4.6) all meet the threshold. UrbanStep (4.2) and TrailRunner (4.3) are excluded. The results are still ranked by vector similarity, not by rating — the filter only controls which products enter the scoring stage.

Closed range with Field.between

Field.between filters within both a lower and upper bound in a single call. Set inclusive=True for a closed range that includes the endpoints.
# Keep products with price between $100 and $200 (inclusive)
f = FilterBuilder().must(Field("price").between(100.0, 200.0, inclusive=True)).build()
results = asyncio.run(search("shoes", f))
print("=== 100 <= price <= 200 ===")
show(results)

Expected output

=== 100 <= price <= 200 ===
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4484  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
SpeedRunner (129.99)andTrailMaster(129.99) and TrailMaster (159.99) fall within the range. UrbanStep (89.99)isbelowthelowerbound;ClassicFit(89.99) is below the lower bound; ClassicFit (249.99) exceeds the upper bound. between is inclusive on both ends.

Flexible range with Field.range

Field.range lets you combine any mix of inclusive and exclusive bounds. At least one bound is required.
# Keep products with rating >= 4.0 and rating < 4.8
f = FilterBuilder().must(Field("rating").range(gte=4.0, lt=4.8)).build()
results = asyncio.run(search("shoes", f))
print("=== 4.0 <= rating < 4.8 ===")
show(results)

Expected output

=== 4.0 <= rating < 4.8 ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4484  TrailMaster blue trail_running $159.99
ClassicFit (4.8) is excluded because the upper bound is strict (lt, not lte). The remaining three products — UrbanStep (4.2), TrailRunner (4.3), SpeedRunner (4.7) — all fall within the half-open interval [4.0, 4.8).

Datetime filters

Datetime filters work the same way as numeric range filters but operate on timestamp fields.

Single-bound datetime filter

from datetime import datetime as dt

# Keep products created on or after January 1, 2026
f = FilterBuilder().must(
    Field("created_at").datetime_gte(dt.fromisoformat("2026-01-01T00:00:00+00:00"))
).build()
results = asyncio.run(search("shoes", f))
print("=== created_at >= 2026-01-01 ===")
show(results)

Expected output

=== created_at >= 2026-01-01 ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4484  TrailMaster blue trail_running $159.99
UrbanStep and TrailRunner were created after 2026-01-01 and pass the filter. The older products (SpeedRunner, TrailMaster, ClassicFit — created in 2025) are excluded. Datetime fields require ISO 8601 format strings and behave like numeric range comparisons.

Datetime range with datetime_between

# Keep products created between October 1 and December 31, 2025 (inclusive)
f = FilterBuilder().must(
    Field("created_at").datetime_between(
        lower=dt.fromisoformat("2025-10-01T00:00:00+00:00"),
        upper=dt.fromisoformat("2025-12-31T23:59:59+00:00"),
        inclusive=True,
    )
).build()
results = asyncio.run(search("shoes", f))
print("=== created_at in Q4 2025 ===")
show(results)

Expected output

=== created_at in Q4 2025 ===
  id=1  score=0.3720  TrailMaster red hiking $189.99
Only TrailMaster (id=1, created 2025-11-20) falls within the Q4 2025 window (2025-10-01 to 2025-12-31). SpeedRunner was created in August 2025 (Q3) and ClassicFit in September 2025, both outside the range.

Geo filters

Geo filters restrict results to points within a geographic area. The payload field must store {"lat": ..., "lon": ...} objects.

Radius with geo_radius

geo_radius finds all points within a given radius (in metres) of a center coordinate. Use it when you want results within a circular area around a specific location. The following code restricts the search to products whose location field falls within 500 km of New York City. Running it returns only the single product stored with NYC coordinates:
# Keep products located within 500 km of New York City
f = FilterBuilder().must(
    Field("location").geo_radius(lat=40.7128, lon=-74.0060, radius=500000)
).build()
results = asyncio.run(search("shoes", f))
print("=== Within 500km of NYC ===")
show(results)

Expected output

=== Within 500km of NYC ===
  id=0  score=0.5152  SpeedRunner blue running $129.99
SpeedRunner is stored with New York coordinates and falls within the 500 km radius. The ClassicFit (London) and other non-US products are outside the radius and excluded. geo_radius uses the Haversine formula over the location payload field.

Bounding box with geo_bounding_box

geo_bounding_box finds all points within a rectangular geographic region defined by top-left and bottom-right corners. It is faster than a polygon check and useful when the region maps naturally to a lat/lon rectangle. The following code restricts the search to products located within the Continental US bounding box. Running it returns four products and excludes the Oxford stored in London:
# Keep products within the Continental US bounding box
f = FilterBuilder().must(
    Field("location").geo_bounding_box(
        top_left=(49.0, -125.0),
        bottom_right=(25.0, -66.0),
    )
).build()
results = asyncio.run(search("shoes", f))
print("=== Within Continental US ===")
show(results)

Expected output

=== Within Continental US ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4910  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
The ClassicFit Oxford (id=3) is stored with London coordinates and is excluded.

Polygon with geo_polygon

geo_polygon finds all points within an arbitrary polygon defined by an ordered list of (lat, lon) vertices. The polygon closes automatically, so the first point does not need to be repeated. The following code restricts the search to products located within a polygon covering the Northeast US. Running it returns only the single product stored with New York City coordinates:
# Keep products located within a polygon covering the Northeast US
f = FilterBuilder().must(
    Field("location").geo_polygon(exterior=[
        (42.5, -80.0),
        (42.5, -70.0),
        (38.0, -70.0),
        (38.0, -80.0),
    ])
).build()
results = asyncio.run(search("shoes", f))
print("=== Within Northeast US polygon ===")
show(results)

Expected output

=== Within Northeast US polygon ===
  id=0  score=0.5152  SpeedRunner blue running $129.99
SpeedRunner (New York) lies inside the Northeast US polygon. geo_polygon checks whether the stored coordinate falls within the closed polygon defined by the exterior ring. Points outside the polygon — including those with no location payload — are excluded before scoring.

Array cardinality filter

Filter by number of values with values_count

values_count keeps only points whose array field has a number of elements that satisfies the given bounds. Use it to filter by the size of an array such as tags or reviews.
# Keep products that have at least 4 tags
f = FilterBuilder().must(Field("tags").values_count(gte=4)).build()
results = asyncio.run(search("shoes", f))
print("=== tags count >= 4 ===")
show(results)

Expected output

=== tags count >= 4 ===
  id=4  score=0.4910  TrailMaster blue trail_running $159.99
To match an exact count, set both gte and lte to the same value:
# Keep products that have exactly 3 tags
f = FilterBuilder().must(Field("tags").values_count(gte=3, lte=3)).build()
results = asyncio.run(search("shoes", f))
print("=== tags count == 3 ===")
show(results)

Expected output

=== tags count == 3 ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=3  score=0.5393  ClassicFit black formal $249.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
values_count supports lt, gt, gte, and lte — at least one bound is required.

Null and empty checks

Check for null with is_null

is_null matches points where a payload field is present but explicitly set to null. Use it to find records that have a field key but no value assigned to it.
# Keep products where clearance_note is explicitly null (not on clearance)
f = FilterBuilder().must(is_null("clearance_note")).build()
results = asyncio.run(search("shoes", f))
print("=== clearance_note IS NULL ===")
show(results)

Expected output

=== clearance_note IS NULL ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4484  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
is_null matches points where the field is explicitly set to null in the payload. Products where clearance_note is absent entirely are not matched — use is_empty for that case. The distinction matters when distinguishing “field set to null” from “field not present”.

Check for empty or missing with is_empty

is_empty matches points where an array or string field contains no elements, or where the field key is absent entirely.
# Keep products with no reviews (empty array)
f = FilterBuilder().must(is_empty("reviews")).build()
results = asyncio.run(search("shoes", f))
print("=== reviews IS EMPTY ===")
show(results)

Expected output

=== reviews IS EMPTY ===
  id=2  score=0.5610  UrbanStep white casual $89.99
UrbanStep has an empty reviews array ([]). is_empty matches fields that are either absent from the payload, set to null, or set to an empty array or string. It is broader than is_null and useful for finding records with missing or unpopulated fields.

Point-level conditions

Restrict search to specific IDs with has_id

has_id narrows a search to a known subset of point IDs. Use it to apply a pre-filtered allowlist — for example, IDs returned by a business rules layer before the vector search runs.
# Only consider points with IDs 0, 2, and 4
f = FilterBuilder().must(has_id([0, 2, 4])).build()
results = asyncio.run(search("comfortable shoes", f))
print("=== has_id [0, 2, 4] ===")
show(results)

Expected output

=== has_id [0, 2, 4] ===
  id=0  score=0.6337  SpeedRunner blue running $129.99
  id=2  score=0.6092  UrbanStep white casual $89.99
  id=4  score=0.5266  TrailMaster blue trail_running $159.99
has_id restricts the search to exactly the listed point IDs regardless of payload content. This is useful for re-ranking or rescoring a known set of candidates — for example, re-searching a shortlist from a previous query.

Check for a named vector with has_vector

has_vector restricts results to points that carry a particular named vector. Pass an empty string to target the default (unnamed) vector.
# Restrict to points that have the default vector populated
f = FilterBuilder().must(has_vector("")).build()
results = asyncio.run(search("shoes", f))
print(f"=== has default vector: {len(results)} results ===")

Expected output

=== has default vector: 5 results ===
All 5 products have a default (unnamed) vector space, so all 5 pass. has_vector is most useful in named-vector collections where some points may be missing a specific vector space — for example, filtering to only products that have an image embedding.

Nested filters

The nested condition filters on fields within nested objects. Each item in an array of objects is evaluated independently — all conditions must be satisfied by the same object, not spread across different objects in the array.
# Build an inner filter: score >= 5 AND verified == True
inner = FilterBuilder().must(Field("score").gte(5.0)).must(Field("verified").eq(True))
# Wrap with nested() so each review object is evaluated independently
f = FilterBuilder().must(nested("reviews", inner)).build()
results = asyncio.run(search("shoes", f))
print("=== nested: reviews with score >= 5 AND verified ===")
show(results)

Expected output

=== nested: reviews with score >= 5 AND verified ===
  id=3  score=0.5393  ClassicFit black formal $249.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4910  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
The UrbanStep (id=2) is excluded because its reviews array is empty.

Boolean logic

FilterBuilder supports four clause types that control how conditions combine.
MethodMeaningSQL equivalent
.must()All conditions must matchAND
.should()At least one condition should matchOR
.must_not()Exclude any points that matchNOT
.min_should(conds, N)At least N conditions matchN-of-M

Require all conditions with must (AND)

must requires every condition in the clause to be satisfied. Points that fail any one condition are excluded. Use it when multiple constraints must all hold simultaneously. The following code requires color == "blue", in_stock == True, and price < 150 to all be true. Running it returns only the SpeedRunner running shoe, which is the only product satisfying all three conditions:
# Require: color == blue AND in_stock == True AND price < 150
f = (
    FilterBuilder()
    .must(Field("color").eq("blue"))
    .must(Field("in_stock").eq(True))
    .must(Field("price").lt(150.0))
    .build()
)
results = asyncio.run(search("running shoes", f))
print("=== must: blue AND in_stock AND price < 150 ===")
show(results)

Expected output

=== must: blue AND in_stock AND price < 150 ===
  id=0  score=0.6502  SpeedRunner blue running $129.99
Only SpeedRunner satisfies all three conditions simultaneously: blue color, in-stock, and price below 150.TrailMasterisblueandinstockbutcosts150. TrailMaster is blue and in-stock but costs 159.99. must applies AND logic — every condition must hold or the point is excluded.

Accept alternatives with should (OR)

should requires at least one condition in the clause to match. Use it when multiple alternative values are acceptable and any of several criteria is sufficient. The following code accepts products where color is "blue" or "red". Running it returns three products and excludes the white and black products:
# Accept products where color is "blue" OR color is "red"
f = (
    FilterBuilder()
    .should(Field("color").eq("blue"))
    .should(Field("color").eq("red"))
    .build()
)
results = asyncio.run(search("outdoor shoes", f))
print("=== should: blue OR red ===")
show(results)

Expected output

=== should: blue OR red ===
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4484  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
Three products pass — TrailMaster (blue, id=4), SpeedRunner (blue, id=0), and TrailMaster red (id=1). should applies OR logic: a point is included if it satisfies at least one condition. The white and black products are excluded.

Exclude results with must_not (NOT)

must_not removes any point that matches the condition from the results. Use it to suppress categories or attribute values that should never appear in the output. The following code requires the footwear category and then excludes both discontinued and formal products. Running it returns four in-catalogue, non-formal products:
# Require footwear category, then exclude discontinued and formal products
f = (
    FilterBuilder()
    .must(Field("category").eq("footwear"))
    .must_not(Field("discontinued").eq(True))
    .must_not(Field("sub_category").eq("formal"))
    .build()
)
results = asyncio.run(search("shoes", f))
print("=== must_not: NOT discontinued AND NOT formal ===")
show(results)

Expected output

=== must_not: NOT discontinued AND NOT formal ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4910  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
must_not excludes any point matching the condition. The discontinued TrailMaster (id=1) is excluded by the first condition; the ClassicFit formal shoe (id=3) is excluded by the second. The remaining three products — UrbanStep, SpeedRunner, TrailRunner — pass both exclusion checks.

Match at least N conditions with min_should

min_should qualifies a point when it satisfies at least min_count of the supplied conditions. Use it when partial matches are acceptable and you want to control the minimum number of criteria that must be met. The following code defines four independent conditions and requires at least three of them to be satisfied. Running it returns the two products that satisfy three or more of the four conditions:
conditions = [
    Field("color").eq("blue"),
    Field("brand").eq("TrailMaster"),
    Field("price").lt(170.0),
    Field("rating").gte(4.5),
]
# Require at least 3 of the 4 conditions to be satisfied
f = FilterBuilder().min_should(conditions, min_count=3).build()
results = asyncio.run(search("trail shoes", f))
print("=== min_should: at least 3 of 4 conditions ===")
show(results)

Expected output

=== min_should: at least 3 of 4 conditions ===
  id=4  score=0.6662  TrailMaster blue trail_running $159.99
  id=0  score=0.5284  SpeedRunner blue running $129.99
id=4 matches 4/4: blue, TrailMaster, price 159.99<170,rating4.6>=4.5.id=0matches3/4:blue,price159.99 < 170, rating 4.6 >= 4.5. `id=0` matches 3/4: blue, price 129.99 < 170, rating 4.5 >= 4.5 (but brand is SpeedRunner, not TrailMaster).

Combining clauses

f = (
    FilterBuilder()
    .must(Field("in_stock").eq(True))
    .must(Field("price").between(100.0, 200.0))
    .should(Field("color").eq("blue"))
    .should(Field("color").eq("red"))
    .must_not(Field("discontinued").eq(True))
    .build()
)
results = asyncio.run(search("shoes for outdoor activities", f))
print("=== Combined: in_stock AND price 100-200 AND (blue OR red) AND NOT discontinued ===")
show(results)

Expected output

=== Combined: in_stock AND price 100-200 AND (blue OR red) AND NOT discontinued ===
  id=4  score=0.6007  TrailMaster blue trail_running $159.99
  id=0  score=0.5992  SpeedRunner blue running $129.99
  id=1  score=0.5064  TrailMaster red hiking $189.99
TrailMaster (id=4) is the only product satisfying all four conditions: in stock, priced between 100100–200, colored blue, and not discontinued. Combining must, should, and must_not in a single FilterBuilder call lets you express complex business rules as a single filter object.

Operator composition

Python operators let you combine conditions and builders without calling clause methods directly.

Condition operators

is_blue    = Field("color").eq("blue")
is_cheap   = Field("price").lt(140.0)
is_running = Field("sub_category").eq("running")

# AND — both conditions must be true
f = (is_blue & is_cheap).build()
results = asyncio.run(search("shoes", f))
print("=== blue & price < 140 ===")
show(results)

Expected output

=== blue & price < 140 ===
  id=0  score=0.5152  SpeedRunner blue running $129.99
The & operator combines two Field conditions with AND logic. SpeedRunner is the only blue product priced below 140.TrailMasterisbluebutcosts140. TrailMaster is blue but costs 159.99 and is excluded by the price condition.
# OR — either condition is sufficient
f = (is_blue | is_running).build()
results = asyncio.run(search("shoes", f))
print("=== blue | running ===")
show(results)

Expected output

=== blue | running ===
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4910  TrailMaster blue trail_running $159.99
The | operator combines conditions with OR logic. SpeedRunner matches both conditions (blue and running category). TrailMaster matches blue. TrailRunner matches the running category. Products matching either condition are included.
# NOT — negate a condition (places it in must_not)
f = (~Field("discontinued").eq(True)).build()
results = asyncio.run(search("shoes", f))
print("=== NOT discontinued ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4484  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99

Expected output

=== NOT discontinued ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4910  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
The ~ prefix operator negates a condition, equivalent to wrapping it in must_not. The discontinued TrailMaster (id=1, discontinued=True) is excluded. All four non-discontinued products pass.

FilterBuilder operators

blue_and_cheap  = FilterBuilder().must(Field("color").eq("blue")).must(Field("price").lt(140.0))
red_and_premium = FilterBuilder().must(Field("color").eq("red")).must(Field("price").gte(150.0))

# OR two builders — each becomes a nested sub-filter in should
f = (blue_and_cheap | red_and_premium).build()
results = asyncio.run(search("shoes", f))
print("=== (blue & cheap) | (red & premium) ===")
show(results)

Expected output

=== (blue & cheap) | (red & premium) ===
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
SpeedRunner passes the first branch (blue and cheap: 129.99<129.99 < 150). TrailMaster red passes the second branch (red and premium: 189.99>189.99 > 150). The Python | operator on FilterBuilder objects creates a top-level should clause combining the two sub-filters.
# AND two builders — merges their must/must_not/should lists into one filter
in_stock_fb   = FilterBuilder().must(Field("in_stock").eq(True))
high_rated_fb = FilterBuilder().must(Field("rating").gte(4.5))
f = (in_stock_fb & high_rated_fb).build()
results = asyncio.run(search("shoes", f))
print("=== in_stock & rating >= 4.5 ===")
show(results)

Expected output

=== in_stock & rating >= 4.5 ===
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4484  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
SpeedRunner (in-stock, rating 4.7) and ClassicFit (in-stock, rating 4.8) both pass. Using & directly on FilterBuilder instances is syntactic sugar for chaining .must() calls — the compiled filter is identical.
# INVERT a builder — swaps must and must_not
exclude_discontinued = ~FilterBuilder().must(Field("discontinued").eq(True))
f = exclude_discontinued.build()
results = asyncio.run(search("shoes", f))
print("=== ~(discontinued) ===")
show(results)

Expected output

=== ~(discontinued) ===
  id=2  score=0.5610  UrbanStep white casual $89.99
  id=0  score=0.5152  SpeedRunner blue running $129.99
  id=4  score=0.4484  TrailMaster blue trail_running $159.99
  id=1  score=0.3720  TrailMaster red hiking $189.99
Applying ~ to a FilterBuilder instance wraps its conditions in must_not. The result is identical to calling .must_not() explicitly. This operator form is useful when building filters programmatically where the negation decision is made separately from the condition definition.

Using filters with different endpoints

The same FilterBuilder objects work across all points.* methods.
f = FilterBuilder().must(Field("brand").eq("TrailMaster")).build()
results = asyncio.run(search("hiking boots", f))
show(results, "points.search — brand=TrailMaster")

Expected output

=== points.search — brand=TrailMaster ===
  id=4  score=0.6052  TrailMaster blue trail_running $159.99
  id=1  score=0.5277  TrailMaster red hiking $189.99
Both TrailMaster products (id=1 and id=4) are returned, ranked by cosine similarity to the query. The filter parameter on points.search is applied server-side before scoring, so only the two brand-matching products are ever scored.

With points.query

async def query_with_filter():
    f = FilterBuilder().must(Field("in_stock").eq(True)).build()
    async with AsyncVectorAIClient(url=SERVER) as client:
        results = await client.points.query(
            COLLECTION,
            query=embed_text("comfortable shoes"),
            filter=f,
            limit=5,
            with_payload=True,
        )
    return results

results = asyncio.run(query_with_filter())
show(results, "points.query — in_stock=True")

Expected output

=== points.query — in_stock=True ===
  id=0  score=0.6337  SpeedRunner blue running $129.99
  id=2  score=0.6092  UrbanStep white casual $89.99
  id=4  score=0.5266  TrailMaster blue trail_running $159.99
  id=1  score=0.4366  TrailMaster red hiking $189.99
points.query with query=vec behaves identically to points.search for single-vector queries but supports the full query DSL including prefetch, fusion, and order_by. The filter is applied at the same stage in both cases.

With points.count

Note: points.count() is not supported in VectorAI DB 1.0.0 and raises UnimplementedError. Use points.scroll() with Python len() as a workaround.
async def count_with_filter():
    async with AsyncVectorAIClient(url=SERVER) as client:
        # Workaround: scroll + len()
        f = FilterBuilder().must(Field("in_stock").eq(True)).build()
        pts, _ = await client.points.scroll(
            COLLECTION, limit=1000, with_payload=False,
            with_vectors=False, filter=f,
        )
        print(f"In-stock products: {len(pts)}")

        f = FilterBuilder().must(Field("price").gt(150.0)).build()
        pts, _ = await client.points.scroll(
            COLLECTION, limit=1000, with_payload=False,
            with_vectors=False, filter=f,
        )
        print(f"Products > $150: {len(pts)}")

asyncio.run(count_with_filter())

Expected output

In-stock products: 4
Products > $150: 3
The scroll-and-count workaround returns exact counts matching the filter. points.count() with a filter is not implemented in VectorAI DB 1.0.0 (raises 501), so points.scroll(limit=1000) followed by a Python sum() is the supported alternative.

With points.delete

async def delete_with_filter():
    f = (
        FilterBuilder()
        .must(Field("discontinued").eq(True))
        .must(Field("in_stock").eq(False))
        .build()
    )
    async with AsyncVectorAIClient(url=SERVER) as client:
        pts, _ = await client.points.scroll(
            COLLECTION, limit=1000, with_payload=False,
            with_vectors=False, filter=f,
        )
        print(f"Discontinued + out of stock: {len(pts)}")
        # Uncomment to delete:
        # await client.points.delete(COLLECTION, filter=f)

asyncio.run(delete_with_filter())

Expected output

Discontinued + out of stock: 1
The filter identifies the one product matching both conditions — discontinued AND out of stock — before deleting it. Passing a Filter to points.delete is more efficient than fetching IDs first and then deleting by list, especially at scale.

With points.set_payload

async def update_with_filter():
    f = FilterBuilder().must(Field("brand").eq("TrailMaster")).build()
    async with AsyncVectorAIClient(url=SERVER) as client:
        await client.points.set_payload(
            COLLECTION,
            payload={"featured": True},
            filter=f,
        )
    print("All TrailMaster products marked as featured.")

asyncio.run(update_with_filter())
points.set_payload with a filter updates every matching point in a single call. Both TrailMaster products (id=1 and id=4) receive the featured: True field without touching their vectors or other payload fields.

Expected output

All TrailMaster products marked as featured.

Utility methods

Check whether a builder has conditions with is_empty

FilterBuilder.is_empty() returns True if no conditions have been added.
fb = FilterBuilder()
print(f"Empty: {fb.is_empty()}")   # True

fb = fb.must(Field("color").eq("blue"))
print(f"Empty: {fb.is_empty()}")   # False

Expected output

Empty: True
Empty: False
A freshly created FilterBuilder() reports is_empty() == True because no conditions have been added. After calling .must(), is_empty() returns False. This lets you skip passing a filter object to search when no conditions are active — sending an empty filter is valid but slightly wasteful.

Branch filter logic with copy

FilterBuilder.copy() creates a shallow copy so you can derive multiple filter variants from a shared base without mutating the original.
base     = FilterBuilder().must(Field("in_stock").eq(True))
branch_a = base.copy().must(Field("color").eq("blue"))
branch_b = base.copy().must(Field("color").eq("red"))

print(f"Base:     {base}")      # FilterBuilder(must=1)
print(f"Branch A: {branch_a}") # FilterBuilder(must=2)
print(f"Branch B: {branch_b}") # FilterBuilder(must=2)

Expected output

Base:     FilterBuilder(must=1)
Branch A: FilterBuilder(must=2)
Branch B: FilterBuilder(must=2)
copy() creates an independent clone so that modifications to Branch A or Branch B do not affect the base builder or each other. Without copy(), all three variables would reference the same mutable object and adding conditions to one would affect all.

Test truthiness with bool

FilterBuilder evaluates to True when it has at least one condition and False when empty. The following code checks an empty builder and prints a message when no filters have been configured:
fb = FilterBuilder()
if not fb:
    print("No filters applied — searching without constraints.")
bool(fb) returns False when the builder has no conditions, letting you guard the filter= parameter with a simple if fb: check. When bool(fb) is False, passing filter=fb.build() to search would send an empty filter object; omitting the filter entirely is cleaner and slightly more efficient.

Expected output

No filters applied — searching without constraints.

Cleanup

async def cleanup():
    async with AsyncVectorAIClient(url=SERVER) as client:
        count = await client.vde.get_vector_count(COLLECTION)
        await client.vde.flush(COLLECTION)
        print(f"Collection '{COLLECTION}' contains {count} vectors.")
        print("Data flushed to disk.")

        # Uncomment to delete:
        # await client.collections.delete(COLLECTION)

asyncio.run(cleanup())

Complete filter reference

Field conditions

MethodTypeExample
eq(value)Exact match (str, int, bool)Field("color").eq("blue")
text(value)Full-text token matchField("text").text("waterproof")
any_of(values)IN listField("color").any_of(["blue", "red"])
except_of(values)NOT IN listField("brand").except_of(["X", "Y"])
gt(value)Greater thanField("price").gt(100.0)
gte(value)Greater than or equalField("rating").gte(4.5)
lt(value)Less thanField("price").lt(200.0)
lte(value)Less than or equalField("rating").lte(5.0)
between(lo, hi)Closed/open rangeField("price").between(50, 150)
range(gt=, gte=, lt=, lte=)Flexible boundsField("price").range(gte=50, lt=200)
datetime_gt(dt)After datetimeField("created_at").datetime_gt(dt)
datetime_gte(dt)At or after datetimeField("created_at").datetime_gte(dt)
datetime_lt(dt)Before datetimeField("created_at").datetime_lt(dt)
datetime_lte(dt)At or before datetimeField("created_at").datetime_lte(dt)
datetime_between(lo, hi)Datetime rangeField("created_at").datetime_between(lo, hi)
values_count(gte=, lte=, ...)Array cardinalityField("tags").values_count(gte=3)
geo_radius(lat, lon, r)Circle (metres)Field("loc").geo_radius(40.7, -74.0, 5000)
geo_bounding_box(tl, br)RectangleField("loc").geo_bounding_box((49,-125),(25,-66))
geo_polygon(exterior)PolygonField("loc").geo_polygon([(a,b),(c,d),...])

Standalone conditions

FunctionPurposeExample
is_null(key)Field is nullis_null("notes")
is_empty(key)Field is empty or missingis_empty("reviews")
has_id(ids)Point ID in listhas_id([0, 1, 2])
has_vector(name)Named vector existshas_vector("")
nested(key, filter)Nested object filternested("reviews", inner_fb)

FilterBuilder clauses

MethodLogicEffect
.must(cond)ANDAll must conditions required
.should(cond)ORAt least one should condition
.must_not(cond)NOTExclude matching
.min_should(conds, N)N-of-MAt least N conditions match

Operators

OperatorOn conditionOn FilterBuilder
a & bmust=[a, b]Merge lists
`a \b`should=[a, b]Nested sub-filters in should
~amust_not=[a]Swap must and must_not

Next steps