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

# Features

> Features are what you want to search by — pick them per file type; Mixpeek resolves the pipeline, defaults the wiring, and prices per natural unit

Features answer the third of the [three pricing questions](https://docs.mixpeek.com/docs/platform/billing.md#how-pricing-works) — after *what kind of files* and *how much content* comes **what you want to search by**. Instead of choosing models or pipelines, you declare the capabilities you want — visual similarity (`image_search`), faces (`faces`), on-screen text (`onscreen_text`), document layout (`document_layout`) — and the platform resolves the implementation internally. Swapping or upgrading the models behind a feature never changes your config, your API calls, or your pricing.

Every feature applies to a **modality** (the *what kind of files* question) and is billed in that modality's natural unit (the *how much* question):

| Modality | Unit | Base feature (included in base rate) | Add-on features |
| - | - | - | - |
| `image` | per image | `image_search` | `faces`, `multimodal_understanding` |
| `video` | per minute | `video_search` | `faces`, `onscreen_text`, `audio_fingerprint`, `multimodal_understanding` |
| `audio` | per minute | `audio_search` | `multimodal_understanding` |
| `document` | per page | `document_search` | `faces`, `document_layout`, `multimodal_understanding` |
| `text` | per token | `text_search` | `multimodal_understanding` |
| `web` | per crawled page | `web_crawl` | — |

Clustering and taxonomy enrichment are **included** on every modality at no additional charge. For rates, tiers, and how usage is billed, see [Billing & Pricing](https://docs.mixpeek.com/docs/platform/billing.md).

## Discover features

`GET /v1/collections/features` returns the live feature menu — modalities, units, feature keys, display names, and current rates. No authentication required. This endpoint is the source of truth: studio, the homepage pricing page, and the billing engine all read the same catalog.

```bash theme={null}
curl "https://api.mixpeek.com/v1/collections/features"
```

```json theme={null}
{
  "modalities": [
    {
      "modality": "video",
      "unit": "minute",
      "unit_display": "per minute",
      "base": {
        "key": "video_search",
        "name": "Video search (scene embeddings)",
        "kind": "base",
        "included_in_base_rate": true,
        "rate_usd": 0.05,
        "per": 1
      },
      "addons": [
        { "key": "faces", "name": "Face detection + identity", "kind": "addon", "rate_usd": 0.10, "per": 1, "companion": true },
        { "key": "onscreen_text", "name": "On-screen text (video OCR)", "kind": "addon", "rate_usd": 0.10, "per": 1, "companion": true },
        { "key": "clustering", "name": "Clustering (included)", "kind": "included", "included": true, "pricing_note": "included — no additional charge" }
      ]
    }
  ],
  "custom_features": [],
  "full_res_multiplier": 2.0
}
```

Response abbreviated — the live payload covers all six modalities. Feature `kind` tells you how it's priced:

| Kind | Meaning |
| - | - |
| `base` | Covered by the modality's per-unit base rate — you get it by creating any collection for that modality |
| `addon` | Priced per the same unit, added on top of the base rate |
| `external` | Backed by external LLM inference — usage-based passthrough pricing, never flat-rated |
| `included` | Bundled enrichment (clustering, taxonomy) — no additional charge |

## Create a collection with features

Pass `features: [...]` to [Create Collection](https://docs.mixpeek.com/docs/api-reference/collections/create-collection.md). The platform resolves the pipeline, pins its version, and defaults the input wiring for you:

```bash cURL theme={null}
curl -X POST "https://api.mixpeek.com/v1/collections" \
  -H "Authorization: Bearer $MIXPEEK_API_KEY" \
  -H "X-Namespace: $NAMESPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "collection_name": "press-photos",
    "source": { "type": "bucket", "bucket_ids": ["'$BUCKET_ID'"] },
    "features": ["image_search"]
  }'
```

```python Python theme={null}
import os
from mixpeek import Mixpeek

# namespace= is required — it's sent as the X-Namespace header on every call
# (collection create returns 403 without it).
client = Mixpeek(
    api_key=os.environ["MIXPEEK_API_KEY"],
    namespace=os.environ["MIXPEEK_NAMESPACE"],
)

collection = client.collections.create(
    collection_name="press-photos",
    source={"type": "bucket", "bucket_ids": [os.environ["BUCKET_ID"]]},
    features=["image_search"],
)
```

```javascript JavaScript theme={null}
import { Mixpeek } from "mixpeek";

// namespace is sent as the X-Namespace header on every call (collection
// create is namespace-scoped).
const client = new Mixpeek({
  apiKey: process.env.MIXPEEK_API_KEY,
  namespace: process.env.MIXPEEK_NAMESPACE,
});

const { data: collection } = await client.collections.createCollection({
  createCollectionRequest: {
    collection_name: "press-photos",
    source: { type: "bucket", bucket_ids: [process.env.BUCKET_ID] },
    features: ["image_search"],
  },
});
```

> [!NOTE]
>
> Both SDKs accept `features` on collection create — Python **`mixpeek>=1.3.29`** (`client.collections.create(...)`) and JavaScript **`mixpeek>=0.81.28`** (`client.collections.createCollection({ createCollectionRequest: { ... } })`). Set the namespace when you construct the client (Python `namespace=`, JS `namespace:`, or the `MIXPEEK_NAMESPACE` env var) — collection create is namespace-scoped and returns 403 without it. The `feature_extractor` field is a [deprecated alias](https://docs.mixpeek.com/docs/processing/extractor-migration.md).

> [!WARNING]
>
> Creating the collection does not process anything on its own. Objects you upload
> to a bucket sit at `status: PENDING`, and a bucket-sourced collection returns zero
> documents until a batch runs. That reads as a failed create, and it is not one.
>
> Two paths start the work:
>
> * **Sync config**, the production path. The bucket submits batches as objects land.
> * **Manual trigger**, for on-demand runs. `POST /v1/collections/{collection_id}/trigger`
>   returns `202` with `batch_id` and `task_id`, plus the `object_count` it picked up.
>
> ```bash theme={null}
> curl -X POST "https://api.mixpeek.com/v1/collections/$COLLECTION_ID/trigger" \
>   -H "Authorization: Bearer $MIXPEEK_API_KEY" \
>   -H "X-Namespace: $NAMESPACE_ID"
> ```
>
> Track it with `GET /v1/tasks/{task_id}`. See [Triggers](https://docs.mixpeek.com/docs/platform/triggers.md) and
> [Syncs](https://docs.mixpeek.com/docs/platform/syncs.md).

### Default input wiring

Every organization's scaffolded `uploads` bucket exposes one standard source property per modality — `image`, `video`, `audio`, `pdf`, `content` (text), and `url` (web). A features-based create maps the pipeline's inputs to those properties automatically, so the collection is runnable with zero wiring.

If your bucket uses different property names, the create returns a teaching `422` naming the fields that exist so you can rename the property — or fall back to explicit `input_mappings` via the [advanced pipeline config](https://docs.mixpeek.com/docs/processing/feature-extractors.md).

### Errors teach

Validation on the features path is designed for humans and agents guessing their way in — every rejection tells you what to do instead:

* **Unknown key** — `features: ["face"]` responds with the full list of valid feature keys (and reminds you about `custom:<plugin>`).
* **One pipeline per collection** — some feature combinations need separate collections (e.g. a base feature plus a `companion: true` add-on). The error lists exactly which features group into which collection: create one collection per group over the same source bucket. Add-ons flagged `companion: true` in discovery always create a companion collection alongside the base.
* **`features` XOR `feature_extractor`** — provide one or the other, never both.
* **Not yet available** — a feature key the platform recognizes but does not serve is rejected explicitly rather than silently ignored.

### Warnings on a create that succeeded

A `features` create wires `input_mappings` to the standard `uploads` properties for you, so this rarely fires on the scaffolded bucket. On a custom bucket it can: the create returns `201` with a `warnings` array when a mapped field is declared on the bucket schema and missing from the objects already in it. The collection exists and will process nothing until the sync writes that property. See [`warnings` on the create response](https://docs.mixpeek.com/docs/processing/feature-extractors).

### Custom features (bring your own)

[Custom extractors](https://docs.mixpeek.com/docs/processing/custom-extractors.md) you publish are *your* vocabulary — they stay explicitly named and are selected as `custom:<plugin_name>`:

```json theme={null}
{
  "collection_name": "product-defects",
  "source": { "type": "bucket", "bucket_ids": ["bkt_123"] },
  "features": ["custom:defect_detector"]
}
```

Custom features are priced per unit from the compute profile the plugin declares — the same machinery that prices native features. See [Custom Extractors](https://docs.mixpeek.com/docs/processing/custom-extractors.md).

### Full resolution opt-out

By default, Mixpeek normalizes content once at ingest (video to a 720p mezzanine, images capped at \~1568px max edge, audio to 16kHz mono) — originals are always kept untouched in your bucket. If a workload needs extraction on the original resolution (fine print OCR, tiny logos), opt out per collection:

```json theme={null}
{
  "collection_name": "fine-print",
  "source": { "type": "bucket", "bucket_ids": ["bkt_123"] },
  "features": ["document_layout"],
  "full_res": true
}
```

Full-res processing bills at a **2x multiplier** on the modality's base and add-on rates (`full_res_multiplier` in the pricing payload).

### Chain collections (advanced)

Searching by one thing often produces content worth searching by another — e.g. search video by scenes, then feed those documents into a second collection to search the results by something else. Collection-to-collection pipelines do exactly this: a collection can read another collection's documents as its source, forming a processing DAG. See [Multi-Tier Feature Extraction](https://docs.mixpeek.com/docs/processing/multi-tier-extractors.md).

### Merge sources with different schemas (advanced)

A collection can read several buckets at once. When those buckets have different
schemas, `source.field_map` chooses which fields from each one land in the merged
collection, and what they are called there.

```json theme={null}
{
  "collection_name": "all-creative",
  "source": {
    "type": "bucket",
    "bucket_ids": ["bkt_video_ads", "bkt_static_ads"],
    "field_map": {
      "bkt_video_ads": [
        { "source_path": "headline", "target_path": "title" }
      ],
      "bkt_static_ads": [
        { "source_path": "name", "target_path": "display_name", "default": "untitled" }
      ]
    }
  },
  "features": ["image_search"]
}
```

Each entry accepts four fields:

| Field | Required | Behavior |
| - | - | - |
| `source_path` | Yes | Field to copy. Dot notation reaches nested fields, as in `metadata.author`. |
| `target_path` | No | Name in the merged collection. Defaults to `source_path`, or its last component for a nested path. |
| `default` | No | Value used when the source field is missing. Without it, the field is omitted from that document. |
| `required` | No | `true` fails processing when the source field is missing. Defaults to `false`. |

`field_map` narrows. A source listed in `bucket_ids` but absent from `field_map`
passes **all** of its fields through, so you only name the sources you want to trim.
The same key works for `source.type: "collection"`, mapping `collection_ids`.

> [!WARNING]
>
> Leaving a source out and giving it an empty list are opposites, and both return
> `200`:
>
> | You write | That source contributes |
> | - | - |
> | no key for the source | every one of its fields |
> | `"bkt_x": []` | nothing |
>
> The branch turns on whether the key is present, not on whether its list has
> entries. A source mapped to `[]` adds no fields to the collection, so its
> documents arrive carrying only `null` for the fields other sources named. That
> reads as broken ingestion rather than as the selection you wrote. Remove the key
> to pass the source through.

#### Two sources cannot write the same field

Every target name in a `field_map` must be unique across the whole map. Create returns
`422` when two entries resolve to the same one, whichever sources they belong to and
whatever their types:

```
field_map target-path collision(s): target_path 'title' is mapped by
sources ['bkt_static_ads', 'bkt_video_ads']. Two sources cannot map fields
to the same target_path (no silent overwrite). Rename one target_path to resolve.
```

A `field_map` key that is not one of the declared `bucket_ids` or `collection_ids` is
rejected the same way.

> [!WARNING]
>
> Names collide before you see them. When `target_path` is omitted the target is the
> last dot-component of `source_path`, so `metadata.author` on one source and
> `content.author` on another both resolve to `author` and fail. Set `target_path`
> explicitly whenever you map nested fields.

This makes `field_map` a selector, never a converger. Two sources that name the same
thing differently, `headline` and `title`, cannot be folded onto one field here. Land
them as separate fields, or align the names in the source schemas.

#### Merging without a field\_map

Leave `field_map` off and every source contributes all of its fields, unioned by name.
That path does converge, and it is the one to reach for when your sources already
agree on names:

| Sources declare | Result |
| - | - |
| Same field name, same type | One field. Each document fills it from its own source. |
| Same field name, different types | `422`: `Field 'title' has conflicting types across sources`. |

#### Reading a merged collection

The output schema is the union of what every source contributes, so a document
carries the fields its own source mapped and `null` for the rest. Given the example
above, a document from `bkt_video_ads` has a `title` value and `display_name` of
`null`, and one from `bkt_static_ads` has the reverse.

Filter on `root_bucket_id` to select the documents one source contributed.

> [!NOTE]
>
> A `null` here means the source never mapped that field, not that the value was
> empty. Set `default` on an entry when you want a placeholder instead, which fills
> only for documents from **that** source when its `source_path` is missing.

### Build a collection from a retriever pipeline (advanced)

A collection normally runs a feature extractor over its source. It can instead run a
**retriever pipeline** over an upstream collection, which makes it a materialized view
of that collection's documents.

```json theme={null}
{
  "collection_name": "scored-scenes",
  "source": { "type": "collection", "collection_ids": ["col_scenes"] },
  "retriever_transform": {
    "stages": [
      {
        "stage_name": "safety",
        "config": {
          "stage_id": "classify",
          "parameters": { "task": "nsfw", "document_field": "content" }
        }
      }
    ],
    "write_back_fields": [
      { "source_field": "classification.label", "target_field": "safety_label" },
      { "source_field": "classification.nsfw_score", "target_field": "safety_score" },
      { "source_field": "classification.is_nsfw", "target_field": "flagged" }
    ]
  }
}
```

| Field | Required | Purpose |
| - | - | - |
| `stages` | Yes | The pipeline, one or more stages, in the same shape a retriever uses. |
| `write_back_fields` | No | Which pipeline outputs become document fields. These define `output_schema`. |
| `input_mappings` | No | Feed upstream document fields into stage inputs. |

Each `write_back_fields` entry takes `source_field` (a dot path into the retriever
result), `target_field` (the field written on the document), and an optional `mode` of
`first`, `all_as_array`, or `concat` when a stage returns several results.

**A collection is extractor-based or transform-based, never both.** `feature_extractor`
became optional when `retriever_transform` arrived, and create returns `422` if you send
neither or both.

> [!WARNING]
>
> `retriever_transform` requires `source.type` of `collection`. A bucket source returns
> `422`. Point it at a collection that already holds the documents you want to transform.

Run it with `POST /v1/collections/{collection_id}/trigger`.

### How the output is shaped

Cardinality follows the pipeline, and the mechanism is the document id:

* **Without a `group_by` stage**, each output document takes its id from the upstream
  document, so the view is 1:1.
* **With one**, the id comes from the value of the grouped field, so upstream rows
  sharing that value collapse into exactly one document. The field is taken from the
  last `group_by` stage in the pipeline, defaulting to `source_object_id`.

Grouping keeps the highest-scoring result for each group, because `group_by` orders
groups by score before the collapse. The rest of that group's rows are not merged into
it; they are dropped.

Ids are deterministic either way, so a re-trigger upserts in place. Re-running after new
upstream documents arrive updates the view incrementally rather than appending a second
copy of it.

A trigger reads the **entire** source collection, cursor-paging through it. The
100-document cap on `POST /v1/documents/list` bounds that endpoint, not this, and there
is no paging option to set. A source of 50,000 documents produces a view over all
50,000.

> [!WARNING]
>
> Transform output is written **payload-only**, with no vectors. `write_back_fields`
> produces fields you can filter, sort, and read; it does not produce an embedding. To
> search the view by similarity, point a second collection with a feature extractor at
> it.

> [!NOTE]
>
> `feature_extractor` cannot be added to a transform collection afterward. It is a
> structural field on every collection, so `PATCH` rejects it with `422` and tells you
> to clone instead. Pick the build mode at create time.

## Find your `feature_uri` (to search)

A [`feature_search`](https://docs.mixpeek.com/docs/retrieval/stages/feature-search) stage needs a **`feature_uri`** — the exact vector index to search. You picked *features* by name; each one produces a vector index whose `feature_uri` looks like `mixpeek://<extractor>@<version>/<model>`.

> [!WARNING]
>
> The last segment is the embedding **model name**, not the literal word `embedding`. A wrong `feature_uri` (e.g. `.../embedding`) does **not** error — the stage matches nothing and silently returns **0 results** (with a self-correcting warning in the response). Always use the exact string.

**The reliable, version-proof way — read it from the collection.** After processing, `GET` the collection and copy the URI; this always matches what you actually built, even if model versions change:

```bash cURL theme={null}
curl "https://api.mixpeek.com/v1/collections/$COLLECTION_ID" \
  -H "Authorization: Bearer $MIXPEEK_API_KEY" \
  -H "X-Namespace: $NAMESPACE_ID"
# → vector_indexes[].feature_uri  e.g. "mixpeek://universal_extractor@v1/gemini-embedding-2"
```

```python Python theme={null}
col = client.collections.get(collection_id)
for vi in col["vector_indexes"]:
    print(vi["feature_uri"])   # paste the one you want into your feature_search stage
```

> [!NOTE]
>
> **On a BYO or promoted collection the value is the bare index name, not a `mixpeek://` URI.**
> Those collections have no extractor, so their `vector_indexes` entries come from the
> namespace vector config and carry `source: "namespace_vector_config"`. A `feature_search`
> stage accepts either form, so paste whatever the `GET` returned.

**Common features → `feature_uri`** (the exact string a fresh `@v1` collection produces):

| Feature (`features: [...]`) | Extractor | `feature_uri` |
| - | - | - |
| `video_search` | `universal_extractor` | `mixpeek://universal_extractor@v1/gemini-embedding-2` |
| `text_search` | `text_extractor` | `mixpeek://text_extractor@v1/multilingual_e5_large_instruct_v1` |
| `multimodal_understanding` | `multimodal_extractor` | `mixpeek://multimodal_extractor@v1/vertex_multimodal_embedding` |

> [!NOTE]
>
> **Video transcript search.** `video_search` produces **one** blended scene embedding (`gemini-embedding-2`); the transcript rides along as a `text` payload field, not a separate searchable vector. To search a transcript as its own vector, add the `multimodal_understanding` feature with transcription enabled — it creates a second index `mixpeek://multimodal_extractor@v1/multilingual_e5_large_instruct_v1`. When in doubt, `GET` the collection and read the `feature_uri`s it actually has.

## Estimate before you run

`POST /v1/organizations/billing/estimate` quotes planned ingestion using the **same rating engine that bills you** — the quote and the charge can't disagree:

```bash cURL theme={null}
curl -X POST "https://api.mixpeek.com/v1/organizations/billing/estimate" \
  -H "Authorization: Bearer $MIXPEEK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "mime_type": "video/mp4", "minutes": 42 },
      { "mime_type": "application/pdf", "pages": 300 }
    ],
    "features": ["faces", "onscreen_text"]
  }'
```

```python Python theme={null}
resp = requests.post(
    "https://api.mixpeek.com/v1/organizations/billing/estimate",
    headers={"Authorization": f"Bearer {os.environ['MIXPEEK_API_KEY']}"},
    json={
        "items": [
            {"mime_type": "video/mp4", "minutes": 42},
            {"mime_type": "application/pdf", "pages": 300},
        ],
        "features": ["faces", "onscreen_text"],
    },
)
quote = resp.json()
```

```javascript JavaScript theme={null}
const resp = await fetch(
  "https://api.mixpeek.com/v1/organizations/billing/estimate",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MIXPEEK_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      items: [
        { mime_type: "video/mp4", minutes: 42 },
        { mime_type: "application/pdf", pages: 300 },
      ],
      features: ["faces", "onscreen_text"],
    }),
  }
);
const quote = await resp.json();
```

```json theme={null}
{
  "pricing_model": "v2",
  "placeholder_rates": false,
  "line_items": [
    { "modality": "video", "key": "video_search", "units": 42.0, "rate_usd": 0.05, "per": 1, "amount_usd": 2.10 },
    { "modality": "video", "key": "faces", "units": 42.0, "rate_usd": 0.10, "per": 1, "amount_usd": 4.20 },
    { "modality": "video", "key": "onscreen_text", "units": 42.0, "rate_usd": 0.10, "per": 1, "amount_usd": 4.20 },
    { "modality": "document", "key": "document_search", "units": 300.0, "rate_usd": 1.50, "per": 1000, "amount_usd": 0.45 },
    { "modality": "document", "key": "faces", "units": 300.0, "rate_usd": 1.00, "per": 1000, "amount_usd": 0.30 }
  ],
  "total_usd": 11.25,
  "batch_minimum_applied": false,
  "allowance_pool_usd": 10.0,
  "allowance_covered_usd": 10.0,
  "estimated_overage_usd": 1.25
}
```

Base features are quoted automatically per the modality of each item — you only list add-ons. Unknown MIME types quote at the cheapest band rather than failing (be forgiving is a design rule here).

## How this relates to extractors

Under the hood every feature resolves to an extraction pipeline, and [custom extractors](https://docs.mixpeek.com/docs/processing/custom-extractors.md) let you register your own. But **built-in extractor names are no longer part of the public API surface** — they remain accepted in configs only as deprecated aliases. If you have existing configs using `feature_extractor`, see the [migration guide](https://docs.mixpeek.com/docs/processing/extractor-migration.md); for advanced pipeline knobs (input mappings, field passthrough, parameters), see [Pipeline Configuration](https://docs.mixpeek.com/docs/processing/feature-extractors.md).


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