Postgres

Utilisez PostgreSQL avec les extensions pgvector et pg_textsearch comme backend Search Toolkit. PostgresStoreIndex implémente VectorStoreIndex, NavigableIndex, PatchableIndex, VersionedIndex et RetrievableIndex. Il utilise donc les mêmes interfaces de pipeline que le backend Vespa, mais le provisionnement, la gestion des schémas et le périmètre des requêtes diffèrent.

Le backend PostgreSQL est disponible en option :

uv add 'mistralai-search-toolkit[postgres]'
Note
Avertissement

Isolation des tenants

Le backend Postgres ne déduit aucun filtre de tenant de IngestContext ou RetrievalContext. Utilisez une collection distincte pour chaque tenant ou appliquez le filtrage avant le store. Pour plus de détails, consultez les comportements de repli et limites.

Fonctionnement du backend Postgres

Fonctionnement du backend Postgres

Trois objets composent le backend Postgres :

  • PostgresCollectionSchema : la forme déclarée d’une collection : le modèle de document, le modèle d’embedding (dimensions, dtype, métrique de distance) et le schéma Postgres dans lequel se trouve la table. C’est la source de vérité unique lue par le store ; aucune information n’est déduite par introspection depuis la base de données.
  • PostgresApp : le handle d’exécution qui résout une collection déclarée vers un store actif. Reprend la forme get_search_index(config, collection) de l’app Vespa. Ce n’est volontairement pas un framework de migration ; il fournit un create_schema minimal pour les déploiements sans chaîne de migration, et attend sinon que vous fassiez évoluer le schéma avec votre propre outil (Alembic, Flyway, …).
  • PostgresStoreIndex : implémente la recherche dense et en environnement hybride, l’indexation, la suppression, la navigation et les mises à jour partielles. Transmettez-le à Pipeline(stores=...) et à un VectorRetriever(client=...), comme avec le backend Vespa.

Le flux est le suivant : provisionner les extensions → déclarer la collection → se connecter → créer la table (avec create_schema ou la CLI mistral-postgres) → rechercher.

Prérequis

Prérequis

La base de données cible doit avoir deux extensions installées avant la création d’une table de collection :

  • vector : fournit l’indexation HNSW des embeddings.
  • pg_textsearch : fournit la méthode d’accès BM25 pour la recherche hybride. Ajoutez-la à shared_preload_libraries avant le démarrage du serveur. CREATE EXTENSION échoue tant que le serveur n’a pas redémarré avec cette bibliothèque préchargée.

Installez les deux extensions une fois par base de données avec un rôle privilégié :

CREATE EXTENSION vector;
CREATE EXTENSION pg_textsearch;

Le plugin n’exécute jamais CREATE EXTENSION. Sur un Postgres managé (RDS, Cloud SQL), cette instruction nécessite des privilèges qu’un rôle applicatif de moindre privilège ne détient généralement pas ; le provisionnement revient donc à la personne ou à l’équipe qui gère la base de données. PostgresApp.create_schema vérifie les deux extensions avant toute tentative de DDL, et lève MissingVectorExtensionError ou MissingBM25AccessMethodError si l’une d’elles est absente.

Déclarer une collection

Déclarer une collection

Une collection se déclare dans le code avec PostgresCollectionSchema. embedding_model accepte le même EmbeddingModel / MistralEmbeddingPreset que le schéma Vespa (voir Modèle d’embedding) :

from mistralai.search.toolkit.document import Document
from mistralai.search.toolkit.embedding import MistralEmbeddingPreset
from mistralai.search.toolkit.plugins.postgres import PostgresApp, PostgresCollectionSchema

DOCS = PostgresCollectionSchema(
    collection_name="docs",
    document_type=Document,
    embedding_model=MistralEmbeddingPreset.MISTRAL_EMBED_DIM_1024,
)
app = PostgresApp([DOCS])
ChampTypePar défautObjectif
collection_namestr(obligatoire)Nom de la collection. Le tableau est nommé <collection_name>_chunks.
document_typetype[Document](obligatoire)Votre sous-classe Document
embedding_modelEmbeddingModel | MistralEmbeddingPreset(obligatoire)Modèle d’embedding (dimensions, dtype, métrique de distance) appliqué à la colonne d’embedding
db_schemastr | NoneNoneSchéma PostgreSQL qui contient la table. None utilise le schéma par défaut de la connexion.
hnsw_mint16Paramètre HNSW m
hnsw_ef_constructionint64Paramètre HNSW ef_construction
text_search_configstr"english"Configuration de recherche textuelle PostgreSQL pour l’index BM25 de content

PostgresCollectionSchema est le seul élément que le store reçoit au sujet de la collection : il construit la table, déclare la métrique utilisée pour le classement et résout les champs personnalisés du modèle de document vers ses colonnes. Aucune information n’est déduite par introspection depuis la base de données, et aucun nom de colonne n’est inféré par convention.

Colonnes personnalisées

Les champs personnalisés de votre sous-classe Document correspondent à des colonnes de table. Ils ne sont pas bornés, sauf si le modèle impose une longueur maximale :

from typing import Annotated

from mistralai.search.toolkit.document import Document
from mistralai.search.toolkit.plugins.postgres import PostgresColumn


class MyDoc(Document):
    section: Annotated[str | None, PostgresColumn(max_length=120)] = None
Connexion

Connexion

PostgresApp.get_search_index accepte soit un PostgresConnectionConfig, soit un AsyncEngine SQLAlchemy, et renvoie un PostgresStoreIndex :

from mistralai.search.toolkit.plugins.postgres import PostgresConnectionConfig

config = PostgresConnectionConfig(dsn="postgresql://user:pw@localhost:5432/db")

await app.create_schema(config, "docs")  # idempotent; verifies the extensions first
store = app.get_search_index(config, "docs")  # -> PostgresStoreIndex

Formes de connexion

PostgresConnectionConfig accepte un DSN :

PostgresConnectionConfig(dsn="postgresql://user:pw@localhost:5432/db")

ou des parties :

PostgresConnectionConfig(host="localhost", port=5432, database="db", user="user", password="pw")

TLS

Les services PostgreSQL managés fournissent un DSN avec ?sslmode=require ou un mode plus strict. Transmettez-le sans modification. La configuration retire sslmode de l’URL et transmet la valeur traduite au driver asyncpg :

PostgresConnectionConfig(dsn="postgresql://user:pw@host:5432/db?sslmode=require")

La forme par parties accepte directement le mode via ssl= (un ssl= explicite remplace aussi celui qui serait intégré dans un DSN) :

PostgresConnectionConfig(host="host", database="db", user="user", ssl="verify-full")
Créer la table

Créer la table

La table doit exister avant que le store puisse l’utiliser. Sa méthode de création dépend de votre configuration de migration :

  • Aucune chaîne de migration : PostgresApp.create_schema(config, collection) crée directement la table et les index. Cette méthode idempotente vérifie d’abord les extensions.
  • Alembic : ajoutez la table de la collection au target_metadata de la chaîne avec PostgresCollectionSchema.to_table(), puis laissez autogenerate écrire la révision. Cette méthode conserve un ordre de migration unique pour la base de données.
  • Autres outils de migration : générez les instructions CREATE TABLE et CREATE INDEX avec la CLI mistral-postgres, puis appliquez-les avec Flyway, Liquibase ou une migration .sql.
CLI mistral-postgres

CLI mistral-postgres

Le plugin fournit une CLI à commande unique qui affiche les instructions CREATE TABLE et CREATE INDEX pour une collection déclarée, à appliquer avec l’outil de migration utilisé par le projet :

mistral-postgres ddl myapp.search:DOCS_COLLECTION

L’argument module:attribute désigne un PostgresCollectionSchema déclaré. Le module doit être importable, comme pour un fichier Alembic env.py. La sortie provient de la même définition to_table() que celle utilisée par le store. Elle décrit donc la table interrogée par le store. Elle n’inclut pas CREATE EXTENSION ; provisionnez les extensions séparément comme indiqué dans les prérequis.

Utilisez render_ddl(collection) pour générer une DDL équivalente depuis un script.

Recherche dense

Une VectorSearchQuery qui contient seulement un embedding lance le retriever HNSW :

from mistralai.search.toolkit.search import VectorSearchQuery

results = await store.search(
    VectorSearchQuery(embedding=vec, top_k=10)
)

Recherche hybride

Une VectorSearchQuery qui contient à la fois query et embedding exécute les deux retrievers sans configuration supplémentaire :

results = await store.search(
    VectorSearchQuery(query="quarterly revenue", embedding=vec, top_k=10)
)

Chaque table de collection comporte un index BM25 sur content (construit par pg_textsearch). Les deux retrievers sont combinés par fusion réciproque pondérée des rangs :

score = w_vector / (k + rank_vector) + w_text / (k + rank_text)

Ajustez la fusion pour chaque requête avec PostgresSearchQuery. Cette classe est obsolète depuis la version 0.1.0 et émet un DeprecationWarning. Pour tout nouveau code, appelez store.retrieve(RRFRetriever(retrievers=[...], weights=[...], rank_constant=...)) depuis mistralai.search.toolkit.plugins.postgres.retriever :

from mistralai.search.toolkit.plugins.postgres import PostgresSearchQuery

results = await store.search(
    PostgresSearchQuery(
        query="quarterly revenue",
        embedding=vec,
        vector_weight=1.0,
        text_weight=2.0,  # favour the lexical half
        rrf_k=60,  # lower sharpens the top of each list
    )
)
ChampTypePar défautObjectif
vector_weightfloat1.0Poids de la contribution du retriever vectoriel au classement fusionné
text_weightfloat1.0Poids de la contribution du retriever BM25
rrf_kint4Constante de lissage de la fusion réciproque des rangs ; une valeur plus basse renforce le haut de chaque liste

Seul le rapport entre les poids affecte le classement. 1.0/1.0 et 0.5/0.5 sont donc équivalents. Choisissez les poids selon votre corpus. Vous pouvez définir text_weight pour chaque requête.

Paramètres de recherche

La recherche prend aussi en charge exclude_ids, exclude_source_ids et approximate_options=ApproximateQueryOptions(max_candidates=...) (depuis mistralai.search.toolkit.search). Le backend associe max_candidates à hnsw.ef_search pour chaque requête et accepte les valeurs de 1 à 1000. Si la valeur est inférieure à top_k, le backend la porte à top_k.

Retrieve et filtres

Retrieve et filtres

Les pages Retrievers et Filtres décrivent le fonctionnement de retrieve sur un Mistral Search Index. PostgresStoreIndex prend en charge la même méthode, avec son propre arbre de retrievers ; les différences sont décrites dans cette section. Les mêmes filtres s'appliquent également à search via VectorSearchQuery.filter.

Récupérateurs

Importez les récupérateurs et les filtres depuis mistralai.search.toolkit.plugins.postgres.retriever, et non depuis le module de l'index de recherche Mistral.

RécupérateurSpécificités Postgres
NearestNeighbourRetrieverNécessite query_embedding : le store ne vectorise pas les requêtes. Une collection keyword-only le rejette.
KeywordRetrieverClasse la colonne couverte par l'index BM25 de la collection (content, sauf si la collection définit bm25_column). field ne peut désigner que cette colonne.
RRFRetrieverAccepte un nombre quelconque de retrievers enfants. top_k est plafonné à 1000.
from mistralai.search.toolkit.plugins.postgres.retriever import (
    Equal,
    KeywordRetriever,
    NearestNeighbourRetriever,
    RRFRetriever,
)

question = "termination clauses"
embedding = await embedder.embed_query(question)

results = await store.retrieve(
    RRFRetriever(
        top_k=10,
        filter=Equal(field="section", value="contracts"),
        retrievers=[
            KeywordRetriever(query=question, top_k=20),
            NearestNeighbourRetriever(query_embedding=embedding, top_k=20),
        ],
        weights=[2.0, 1.0],
    )
)

Contrairement à search en environnement hybride, retrieve n'a pas de repli sur les résultats vectoriels : si l'index BM25 est absent ou inutilisable, un récupérateur qui inclut un KeywordRetriever lève une SearchError.

Champs filtrables

Vous pouvez filtrer sur n'importe quelle colonne scalaire de la table de la collection. Cela inclut les colonnes standard du fragment, telles que id, source_id et chunk_type, ainsi que vos colonnes personnalisées. Vous ne pouvez pas filtrer sur les colonnes de type tableau et JSON. Avant d'envoyer la requête, le store vérifie que le type de chaque valeur correspond à celui de la colonne. Il lève une SearchError en cas de champ inconnu ou de type incompatible.

Un nom de champ en notation pointée, tel que metadata.claim.field, est un chemin dans les métadonnées du fragment. Equal et In fonctionnent sur les chemins de métadonnées ; Range non.

Un And vide correspond à tous les fragments, et un Or vide ne correspond à aucun d'entre eux.

Comportements de repli et limites

Comportements de repli et limites

Lorsque l’index lexical n’est pas utilisable

Lors de la première requête hybride, le store vérifie si le classement lexical est disponible. Si l’index BM25 est absent ou INVALID, ou si pg_textsearch n’est pas installé, le store renvoie les résultats vectoriels au lieu d’échouer. Chaque requête dégradée écrit un log au niveau ERROR avec le nom de la collection et le correctif suggéré. PostgresStoreIndex.lexical_ranking_resolved renvoie True, False ou None avant la première requête hybride. Le résultat est mis en cache pendant toute la durée de vie du processus.

Aucun filtrage dérivé du contexte

IngestContext et RetrievalContext sont acceptés par chaque point d’entrée, mais le backend les propage uniquement. Le backend Postgres ne déduit aucun périmètre de requête de ces contextes. Contrairement au backend Vespa, il n’applique pas group_id ou yql_filter pour isoler les tenants. Chaque requête peut voir l’ensemble de la collection. Isolez chaque tenant dans une collection et une table distinctes, ou appliquez le filtrage avant le store.

Versions de pgvector

Nous recommandons l’extension pgvector 0.8 ou ultérieure. Les versions antérieures prennent en charge toutes les fonctionnalités, sauf les ensembles de résultats exclude_ids complets. HNSW applique les filtres après la récupération, les lignes exclues ne sont donc pas remplacées et une recherche filtrée peut renvoyer moins de résultats que top_k. La version 0.8 a ajouté hnsw.iterative_scan, que le store active pour ces requêtes. Sur un serveur antérieur, le store écrit un avertissement et peut renvoyer moins de résultats au lieu d’échouer.

Versions de pg_textsearch

Nous recommandons pg_textsearch 1.5 ou une version ultérieure. Les versions antérieures interrompent un parcours ordonné de l’index BM25 après 100 000 entrées. Une requête par mots-clés ou en environnement hybride sur une grande collection peut donc renvoyer moins de top_k lignes, voire aucune, sans erreur. Le store enregistre un avertissement lors de sa première requête par mots-clés ou en environnement hybride sur un serveur utilisant une version antérieure.

Voir aussi

Voir aussi

  • Modèle d’embedding : l’argument embedding_model de PostgresCollectionSchema.
  • Modèle de document : les types Document / DocumentChunk à partir desquels une collection est construite.
  • Index de recherche : comment le backend Postgres s’intègre à l’index de recherche.
  • Vespa : un autre backend de recherche, pour le cas où vous auriez besoin de clustering et de réplication.
  • Index de recherche Mistral : le backend hébergé, pour le cas où vous ne souhaiteriez pas administrer vous-même une base de données.