> ## Documentation Index
> Fetch the complete documentation index at: https://actianvectorai-ml-crtx-1153-academy-tutorial-rewrites.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Predicate filters

> Learn how to combine vector similarity search with structured payload filters using Actian VectorAI DB's type-safe Filter DSL.

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

```bash theme={null}
pip install actian-vectorai-client sentence-transformers
```

***

## Setup: Create a collection and ingest sample data

### Step 1: Configure and initialize

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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:

```python theme={null}
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.

```python theme={null}
# 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:

```python theme={null}
# 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.

```python theme={null}
# 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.

```python theme={null}
# 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.

```python theme={null}
# 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

```python theme={null}
# 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`:

```python theme={null}
# 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.

```python theme={null}
# 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) and TrailMaster ($159.99) fall within the range. UrbanStep ($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.

```python theme={null}
# 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

```python theme={null}
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

```python theme={null}
# 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:

```python theme={null}
# 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:

```python theme={null}
# 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:

```python theme={null}
# 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`.

```python theme={null}
# 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:

```python theme={null}
# 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.

```python theme={null}
# 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.

```python theme={null}
# 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.

```python theme={null}
# 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.

```python theme={null}
# 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.

```python theme={null}
# 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.

| Method                  | Meaning                             | SQL equivalent |
| ----------------------- | ----------------------------------- | -------------- |
| `.must()`               | All conditions must match           | AND            |
| `.should()`             | At least one condition should match | OR             |
| `.must_not()`           | Exclude any points that match       | NOT            |
| `.min_should(conds, N)` | At least N conditions match         | N-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:

```python theme={null}
# 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. 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:

```python theme={null}
# 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:

```python theme={null}
# 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:

```python theme={null}
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, 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

```python theme={null}
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 $100–$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

```python theme={null}
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. TrailMaster is blue but costs $159.99 and is excluded by the price condition.

```python theme={null}
# 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.

```python theme={null}
# 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

```python theme={null}
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 < $150). TrailMaster red passes the second branch (red and premium: $189.99 > $150). The Python `|` operator on `FilterBuilder` objects creates a top-level `should` clause combining the two sub-filters.

```python theme={null}
# 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.

```python theme={null}
# 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.

### With points.search

```python theme={null}
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

```python theme={null}
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.

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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.

```python theme={null}
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.

```python theme={null}
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:

```python theme={null}
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

```python theme={null}
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

| Method                          | Type                         | Example                                             |
| ------------------------------- | ---------------------------- | --------------------------------------------------- |
| `eq(value)`                     | Exact match (str, int, bool) | `Field("color").eq("blue")`                         |
| `text(value)`                   | Full-text token match        | `Field("text").text("waterproof")`                  |
| `any_of(values)`                | IN list                      | `Field("color").any_of(["blue", "red"])`            |
| `except_of(values)`             | NOT IN list                  | `Field("brand").except_of(["X", "Y"])`              |
| `gt(value)`                     | Greater than                 | `Field("price").gt(100.0)`                          |
| `gte(value)`                    | Greater than or equal        | `Field("rating").gte(4.5)`                          |
| `lt(value)`                     | Less than                    | `Field("price").lt(200.0)`                          |
| `lte(value)`                    | Less than or equal           | `Field("rating").lte(5.0)`                          |
| `between(lo, hi)`               | Closed/open range            | `Field("price").between(50, 150)`                   |
| `range(gt=, gte=, lt=, lte=)`   | Flexible bounds              | `Field("price").range(gte=50, lt=200)`              |
| `datetime_gt(dt)`               | After datetime               | `Field("created_at").datetime_gt(dt)`               |
| `datetime_gte(dt)`              | At or after datetime         | `Field("created_at").datetime_gte(dt)`              |
| `datetime_lt(dt)`               | Before datetime              | `Field("created_at").datetime_lt(dt)`               |
| `datetime_lte(dt)`              | At or before datetime        | `Field("created_at").datetime_lte(dt)`              |
| `datetime_between(lo, hi)`      | Datetime range               | `Field("created_at").datetime_between(lo, hi)`      |
| `values_count(gte=, lte=, ...)` | Array cardinality            | `Field("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)`      | Rectangle                    | `Field("loc").geo_bounding_box((49,-125),(25,-66))` |
| `geo_polygon(exterior)`         | Polygon                      | `Field("loc").geo_polygon([(a,b),(c,d),...])`       |

### Standalone conditions

| Function              | Purpose                   | Example                       |
| --------------------- | ------------------------- | ----------------------------- |
| `is_null(key)`        | Field is null             | `is_null("notes")`            |
| `is_empty(key)`       | Field is empty or missing | `is_empty("reviews")`         |
| `has_id(ids)`         | Point ID in list          | `has_id([0, 1, 2])`           |
| `has_vector(name)`    | Named vector exists       | `has_vector("")`              |
| `nested(key, filter)` | Nested object filter      | `nested("reviews", inner_fb)` |

### FilterBuilder clauses

| Method                  | Logic  | Effect                        |
| ----------------------- | ------ | ----------------------------- |
| `.must(cond)`           | AND    | All must conditions required  |
| `.should(cond)`         | OR     | At least one should condition |
| `.must_not(cond)`       | NOT    | Exclude matching              |
| `.min_should(conds, N)` | N-of-M | At least N conditions match   |

### Operators

| Operator | On condition   | On FilterBuilder        |                              |
| -------- | -------------- | ----------------------- | ---------------------------- |
| `a & b`  | must=\[a, b]   | Merge lists             |                              |
| \`a \\   | b\`            | should=\[a, b]          | Nested sub-filters in should |
| `~a`     | must\_not=\[a] | Swap must and must\_not |                              |

## Next steps

* [Similarity search fundamentals](/academy/tutorials/similarity-search) — Learn the core retrieval workflow
* [Reranking search results](/academy/tutorials/re-ranking) — Improve relevance with cross-encoder and reciprocal rank fusion reranking
* [Retrieval quality](/academy/tutorials/retrieval-quality) — Measure and optimize search accuracy using precision, recall, and MRR
* [Open-source embedding models](/academy/tutorials/leverage-open-source-embedding-models) — Integrate open-source models like Sentence Transformers and BGE
