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

<Frame>
  <img src="https://mintcdn.com/mixpeek/pDBzbsnRaRIThJZv/assets/mixpeek-filters.svg?fit=max&auto=format&n=pDBzbsnRaRIThJZv&q=85&s=1ab3a765413b2e1644fa88ce899e201d" alt="Filter composition: AND, OR, and NOT operators nest to build complex filter logic on document payloads" width="900" height="280" data-path="assets/mixpeek-filters.svg" />
</Frame>

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.
</Warning>

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`     | Full-text search                            |
| `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.
</Note>

## 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                                         |
| `starts_with` | String starts with prefix                                         |
| `ends_with`   | String ends with suffix                                           |
| `regex`       | Matches regular expression                                        |
| `text`        | Full-text search (token-based; word order **not** preserved)      |
| `phrase`      | Exact phrase — matches the words **in order**, word-boundary safe |

<Note>
  Use `text` to match any of the query tokens (BM25); use `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>

## 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](/docs/retrieval/stages/attribute-filter#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).
</Note>

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

<Warning>
  **Date-range filtering on `_internal.created_at` and `_internal.updated_at` is not
  yet supported, and it fails silently.** A range filter (`gte`/`lte`/`gt`/`lt`) on
  either field returns an **empty result set with HTTP 200 and no error** — which is
  indistinguishable from "no documents matched your dates." Equality on the exact
  stored value works. Until this lands (BACKE-3289), filter by these timestamps with
  `eq`, or filter by date at the application layer. Verified on both storage planes;
  `created_at` and `updated_at` are the only two timestamp fields, so this covers the
  whole class. The cause is that the registry's field kind is not yet read by index
  provisioning, so the timestamps get an exact-match index instead of a range one.
</Warning>

### Every `_internal` field

This table is generated from the server's field registry (`shared/databases/internal_field_registry.py`), 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` only today — **range operators (`gte`/`lte`/`gt`/`lt`) are not yet wired and return an empty result with no error** (see the callout below, BACKE-3289) |
| `updated_at`  | `_internal.updated_at` | `eq` only today — **range operators (`gte`/`lte`/`gt`/`lt`) are not yet wired and return an empty result with no error** (see the callout below, BACKE-3289) |

**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": {
      "strategy": "structured",
      "structured_filter": {
        "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.
</Warning>

**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.
</Note>

## Options

| Option           | Default | Description                              |
| ---------------- | ------- | ---------------------------------------- |
| `case_sensitive` | `false` | Enable case-sensitive string comparisons |

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