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 :
| Annotation | Type transmis | Indexé par défaut |
|---|---|---|
str | text | oui |
StrEnum | keyword | oui |
int | long | non |
float | float | non |
bool | bool | non |
datetime | timestamp | non |
list[T] des types précédents | T[] | non |
dict, un modèle pydantic | json | non |
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
Noneest 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 sansNone, commekeywordsdans l'exemple, reste obligatoire. UtilisezMistralSearchIndexField(required=...)pour surcharger ce comportement ; en REST,requiredvautfalsepar défaut. indexedcrée un index de base de données (btree pour les scalaires, plein texte pourtext). Les filtres fonctionnent sans index ; activezindexed=Truesur les champs sur lesquels vous filtrez souvent. La recherche par mots-clés ne fonctionne que sur les champstextindexés.- Les champs filtrables sont des scalaires :
text,keyword,int,long,floatettimestamp. Les champs de type tableau etjsonsont stockés et renvoyés avec chaque résultat, mais vous ne pouvez pas les utiliser dans un filtre. Les champsboolne 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 portentpage_numberdans leurs métadonnées, c'est pourquoi le champ de fragment de l'exemple s'appellepage), et les champs système ainsi quechunkssont réservés : en déclarer un comme champ personnalisé renvoie une erreur422. Document.contentn'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_indexpeut ajouter des champs optionnels ou activerindexedsur 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 erreur422. 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. PUTremplace 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ée | Champ transmis | Côté toolkit | Notes |
|---|---|---|---|
| document | source_id | Document.source_id | Identifie le document source. |
| document | document_id | non envoyé | Calculé côté serveur à partir de source_id, de la même manière que Document.id. |
| document | document_metadata | Document.metadata | Vaut {} par défaut. |
| fragment | id | non envoyé | Calculé côté serveur à partir de (source_id, locator), de la même manière que DocumentChunk.id. |
| fragment | locator | DocumentChunk.locator | Position dans la source, par exemple char:0-512. |
| fragment | start_offset, end_offset | identique | |
| fragment | content | DocumentChunk.content | Le texte du passage. |
| fragment | content_embedding | DocumentChunk.embedding | Obligatoire : 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. |
| fragment | chunk_type | DocumentChunk.chunk_type | Vaut content par défaut. |
| fragment | parent_ref | DocumentChunk.parent_ref | Optionnel. |
| fragment | metadata | DocumentChunk.metadata | Vaut {} 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.
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ération | Endpoint |
|---|---|
| Créer un index | POST /v1/context/search-indexes |
| Lister les index | GET /v1/context/search-indexes |
| Obtenir un index | GET /v1/context/search-indexes/{index_name} |
| Mettre à jour le schéma (ajouts uniquement) | PUT /v1/context/search-indexes/{index_name} |
| Supprimer un index | DELETE /v1/context/search-indexes/{index_name} |
| Ingérer des documents | POST /v1/context/search-indexes/{index_name}/documents |
| Supprimer des documents | DELETE /v1/context/search-indexes/{index_name}/documents |
| Rechercher | POST /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 :
| Retriever | Objectif |
|---|---|
NearestNeighbourRetriever | Recherche 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. |
KeywordRetriever | Correspondance plein texte sur content, ou sur un autre champ text indexé via field. |
RRFRetriever | Fusionne 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
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
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
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")