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


Setup: Create a collection and ingest sample data

Step 1: Configure and initialize

Step 2: Create the collection

Expected output

The get_or_create call is idempotent — safe to run on every application startup without creating duplicate collections.

Step 3: Ingest sample products

Expected output

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:

Equality filters

Exact match with Field.eq

Field.eq matches a string, integer, or boolean value exactly.

Expected output

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:

Expected output

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.

Expected output

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.

Expected output

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.

Expected output

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

Expected output

The same approach works with gte, lt, and lte:

Expected output

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.

Expected output

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.

Expected output

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

Expected output

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

Expected output

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:

Expected output

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:

Expected output

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:

Expected output

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.

Expected output

To match an exact count, set both gte and lte to the same value:

Expected output

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.

Expected output

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.

Expected output

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.

Expected output

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.

Expected output

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.

Expected output

The UrbanStep (id=2) is excluded because its reviews array is empty.

Boolean logic

FilterBuilder supports four clause types that control how conditions combine.

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:

Expected output

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:

Expected output

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:

Expected output

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:

Expected output

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

Expected output

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

Expected output

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.

Expected output

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.

Expected output

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

Expected output

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.

Expected output

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.

Expected output

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.

Expected output

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

Expected output

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.

Expected output

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

Expected output

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

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


Utility methods

Check whether a builder has conditions with is_empty

FilterBuilder.is_empty() returns True if no conditions have been added.

Expected output

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.

Expected output

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


Cleanup


Complete filter reference

Field conditions

Standalone conditions

FilterBuilder clauses

Operators

Next steps