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

# Documents & Search

> Upsert vectors, search with dense/sparse/BM25/hybrid, and manage documents

## Upsert Documents

Insert or update documents with your pre-computed vectors and JSON payload. Up to 1,000 documents per request.

<CodeGroup>
  ```python Python theme={null}
  from mixpeek import Mixpeek

  client = Mixpeek(api_key="mxp_sk_...")

  result = client.namespaces.documents.upsert(
      namespace_id="product-search",
      documents=[
          {
              "document_id": "prod-001",
              "vectors": {"text_embedding": [0.12, -0.34, 0.56]},  # 1536 floats
              "payload": {"title": "Noise-Canceling Headphones", "price": 149.99, "in_stock": True},
          },
          {
              "document_id": "prod-002",
              "vectors": {"text_embedding": [0.08, 0.22, -0.41]},  # 1536 floats
              "payload": {"title": "Bluetooth Speaker", "price": 59.99, "in_stock": True},
          },
      ],
  )
  # → {"inserted": 2, "document_ids": ["prod-001", "prod-002"]}
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.mixpeek.com/v1/namespaces/product-search/documents/upsert" \
    -H "Authorization: Bearer $MIXPEEK_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "documents": [{
        "document_id": "prod-001",
        "vectors": {"text_embedding": [0.12, -0.34, 0.56]},
        "payload": {"title": "Noise-Canceling Headphones", "price": 149.99}
      }]
    }'
  ```
</CodeGroup>

Vector dimensions are validated against namespace config. If a document with the same ID exists, it is overwritten.

Mixpeek writes its own `_internal` envelope alongside your payload. Some of those
fields are system-owned and are rejected if you send them on a write. See
[The `_internal` envelope](/docs/retrieval/filters#the-_internal-envelope) for the
complete list of what Mixpeek writes and which fields you may set.

### Large Imports

Repeat the upsert call in batches of up to 1,000 documents. There is no
separate bulk-import endpoint.

### Carry lineage on the way in

A document you upsert defaults to `source_type: "direct_upsert"` and no lineage.
Send the lineage fields instead and the document arrives already knowing where it
came from, which is what makes this route usable for importing derived documents
rather than only standalone ones.

```bash cURL theme={null}
curl -X POST "https://api.mixpeek.com/v1/namespaces/$NS/documents/upsert" \
  -H "Authorization: Bearer $MIXPEEK_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "documents": [{
      "document_id": "chunk-001",
      "vectors": {"text_embedding": [0.12, -0.34, 0.56]},
      "payload": {"text": "..."},
      "source_type": "collection",
      "source_document_id": "doc-parent-001",
      "source_collection_id": "col_transcripts",
      "root_object_id": "obj_meeting_2026_08_22",
      "root_bucket_id": "bkt_recordings",
      "lineage_path": "bkt_recordings/col_transcripts/col_chunks",
      "content_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
    }]
  }'
```

| Field | Meaning |
| - | - |
| `source_type` | Immediate-parent type: `bucket`, `collection`, `direct_upsert` or `signal`. Defaults to `direct_upsert`. |
| `root_object_id` | Decomposition-tree root. Every document derived from the same source object shares it. |
| `root_bucket_id` | Bucket holding the root object. |
| `source_object_id` | Immediate parent object id, when `source_type` is `bucket`. |
| `source_document_id` | Immediate parent document id, when `source_type` is `collection`. |
| `source_collection_id` | Immediate parent collection id, when `source_type` is `collection`. |
| `lineage_path` | Materialized path, for example `bkt_123/col_456/col_789`. |
| `lineage_chain` | Ordered steps from root to this document. Each step carries `collection_id`, `feature_extractor_id`, `document_id` and `timestamp`. |
| `content_hash` | SHA256 of the source object, stored at `_internal.content_hash`. |

Every field is optional. Send none and you get the previous behaviour, so
existing callers are unaffected.

<Tip>
  `content_hash` is worth sending even when you do not care about lineage. A
  later heal or reprocess uses it to recognise a cached derivation and skip the
  recompute, so supplying it is the difference between re-paying for extraction
  and not.
</Tip>

<Note>
  This is the only route that imports N documents with your own vectors, full
  lineage and a content hash in one call, at up to 1,000 documents per request.
  Which parent id applies depends on `source_type`: `bucket` uses
  `source_object_id`, `collection` uses `source_document_id` plus
  `source_collection_id`.
</Note>

### Write vectors into a managed collection

`POST /v1/collections/{collection_id}/documents` takes `vectors` for the document you
create. Each key names one of the collection's vector indexes. Read the names from
`vector_indexes` on `GET /v1/collections/{collection_id}`.

A key is either the last segment of the index `feature_uri` or the full `index.name`.
For a `text_extractor` collection:

| Where you read it | Value |
| - | - |
| `feature_uri` | `mixpeek://text_extractor@v1/multilingual_e5_large_instruct_v1` |
| Key from the last segment | `multilingual_e5_large_instruct_v1` |
| Key from `index.name` | `text_extractor_v1_embedding` |

Both keys work, and the vector is stored under `index.name`. The array length must match
the index `dimensions`. A key the collection does not have returns a 422 that lists the
available index names and dimensions.

```bash cURL theme={null}
curl -X POST "https://api.mixpeek.com/v1/collections/$COLLECTION_ID/documents" \
  -H "Authorization: Bearer $MIXPEEK_API_KEY" \
  -H "X-Namespace: $NAMESPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {"title": "Noise-Canceling Headphones"},
    "vectors": {"multilingual_e5_large_instruct_v1": [0.12, -0.34, 0.56, "...1024 floats"]}
  }'
```

## Search

<Note>
  **Querying is unified through retrievers** — Mixpeek has one query path, so there is
  no direct `POST /v1/namespaces/{ns}/documents/search` REST endpoint. The `client.search(...)`
  helper below is SDK sugar that authors and runs a retriever for you. From raw HTTP/curl,
  create a one-stage `feature_search` retriever and execute it — the verified BYO example
  immediately below does exactly that in three calls.
</Note>

### Query BYO vectors from raw HTTP (verified)

Bring your own vectors and query them with a raw query vector — no extractor, no re-processing.
The query is an **object** (`{"input_mode": "vector", "value": "{{INPUT.qv}}"}`), and the
retriever's `input_schema` declares the input variable your execute call fills in.

```bash cURL theme={null}
# 1. Create a feature_search retriever over your BYO vector index ("my_vec").
#    Note: stage_type is "filter"; the operation is stage_id "feature_search".
curl -X POST "https://api.mixpeek.com/v1/retrievers" \
  -H "Authorization: Bearer $MIXPEEK_API_KEY" -H "X-Namespace: $NS" -H "Content-Type: application/json" \
  -d '{
    "retriever_name": "byo-knn",
    "input_schema": {"qv": {"type": "array", "required": true}},
    "stages": [{
      "stage_name": "knn", "stage_type": "filter",
      "config": {"stage_id": "feature_search", "parameters": {
        "searches": [{
          "feature_uri": "my_vec",
          "query": {"input_mode": "vector", "value": "{{INPUT.qv}}"}
        }],
        "final_top_k": 10
      }}
    }]
  }'
# → {"retriever_id": "ret_...", ...}

# 2. Execute it with your query vector. pagination.method is required.
curl -X POST "https://api.mixpeek.com/v1/retrievers/$RETRIEVER_ID/execute" \
  -H "Authorization: Bearer $MIXPEEK_API_KEY" -H "X-Namespace: $NS" -H "Content-Type: application/json" \
  -d '{"inputs": {"qv": [1,0,0,0,0,0,0,0]}, "pagination": {"method": "offset", "page_size": 10, "page_number": 1}}'
# → {"documents": [{"document_id": "d1", "score": 1.0, ...}, ...]}
```

The execute cache is invalidated by upserts — a query re-run after an upsert returns fresh
results (`cache_hit: false`), so the write-then-verify loop is safe.

### Dense (Vector) — SDK

```python theme={null}
results = client.search(
    namespace_id="product-search",
    queries=[{
        "vector_name": "text_embedding",
        "vector": query_embedding,
        "top_k": 10,
        "score_threshold": 0.7,
    }],
)
```

### BM25 (Keyword)

Requires a [text index](/docs/vector-store/namespaces#text-indexes-bm25) on the target field.

```python theme={null}
results = client.search(
    namespace_id="product-search",
    queries=[{"text": "wireless noise canceling", "top_k": 10}],
)
```

### Sparse

```python theme={null}
results = client.search(
    namespace_id="product-search",
    queries=[{"sparse_vector": {42: 0.8, 1337: 0.5, 9001: 0.3}, "top_k": 10}],
)
```

### Hybrid

Combine multiple query types with RRF or DBSF fusion.

```python theme={null}
results = client.search(
    namespace_id="product-search",
    queries=[
        {"vector_name": "text_embedding", "vector": query_embedding, "top_k": 20},
        {"text": "wireless headphones", "top_k": 20},
    ],
    fusion="rrf",
)
```

### Filtered

Add payload filters to any query type. Filters narrow results before scoring.

```python theme={null}
results = client.search(
    namespace_id="product-search",
    queries=[{
        "vector_name": "text_embedding",
        "vector": query_embedding,
        "top_k": 10,
        "filters": {
            "must": [
                {"key": "price", "range": {"lte": 100}},
                {"key": "in_stock", "match": {"value": True}}
            ]
        },
    }],
)
```

## Document Operations

| Operation | Method | Endpoint |
| - | - | - |
| Get by ID | `GET` | `/v1/namespaces/{ns}/documents/{doc_id}` |
| Read many by ID | `POST` | `/v1/documents/batch-get` |
| List and paginate | `POST` | `/v1/documents/list` |
| Update payload | `PATCH` | `/v1/documents/{doc_id}` |
| Delete | `DELETE` | `/v1/collections/{collection_id}/documents/{doc_id}` |

Only upsert and get-by-ID carry the namespace in the path. The rest read it from
the `X-Namespace` header. Delete is collection-scoped, so a document you reach
by namespace is deleted through its collection.

### Scope a list to one collection

`POST /v1/documents/list` is namespace-scoped. Narrow it with `collection_ids`, which
takes a list even for one collection, or with a `filters` condition:

```json theme={null}
{ "collection_ids": ["col_2e13178d4f"], "limit": 50 }
```

```json theme={null}
{ "filters": { "field": "collection_id", "operator": "eq", "value": "col_2e13178d4f" } }
```

<Warning>
  **A singular `collection_id` in the body does nothing, and the call still returns
  `200`.** The request model declares `collection_ids`, and an unrecognized key is
  dropped rather than rejected. You get every document in the namespace, and
  `total_documents` counts that wider set, so the response looks like a valid answer
  to the question you thought you asked.

  Nothing in the response marks the difference. `unknown_collection_ids` and
  `warnings` report ids that do not resolve, not keys that were ignored.

  Check the plural before you trust a count. Retriever `inputs` behave the opposite
  way and reject an undeclared key with `400`.
</Warning>

### Read Many by ID

Resolve up to 1,000 documents in one call. The namespace comes from the
`X-Namespace` header here, not from the path.

```bash theme={null}
curl -X POST "https://api.mixpeek.com/v1/documents/batch-get" \
  -H "Authorization: Bearer $MIXPEEK_API_KEY" \
  -H "X-Namespace: $MIXPEEK_NAMESPACE" \
  -H "Content-Type: application/json" \
  -d '{"document_ids": ["prod-001", "prod-002", "prod-404"]}'
# → {"documents": [...], "not_found": ["prod-404"]}
```

| Field | Meaning |
| - | - |
| `document_ids` | 1 to 1,000 ids. Required. |
| `return_presigned_urls` | Defaults to `true` on this endpoint. Pass `false` to skip presigning and get raw storage URIs back. |
| `documents` | The documents that resolved. |
| `not_found` | Each id that did not resolve, listed individually. |

<Note>
  `documents` follows the order of `document_ids`. Ids that did not resolve are
  omitted from `documents` and listed in `not_found`, and a repeated id yields
  its document once, at the first position you asked for it.

  Both of those shift later array positions, so match by `document_id` rather
  than by index.

  An id that does not resolve does not fail the call. It lands in `not_found`
  and every other id still returns, so read `not_found` rather than comparing
  lengths.
</Note>

### Media URLs

Stored media fields hold a raw storage URI: `gs://` on Mixpeek Cloud, `s3://` on
S3-backed deployments. Whether you get a fetchable `https://` URL back depends on
the endpoint you called, not on the field name.

| Surface | Presigns |
| - | - |
| Retriever execute | Always. `return_presigned_urls` is not read on this path. |
| `POST /v1/documents/batch-get` | By default. Pass `"return_presigned_urls": false` to skip it. |
| `POST /v1/documents/list` | Only with `"return_presigned_urls": true` in the body. |
| `GET /v1/documents/{doc_id}` | Only with the flag, and these URLs last 1 hour. |

<Warning>
  `POST /v1/documents/list` reads that flag from the request body only. A
  `?return_presigned_urls=true` query string is ignored, and you get raw `gs://`
  values back, which most HTTP clients reject with `unknown url type: gs`.
</Warning>

Selection works on the value, not the name. Any field whose value starts with
`gs://` or `s3://` gets signed, whatever the field is called. These stay raw:

* anything under `metadata`
* values nested inside dicts or lists, on documents carrying a `collection_id`
* `original_url`, `source_blobs`, and `presigned_urls`
* any URI whose signing call fails

Signed URLs last 24 hours everywhere except the single-document `GET` above.

### Update Vectors

Re-upsert the document through `POST /v1/namespaces/{ns}/documents/upsert`.
Sending an existing `document_id` overwrites that document, vectors and payload
together, so send the payload you want to keep alongside the new vectors.

No endpoint replaces vectors while leaving the payload untouched.

## Related

* [Namespace Configuration](/docs/vector-store/namespaces)
* [Promote to Managed](/docs/vector-store/promote)


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