> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quivr.thevibecompany.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Fill a new vector space for past articles

> After you add an embedding model to your ingestion plugin, give the articles you already have vectors in its space, then make search use it.

A new embedding model enters as an evaluation space of your ingestion plugin. Articles ingested from then on get vectors in it, but the ones you already have do not. A backfill fills that space for a Corpus's past articles, at low priority, and a promotion then makes search use it.

## Prerequisites

* A version of your ingestion plugin whose manifest declares the new space, activated with it as `evaluation` and the current one as `served` (see [Switch plugins without restarting](/plugins/switch-plugins-without-restarting)). It must cut articles into the same segments as the version that ingested them.
* A key of the Organization that owns the Corpus with the actions `plugins:admin`, `operations:read` (to follow the backfill) and `operations:write` (to pause, resume or cancel it), in `QUIVR_OPERATOR_KEY`, and the Corpus id in `CORPUS_ID`.
* Optional: an `input_price` on the new space in the manifest (`usd_per_million_tokens`), so the estimate shows a cost. Without it the cost is unknown.

## Steps

<Steps>
  <Step title="Estimate it">
    A dry run is required. It counts the articles to fill and estimates how long it takes and what it costs:

    ```bash theme={null}
    curl -s -X POST "$QUIVR_API_URL/v0/admin/backfills" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
      -H "Content-Type: application/json" -d @- <<EOF | jq
    {"idempotency_key": "large-model-2026", "corpus_id": "$CORPUS_ID", "dry_run": true}
    EOF
    ```

    ```json theme={null}
    {"registration_id": "plugin_registration_…", "spaces": ["acme.embedder.large@1"], "versions": 12000, "segments": 48000,
     "input_tokens": 9600000, "estimated_seconds": 6000, "duration_basis": "rate", "estimated_cost_usd": 0.2, "confirmation_required": true}
    ```

    `spaces` defaults to the evaluation spaces of the active ingestion plugin; name them in `spaces` to choose. To fill only part of the Corpus, add `accepted_after` or `accepted_before`: the time Quivr accepted each article. `estimated_seconds` uses the deployment's `backfill.rate`, or the recent throughput when it is slower (`duration_basis`).
  </Step>

  <Step title="Start it">
    Send the same body with `dry_run` set to `false`. When `confirmation_required` is `true`, because the cost exceeds `backfill.max_cost_without_confirmation`, add `"confirm_cost": true`:

    ```bash theme={null}
    curl -s -X POST "$QUIVR_API_URL/v0/admin/backfills" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
      -H "Content-Type: application/json" -d @- <<EOF | jq '{operation_id, state}'
    {"idempotency_key": "large-model-2026", "corpus_id": "$CORPUS_ID", "dry_run": false, "confirm_cost": true}
    EOF
    ```

    ```json theme={null}
    {"operation_id": "operation_…", "state": "queued"}
    ```

    The answer is an Operation, Quivr's record of a long-running administrative task. The same key and body return the same Operation. Keep its id:

    ```bash theme={null}
    export OPERATION_ID=<the operation_id above>
    ```
  </Step>

  <Step title="Follow it">
    ```bash theme={null}
    curl -s "$QUIVR_API_URL/v0/operations/$OPERATION_ID" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
      | jq '{state, counters, checkpoint: .backfill.checkpoint}'
    ```

    `counters` shows `versions_in_scope`, `versions_done`, `versions_skipped` and `segments`. One backfill of a Corpus runs at a time: another one is refused with `409 backfill_in_progress` until it ends. Pause, resume or cancel it with `POST /v0/operations/{id}/pause`, `/resume` or `/cancel`, each with an `idempotency_key` body. A paused or restarted backfill continues after its `checkpoint` and never fills an article twice.
  </Step>

  <Step title="Promote the space">
    When the Operation is `succeeded`, make search use the new space:

    ```bash theme={null}
    curl -s -X POST "$QUIVR_API_URL/v0/admin/spaces/acme.embedder.large@1/promote" -H "Authorization: Bearer $QUIVR_OPERATOR_KEY" \
      -H "Content-Type: application/json" -d '{}' | jq
    ```

    ```json theme={null}
    {"served_space_id": "acme.embedder.large@1", "previous_space_id": "acme.embedder.base@1", "generations_switched": 3, "corpora_incomplete": 0, "segments_missing": 0}
    ```

    A promotion applies to the whole deployment. It is refused with `409 coverage_incomplete` while some Corpus has current articles without a vector in the space: backfill those Corpora first. `{"force": true}` promotes anyway, and the articles without a vector drop out of semantic results until a backfill fills them.
  </Step>
</Steps>

## Check it worked

```bash theme={null}
curl -s "$QUIVR_API_URL/v0/corpora/$CORPUS_ID/vector-spaces" -H "Authorization: Bearer $QUIVR_API_KEY" \
  | jq '{segments, spaces: [.items[] | {vector_space_id, role, covered: .coverage.segments}]}'
```

The new space is `served` and covers every segment. Semantic hits name it in `vector_space_id`.

## Go back to the previous model

Promote the former space the same way. It stayed an evaluation space and kept its vectors, so search uses it again at once. A promotion survives restarts, activations and rollbacks while the ingestion plugin enables both spaces. Once a plugin version stops declaring one of them, its own `spaces` settings decide again.

## How a backfill runs

* It runs on its own worker queue, one article at a time, at most `backfill.rate` articles per second (see [Configuration](/reference/configuration#backfill)), so live ingestion keeps its capacity.
* It is pinned to the Pipeline Plan active when it started, like other work, and fails with `pinned_plugin_unavailable` or `pinned_plan_stopped` rather than moving to another plugin version.
* On its first step, the Corpus's search index starts carrying the new space, so articles ingested from then on get their vectors from live ingestion. The backfill fills only the articles that already had theirs.
* It creates no new Version and no content event in the change feed. Articles outside its window are not touched.
* An article the plugin now cuts into other segments is skipped and counted as `skipped_segmentation_differs`: rebuild the Corpus to re-cut it (see [Write an ingestion plugin](/plugins/write-an-ingestion-plugin)). A refusal by the plugin is counted as `skipped_ingestion_refused`, and an article the plugin cannot embed within its deadline as `skipped_plugin_deadline`. Rerun the backfill to try skipped articles again.

## Troubleshooting

| Answer | Cause | Fix |
| - | - | - |
| `409 dry_run_required` | No dry run with this key and body | Send it with `"dry_run": true` first |
| `409 cost_confirmation_required` | The estimate exceeds `backfill.max_cost_without_confirmation` | Add `"confirm_cost": true` |
| `409 idempotency_conflict` | The key was used with another scope | Use a new key |
| `409 backfill_in_progress` | Another backfill of the Corpus has not finished | Wait for it, or cancel it |
| `409 registration_not_active` | `registration_id` is not the active ingestion plugin | Omit it, or activate that registration |
| `409 rebuild_required` | The Corpus's index predates named vector spaces | Rebuild the Corpus first |
| `422 invalid_backfill` | A space the plugin does not declare or the deployment does not enable, or an empty window | Check `spaces` and the window |
