Guide de démarrage : créer et interroger un index

Dans ce guide de démarrage, vous créez un index de recherche, vous y ingérez quelques documents et vous exécutez une première requête hybride filtrée — l'index est alors prêt à être utilisé depuis un agent ou un workflow. Vous avez besoin d'une clé API Mistral, de Python 3.12 ou version ultérieure, de uv et d'un espace de travail sur lequel les index de recherche sont activés.

Obtenir la clé API

Obtenir la clé API

Générez une clé API dans la Console Mistral et exportez-la — les clés API ne sont accessibles qu'une seule fois :

export MISTRAL_API_KEY=your-api-key
Note

La création d'un index nécessite l'autorisation create_search_indexes au niveau de l'espace de travail. Sans elle, la création renvoie 403. Le créateur devient le propriétaire de l'index et peut ensuite consulter, interroger, ingérer, modifier et supprimer l'index ; un refus sur un index existant est signalé par 404, jamais par 403.

Installez le Search Toolkit, le client des index de recherche :

uv init search-index-quickstart && cd search-index-quickstart
uv add "mistralai-search-toolkit>=0.1.0"

Créer, ingérer et interroger

Créer, ingérer et interroger

Construisez quickstart.py étape par étape : déclarez le schéma à l'aide de sous-classes Document / DocumentChunk, créez l'index, ingérez trois articles, exécutez deux requêtes hybrides, puis nettoyez une fois terminé.

Imports et données d'exemple. MistralSearchIndexService gère le cycle de vie de l'index, et les imports de retrievers servent à la requête hybride de la fin. Les noms d'index sont uniques par espace de travail — le suffixe aléatoire rend ce guide rejouable. Chaque article d'exemple porte un identifiant de source, un titre, un produit et un texte.

import asyncio
import secrets
from typing import Annotated

from mistralai.search.toolkit.document import Document, DocumentChunk, SourceId
from mistralai.search.toolkit.embedding import MistralEmbedder, MistralEmbeddingPreset
from mistralai.search.toolkit.mistral_search_index import MistralSearchIndex, MistralSearchIndexService
from mistralai.search.toolkit.mistral_search_index.models import MistralSearchIndexStatus
from mistralai.search.toolkit.mistral_search_index.retriever import (
    Equal,
    KeywordRetriever,
    NearestNeighbourRetriever,
    RRFRetriever,
)
from mistralai.search.toolkit.mistral_search_index.schema import (
    MistralSearchIndexCollectionSchema,
    MistralSearchIndexField,
    MistralSearchIndexFieldType,
)
from mistralai.search.toolkit.search import SearchResult

# Index names are unique per workspace, so a shared workspace needs a unique name.
INDEX = f"support_articles_{secrets.token_hex(4)}"
EMBEDDING = MistralEmbeddingPreset.MISTRAL_EMBED_DIM_1024

ARTICLES = [
    # (source_id, title, product, text)
    ("s3://docs/api-keys.md", "Managing API keys", "console",
     "Rotate an API key from the console under Workspace > API keys."),
    ("s3://docs/billing.md", "Invoices and billing", "billing",
     "Download monthly invoices and update your card on the billing page."),
    ("s3://docs/members.md", "Inviting members", "console",
     "Invite teammates to your workspace from the Members tab and assign roles."),
]

Déclarez le schéma. Chaque champ ajouté aux modèles de base devient un champ d'index ; le type est déduit de l'annotation Python, et MistralSearchIndexField permet de le remplacer. page_number est un INT indexé et product est un KEYWORD, utilisable en filtre exact.

# Every field added on top of the base models becomes an index field. The wire
# type is inferred from the annotation; MistralSearchIndexField overrides it.
class ArticleChunk(DocumentChunk):
    page_number: Annotated[int, MistralSearchIndexField(type=MistralSearchIndexFieldType.INT, indexed=True)]


class Article(Document):
    title: str
    product: Annotated[str, MistralSearchIndexField(type=MistralSearchIndexFieldType.KEYWORD)]
    chunks: list[ArticleChunk]

Créez l'index. from_document_type déduit le schéma de collection du modèle Article. La création réserve le nom et provisionne la table sous-jacente de manière asynchrone : interrogez le statut jusqu'à READY. Le modèle d'embedding est immuable — il détermine le type physique de la colonne et la classe d'opérateur vectoriel, il ne peut donc pas être modifié après la création.

async def create_index(service: MistralSearchIndexService) -> None:
    schema = MistralSearchIndexCollectionSchema.from_document_type(
        collection_name=INDEX,
        document_type=Article,
        embedding_model=EMBEDDING,
    )
    info = await service.create_index(schema)
    while info.status == MistralSearchIndexStatus.PROVISIONING:
        await asyncio.sleep(1)
        info = await service.get_index_info(INDEX)
    if info.status != MistralSearchIndexStatus.READY:
        raise RuntimeError(f"provisioning failed: {info.status_message}")

Ingérez les articles. Embeddez les textes en un seul appel, puis écrivez chaque article sous forme d'un chunk portant son embedding — l'ingestion n'embedde jamais. Réingérer le même source_id remplace le document : ses chunks existants sont supprimés et les nouveaux insérés, en une seule transaction.

async def ingest(index: MistralSearchIndex, embedder: MistralEmbedder) -> None:
    texts = [text for *_, text in ARTICLES]
    embeddings = (await embedder.embed(texts)).embeddings
    for (source_id, title, product, text), embedding in zip(ARTICLES, embeddings, strict=True):
        await index.index_document(
            Article(
                source_id=SourceId(source_id),
                content=text,
                title=title,
                product=product,
                chunks=[
                    ArticleChunk(
                        source_id=SourceId(source_id),
                        locator=f"char:0-{len(text)}",
                        start_offset=0,
                        end_offset=len(text),
                        content=text,
                        embedding=embedding,
                        page_number=1,
                    )
                ],
            )
        )

Exécutez une requête hybride. Les classements par mots-clés et vectoriels sont fusionnés par reciprocal rank fusion. Pour un modèle d'embedding Mistral, le serveur embedde la requête lui-même, et le filtre Equal sur product est poussé vers les deux branches.

async def search(index: MistralSearchIndex, query: str, product: str | None = None) -> list[SearchResult]:
    return await index.retrieve(
        RRFRetriever(
            top_k=5,
            filter=Equal(field="product", value=product) if product else None,
            retrievers=[
                KeywordRetriever(query=query, top_k=5),
                NearestNeighbourRetriever(query=query, top_k=5),
            ],
        )
    )

Assemblez le tout. Créez l'index, récupérez son handle, ingérez avec un embedder sur le preset choisi, affichez les résultats d'une requête non filtrée puis d'une requête filtrée sur les articles console, et supprimez l'index dans finally — la suppression est également asynchrone : l'index passe à l'état deleting, puis disparaît.

async def main() -> None:
    service = MistralSearchIndexService()
    await create_index(service)
    try:
        index = await service.get_search_index(INDEX)
        await ingest(index, MistralEmbedder(model_name=EMBEDDING.full_model_name))

        for hit in await search(index, "how do I rotate an api key"):
            print(f"{hit.score:.4f}  {hit.chunk.source_id}")
        for hit in await search(index, "add a colleague", product="console"):
            print(f"{hit.score:.4f}  {hit.chunk.source_id}")
    finally:
        # Deleting is asynchronous too: the index goes to deleting, then disappears.
        await service.delete_index(INDEX)


if __name__ == "__main__":
    asyncio.run(main())

Exécutez-le :

uv run quickstart.py

Vous devriez voir

Vous devriez voir

L'index est provisionné et atteint l'état READY, puis le script affiche les résultats de recherche sous forme de lignes score source_id. La première requête renvoie les articles correspondant à la question, tous produits confondus ; la seconde filtre sur product="console", de sorte que seuls les articles liés à la console peuvent apparaître.

Aller plus loin

Aller plus loin