> ## 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](/platform/billing#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](/platform/billing).

## 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](/api-reference/collections/create-collection). The platform resolves the pipeline, pins its version, and defaults the input wiring for you:

<CodeGroup>
  ```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"],
    },
  });
  ```
</CodeGroup>

<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](/processing/extractor-migration).
</Note>

### 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](/processing/feature-extractors).

### 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** — feature keys that appear on the roadmap but haven't shipped are rejected explicitly rather than silently ignored.

### Custom features (bring your own)

[Custom extractors](/processing/custom-extractors) 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](/processing/custom-extractors).

### 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](/processing/multi-tier-extractors).

## Find your `feature_uri` (to search)

A [`feature_search`](/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.
</Warning>

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

<CodeGroup>
  ```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
  ```
</CodeGroup>

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

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

<CodeGroup>
  ```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();
  ```
</CodeGroup>

```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](/processing/custom-extractors) 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](/processing/extractor-migration); for advanced pipeline knobs (input mappings, field passthrough, parameters), see [Pipeline Configuration](/processing/feature-extractors).
