> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mixpeek.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Filters

> Compose filter conditions with logical operators

![Filter composition: AND, OR, and NOT operators nest to build complex filter logic on document payloads](https://mintcdn.com/mixpeek/pDBzbsnRaRIThJZv/assets/mixpeek-filters.svg?fit=max\&auto=format\&n=pDBzbsnRaRIThJZv\&q=85\&s=1ab3a765413b2e1644fa88ce899e201d)

Filters narrow results using logical operators to combine conditions. They operate on document payloads (metadata, enrichments, passthrough fields) and can be applied in retriever execution or as dedicated `filter@v1` stages.

## Payload Indexes

> [!WARNING]
>
> Filters require **payload indexes** on the fields you filter by. Without an index, the vector store performs a full scan — which is slow on large collections and may return incomplete results.

Create indexes on your namespace before using filters:

```bash theme={null}
curl -X PATCH https://api.mixpeek.com/v1/namespaces/{namespace_id} \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "payload_indexes": [
      {"field_name": "metadata.category", "type": "keyword"},
      {"field_name": "metadata.price", "type": "integer"},
      {"field_name": "metadata.status", "type": "keyword"}
    ]
  }'
```

Supported index types:

| Type | Use for |
| - | - |
| `keyword` | Exact-match strings (categories, IDs, tags) |
| `integer` | Whole numbers |
| `float` | Decimal numbers |
| `bool` | Boolean fields |
| `datetime` | Timestamps |
| `text` | Case-insensitive substring match, the same as `contains` |
| `geo` | Geospatial queries |

If you filter on an unindexed field, the response includes a `warnings` array telling you which fields need indexes:

```json theme={null}
{
  "warnings": [
    "Filter field 'brand' has no payload index — filtering may be slow or unreliable on large collections. Create an index via PATCH /v1/namespaces/{namespace_id} with payload_indexes: [{\"field_name\": \"brand\", \"type\": \"keyword\"}]"
  ]
}
```

> [!NOTE]
>
> System fields (`collection_id`, `bucket_id`, `object_id`, `batch_id`) and `_internal.*` fields are indexed automatically — you only need to create indexes for your own fields.

## Logical Operators

Mixpeek filters support three logical operators for composing conditions:

| Operator | Description | Usage |
| - | - | - |
| `AND` | All conditions must be true | Combine multiple required constraints |
| `OR` | At least one condition must be true | Match any of several alternatives |
| `NOT` | Inverts the condition | Exclude matching documents |

### AND Operator

Requires all nested conditions to match:

```json theme={null}
{
  "AND": [
    { "field": "metadata.status", "operator": "eq", "value": "published" },
    { "field": "metadata.price", "operator": "lte", "value": 100 }
  ]
}
```

### OR Operator

Matches if any nested condition is true:

```json theme={null}
{
  "OR": [
    { "field": "metadata.category", "operator": "eq", "value": "video" },
    { "field": "metadata.category", "operator": "eq", "value": "audio" }
  ]
}
```

### NOT Operator

Excludes documents matching the condition:

```json theme={null}
{
  "NOT": {
    "field": "metadata.status", "operator": "eq", "value": "draft"
  }
}
```

## Nesting Operators

Logical operators can be nested to create complex filter logic:

```json theme={null}
{
  "AND": [
    { "field": "metadata.status", "operator": "eq", "value": "published" },
    {
      "OR": [
        { "field": "metadata.category", "operator": "eq", "value": "video" },
        { "field": "metadata.category", "operator": "eq", "value": "audio" }
      ]
    },
    {
      "NOT": {
        "field": "metadata.restricted", "operator": "eq", "value": true
      }
    }
  ]
}
```

This filter matches documents that are:

* Published **AND**
* Either video or audio **AND**
* Not restricted

## Comparison Operators

Use these operators within conditions:

| Operator | Description |
| - | - |
| `eq` | Equals |
| `ne` | Not equals |
| `gt` | Greater than |
| `gte` | Greater than or equal |
| `lt` | Less than |
| `lte` | Less than or equal |
| `in` | Value in list |
| `nin` | Value not in list |
| `exists` | Field exists |
| `is_null` | Field is null |
| `contains` | String contains substring. On an array field, exact element equality instead (see the note below) |
| `starts_with` | String starts with prefix |
| `ends_with` | String ends with suffix |
| `regex` | Matches regular expression |
| `text` | Case-insensitive substring match, the same as `contains` (see the note below) |
| `words` | Every word of the value appears in the field as a whole word, matched on its English stem ("drinking" finds "drinks") |
| `phrase` | Exact phrase — matches the words **in order**, word-boundary safe |

> [!NOTE]
>
> `text` matches the value as a substring on retriever filters and document
> lists. It does not split the value into words or rank. Use `words` for
> whole-word matching, and `phrase` when word
> order matters — e.g. find a transcript where someone says an exact quote.
> `{ "field": "transcription", "operator": "phrase", "value": "make america great again" }`
> matches "...make america great again..." but not "america will be great again".

> [!NOTE]
>
> Changed 2026-10-14: on an array field, `contains` and `text` now require an
> exact match against one of the array's elements, not a substring of one.
> `{ "field": "tags", "operator": "contains", "value": "car" }` matches a
> document tagged `["car", "engine"]` but not one tagged only
> `["car_interior"]`. Before this change it matched both. A scalar string
> field is unaffected: `contains` and `text` there are still a substring
> match. There is currently no operator for a substring match against one
> element of an array; file a request if you need one.

## Geospatial Operators

Geospatial operators filter documents by a **location field**, a payload value
holding a geographic point. A point may be either an object `{ "lat": <num>, "lon": <num> }`
or a GeoJSON-style `[lon, lat]` array. A field holding a **list** of points matches
if **any** point satisfies the predicate. See
[Geospatial filtering](https://docs.mixpeek.com/docs/retrieval/stages/attribute-filter.md#geospatial-filtering) for
how the same operators behave inside the `attribute_filter` stage.

| Operator | Matches documents whose location… |
| - | - |
| `geo_radius` | falls within `radius` **meters** of a center point (haversine distance) |
| `geo_bounding_box` | falls inside an axis-aligned box defined by two corners |
| `geo_polygon` | falls inside an arbitrary polygon (ray-casting, point-in-polygon) |

Each operator takes a structured `value`:

```json theme={null}
// geo_radius — within 5 km of the Eiffel Tower
{ "field": "location", "operator": "geo_radius",
  "value": { "center": { "lat": 48.8584, "lon": 2.2945 }, "radius": 5000 } }

// geo_bounding_box — top_left (NW) and bottom_right (SE) corners
{ "field": "location", "operator": "geo_bounding_box",
  "value": { "top_left":     { "lat": 49.0, "lon": 2.0 },
             "bottom_right": { "lat": 48.0, "lon": 3.0 } } }

// geo_polygon — exterior ring of >= 3 points (auto-closed)
{ "field": "location", "operator": "geo_polygon",
  "value": { "exterior": { "points": [
    { "lat": 48.0, "lon": 2.0 }, { "lat": 49.0, "lon": 2.0 },
    { "lat": 49.0, "lon": 3.0 }, { "lat": 48.0, "lon": 3.0 } ] } } }
```

> [!NOTE]
>
> Distances use the haversine formula on a spherical earth (R = 6,371,000 m).
> Bounding boxes handle the **antimeridian**: when `top_left.lon > bottom_right.lon`
> the box is treated as wrapping across ±180°. Malformed geometry (out-of-range
> `lat`/`lon`, a missing corner, or fewer than 3 polygon points) is **rejected at
> request time** with a descriptive error; a document whose location field is
> missing or unparseable is a non-match (it is never an error).

## Lineage Shortcuts

Every Mixpeek document carries a `_internal.lineage` block recording where it
came from. To filter by lineage you don't have to use the underscore-prefixed
paths — use the friendly aliases below in any `field` position.

| Alias | Resolves to | Use for |
| - | - | - |
| `from_object` | `_internal.lineage.root_object_id` | "Everything derived from this bucket object" |
| `from_bucket` | `_internal.lineage.root_bucket_id` | "Everything derived from this bucket" |
| `from_document` | `_internal.lineage.source_document_id` | Direct children of one upstream document |
| `from_collection` | `_internal.lineage.source_collection_id` | Documents whose immediate parent was in this collection |

```json theme={null}
{
  "AND": [
    { "field": "from_object", "operator": "eq", "value": "obj_video_123" }
  ]
}
```

You can mix lineage aliases with regular fields and templates:

```json theme={null}
{
  "AND": [
    { "field": "from_object", "operator": "eq", "value": "{{INPUT.object_id}}" },
    { "field": "metadata.scene_score", "operator": "gte", "value": 0.8 }
  ]
}
```

The aliases are also accepted by document list endpoints and retriever filter
stages — the same vocabulary works everywhere `field` is used.

## The `_internal` Envelope

Every document carries an `_internal` envelope that Mixpeek writes and owns. The
rule for what lives there: **content at the root, record-keeping in `_internal`.**
Content is what the asset *is* — anything you filter on, rank by, or show a user,
including everything Mixpeek derived for you (embeddings, transcripts, taxonomy
labels). `_internal` is what the *row* is — how it came to exist, who may read it,
and the identity and timestamps of the record. You can filter on these fields
anywhere `field` is used; system-owned ones are rejected if you try to set them on
write.

Range filtering works on these timestamps and on any Datetime index you define.
Pass RFC 3339 strings as the bounds:

```json theme={null}
{
  "filters": {
    "AND": [
      { "field": "_internal.created_at", "operator": "gte", "value": "2026-01-01T00:00:00Z" },
      { "field": "_internal.created_at", "operator": "lte", "value": "2027-01-01T00:00:00Z" }
    ]
  }
}
```

### Every `_internal` field

This table is generated from the server's field registry, so it is the complete, authoritative set — if a name is not here, Mixpeek does not write it under `_internal`. Filter on the **field / alias** column; the storage path is shown so you can address a nested value explicitly.

**Identity**

| Field / alias | Storage path | Filtering |
| - | - | - |
| `document_id` | `_internal.document_id` | `eq`, `in` — system-owned, rejected if you send it on write |
| `collection_id` | `_internal.collection_id` | `eq`, `in` — system-owned, rejected if you send it on write |

**Timestamps**

| Field / alias | Storage path | Filtering |
| - | - | - |
| `created_at` | `_internal.created_at` | `eq` and range operators (`gte`/`lte`/`gt`/`lt`). Pass RFC 3339 strings as bounds |
| `updated_at` | `_internal.updated_at` | `eq` and range operators (`gte`/`lte`/`gt`/`lt`). Pass RFC 3339 strings as bounds |

**Lineage**

| Field / alias | Storage path | Filtering |
| - | - | - |
| `root_object_id` · aliases `from_object` | `_internal.lineage.root_object_id` | `eq`, `in` |
| `root_bucket_id` · aliases `from_bucket` | `_internal.lineage.root_bucket_id` | `eq`, `in` |
| `source_type` | `_internal.lineage.source_type` | `eq`, `in` |
| `source_object_id` | `_internal.lineage.source_object_id` | `eq`, `in` |
| `source_document_id` · aliases `from_document` | `_internal.lineage.source_document_id` | `eq`, `in` |
| `source_collection_id` · aliases `from_collection` | `_internal.lineage.source_collection_id` | `eq`, `in` |
| `lineage_path` | `_internal.lineage.path` | `eq`, `in` |
| `lineage_chain` | `_internal.lineage.chain` | `eq`, `in` |
| `bucket_id` | `_internal.lineage.root_bucket_id` | `eq`, `in` |
| `object_id` | `_internal.lineage.source_object_id` | `eq`, `in` |
| `processing_tier` | — (not stored) | not stored anywhere — a filter on it matches nothing |
| `parent_document_id` | — (not stored) | not stored anywhere — a filter on it matches nothing |

**System metadata**

| Field / alias | Storage path | Filtering |
| - | - | - |
| `modality` | `_internal.modality` | `eq`, `in` |
| `mime_type` | `_internal.mime_type` | `eq`, `in` |
| `size_bytes` | `_internal.size_bytes` | `eq`, `in` |
| `content_hash` | `_internal.content_hash` | `eq`, `in` — system-owned, rejected if you send it on write |
| `metadata` | `_internal.metadata` | `eq`, `in` |

**Blobs**

| Field / alias | Storage path | Filtering |
| - | - | - |
| `source_blobs` | `_internal.source_blobs` | `eq`, `in` |
| `document_blobs` | `_internal.document_blobs` | `eq`, `in` |

**Provenance**

| Field / alias | Storage path | Filtering |
| - | - | - |
| `source_details` | `_internal.source_details` | `eq`, `in` |
| `source_url` | `_internal.processing.source_url` | filter by the exact dotted path only (the bare name is a real customer field name, so it is not auto-resolved) |
| `object_key_source` | `_internal.processing.object_key_source` | filter by the exact dotted path only (the bare name is a real customer field name, so it is not auto-resolved) |
| `detected_mime_type` | `_internal.processing.detected_mime_type` | filter by the exact dotted path only (the bare name is a real customer field name, so it is not auto-resolved) |
| `processing_history` | `_internal.processing.history` | filter by the exact dotted path only (the bare name is a real customer field name, so it is not auto-resolved) |
| `taxonomy_lineage` | `_internal.processing.taxonomy_lineage` | filter by the exact dotted path only (the bare name is a real customer field name, so it is not auto-resolved) |
| `last_health_check` | `_internal.processing.last_health_check` | filter by the exact dotted path only (the bare name is a real customer field name, so it is not auto-resolved) |
| `include_processing_history` | `_internal.processing.include_history` | filter by the exact dotted path only (the bare name is a real customer field name, so it is not auto-resolved) |

**Tenancy**

| Field / alias | Storage path | Filtering |
| - | - | - |
| `internal_id` | `_internal.internal_id` | `eq`, `in` — system-owned, rejected if you send it on write |
| `namespace_id` | `_internal.namespace_id` | `eq`, `in` — system-owned, rejected if you send it on write |

**Access control**

| Field / alias | Storage path | Filtering |
| - | - | - |
| `_acl` | `_internal._acl` | `eq`, `in` — system-owned, rejected if you send it on write |

## Using Templates

Reference request inputs or stage outputs in filter values:

```json theme={null}
{
  "AND": [
    { "field": "metadata.category", "operator": "eq", "value": "{{INPUT.category}}" },
    { "field": "metadata.price", "operator": "lte", "value": "{{INPUT.max_price}}" }
  ]
}
```

## Filter Stage Example

```json theme={null}
{
  "stage_name": "filter",
  "stage_type": "filter",
  "config": {
    "stage_id": "attribute_filter",
    "parameters": {
      "conditions": {
        "AND": [
          { "field": "metadata.category", "operator": "eq", "value": "audio" },
          { "field": "metadata.price", "operator": "lte", "value": "{{INPUT.max_price}}" }
        ]
      }
    }
  }
}
```

## Stage Pre-Filters and Post-Filters

Every stage accepts optional `pre_filters` and `post_filters` as siblings of
`parameters`. Use `pre_filters`. They narrow the candidate set **before** the
stage runs, pushed down into the vector store as native filters. Both take the
same logical-operator shape as any other filter.

> [!WARNING]
>
> **`post_filters` is accepted and never applied.** The field is declared on
> every stage and passes validation, and no stage applies it. A `post_filters`
> predicate returns the same documents as sending no filter at all, with HTTP
> 200 and no warning.
>
> Measured on `feature_search`: a nonsense value returns the full unfiltered
> set, identical to an unfiltered run. The identical predicate in `pre_filters`
> filters correctly.
>
> Put any predicate you rely on in `pre_filters`. Never use `post_filters` to
> restrict scope, because it does not restrict anything.

**Canonical shape** — wrap conditions in an explicit logical operator:

```json theme={null}
{
  "pre_filters": {
    "AND": [
      { "field": "archive_status", "operator": "ne", "value": "ARCHIVED" },
      { "field": "Keywords", "operator": "contains", "value": "skincare" }
    ]
  }
}
```

Always prefer the explicit `{ "AND": [ ... ] }` form — it is unambiguous and
nests cleanly with `OR`/`NOT`.

> [!NOTE]
>
> For convenience, two shorthand forms are coerced to an `AND` group:
>
> * a **single bare condition** — `{ "field": "...", "operator": "...", "value": "..." }` becomes `{ "AND": [ <condition> ] }`
> * a **list of conditions** — `[ { ... }, { ... } ]` becomes `{ "AND": [ ... ] }`
>
> Each condition must carry all three of `field`, `operator`, and `value`. An
> **incomplete** condition (for example, a missing `operator`) is rejected with a
> clear error rather than silently ignored. That guarantee covers the shape of a
> condition, not where you place it. A well-formed condition placed in
> `post_filters` still degrades into an unfiltered result, as described above.

## When a filter is refused

The API refuses a filter it cannot compile with HTTP 422, before it reads any document.
The error's `details` names the node by its path in your request and says why:

```json theme={null}
{
  "success": false,
  "status": 422,
  "error": {
    "message": "filter node AND[0].NOT[0]: an empty group: give it AND, OR or NOT with at least one condition",
    "details": {
      "node": "AND[0].NOT[0]",
      "reason": "an empty group: give it AND, OR or NOT with at least one condition"
    }
  }
}
```

The API refuses an empty group inside `AND`, `OR` or `NOT`, an unknown operator, and a key the filter grammar does not know.
An empty top-level filter (`{}` or `{"AND": []}`) means no filter.

## Options

| Option | Default | Description |
| - | - | - |
| `case_sensitive` | `false` | Case-sensitive matching for `contains`, `starts_with`, `ends_with`, `text`, `regex` and `phrase`. `eq`, `ne`, `in` and `nin` always compare exactly. |

```json theme={null}
{
  "field": "metadata.title",
  "operator": "contains",
  "value": "AI",
  "case_sensitive": true
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.