Opérations de base

Cette page présente les principales opérations de Search Index : définir les champs, créer un index, ingérer et interroger des documents, modifier le schéma et supprimer des données. Les exemples Python utilisent le schéma cookbook_docs de Champs personnalisés. Le toolkit est asynchrone, exécutez-les donc dans une fonction async. Les exemples REST utilisent un index distinct, support_articles. Pour un script complet et exécutable, consultez le guide de démarrage.

Champs personnalisés

Champs personnalisés

Chaque champ qu'une sous-classe de Document ou de DocumentChunk ajoute aux modèles de base devient une colonne typée dans l'index. Les champs de document décrivent un document source entier ; les champs de fragment décrivent un passage récupérable. La table sous-jacente est plate, les champs de document sont donc copiés sur chaque ligne de fragment, et un seul filtre peut combiner des champs de document (comme category) et des champs de fragment (comme page).

Le toolkit déduit le type transmis de l'annotation, et MistralSearchIndexField permet de surcharger n'importe laquelle de ses parties :

AnnotationType transmisIndexé par défaut
strtextoui
StrEnumkeywordoui
intlongnon
floatfloatnon
boolboolnon
datetimetimestampnon
list[T] des types précédentsT[]non
dict, un modèle pydanticjsonnon
from typing import Annotated, Sequence

from mistralai.search.toolkit.document import Document, DocumentChunk
from mistralai.search.toolkit.embedding import MistralEmbeddingPreset
from mistralai.search.toolkit.mistral_search_index.schema import (
    MistralSearchIndexCollectionSchema,
    MistralSearchIndexField,
    MistralSearchIndexFieldType,
)

class ReportChunk(DocumentChunk):
    # Chunk metadata is stored as an opaque JSON blob and cannot be filtered on;
    # a field of its own makes the page number a filterable column.
    page: Annotated[int | None, MistralSearchIndexField(indexed=True)] = None

class ReportDocument(Document):
    title: str  # text: full-text searchable
    category: Annotated[str, MistralSearchIndexField(type=MistralSearchIndexFieldType.KEYWORD, indexed=True)]
    page_count: int  # long: Range filters
    published_year: Annotated[int | None, MistralSearchIndexField(type=MistralSearchIndexFieldType.INT, indexed=True)] = None
    keywords: Annotated[list[str], MistralSearchIndexField(type=MistralSearchIndexFieldType.KEYWORD_ARRAY)] = []
    chunks: Sequence[ReportChunk] = []

schema = MistralSearchIndexCollectionSchema.from_document_type(
    collection_name="cookbook_docs",
    document_type=ReportDocument,
    embedding_model=MistralEmbeddingPreset.MISTRAL_EMBED_DIM_1024,
)

À savoir sur les champs personnalisés :

  • Optionnel signifie nullable. Un champ dont l'annotation inclut None est optionnel ; tous les autres champs sont obligatoires, et l'index rejette tout document qui ne les contient pas. Un champ avec une valeur par défaut mais sans None, comme keywords dans l'exemple, reste obligatoire. Utilisez MistralSearchIndexField(required=...) pour surcharger ce comportement ; en REST, required vaut false par défaut.
  • indexed crée un index de base de données (btree pour les scalaires, plein texte pour text). Les filtres fonctionnent sans index ; activez indexed=True sur les champs sur lesquels vous filtrez souvent. La recherche par mots-clés ne fonctionne que sur les champs text indexés.
  • Les champs filtrables sont des scalaires : text, keyword, int, long, float et timestamp. Les champs de type tableau et json sont stockés et renvoyés avec chaque résultat, mais vous ne pouvez pas les utiliser dans un filtre. Les champs bool ne sont pas filtrables non plus.
  • Les noms sont soumis à des contraintes. Les noms de champs doivent correspondre au motif [a-z][a-z0-9]*(_[a-z0-9]+)* et comporter au maximum 32 caractères. Les noms d'index suivent la même règle. Un champ personnalisé ne doit pas reprendre une clé des métadonnées du même fragment (les fragments paginés issus de l'OCR portent page_number dans leurs métadonnées, c'est pourquoi le champ de fragment de l'exemple s'appelle page), et les champs système ainsi que chunks sont réservés : en déclarer un comme champ personnalisé renvoie une erreur 422.
  • Document.content n'est pas stocké. Seuls ses fragments le sont. Les identifiants de fragment dérivent de (source_id, locator), donc deux fragments d'un même document doivent avoir des locators distincts.
  • Les schémas évoluent de manière additive. MistralSearchIndexService.update_index peut ajouter des champs optionnels ou activer indexed sur un champ existant. Supprimer un champ, changer son type, le déplacer entre la portée document et la portée fragment, le désindexer ou en ajouter un obligatoire est refusé avec une erreur 422. Pour appliquer l'une de ces modifications, recréez l'index. Un index créé avant l'introduction des champs personnalisés n'en contient aucun : recréez-le et relancez l'ingestion.
  • PUT remplace le schéma. En REST, envoyez le schéma complet à chaque mise à jour, pas seulement les champs modifiés.

Champs système

Champs système

Les champs canoniques de Document / DocumentChunk correspondent aux champs propres de l'index :

PortéeChamp transmisCôté toolkitNotes
documentsource_idDocument.source_idIdentifie le document source.
documentdocument_idnon envoyéCalculé côté serveur à partir de source_id, de la même manière que Document.id.
documentdocument_metadataDocument.metadataVaut {} par défaut.
fragmentidnon envoyéCalculé côté serveur à partir de (source_id, locator), de la même manière que DocumentChunk.id.
fragmentlocatorDocumentChunk.locatorPosition dans la source, par exemple char:0-512.
fragmentstart_offset, end_offsetidentique
fragmentcontentDocumentChunk.contentLe texte du passage.
fragmentcontent_embeddingDocumentChunk.embeddingObligatoire : l'ingestion ne génère jamais de représentation vectorielle, et un fragment qui en est dépourvu lève une erreur avant l'envoi de la requête.
fragmentchunk_typeDocumentChunk.chunk_typeVaut content par défaut.
fragmentparent_refDocumentChunk.parent_refOptionnel.
fragmentmetadataDocumentChunk.metadataVaut {} par défaut.

Créer un index

Créer un index

MistralSearchIndexService gère le cycle de vie de l'index, et sa méthode get_search_index renvoie un MistralSearchIndex pour les opérations sur les documents et les requêtes. La même API est disponible dans le SDK mistralai sous client.beta.context.search_indexes.

Note

Pour créer un index, vous devez disposer de l'autorisation create_search_indexes au niveau de l'espace de travail. Sans cette autorisation, ou lorsque l'organisation atteint sa limite d'index, la requête renvoie une erreur 403. Le créateur devient propriétaire de l'index, avec les autorisations see, query, ingest, modify et delete sur celui-ci. Si vous ne disposez pas d'une autorisation sur un index existant, l'API renvoie une erreur 404, et non 403.

Avec le toolkit, déduisez le schéma à partir de votre type de document et attendez la fin du provisionnement :

import asyncio

from mistralai.search.toolkit.mistral_search_index import MistralSearchIndexService
from mistralai.search.toolkit.mistral_search_index.models import MistralSearchIndexStatus

service = MistralSearchIndexService()
info = await service.create_index(schema)
while info.status == MistralSearchIndexStatus.PROVISIONING:
    await asyncio.sleep(1)
    info = await service.get_index_info("cookbook_docs")
if info.status != MistralSearchIndexStatus.READY:
    raise RuntimeError(f"provisioning failed: {info.status_message}")

Vous ne pouvez pas changer le modèle de représentations vectorielles après la création : il détermine le type de la colonne vectorielle et la classe d'opérateur vectorielle.

En REST, le même appel est POST /v1/context/search-indexes, avec le schéma déclaré directement dans la requête :

curl -X POST https://api.mistral.ai/v1/context/search-indexes \
  -H "Authorization: Bearer $MISTRAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support_articles",
    "schema": {
      "document_fields": {
        "title": { "type": "text", "indexed": true },
        "product": { "type": "keyword", "indexed": true }
      },
      "chunk_fields": {
        "page_number": { "type": "int", "indexed": true }
      }
    },
    "config": {
      "embedding": { "type": "mistral", "name": "mistral-embed", "dimensions": 1024 }
    }
  }'

Le tableau suivant liste les endpoints de cycle de vie, de document et de recherche :

OpérationEndpoint
Créer un indexPOST /v1/context/search-indexes
Lister les indexGET /v1/context/search-indexes
Obtenir un indexGET /v1/context/search-indexes/{index_name}
Mettre à jour le schéma (ajouts uniquement)PUT /v1/context/search-indexes/{index_name}
Supprimer un indexDELETE /v1/context/search-indexes/{index_name}
Ingérer des documentsPOST /v1/context/search-indexes/{index_name}/documents
Supprimer des documentsDELETE /v1/context/search-indexes/{index_name}/documents
RechercherPOST /v1/context/search-indexes/{index_name}/search

La création, la mise à jour et la suppression sont asynchrones et renvoient 202. La suppression fait passer l'index à l'état deleting ; le nom reste réservé jusqu'à la fin de l'opération, et vous ne pouvez recréer l'index qu'après. Tout appel de document ou de requête sur un index qui n'est pas ready renvoie 409.

Ingérer des documents

Ingérer des documents

L'ingestion ne calcule pas les représentations vectorielles : chaque fragment apporte les siennes, et le toolkit lève une erreur avant d'envoyer un fragment qui n'en contient pas. Le source_id d'un document constitue son identité. Si vous ingérez à nouveau le même source_id, la nouvelle version remplace le document : ses fragments sont supprimés et les nouveaux insérés en une seule transaction. Dans un pipeline d'ingestion du toolkit, source_id vaut par défaut le chemin ou le nom du fichier ; ingérer à nouveau le même fichier le met donc à jour au lieu d'ajouter une copie.

En REST, envoyez jusqu'à 100 documents par requête à POST /v1/context/search-indexes/{index_name}/documents. Chaque document contient ses champs personnalisés et jusqu'à 1 000 fragments. Chaque fragment doit inclure une représentation vectorielle complète, avec les dimensions configurées sur l'index. La réponse est 200, avec une entrée accepted ou rejected par document ; vérifiez donc chaque entrée.

Pour l'index support_articles, le script suivant génère la représentation vectorielle d'un article avec l'API de représentations vectorielles, puis l'ingère. Il utilise jq pour construire les corps de requête :

TEXT="Rotate an API key from the console under Workspace > API keys."
EMBEDDING=$(curl -s https://api.mistral.ai/v1/embeddings \
  -H "Authorization: Bearer $MISTRAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg text "$TEXT" '{model: "mistral-embed", input: [$text]}')" \
  | jq -c '.data[0].embedding')

curl -X POST https://api.mistral.ai/v1/context/search-indexes/support_articles/documents \
  -H "Authorization: Bearer $MISTRAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg text "$TEXT" --argjson embedding "$EMBEDDING" '{
    documents: [{
      source_id: "s3://docs/api-keys.md",
      title: "Managing API keys",
      product: "console",
      chunks: [{
        locator: "char:0-\($text | length)",
        start_offset: 0,
        end_offset: ($text | length),
        content: $text,
        content_embedding: $embedding,
        page_number: 1
      }]
    }]
  }')"

La réponse indique chaque document par sa position dans la requête :

{
  "accepted": 1,
  "rejected": 0,
  "results": [
    {
      "status": "accepted",
      "position": 0,
      "document_id": "4bf49690-f3c9-5045-a170-9eaea02f5775",
      "chunk_count": 1
    }
  ]
}

Une entrée rejetée contient errors à la place, par exemple {"location": ["chunks", 0, "content_embedding"], "type": "missing", "message": "field is required"}.

Avec le toolkit, calculez les embeddings puis ingérez le document :

from mistralai.search.toolkit.document import SourceId
from mistralai.search.toolkit.embedding import MistralEmbedder

text = "Rotate an API key from the console under Workspace > API keys."
embedder = MistralEmbedder(model_name="mistral-embed")
embedding = (await embedder.embed([text])).embeddings[0]
article = ReportDocument(
    source_id=SourceId("s3://docs/api-keys.md"),
    content=text,
    title="Managing API keys",
    category="console",
    page_count=1,
    keywords=["api", "keys"],
    chunks=[ReportChunk(
        source_id=SourceId("s3://docs/api-keys.md"),
        locator=f"char:0-{len(text)}",
        start_offset=0,
        end_offset=len(text),
        content=text,
        embedding=embedding,
        page=1,
    )],
)
index = await service.get_search_index("cookbook_docs")
await index.index_document(article)

Interroger un index

Interroger un index

Interrogez via POST /v1/context/search-indexes/{index_name}/search, ou depuis Python avec MistralSearchIndex.retrieve, qui accepte le DSL de requête des retrievers :

RetrieverObjectif
NearestNeighbourRetrieverRecherche approximative des plus proches voisins sur content_embedding. Pour un modèle de représentation vectorielle mistral, passez query et le serveur génère sa représentation vectorielle ; un modèle custom n'a pas de générateur de représentations vectorielles côté serveur, passez donc query_embedding vous-même.
KeywordRetrieverCorrespondance plein texte sur content, ou sur un autre champ text indexé via field.
RRFRetrieverFusionne les classements de ses enfants par reciprocal rank fusion. weights et rank_constant permettent de l'ajuster, et son filter est appliqué en AND sur chaque enfant.

Les filtres sont Equal, In, Range (gt / gte / lt / lte), And, Or et Not, et s'appliquent à n'importe quel champ filtrable. Tous les retrievers acceptent un filter, pas seulement RRFRetriever. top_k vaut 20 par défaut et est plafonné à 100 sur le retriever de plus haut niveau ; le dépasser renvoie une erreur 400.

Les enfants d'un RRFRetriever peuvent définir top_k jusqu'à 1 000. Le top_k de chaque enfant doit être au moins égal à celui du parent.

from mistralai.search.toolkit.mistral_search_index.retriever import (
    Equal,
    KeywordRetriever,
    NearestNeighbourRetriever,
    RRFRetriever,
)

results = await index.retrieve(
    RRFRetriever(
        top_k=5,
        filter=Equal(field="category", value="console"),
        retrievers=[
            KeywordRetriever(query="how do I rotate an api key", top_k=5),
            NearestNeighbourRetriever(query="how do I rotate an api key", top_k=5),
        ],
    )
)

Pour l'index support_articles, interrogez via REST :

curl -X POST https://api.mistral.ai/v1/context/search-indexes/support_articles/search \
  -H "Authorization: Bearer $MISTRAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "retriever": {
      "type": "rrf",
      "top_k": 5,
      "filter": { "type": "equal", "field": "product", "value": "console" },
      "retrievers": [
        { "type": "keyword", "query": "how do I rotate an api key", "top_k": 5 },
        { "type": "nearest_neighbour", "query": "how do I rotate an api key", "top_k": 5 }
      ]
    }
  }'

VectorRetriever et QueryEngine appellent MistralSearchIndex.search(VectorSearchQuery), qui construit le retriever pour vous : une recherche du plus proche voisin lorsque la requête ne contient pas de texte, et RRF sur les mots-clés et le plus proche voisin lorsqu'elle en contient. La correspondance ne porte que sur content, et les champs personnalisés sont renvoyés avec chaque résultat. search applique VectorSearchQuery.filter, mais VectorRetriever et QueryEngine n'en définissent jamais. Pour rechercher des mots-clés sur un champ personnalisé, ou pour filtrer les résultats d'un pipeline QueryEngine, utilisez le DSL de retriever.

Chaque résultat est un SearchResult dont le champ chunk contient les champs de portée document en plus des champs de fragment. Les représentations vectorielles ne sont jamais renvoyées. Plus le score est élevé, meilleur est le résultat : 1 - distance pour cosine, -distance pour l2 et inner_product, le rang de mot-clé pour KeywordRetriever, et le score RRF fusionné pour RRFRetriever. distance est la distance vectorielle brute, ou None pour les résultats par mot-clé et RRF.

Un index de recherche est aussi un index navigable : navigate, read, grep et get_chunk parcourent un document source dans l'ordre de lecture plutôt que par pertinence. Ce sont les mêmes primitives sur lesquelles repose Agentic Search, et elles sont exposées sur les endpoints de l'index (POST .../navigate, POST .../read, POST .../grep, GET .../chunks/{chunk_id}).

Modifier le schéma

Modifier le schéma

Déclarez le schéma étendu puis appelez update_index. Les modifications sont additives : conservez les champs existants. Attendez le retour à READY avant d'utiliser les nouveaux champs.

class UpdatedReportDocument(ReportDocument):
    owner: str | None = None

updated_schema = MistralSearchIndexCollectionSchema.from_document_type(
    collection_name="cookbook_docs",
    document_type=UpdatedReportDocument,
    embedding_model=MistralEmbeddingPreset.MISTRAL_EMBED_DIM_1024,
)
await service.update_index(updated_schema)
Supprimer des documents

Supprimer des documents

Pour supprimer un document et ses fragments, transmettez son identifiant de document, et non son identifiant de source. Une suppression ne peut pas être annulée, mais vous pouvez ingérer le document à nouveau.

await index.delete_document(article.id)

Si aucun document ne correspond à l'identifiant, delete_document lève une DocumentNotFoundError. En REST, DELETE /v1/context/search-indexes/{index_name}/documents accepte jusqu'à 100 document_ids et renvoie les identifiants supprimés, ainsi que les identifiants introuvables dans missing.

Supprimer un index

Supprimer un index

Avertissement

La suppression d'un index efface tous ses documents et fragments, et ne peut pas être annulée. Vérifiez le nom de l'index avant d'appeler delete_index.

La suppression est asynchrone : l'index passe à l'état DELETING, puis les requêtes le concernant renvoient une erreur 404.

await service.delete_index("cookbook_docs")