Skip to main content
Features answer the third of the three pricing questions — 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): Clustering and taxonomy enrichment are included on every modality at no additional charge. For rates, tiers, and how usage is billed, see Billing & Pricing.

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.
Response abbreviated — the live payload covers all six modalities. Feature kind tells you how it’s priced:

Create a collection with features

Pass features: [...] to Create Collection. The platform resolves the pipeline, pins its version, and defaults the input wiring for you:
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.
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.
Track it with GET /v1/tasks/{task_id}. See Triggers and Syncs.

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.

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 keyfeatures: ["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.

Custom features (bring your own)

Custom extractors you publish are your vocabulary — they stay explicitly named and are selected as custom:<plugin_name>:
Custom features are priced per unit from the compute profile the plugin declares — the same machinery that prices native features. See 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:
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.

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.
Each entry accepts four fields: 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.
Leaving a source out and giving it an empty list are opposites, and both return 200: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:
A field_map key that is not one of the declared bucket_ids or collection_ids is rejected the same way.
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:

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.
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.
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.
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.
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.
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.
A 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>.
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:
Common features → feature_uri (the exact string a fresh @v1 collection produces):
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_uris 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:
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 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; for advanced pipeline knobs (input mappings, field passthrough, parameters), see Pipeline Configuration.