Skip to main content
You’ve ingested a corpus and built search over it. Now you want to add another understanding — a new extractor, a new model version, a classifier — over the same content, without re-uploading anything and without re-paying for the extraction you already ran. This is a first-class workflow. Three pieces make it cost-safe by default:
  1. Batch scopingcollection_ids on a batch runs only that collection’s extractor over the bucket.
  2. Dedup protectiondedup_strategy: "skip" (the default) never re-runs extraction for content a collection has already processed, even if you forget to scope.
  3. Pre-flight pricingPOST /batches/{id}/estimate-cost is a true dry run: it tells you exactly what would run and what it costs before you commit, including an already_extracted_count showing the work you will NOT be re-billed for.
Everything below operates on content already in a bucket. Buckets hold your source objects once; collections are independent processing pipelines over them. That separation is what makes iterating on models cheap.

1. Create a second collection over the same bucket

Your existing collection keeps serving queries untouched. The new understanding gets its own collection pointed at the same bucket:
new_col = client.collections.create(
    collection_name="scripts-v2-understanding",
    source={"type": "bucket", "bucket_id": bucket_id},
    features=["text_search"],  # or a feature key from GET /v1/collections/features
)
const newCol = await client.collections.create({
  collection_name: "scripts-v2-understanding",
  source: { type: "bucket", bucket_id: bucketId },
  features: ["text_search"],
});
curl -X POST "https://api.mixpeek.com/v1/collections" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Namespace: $NAMESPACE" \
  -H "Content-Type: application/json" \
  -d '{
    "collection_name": "scripts-v2-understanding",
    "source": {"type": "bucket", "bucket_id": "'$BUCKET_ID'"},
    "features": ["text_search"]
  }'
If the extractor behind your chosen feature isn’t registered on the namespace yet, the platform registers it for you at create time. (On older deployments a 422 tells you the exact PATCH /v1/namespaces body to run first.)

2. Create a batch scoped to the new collection

Scope the batch with collection_ids so only the new collection’s pipeline runs:
batch = client.batches.create(
    bucket_id=bucket_id,
    object_ids=object_ids,           # or filters selecting the slice you want
    collection_ids=[new_col.collection_id],
)
const batch = await client.batches.create({
  bucket_id: bucketId,
  object_ids: objectIds,
  collection_ids: [newCol.collection_id],
});
curl -X POST "https://api.mixpeek.com/v1/buckets/$BUCKET_ID/batches" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Namespace: $NAMESPACE" \
  -H "Content-Type: application/json" \
  -d '{"object_ids": ["obj_..."], "collection_ids": ["'$NEW_COLLECTION_ID'"]}'

3. Price it before you run it

estimate-cost is a dry run — nothing processes, nothing is charged. (For quoting planned ingestion in dollars by modality and feature before you even build the batch, use POST /v1/organizations/billing/estimate — same rating engine that bills you.)
estimate = client.batches.estimate_cost(bucket_id=bucket_id, batch_id=batch.batch_id)
print(estimate)
# {
#   "object_count": 8,
#   "already_extracted_count": 0,   # nothing skipped — all new work
#   "estimated_credits": 10,        # legacy field: internal ledger unit (1 credit = $0.001 → $0.01)
#   "extractors": ["text_extractor"]
# }
const estimate = await client.batches.estimateCost(bucketId, batch.batch_id);
curl -X POST "https://api.mixpeek.com/v1/buckets/$BUCKET_ID/batches/$BATCH_ID/estimate-cost" \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Namespace: $NAMESPACE"
The trap probe worth knowing: if you estimate an unscoped batch over a bucket that an existing collection already processed, the estimate shows that prior work under already_extracted_count and prices it at zero — the default dedup_strategy: "skip" refuses to re-run (and re-bill) extraction whose inputs haven’t changed. Your existing collection’s GPU spend is protected even when you forget to scope.

4. Submit, then verify only new work ran

Submit the batch and let it complete. Two checks confirm the cost boundary held:
  • The new collection has documents; the old collection’s documents show no changed updated_at.
  • The batch record carries its own cost: batch.cost → {"credits_consumed": 10, "cost_usd": 0.01}cost_usd is the number that matters (the credits_consumed field is the internal ledger unit, 1 credit = 0.001).Batchesbillatleastthe0.001). Batches bill at least the 0.01 minimum.
For a multi-batch rollout, sum the cost_usd of the rollout’s batches — each batch is its own attribution record.

5. Compare the two understandings side by side

Both collections index the same source objects, so retrieval comparisons need no second corpus copy: run the same query against each collection (or one retriever spanning both) and compare. For systematic comparison, build a small evaluation dataset once and run it against both. When a new version of an extractor ships (versions coexist in the catalog — e.g. v1 and v2 side by side), the same pattern applies: pin the new version in a new collection, roll a scoped batch, compare, and cut over when the numbers say so. For whole-namespace model swaps, use model migration instead.

6. Re-run only failures

If some objects fail, you don’t resubmit the batch:
  • GET /v1/buckets/{bucket_id}/batches/{batch_id}/failed-documents lists failures with per-tier detail and what’s retryable.
  • POST /v1/buckets/{bucket_id}/batches/{batch_id}/retry re-runs just those.

Next steps

Feature Extractors

How a single extractor is configured — inputs, outputs, inference cache.

Model Migration

Swap a namespace’s embedding model wholesale with validation and dry-run.

Multi-Tier Extraction

Chain collections into a DAG — transcribe, then embed, then classify.

Custom Extractor Quickstart

Build and test your own extractor end-to-end.