Gérer les bases via l'API
Vous pouvez tout piloter par API : créer des bases, y ajouter des documents et des extraits
(« chunks »), et faire de la recherche directe (sans modèle). La même clé sk- que pour les
appels au modèle authentifie ces requêtes.
export AUTH="Authorization: Bearer $MIRACA_API_KEY"export BASE="https://api.miraca.fr/v1/kb"Bases de connaissance
Section intitulée « Bases de connaissance »| Méthode | Route | Effet |
|---|---|---|
GET | /v1/kb | Lister vos bases |
POST | /v1/kb | Créer une base |
GET | /v1/kb/{slug} | Détail d’une base |
PATCH | /v1/kb/{slug} | Modifier (nom, slug, intention, facettes, type, découpage par défaut ; pas le modèle d’embedding) |
DELETE | /v1/kb/{slug} | Supprimer (+ documents + chunks) |
GET | /v1/kb/{slug}/export | Exporter la base entière (bundle .kb.json.gz) |
POST | /v1/kb/import | Importer un bundle dans une base neuve |
# Créer une base, avec un schéma de facettes (structure des métadonnées)curl "$BASE" -H "$AUTH" -H "Content-Type: application/json" -d '{ "name": "FAQ Support", "intent": "FAQ : une question/réponse par extrait.", "facet_schema": { "facets": [ { "key": "theme", "values": ["facturation", "securite", "compte"] }, { "key": "montant", "type": "number", "description": "Montant TTC en euros, sans symbole." }, { "key": "resolu", "type": "boolean", "default": "false" } ]}}'# → { "slug": "faq-support", "name": "FAQ Support", "facet_schema": {…}, … }Le slug renvoyé est l’identifiant à utiliser partout ensuite (y compris dans
knowledge_base lors des appels RAG).
Champs d’une facette
Section intitulée « Champs d’une facette »Chaque entrée de facet_schema.facets contraint ce que l’IA doit extraire pour chaque
extrait lors du découpage piloté par l’IA :
| Champ | Type | Rôle |
|---|---|---|
key | string | Requis. Nom de la facette (ex. theme). |
type | string | Forme de la valeur : text (défaut), number, boolean, date (ISO AAAA-MM-JJ), personne, email, url (cf. ci-dessous). Force l’IA à renvoyer un nombre/booléen plutôt qu’un texte approximatif. |
description | string | (optionnel) Intention : décrit en clair ce que l’IA doit mettre dans cette facette. |
values | string[] | (optionnel, type text uniquement) Valeurs autorisées → l’IA est contrainte à cet enum. |
multiple | boolean | (optionnel, types text, personne, email, url) true = plusieurs valeurs par document (ex. plusieurs thématiques, les participants d’une réunion). La valeur est alors stockée en liste, même d’un seul élément. |
default | string | (optionnel) Valeur de repli si l’IA ne sait pas trancher (coercée selon le type). |
Une facette sans type est traitée comme text (rétrocompatible). Les valeurs sont
ensuite filtrables à la recherche (knowledge_base_facets).
Un schéma incohérent est refusé (400) : une clé déclarée deux fois, ou une valeur par
défaut absente des valeurs autorisées ou impossible à convertir dans le type.
Personnes, adresses e-mail et liens
Section intitulée « Personnes, adresses e-mail et liens »Trois types donnent une forme à une valeur texte : personne, email et url. Pour tout ce
qui filtre, groupe, agrège ou liste les valeurs, ils se comportent exactement comme du
texte : la valeur stockée est une chaîne, cherchée avec les mêmes opérateurs (eq, neq, in,
contains…). Ils ne diffèrent qu’à l’écriture (validation et forme canonique) et à
l’affichage au portail.
{ "facets": [ { "key": "participants", "type": "personne", "multiple": true }, { "key": "contact", "type": "email" }, { "key": "visio", "type": "url" }]}| Type | Accepté à l’écriture | Forme stockée |
|---|---|---|
personne | Camille Ferrand <Camille@Asso.org>, "Ferrand, Camille" <c@asso.org>, camille@asso.org, mailto:camille@asso.org, ou le nom seul | Camille Ferrand <camille@asso.org> : le nom, puis l’adresse entre chevrons ; ou l’adresse seule ; ou le nom seul |
email | une adresse, éventuellement entre chevrons ou préfixée de mailto: | l’adresse, en minuscules |
url | un lien http ou https ; sans schéma, https:// est ajouté | le lien complet (https://meet.google.com/abc-defg-hij) |
-
Validation : une valeur illisible dans son type est refusée (
400) quand vous la posez (facet_overrides, ajout ou modification de document), avec un message qui dit ce qui est attendu. Une adresse invalide entre chevrons (Paul <pas-une-adresse>) n’est pas une personne. Un lienjavascript:oufile:n’est jamais accepté. Une valeur produite par l’IA lors du découpage qui ne se lit pas est écartée (le document, lui, est gardé). -
Forme canonique : l’adresse passe en minuscules, les espaces du nom sont aplatis, les guillemets retirés. Une même personne est donc toujours la même valeur, ce qui fait fonctionner l’égalité, le groupement et le comptage. Les filtres d’égalité (
eq,neq,in,not_in) mettent leur valeur dans la même forme avant de comparer. -
Chercher une personne par son adresse : la valeur stockée contient le nom, donc une égalité sur l’adresse seule ne la trouve pas. Utilisez
contains:{ "filters": [{ "key": "participants", "op": "contains", "value": "camille@asso.org" }] } -
Découverte (
/facets) : ces facettes rendent leur vraitype(personne,email,url) avec la même forme que le texte (values,distinct_count,truncated). Un client qui ne connaît pas ces types peut les traiter comme du texte. -
Pas d’énumération :
valuesest réservé au typetext. La valeur par défaut (default), elle, doit se lire dans le type, sinon le schéma est refusé.
Facettes à plusieurs valeurs (multiple)
Section intitulée « Facettes à plusieurs valeurs (multiple) »{ "key": "themes", "values": ["climat", "emploi", "sante"], "multiple": true }- Écriture :
facet_overridesaccepte une liste ({"themes": ["climat", "emploi"]}) ou une valeur seule, rangée en liste d’un élément. Chaque élément doit appartenir àvaluess’il est déclaré. Une liste sur une facette à valeur unique est refusée (400) : déclarezmultiple: true. - Filtres : l’égalité (
facets,eq,in,contains) trouve les documents qui portent au moins la valeur ;neqetnot_inceux qui n’en portent aucune. Une liste passée dansfacetsse lit « porte toutes ces valeurs ». - Découverte (
/facets) : chaque valeur est comptée à part, au nombre de documents. mode: "query"+group_by: un document compte dans le groupe de chacune de ses valeurs. La somme des groupes dépasse donc le total, et la réponse le signale parmulti_valued_groups.- État courant par entité (
group_by: {facet, latest}) : refusé sur une facette multiple, qui ne désigne pas une entité.
Faire évoluer le schéma d’une base existante
Section intitulée « Faire évoluer le schéma d’une base existante »Modifier facet_schema (PATCH /v1/kb/{slug}) relit les valeurs déjà posées sur les documents
pour chaque facette dont la contrainte change : type, valeurs autorisées, valeur unique ou
multiple. L’intention et la valeur par défaut se modifient librement.
- Ce qui se convertit sans perte est converti automatiquement :
"12"devient12quand la facette passe ennumber, une valeur devient une liste d’un élément quand elle passe enmultiple, une liste d’un élément redevient une valeur quand elle repasse en valeur unique. - Ce qui ne passe plus bloque la modification : réponse
409, rien n’est modifié, etconflictsliste chaque valeur en cause avec le nombre de documents qui la portent. - Retirer une facette du schéma ne bloque jamais : ses valeurs deviennent une facette libre, toujours filtrable.
{ "error": { "message": "Ce changement de schéma laisserait des documents avec une valeur que la base n'accepte plus : …" }, "conflicts": [ { "key": "statut", "value": "en cours", "documents": 12, "reason": "valeur \"en cours\" non autorisée. Valeurs acceptées : \"a_faire\", \"fait\"." } ]}Renvoyez alors la même modification avec facet_value_migrations : pour chaque valeur, où la
mettre (to), ou null pour la retirer. C’est aussi la façon de renommer une valeur.
curl -X PATCH "$BASE/suivi" -H "$AUTH" -H "Content-Type: application/json" -d '{ "facet_schema": { "facets": [ { "key": "statut", "values": ["a_faire", "en_cours", "fait"] } ] }, "facet_value_migrations": [ { "key": "statut", "from": "en cours", "to": "en_cours" }, { "key": "statut", "from": "abandonné", "to": null } ]}'# → { "slug": "suivi", …, "facet_migration": [ { "key": "statut", "from": "en cours", "to": "en_cours", "documents": 12 }, … ] }La réécriture porte sur les extraits et sur les facettes mémorisées du document (celles que
relit un re-découpage), dans la même transaction que le schéma. Sur une facette multiple, une
correspondance s’applique à chaque élément de la liste. facet_migration rend ce qui a été
réécrit, conversions automatiques comprises. Cette réécriture ne déclenche pas les tâches qui
surveillent la base : aucune fiche n’a bougé, seule l’étiquette d’une valeur a changé.
Type de base (role)
Section intitulée « Type de base (role) »À la création (POST /v1/kb) ou en modification (PATCH), une base a un role :
| Valeur | Usage |
|---|---|
data (défaut) | Base de données classique : des faits que l’IA restitue. |
process | Base de procédures (playbooks) : chaque document décrit, étape par étape, comment mener une analyse (quelles recherches lancer, quelles facettes filtrer, quoi vérifier). |
En mode agentique, quand une base process
est sélectionnée dans knowledge_base, les fiches les plus proches de la demande sont
retrouvées automatiquement et fournies à l’IA avant qu’elle ne commence : elle ne
dépense pas d’étape à les chercher. Elle suit ensuite leurs étapes sur les bases de
données, et se replie sur une démarche autonome si aucune fiche ne correspond. La liste
complète des fiches lui reste présentée — elle peut en ouvrir une autre. Voir le guide RAG
pour un exemple complet.
# Créer une base de procédurescurl "$BASE" -H "$AUTH" -H "Content-Type: application/json" -d '{ "name": "Procédures — Fonds Impact", "role": "process", "intent": "Playbooks : comment mener nos analyses récurrentes."}'# → { "slug": "procedures-fonds-impact", "role": "process", … }Le role est renvoyé par GET /v1/kb et GET /v1/kb/{slug}. Une base sans role
explicite reste data.
Modèle d’embedding de la base (embed_model)
Section intitulée « Modèle d’embedding de la base (embed_model) »Chaque base est vectorisée par un seul modèle d’embedding, choisi à la création :
tous les documents ajoutés, et toutes les questions posées à la base, passent par lui. Il
se règle par le champ embed_model de POST /v1/kb, avec l’identifiant d’un modèle
d’embedding du catalogue qui rend 1024 dimensions, la
dimension des bases.
| Valeur | Modèle | Hébergeur | Prix |
|---|---|---|---|
bge-embed (défaut) | BGE-M3 | OVHcloud, Gravelines | 0,012 €/M |
mistral-embed | Mistral Embed | Mistral, endpoint UE | 0,132 €/M |
qwen-embed | Qwen3 Embed | Scaleway, Paris | 0,120 €/M |
Les trois acceptent jusqu’à 8 192 tokens par texte (mesuré chez chaque fournisseur ;
qwen-embed monte à 32 768), très au-dessus du plafond de 6 000 caractères d’un extrait.
Le tokenizer de BGE-M3 est plus économe sur le français que celui de Mistral : le même
texte de 27 500 caractères compte 5 753 tokens chez l’un, 7 753 chez l’autre, et c’est le
compte de tokens qui est facturé. Deux modèles ne classent pas les résultats à
l’identique, mais sur nos bases ils se valent : mesuré le 10 septembre 2026 sur cinq
bases et 400 recherches par modèle, le bon document remonte en tête dans 67 % des cas
pour l’un comme pour l’autre, et dans le top 10 dans 95 à 96 % des cas ; les désaccords
sont symétriques (38 questions où seul Mistral trouve, 36 où seul BGE trouve). Le défaut
a changé ce jour-là (mistral-embed auparavant) ; les bases créées avant restent sur leur
modèle.
La liste vivante est celle de GET /v1/pricing : les modèles de mode: "embedding" dont
dimensions vaut 1024. codestral-embed (1536 dimensions) et les modèles de chat sont
refusés avec un 400 qui nomme les modèles admissibles ; rien n’est créé.
# Créer une base vectorisée par Mistral Embed plutôt que par le défautcurl "$BASE" -H "$AUTH" -H "Content-Type: application/json" -d '{ "name": "Comptes rendus 2026", "embed_model": "mistral-embed"}'# → { "slug": "comptes-rendus-2026", "embed_model": "mistral-embed", … }Le modèle est renvoyé par GET /v1/kb et GET /v1/kb/{slug} (embed_model). Au portail,
il se choisit dans la modale « Nouvelle base » (section Vectorisation) ; dans Make et
Zapier, c’est le champ Embedding model du module Create a Knowledge Base.
Découpage par défaut de la base
Section intitulée « Découpage par défaut de la base »Chaque base porte ses propres réglages de découpage par défaut. Toute ingestion qui ne précise rien les suit : vous réglez le découpage une fois sur la base, au lieu de le répéter à chaque ajout de document.
C’est utile parce que toutes les bases ne se découpent pas de la même façon. Une base de fiches atomiques (CRM, factures, contacts) gagne à garder un document par extrait, alors qu’une base de longs rapports gagne à être découpée en paragraphes.
| Champ | Valeurs | Défaut |
|---|---|---|
default_chunking | "simple" · "ai" · "whole" | "simple" |
default_chunk_size | Entier 200-6000, ou null | null (défaut produit : 800) |
default_chunk_model | Identifiant de modèle de chat, ou null | null (défaut produit : mistral-small) |
Les trois modes de découpage
Section intitulée « Les trois modes de découpage »| Mode | Ce qu’il fait |
|---|---|
simple (défaut) | Regroupe les paragraphes jusqu’à atteindre une taille cible (chunk_size). Déterministe et gratuit. |
ai | Un modèle (chunk_model) réémet le document en extraits, guidés par l’intent de la base et son schéma de facettes. Facturé (un appel modèle par document). |
whole | Un document = un extrait. Le texte n’est recoupé que s’il dépasse le plafond technique d’embedding (6000 caractères). Gratuit. |
Le mode whole existe pour les bases dont chaque document est déjà une unité de sens
complète : une fiche CRM, une facture, un playbook. Le découper en morceaux disperse
l’information entre plusieurs extraits qui, pris isolément, perdent leur contexte. Avant, il
fallait tenter de forcer ce comportement avec une valeur de chunk_size très élevée, ce qui
ne fonctionnait pas (chunk_size est borné à 6000). whole est le réglage explicite qui
répond à ce besoin.
Règle de priorité
Section intitulée « Règle de priorité »Pour chaque ingestion, le réglage effectif se résout dans cet ordre :
- le paramètre de l’appel (
chunking,chunk_size,chunk_model), s’il est fourni ; - sinon, le défaut de la base (
default_chunking,default_chunk_size,default_chunk_model) ; - sinon, le défaut produit (
simple, 800 caractères,mistral-small).
Un paramètre passé à l’appel prime donc toujours. Régler un défaut de base ne vous enferme pas : vous gardez la main document par document.
# Créer une base dont chaque document reste entier (une fiche = un extrait)curl "$BASE" -H "$AUTH" -H "Content-Type: application/json" -d '{ "name": "CRM", "intent": "Une fiche entreprise, contact ou opportunité par document.", "default_chunking": "whole"}'# → { "slug": "crm", "default_chunking": "whole", "default_chunk_size": null,# "default_chunk_model": null, … }
# Changer les défauts d'une base existante (découpage simple, extraits plus longs)curl -X PATCH "$BASE/rapports" -H "$AUTH" -H "Content-Type: application/json" -d '{ "default_chunking": "simple", "default_chunk_size": 1500}'
# Ingestion SANS paramètre de découpage → la base décidecurl "$BASE/crm/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{ "title": "GreenLoop", "text": "…", "external_id": "acc-greenloop"}'# → découpé en "whole" (le défaut de la base « crm »)
# Ingestion AVEC paramètre → l'appel prime, pour ce document seulementcurl "$BASE/crm/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{ "title": "Rapport annuel 2025", "text": "…", "chunking": "simple", "chunk_size": 1000}'Ces trois champs sont acceptés à la création (POST /v1/kb) comme en modification
(PATCH /v1/kb/{slug}), et renvoyés par GET /v1/kb et GET /v1/kb/{slug}. Les bases
créées avant cette fonctionnalité se comportent comme si elles portaient les défauts produit.
Les défauts de la base s’appliquent à toutes les surfaces d’ingestion : ajout de document par API ou depuis le portail, re-découpage d’un document ou de la base entière, outils d’écriture MCP, et synchronisation d’un connecteur.
Documents
Section intitulée « Documents »| Méthode | Route | Effet |
|---|---|---|
GET | /v1/kb/{slug}/documents | Lister les documents |
POST | /v1/kb/{slug}/documents | Ajouter un document — texte (JSON) ou fichier (multipart) |
GET | /v1/kb/{slug}/documents/{id} | Détail |
PATCH | /v1/kb/{slug}/documents/{id} | Modifier (filename, date, external_id, source_url) |
GET | /v1/kb/{slug}/documents/{id}/content | Lire le texte source (déchiffré) + état des chunks |
PUT | /v1/kb/{slug}/documents/{id}/content | Remplacer le texte source (sans re-découper) |
POST | /v1/kb/{slug}/documents/{id}/rechunk | Re-découper un document depuis sa source (régénère les chunks) |
POST | /v1/kb/{slug}/rechunk | Re-découper toute la base (job de fond, asynchrone) |
GET | /v1/kb/{slug}/rechunk | État du dernier re-découpage de la base |
DELETE | /v1/kb/{slug}/documents/{id} | Supprimer (+ ses chunks) |
{id} accepte l’UUID renvoyé ou votre external_id (voir plus bas).
# Ajouter du texte : il est découpé, vectorisé puis chiffrécurl "$BASE/faq-support/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{ "title": "Crédits", "text": "Les crédits se rechargent dans le portail, par carte via Stripe.", "date": "2024-03-01", "external_id": "doc-credits", "source_url": "https://docs.miraca.fr/credits"}'# → { "id": "…", "external_id": "doc-credits", "filename": "Crédits",# "doc_date": "2024-03-01T00:00:00.000Z", "source_url": "https://docs.miraca.fr/credits",# "status": "ready", "chunk_count": 1 }Identifiant client (external_id) et upsert
Section intitulée « Identifiant client (external_id) et upsert »Plutôt que de retenir l’UUID interne, attachez votre propre external_id (unique par base)
à un document. Vous pouvez alors l’adresser directement (GET/PATCH/DELETE /v1/kb/{slug}/documents/{external_id}), et ré-ingérer le même document de façon idempotente :
# Même external_id → le document (et ses chunks) est REMPLACÉ (upsert)curl "$BASE/faq-support/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{ "text": "Texte mis à jour…", "external_id": "doc-credits"}'# Puis : récupérer / modifier / supprimer par cet identifiantcurl "$BASE/faq-support/documents/doc-credits" -H "$AUTH"Date métier (date) et récence
Section intitulée « Date métier (date) et récence »Le date est la date du contenu (≠ created_at, la date d’insertion en base). Stockée comme
doc_date ; les chunks du document en héritent (chunk_date) sauf date explicite. Format
YYYY-MM-DD, ou un instant ISO 8601 complet (2026-03-15T09:30:00+01:00) : la fraction de seconde
s’écrit avec un point (09:30:00.5, jamais une virgule) et le décalage va de -15:59 à +15:59.
Exploitable à la
recherche via date_range (voir plus bas) : un filtre par plage de dates, là où les
facettes ne permettent qu’une égalité.
Modifier la date d’un document (PATCH …/documents/{id} avec date) la reporte sur ses chunks
qui portaient l’ancienne ; un chunk daté à part garde la sienne. Un re-découpage redonne à tous les
chunks la date du document.
Lien source (source_url)
Section intitulée « Lien source (source_url) »Attachez à un document un lien vers la ressource d’origine (page web, fichier dans un drive,
ticket…) via source_url. C’est purement informatif : stocké en clair, jamais vectorisé, et
remonté tel quel à la recherche directe et dans les
sources RAG — pour que votre process puisse
retrouver la ressource d’origine d’un extrait. Disponible à l’ajout (texte ou fichier) et
modifiable par PATCH (chaîne vide ou null pour le retirer). ⚠️ Seul un lien http(s) est accepté : un schéma différent (javascript:, data:…) est silencieusement remplacé par null avant stockage, garde posée après un constat d’audit (XSS stockée exploitable au clic sur un document cité).
Contenu source et re-découpage
Section intitulée « Contenu source et re-découpage »Chaque document conserve son texte source canonique (chiffré, comme les chunks). Les chunks en sont une projection régénérable : vous pouvez relire la source, la modifier, puis re-découper pour régénérer les chunks et leurs embeddings.
# Lire le texte source (déchiffré) + l'état des chunkscurl "$BASE/faq-support/documents/doc-credits/content" -H "$AUTH"# → { "id": "…", "filename": "Crédits", "external_id": null, "source_url": null, "doc_date": null, "has_source": true, "source_format": "markdown",# "source_origin": "original", "content": "Les crédits se rechargent…",# "chunk_count": 1, "chunks_stale": false }
# Remplacer le texte source (n'écrit PAS de chunks → ils deviennent « périmés »)curl -X PUT "$BASE/faq-support/documents/doc-credits/content" -H "$AUTH" -H "Content-Type: application/json" -d '{ "content": "Les crédits se rechargent dans le portail (carte via Stripe) ou par virement.", "format": "markdown"}'# → { …, "chunks_stale": true } ← la source a changé, re-découpez pour régénérer
# Variante « tout-en-un » : remplacer la source ET re-découper dans le même appelcurl -X PUT "$BASE/faq-support/documents/doc-credits/content" -H "$AUTH" -H "Content-Type: application/json" -d '{ "content": "Les crédits se rechargent dans le portail (carte via Stripe) ou par virement.", "rechunk": "simple"}'# → { …, "chunks_stale": false } ← source écrite + chunks régénérés en une requête
# Re-découper depuis la source : régénère les chunks (simple = gratuit ; ai = facturé)# `chunk_size` (200-6000, défaut 800) règle la taille cible des extraits du découpage simple.curl -X POST "$BASE/faq-support/documents/doc-credits/rechunk" -H "$AUTH" -H "Content-Type: application/json" -d '{ "chunking": "simple", "chunk_size": 1200}'# → le document (ApiDocument), "chunks_stale": falsesource_origin:original(source fidèle, captée à l’ingestion ou éditée) oureconstructed(source recollée depuis les chunks pour un document antérieur — pour un document découpé en IA, ce n’est pas l’original).chunks_stale:truequand la source a été modifiée depuis le dernier découpage — exposé aussi sur le détail du document (GET …/documents/{id}). Re-découpez pour le remettre à jour.POST …/rechunkacceptechunking("simple","ai"+chunk_model, ou"whole"),chunk_size(200-6000, défaut 800, taille cible des extraits du découpagesimple),facet_overrides(remplace ceux mémorisés à l’ingestion),bypass_conformite. Il remplace TOUS les chunks du document, y compris ceux ajoutés ou édités à la main.chunking_notice: présent dans la réponse d’un ajout, d’unPOST …/rechunkou d’unPUT …/contentavecrechunk, en modeai, quand des passages rendus par le modèle ont été écartés, ou quand son plan a été remplacé par le découpagesimple(voir les trois modes de découpage). Re-découper un document en modeaiest aussi la façon de corriger un document découpé avant cette vérification : ses extraits sont régénérés depuis le texte source.PUT …/contentaccepte une optionrechunk("simple","ai","whole", outruepour suivre le défaut de la base ; +chunk_model,chunk_size,facet_overrides,bypass_conformite) pour écrire la source ET régénérer les chunks en un seul appel (pratique pour l’automatisation). Sans elle, les chunks restent périmés jusqu’auPOST …/rechunk.chunkingetchunk_sizesont aussi acceptés à l’ingestion (POST …/documents, JSON ou champ multipart) pour fixer le découpage du nouveau document.- Sans paramètre de découpage, ces trois appels suivent le découpage par défaut de la base. Un paramètre fourni prime toujours.
Re-découper toute la base
Section intitulée « Re-découper toute la base »POST /v1/kb/{slug}/rechunk re-découpe tous les documents de la base depuis leur texte source.
Comme l’opération peut porter sur des centaines de documents (et, en ai, appeler un modèle par
document), elle s’exécute en job de fond : la requête enfile le travail et répond 202 avec
un job_id et l’état initial. Suivez l’avancée avec GET /v1/kb/{slug}/rechunk.
# Lancer le re-découpage de toute la base (asynchrone)curl -X POST "$BASE/faq-support/rechunk" -H "$AUTH" -H "Content-Type: application/json" -d '{ "chunking": "simple", "chunk_size": 1000}'# → 202 { "job_id": "…", "status": { "state": "running", "chunking": "simple", … } }
# Suivre l'avancée (polling)curl "$BASE/faq-support/rechunk" -H "$AUTH"# → { "status": { "state": "done", "total": 42, "rechunked": 40, "skipped": 2, "errors": 0, … } }- Body :
chunking("simple","ai"+chunk_model, ou"whole"),chunk_size(mode simple). Sanschunking, la base re-découpe selon son réglage par défaut. - Statut : quand des documents ont échoué (
errors> 0),first_errordonne le motif du premier échec (par exemple un modèle de découpage retiré du catalogue de l’organisation). Les documents en échec gardent leur découpage précédent. - Un modèle nommé (
default_chunk_modeld’une base,chunk_modeloufacet_modeld’un appel) doit être disponible pour votre organisation (Gestion → Modèles) : sinon la requête est refusée en400, avant tout appel au modèle. - Les documents sans texte source stocké ne sont pas re-découpables → comptés en
skipped. - Un seul re-découpage à la fois par base : relancer pendant qu’il tourne renvoie
409. - Métré : en
ai, chaque document appelle le modèle ; l’embedding des chunks est facturé (simple comme ai) sur la dernière clé API créée sur le compte (les clés internes de Miraca — assistant, accès MCP d’un membre, copilote — ne sont jamais retenues).
Qualifier les facettes par IA (qualify_facets)
Section intitulée « Qualifier les facettes par IA (qualify_facets) »Le découpage ai renseigne les facettes, mais il réémet le document mot pour mot, donc il
plafonne (cf. l’encadré plus haut). Or ce sont deux besoins différents : découper, c’est
décider où couper ; qualifier, c’est lire un document et dire de quoi il parle. Le second
ne demande aucune réécriture — sa sortie tient en quelques centaines de tokens, quelle que
soit la taille de l’entrée.
qualify_facets: true déclenche une passe courte et séparée : un modèle lit le document
(par échantillon tête / milieu / fin s’il est long) et n’en renvoie que les valeurs de
facettes, appliquées ensuite à tous les extraits. Combinez-la avec chunking: "simple" pour
obtenir des métadonnées qualifiées à n’importe quelle taille de document.
curl "$BASE/comptes-rendus/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{ "title": "Comité budgétaire 12 mars", "text": "…un transcript de 120 000 caractères…", "chunking": "simple", "qualify_facets": true}'| Champ | Effet |
|---|---|
qualify_facets | true = lance la passe de qualification. |
facets_from | Texte court à lire à la place du document (un résumé, un chapeau). Quand vous en disposez, c’est plus fiable qu’un échantillon : il est déjà dense. |
facet_model | Modèle de la passe (facturé sur votre clé). Défaut : le modèle de découpage par défaut. |
- Les
facet_overridesque vous fournissez priment toujours sur les valeurs déduites, et les facettes ainsi fixées ne sont même pas soumises au modèle. - Le modèle omet une facette qu’il ne peut pas trancher plutôt que d’inventer une valeur : une facette fausse filtrerait des recherches, une facette absente non.
- Les valeurs déduites sont mémorisées sur le document, donc réappliquées à un
re-découpage
simpleultérieur. qualify_facetsavecchunking: "ai"est refusé : le découpage IA renseigne déjà les facettes, extrait par extrait.- Sur une base sans schéma de facettes, l’appel est refusé : il n’y aurait rien à renseigner.
Disponible aussi dans la synchronisation d’un connecteur (sync.qualify_facets), pour
qualifier une source dont les enregistrements sont trop longs pour le découpage IA.
Forcer des facettes (facet_overrides)
Section intitulée « Forcer des facettes (facet_overrides) »Passez facet_overrides pour imposer des valeurs de facettes sur tous les chunks du document,
sans laisser l’IA les deviner :
# Découpage simple : catégorie fixée, l'IA n'intervient pascurl "$BASE/faq-support/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{ "title": "CR réunion 15 jan.", "text": "…", "external_id": "cr-2025-01-15", "facet_overrides": { "categorie": "compte-rendu", "client": "Acme" }}'
# Découpage IA : l'IA ne remplit que les facettes non couvertes (ex. "date_reunion")curl "$BASE/faq-support/documents" -H "$AUTH" -H "Content-Type: application/json" -d '{ "title": "CR réunion 15 jan.", "text": "…", "chunking": "ai", "facet_overrides": { "categorie": "compte-rendu" }}'| Comportement | Détail |
|---|---|
Mode simple | Les overrides sont appliqués à chaque chunk, les facettes restantes restent {}. |
Mode ai | L’IA ne voit que les facettes non couvertes (tokens économisés) et sa sortie est fusionnée ; les overrides écrasent toujours ce qu’elle aurait produit. |
| Priorité | Les overrides gagnent sur les valeurs IA et sur les default du schéma. |
| Format | Objet JSON ou chaîne JSON (pour les clients no-code comme Make). Une facette multiple reçoit une liste. |
| Validation | Une valeur hors des values déclarées, ou une liste sur une facette à valeur unique, est refusée en 400 avec le détail. |
Importer un fichier
Section intitulée « Importer un fichier »Le même endpoint accepte un fichier en multipart/form-data (champ file). Le texte est
extrait côté serveur puis découpé, vectorisé et chiffré comme un document texte. Le binaire
n’est pas conservé.
# Importer un PDF avec facettes forcéescurl "$BASE/faq-support/documents" -H "$AUTH" \ -F "file=@guide.pdf" \ -F "title=Guide d'installation" \ -F "source_url=https://drive.exemple.org/guide.pdf" \ -F 'facet_overrides={"categorie":"guide"}' \ -F "bypass_conformite=false"# → { "id": "…", "filename": "Guide d'installation", "source_type": "file", "status": "ready", "chunk_count": 12 }Formats : PDF, DOCX, TXT, MD, RTF, CSV, TSV, HTML (max 15 Mo). Le DOCX et le HTML sont
convertis en Markdown (titres, listes et tableaux préservés), ce qui améliore le découpage
(notamment le découpage IA) et l’extraction de facettes ; le PDF est extrait en texte à plat. Un
.doc (Word legacy) ou un PDF scanné (image, sans couche texte) est refusé (415/422) —
convertissez-le d’abord. Le champ title est optionnel (par défaut : le nom du fichier).
Chunks (extraits)
Section intitulée « Chunks (extraits) »| Méthode | Route | Effet |
|---|---|---|
GET | …/documents/{id}/chunks | Lister les extraits (déchiffrés, avec date et created_at) |
POST | …/documents/{id}/chunks | Ajouter un extrait à la main (content, facets?, date?) |
PATCH | …/chunks/{chunkId} | Modifier (content, facets et/ou date) |
DELETE | …/chunks/{chunkId} | Supprimer |
{id} du document = UUID ou external_id. Le date d’un extrait est optionnel : s’il est
absent, l’extrait hérite de la date du document.
# Ajouter un extrait précis, avec ses facettes et sa date métiercurl "$BASE/faq-support/documents/doc-credits/chunks" -H "$AUTH" -H "Content-Type: application/json" -d '{ "content": "Q : Quels paiements ? R : Carte bancaire et virement SEPA.", "facets": { "theme": "facturation" }, "date": "2024-03-01"}'Modifier le content d’un chunk le re-vectorise (facturé comme un embedding).
Recherche globale (toutes les bases)
Section intitulée « Recherche globale (toutes les bases) »POST /v1/search cherche dans toutes vos bases à la fois (au lieu d’une seule), sans IA, et
renvoie les documents correspondants. Pour aussi chercher dans une source externe (Airtable,
Notion, Pennylane…), synchronisez le connecteur dans une base (cf. Connecteurs API) : ses
enregistrements deviennent des documents et sont couverts par cette même recherche.
Deux régimes, via mode :
relevance(défaut siquery) : classement par pertinence sémantique. L’embedding de la question est facturé. Pagination superficielle (top_k), pas detotal.browse(défaut sansquery) : exploration — facettes + tri + recherche par titre (le textequeryfiltre alors le titre du document), avec pagination profonde (limit/offset) ettotalexact (nombre de documents, pas d’extraits). Aucun embedding facturé. C’est le régime « Algolia interne » : lister, filtrer, trier, paginer un jeu de documents.
# Pertinence : classement sémantiquecurl "https://api.miraca.fr/v1/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "query": "facture impayée Greenloop", "knowledge_base": ["factures", "crm"]}'
:::caution[Un filtre mal formé est REFUSÉ, pas ignoré]`filters` attend une **liste** de `{ key, op, value }` ; `facets` attend un **objet**`{ "cle": "valeur" }` (égalité simple). Intervertir les deux — ou employer un opérateurincompatible avec le type de la facette — renvoie une **erreur `400`** qui nomme le problème.
C'est délibéré : auparavant le filtre était écarté et la recherche répondait `200` avec**tous** les documents. Une réponse complète à une question filtrée est le pire résultatpossible — elle est plausible, et rien ne signale qu'elle répond à autre chose.
Les facettes **non déclarées au schéma** restent filtrables : elles sont acceptées àl'écriture, donc elles doivent l'être à la lecture. En revanche, une clé qu'**aucun**enregistrement de la base ne porte est refusée elle aussi (`400`, avec la liste des facettesréelles) : un filtre sur une facette qui n'existe nulle part ne peut rien attraper, et le`total: 0` qu'il rendrait se lit comme « aucune facture » alors qu'il ne dit rien.:::
# Exploration : facettes + tri + pagination profondecurl "https://api.miraca.fr/v1/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "mode": "browse", "knowledge_base": ["factures"], "filters": [{ "key": "statut", "op": "in", "value": ["impayée"] }], "sort": { "by": "date", "dir": "desc" }, "limit": 25, "offset": 50}'{ "query": "", "mode": "browse", "searched": ["factures"], "total": 312, "offset": 50, "page_size": 25, "has_more": true, "results": [ { "knowledge_base": "factures", "document": "F-2023-6051 — GreenLoop", "external_id": "F-2023-6051", "date": "2023-04-11", "source_url": null, "matched_chunks": 3, "snippet": "…", "score": null } ], "sources": [ { "slug": "factures", "hits": 25 } ]}knowledge_base(slug ou tableau) : bases à interroger ; absent = toutes celles du compte.filters: mêmes filtres de facettes typés que la recherche par base (eq/neq/gt/gte/lt/lte/in/contains), en liste ou en chaîne JSON d’une liste (clients no-code). Toute autre forme (un objet, un texte) renvoie une erreur400, dans les deux régimes.date_range:{ "from"?, "to"? }, bornes enAAAA-MM-JJ. Appliqué dans les deux régimes ; une borne dans un autre format renvoie une erreur400.skipped(réponse, mode relevance) : les bases écartées de cette recherche, chacune avec sa raison, par exemple{ "slug": "crm", "reason": "ne déclare pas la facette « statut »" }. Une base qui ne déclare pas une facette filtrée est écartée, de même qu’une base où le filtre ne peut pas s’appliquer ou qui n’a pas répondu. Le champ est absent quand aucune base n’a été écartée. Une listeresultsvide accompagnée d’unskippedne veut donc pas dire « rien ne correspond ». Un filtre refusé par toutes les bases interrogées renvoie une erreur400, avec le motif.sort(mode browse) :{ "by": "date" | "alpha", "dir": "asc" | "desc" }(alpha= titre). Ignoré en pertinence (classé par score).top_k(mode relevance, défaut 10, max 50) ;limit(mode browse, défaut 25, max 100) +offset(mode browse, pagination profonde).total: nombre exact de documents (mode browse) ;has_moreindique s’il reste des pages.- Compteurs au grain document : les facettes (
/v1/search/facetsci-dessous) comptent des documents, pas des extraits internes. - Facettes cross-bases :
POST /v1/search/facets(facettes filtrables, partagées d’abord) etPOST /v1/search/facet-values({ "key": "...", "query": "..." }→ valeurs contenant le texte, sans tenir compte de la casse ni de la typographie).
Note (chiffrement) : le corps des documents est chiffré au repos → pas de recherche plein-texte par mots-clés sur le contenu. L’exploration recherche par titre/
external_id(en clair), et cette recherche est tolérante aux fautes (« recrutment » retrouve « recrutement »). Pour chercher dans le contenu, utilisez le mode pertinence (sémantique), lui aussi tolérant aux fautes (embeddings).
Recherche directe (sans IA)
Section intitulée « Recherche directe (sans IA) »Interroger une base sans passer par un modèle : utile pour intégrer la recherche dans vos propres workflows. Renvoie les extraits les plus proches, déchiffrés, avec leur score.
curl "$BASE/faq-support/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "query": "comment payer", "facets": { "theme": "facturation" }, "top_k": 5}'{ "group_by": "chunk", "data": [ { "n": 1, "knowledge_base": "faq-support", "document": "Crédits", "document_id": "a1b2c3d4-…", "external_id": "doc-credits", "source_url": "https://docs.miraca.fr/credits", "score": 0.87, "facets": { "theme": "facturation" }, "content": "Q : Quels paiements ? R : Carte bancaire et virement SEPA.", "relation": "semantic" } ]}La réponse contient toujours group_by (granularité utilisée) et data.
Score et ordre des extraits
Section intitulée « Score et ordre des extraits »Avec une query, les extraits sont retrouvés par proximité sémantique, puis les 20 premiers sont
reclassés en lisant la question et les extraits ensemble, ce qui écarte mieux un extrait qui
partage des mots avec la question sans y répondre. Leur score (de 0 à 1) est celui de ce
reclassement. Au-delà de ces 20 premiers, les extraits suivent dans l’ordre de proximité, avec
"score": null : les deux mesures ne sont pas sur la même échelle, et un tri par score les
mélangerait. Le rang n donne l’ordre de toute la liste, et le reclassement porte toujours sur les
mêmes 20 extraits, quelle que soit la page demandée (offset). Si le reclassement est
indisponible, tous les extraits portent leur score de proximité (cosinus, de 0 à 1).
query optionnelle (mode listing)
Section intitulée « query optionnelle (mode listing) »query est facultative. Sans requête, la base bascule en mode listing : pas de recherche
sémantique, pas de score ("score": null), aucun coût d’embedding — uniquement le filtrage
(facets, filters, date_range) et le tri. Idéal pour « tous les documents tels que … » et
pour compter / lister de façon exacte (le listing est déterministe, là où la recherche
sémantique est approximative et plafonnée).
Chaque résultat porte aussi l’external_id du document (votre identifiant client, s’il est
renseigné) à côté de l’UUID interne.
Lookup exact par identifiant client (external_id)
Section intitulée « Lookup exact par identifiant client (external_id) »Quand vous connaissez la référence métier d’un document (n° de facture, id CRM, id de note…
c’est-à-dire votre external_id), ne partez pas en
recherche sémantique approximative : passez external_id pour restreindre la recherche aux
documents correspondants (une chaîne, ou un tableau). Combinable avec les autres paramètres.
# Retrouver un document précis par sa référence, et lire ses extraitscurl "$BASE/factures/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "external_id": "F-2025-089"}'# Plusieurs références d'un coupcurl "$BASE/factures/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "external_id": ["F-2025-089", "F-2025-090"], "group_by": "document"}'Pour lire le contenu entier d’un document dont vous avez l’external_id, vous pouvez aussi
appeler directement GET /v1/kb/{slug}/documents/{external_id} (l’UUID n’est pas nécessaire).
Recherche par titre (title_contains)
Section intitulée « Recherche par titre (title_contains) »Pour retrouver un document par son nom/titre (et non par le sens de son contenu), passez
title_contains : recherche sous-chaîne sur le titre (filename) et l’external_id, tolérante
aux fautes (« recrutment » retrouve « recrutement ») et, pour le titre, à la
casse et à la typographie : un titre importé depuis
macOS, dont les accents sont enregistrés sous une autre forme, se retrouve en le tapant normalement.
Se combine aux filtres, au tri, à la pagination et au total analytique. Le contenu (chiffré)
reste couvert par query (sémantique).
# Documents dont le titre ressemble à « recrutement », comptés au grain documentcurl "$BASE/rh/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "title_contains": "recrutment", "group_by": "document"}'Pagination (offset) et complétude (truncated)
Section intitulée « Pagination (offset) et complétude (truncated) »top_k est plafonné à 50. En régime analytique (sans query), le défaut est d’ailleurs
50 (on veut généralement tout l’ensemble, pas un top-k). Pour parcourir un ensemble plus grand
de façon exhaustive, utilisez offset (mode listing surtout) : offset: 0, puis 50, 100…
jusqu’à recevoir moins de top_k résultats.
La réponse porte truncated: true quand la page ne contient pas tout l’ensemble filtré
(total > nombre de lignes renvoyées) : ne concluez jamais « tous les X » sur une page tronquée
— paginez (ou lisez total pour un simple comptage). Pour lister les valeurs distinctes d’une
facette sous condition (toutes les entités concernées), GET …/facets avec le filtre en scope les
renvoie toutes d’un coup, sans pagination.
Granularité : extraits, documents ou dates (group_by)
Section intitulée « Granularité : extraits, documents ou dates (group_by) »"chunk"(défaut) : un résultat par extrait."document": les extraits d’un même document sont regroupés (le document n’apparaît qu’une fois) et classés par score moyen des extraits trouvés. Chaque résultat portedocument_id,matched_chunks(nombre d’extraits trouvés) etchunks(les extraits). C’est la granularité pour COMPTER ou LISTER des enregistrements (réunions, factures, dossiers… — un enregistrement = un document). En régime analytique (sansquery), la réponse portetotal= le compte exact de documents correspondant aux filtres, calculé côté serveur indépendamment detop_k/offset: pour « combien », liseztotal(inutile de paginer).datareste plafonné àtop_k(la page de résultats)."date": un résultat par date métier distincte (chunk_date), pour les questions par jour / chronologiques (combien de jours d’activité, occurrences par date). ⚠️ Plusieurs documents peuvent tomber le même jour :data.length= nombre de dates, ce qui sous-compte les enregistrements. La réponse expose donc, au niveau racine,distinct_documents(= nombre d’enregistrements, ≥ nombre de dates) et, par jour,documents(documents distincts ce jour-là), plustotal_dates(nombre de jours, analytique). Pour « combien de rendez-vous avec X en 2025 », utilisezgroup_by: "document"et liseztotal(oudistinct_documentsen modedate), jamais le nombre de dates.
curl "$BASE/faq-support/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "query": "comment payer", "group_by": "document", "top_k": 5}'{ "group_by": "document", "data": [ { "n": 1, "knowledge_base": "faq-support", "document": "Crédits", "document_id": "a1b2c3d4-…", "score": 0.86, "matched_chunks": 2, "chunks": [ { "n": 1, "content": "…", "score": 0.87, "facets": {} } ] } ]}En group_by: "date", la réponse porte distinct_documents (le nombre d’enregistrements) à
côté des dates — ici 12 jours mais 13 réunions, car deux ont eu lieu le même jour :
{ "group_by": "date", "total_dates": 12, "distinct_documents": 13, "data": [ { "n": 1, "date": "2025-05-21", "documents": 2, "matched_chunks": 8, "facets": { … }, "chunks": [ … ] } ]}État courant par entité (group_by: { facet, latest } + having)
Section intitulée « État courant par entité (group_by: { facet, latest } + having) »Pour les questions du type « combien de X sont actuellement Y » ou « le dernier état de
chaque X » (X = une entité : startup, client, fournisseur… ; Y = un état : en alerte, en retard,
churné…), passez un group_by objet : la base réduit à un résultat par entité, ramené à son
enregistrement le plus récent. La condition d’état courant se pose avec having (≠ filters,
voir l’encart) :
curl "$BASE/suivi-participations/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "group_by": { "facet": "startup", "latest": true }, "having": { "key": "alerte", "op": "eq", "value": true }}'{ "group_by": "entity", "entity_field": "startup", "total": 11, "data": [ { "entity": "AgriBoost", "date": "2025-06-02", "facets": { "startup": "AgriBoost", "statut": "…" } } ]}total= nombre d’entités distinctes dont le dernier enregistrement satisfaithaving— exact, indépendant detop_k.aggregatese combine :aggregate: "mrr_keur"donne alors la valeur d’état courant agrégée (ex. le MRR moyen actuel du portefeuille = moyenne du dernier MRR de chaque startup).
Filtres typés sur les facettes (filters)
Section intitulée « Filtres typés sur les facettes (filters) »En plus de facets (égalité simple), filters permet des comparaisons sur les valeurs de
facettes, validées contre le type déclaré de chaque facette (cf. facet_schema) :
curl "$BASE/factures/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "group_by": "document", "filters": [ { "key": "montant", "op": "gte", "value": 1000 }, { "key": "echeance", "op": "lt", "value": "2026-06-15" }, { "key": "categorie", "op": "in", "value": ["A", "B"] } ], "sort": { "by": "date", "dir": "desc" }}'Opérateurs : eq, neq, gt, gte, lt, lte, in, contains. Les opérateurs disponibles
dépendent du type de la facette — nombre/date : comparaisons ; texte : eq/neq/in/
contains ; booléen : eq/neq/in. Un filtre incohérent (ex. > sur un booléen) ou une valeur
qui ne se lit pas dans le type de la facette est refusé (erreur 400 qui nomme le problème),
jamais ignoré. Les dates sont au format ISO AAAA-MM-JJ, et doivent exister au calendrier.
Nombre écrit en chaîne. Une valeur numérique peut être passée en nombre JSON (1250) ou en
chaîne. En chaîne, la convention est française : la virgule est décimale ("1,250" vaut
1,25, jamais mille deux cent cinquante) et une espace sépare les milliers ("1 250" vaut
1250, que l’espace soit ordinaire ou insécable). Une chaîne vide est refusée : elle n’est plus
lue comme zéro. Pour éviter toute ambiguïté, préférez un nombre JSON.
Nom de la facette. La clé d’un filtre (key), d’un facets, d’un group_by, d’un aggregate
ou d’un having est rapprochée de celles du schéma : "statut" désigne la facette déclarée
Statut. Si deux facettes du schéma ne diffèrent que par la casse ou la typographie, la demande
est refusée et les nomme : il faut alors écrire la clé exacte.
Comparaison d’un texte : casse et typographie
Section intitulée « Comparaison d’un texte : casse et typographie »Pour les facettes texte, l’égalité (eq/neq/in), contains, le filtre rapide facets
(une valeur ou une liste) et la recherche de valeurs de facette comparent deux textes sans
tenir compte :
- de la casse :
{"type_rdv": "Gestion de crise"}correspond à la valeur stockéegestion de crise; - de la forme des accents (composée ou décomposée, comme dans les noms de fichiers de macOS) ;
- des espaces : insécables ou ordinaires, doublées, en début ou en fin ;
- de la forme des apostrophes, des guillemets et des tirets (droits ou typographiques) ;
- des caractères invisibles (espace de largeur nulle, trait d’union conditionnel).
Ce n’est pas une recherche approximative : une lettre, un chiffre ou un accent de différence
donnent deux valeurs distinctes (été n’est pas ete, Cœur n’est pas Coeur). Les facettes
nombre, booléen et date restent en comparaison exacte.
À l’écriture par l’API (documents, extraits, synchronisation d’un connecteur, découpage par IA), une valeur de facette texte est rangée dans une forme stable : accents composés, espaces repliées, bords coupés. La casse, les apostrophes et les guillemets sont conservés tels que fournis. Un texte de plusieurs lignes garde ses retours à la ligne.
Sur une facette à plusieurs valeurs, eq, in et
contains trouvent les documents qui portent au moins une valeur correspondante ; neq ceux
qui n’en portent aucune.
Un fragment contains sans caractère visible (des espaces, un caractère invisible) ne trouve
rien, et son négatif not_contains (mode query) ne retire aucun document. Il en va de même
pour starts_with.
Agrégats numériques (aggregate)
Section intitulée « Agrégats numériques (aggregate) »Pour sommer / moyenner une facette numérique (« montant moyen des factures », « total facturé »,
« MRR cumulé »), passez aggregate = le nom d’une facette numérique, ou une liste de noms.
La réponse porte un champ aggregates : un objet par champ avec count, sum, avg, min,
max, calculés exactement côté serveur et par enregistrement (un document = une valeur ;
une facette dénormalisée sur plusieurs extraits n’est comptée qu’une fois), indépendamment de
top_k/pagination. Régime analytique uniquement (sans query).
curl "$BASE/factures/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "facets": { "sens": "client", "type_piece": "facture" }, "aggregate": "montant_ttc_eur"}'{ "group_by": "chunk", "total": 122, "aggregates": [ { "field": "montant_ttc_eur", "count": 122, "sum": 435155.91, "avg": 3566.85, "min": 484.13, "max": 12240 } ], "data": []}count = nombre d’enregistrements ayant une valeur numérique (le dénominateur de avg). Un
nom non numérique ou inconnu du schéma est refusé (erreur 400 qui liste les facettes
numériques de la base) : auparavant il était ignoré et son agrégat disparaissait de la réponse
sans explication. N’additionnez jamais les montants à la main : lisez aggregates.
Requête analytique complète (mode: "query")
Section intitulée « Requête analytique complète (mode: "query") »aggregate rend une somme globale ; group_by: {facet} rend l’état courant par
entité. Aucun des deux ne répond à « les trois clients les plus facturés en 2024 » : il
faut une somme par groupe, triée. mode: "query" donne l’algèbre complète en un appel,
filtrer → grouper → agréger → having → trier → borner :
{ "mode": "query", "facets": { "exercice": "2024" }, "group_by": "structure", "aggregate": "montant_ht", "sort": "montant_ht:sum", "limit": 3}{ "group_by": "group", "group_fields": ["structure"], "total": 40, "data": [ { "group": { "structure": "Passerelle Emploi" }, "count": 7, "aggregates": [{ "field": "montant_ht", "count": 7, "sum": 179558.38, "avg": 25651.20, "min": 3210.5, "max": 44100 }] } ]}group_by: une facette, une liste (["structure","exercice"]= deux axes), et/ou une tranche de date :{ "date": "month" }(day,month,quarter,year), combinable avec des facettes :{ "facets": ["secteur"], "date": "quarter" }.aggregate: facette(s) numérique(s) ; chaque groupe rend{count, sum, avg, min, max}, exacts, calculés en base sur tous ses enregistrements. Sansaggregate, uncountpar groupe.having: filtre sur les agrégats —{ "metric": "montant_ht:sum", "op": "gte", "value": 10000 }(métriques :countou<facette>:sum|avg|min|max).sort:"count","group"(par clé), ou"<facette>:sum|avg|min|max";{by, dir}. Défaut : première somme décroissante.limit/offset: pagination des groupes ;total= nombre exact de groupes.filtersaccepte, en plus des opérateurs ci-dessus :not_in,not_contains,starts_with,regex(motif borné à 200 caractères),between([a, b], bornes incluses),exists/missing(sans valeur),before/after(dates, synonymes delt/gt).text: filtre sur le contenu des extraits. Le texte est chiffré au repos, donc ce filtre s’applique après déchiffrement à l’ensemble déjà réduit par les facettes — une chaîne (contient), ou{ "contains", "not_contains", "regex" }. Borné à 2 000 extraits ;text_truncated: trueau-delà.query(sémantique) se combine : les agrégats portent alors sur les extraits retrouvés, et la réponse le dit (aggregates_scope: "retrieved"). Sansquery, sur toute la base.- Sans
group_by:group_by: "rows", avectotal,aggregatesglobaux et une page de lignes réduites à leurs facettes (jamais leur contenu). - Grouper sur une facette à plusieurs valeurs place chaque document dans le groupe de chacune
de ses valeurs :
multi_valued_groupsle signale, et la somme des groupes n’est plus le total.
Une demande mal formée est refusée avec la raison (facette de groupement inconnue, facette de filtre qu’aucun enregistrement ne porte, opérateur incompatible avec le type, facette non numérique agrégée) et jamais écartée en silence : un groupement perdu rendrait un total global lu comme un total par groupe, un filtre sur une facette inexistante un zéro lu comme une valeur.
Ce que cette algèbre ne fait pas, et qui relève de votre code (ou de l’exécution de code côté agent) : joindre deux bases, comparer deux périodes, croiser avec un connecteur.
Tri (sort)
Section intitulée « Tri (sort) »{ "by": …, "dir": "asc" | "desc" } — by vaut :
"relevance"(défaut avecquery) : par score décroissant. Exige une requête."date": par date métier (chunk_date), valeurs absentes en dernier."created": par date d’insertion en base (created_at) — l’ordre d’ajout, indépendant de la date métier."alpha": par nom de document.
Sans query, relevance retombe automatiquement sur date desc.
Plage de dates et « frères » par facettes
Section intitulée « Plage de dates et « frères » par facettes »Deux options avancées, identiques à celles du RAG :
date_range{ from?, to? }: ne retient que les extraits dont la date métier (chunk_date) tombe dans la plage (les extraits sans date sont alors écartés). Chaque borne s’écritAAAA-MM-JJ(ou un instant ISO 8601 complet). Une borne dans un autre format (01/09/2026,septembre 2026), une liste ou d’autres noms de bornes renvoient une erreur400: la plage n’est jamais ignorée, ni interprétée au hasard.expand(requête uniquement) : en plus des hits sémantiques, ramène les extraits reliés par facettes (relation métier). Champs :seeds(graines, défaut 3),facets(clés de match ; omis = toutes),match("all"/"any"),depth(niveaux, défaut 1),max_chunks(plafond, défaut 30, max 200).
curl "$BASE/crm/search" -H "$AUTH" -H "Content-Type: application/json" -d '{ "query": "point sur le dossier", "date_range": { "from": "2024-01-01" }, "expand": { "seeds": 3, "facets": ["client"], "depth": 1 }}'Les extraits ramenés par relation portent "relation": "facet" (et leur level) ; les hits
sémantiques portent "relation": "semantic". ⚠️ L’expansion ajoute des extraits (donc des tokens
en aval) : max_chunks borne le total ; match: "any" + depth > 1 font grossir l’arbre vite.
Exporter et importer une base
Section intitulée « Exporter et importer une base »Une base entière s’exporte en bundle autonome (un fichier .kb.json.gz : métadonnées,
documents, extraits déchiffrés et vecteurs embarqués), puis se réimporte ailleurs — autre
compte, autre environnement, ou simple sauvegarde. Comme les vecteurs voyagent dans le bundle,
l’import est instantané et gratuit : aucun ré-embedding, aucun coût.
Les vecteurs du bundle sont ceux du modèle d’embedding, pas ceux rangés dans la base (chaque base les transforme par sa propre clé, cf. Bases de connaissance) : un bundle se réimporte donc dans n’importe quelle base, qui les transforme à son tour.
# Exporter → fichier gzip (le corps EST le .gz, ne pas le décompresser en transit)curl -L "$BASE/faq-support/export" -H "$AUTH" -o faq-support.kb.json.gz
# Sans les vecteurs : bundle bien plus léger, ré-importable tel quelcurl -L "$BASE/faq-support/export?vectors=0" -H "$AUTH" -o faq-support.kb.json.gz
# Importer → crée une base NEUVE (slug auto-dédupliqué, jamais d'écrasement)curl "$BASE/import" -H "$AUTH" \ -H "Content-Type: application/gzip" --data-binary @faq-support.kb.json.gz# → { "slug": "faq-support-2", "document_count": 12, "chunk_count": 240, … }L’export est lui aussi streamé : le bundle n’est jamais construit en entier en
mémoire, quelle que soit la taille de la base. La réponse arrive donc en transfert
« chunked », sans Content-Length — un client HTTP le gère, mais une barre de
progression ne pourra pas afficher de pourcentage.
L’envoi en corps brut (Content-Type: application/gzip ou application/json, --data-binary)
est streamé de bout en bout : le bundle n’est jamais chargé en entier en mémoire, ce qui permet
d’importer de grosses bases (plusieurs dizaines de milliers de documents) sans risque de
saturation. Un envoi multipart/form-data (champ file) reste accepté (pratique depuis un
formulaire). Nom et slug de la base créée sont repris du bundle ; on peut les surcharger via
?name= / ?slug= (ou les champs name / slug du formulaire).
| Point | Détail |
|---|---|
| Cible | L’import crée toujours une base neuve — il ne fusionne ni n’écrase une base existante. |
| Vecteurs | Embarqués (base64 float32). Un extrait sans vecteur (bundle fabriqué à la main, ou export en ?vectors=0) est ré-embeddé à l’import, facturé sur la clé qui appelle l’import. |
?vectors=0 | Exporte sans les vecteurs. Ils représentent l’essentiel du poids d’un bundle (sur une base de 16 000 documents : 894 Mo sur 930). À réserver aux sauvegardes et aux transferts : l’import recalcule les vecteurs, ce qui prend du temps et est facturé. |
| Dimension | Le modèle d’embedding et la dimension (1024) doivent correspondre, sinon l’import est rejeté. La base importée reprend le collection.embed_model de l’en-tête du bundle (mistral-embed s’il manque). |
| Taille | Bundle plafonné à 200 Mo (compressé). Import streamé (mémoire bornée) et par lots. Atomique : en cas d’erreur, aucune base partielle n’est créée. |
| Confidentialité | Le bundle contient votre texte en clair : traitez-le comme une donnée sensible. À l’import, une clé de chiffrement neuve est générée pour la nouvelle base. |
| Lien source | Comme à l’ajout d’un document (voir « Lien source » plus haut), un source_url qui n’est pas un lien http(s) est remplacé par null à l’import. |
Au portail, ces actions sont les boutons « Exporter » (page d’une base) et « Importer » (liste des bases).
Valeurs de facettes (découverte)
Section intitulée « Valeurs de facettes (découverte) »Le facet_schema déclare la structure des facettes (et l’enum des valeurs autorisées pour les
facettes texte), mais pas les valeurs réellement présentes dans une base. Cette route les
expose — pratique pour construire une UI de filtres ou laisser un agent découvrir sur quoi
filtrer :
curl "$BASE/factures/facets" -H "$AUTH"{ "facets": [ { "key": "theme", "type": "text", "declared_values": ["facturation","securite"], "values": [ {"value":"facturation","count":2}, {"value":"securite","count":1} ], "distinct_count": 2, "truncated": false }, { "key": "montant", "type": "number", "min": 50, "max": 4200, "distinct_count": 3 }, { "key": "echeance", "type": "date", "min": "2024-03-01", "max": "2026-06-15", "distinct_count": 3 } ]}text/boolean(etpersonne/email/url, qui sont du texte) :values= valeurs présentes triées par fréquence (count), plafonnées (cap 50) →truncated: trueau-delà ;distinct_count= nombre total de valeurs distinctes ;declared_valuesrappelle l’enum du schéma s’il existe.number/date:min,maxetdistinct_count(pas la liste complète).- Couvre toutes les clés rencontrées, y compris des facettes hors schéma.
- Une facette à plusieurs valeurs est éclatée : chaque valeur a son propre
count. - Gratuit (agrégation, pas d’embedding) ; le texte des extraits reste chiffré (seules les facettes, en clair, sont agrégées).
Restreindre à un sous-ensemble (scope) — superlatifs exacts
Section intitulée « Restreindre à un sous-ensemble (scope) — superlatifs exacts »Ajoutez un scope en query params (JSON encodé) — facets, filters, date_range — pour
n’agréger que les extraits correspondants. Les min/max portent alors sur ce filtre : c’est la
façon exacte et déterministe de répondre à un superlatif numérique (« le MRR le plus élevé de
X », « le montant max de la catégorie A »), sans scanner les extraits.
# min/max d'une facette numérique pour une entité donnéecurl "$BASE/factures/facets?facets=%7B%22startup%22%3A%22ReFarm%22%7D" -H "$AUTH"# → { "documents": 24, "facets": [ { "key": "mrr_keur", "type": "number", "min": 18, "max": 127, … }, … ] }Avec un scope, la réponse porte documents : le nombre de documents qu’il contient.
- Un scope qui ne contient aucun document répond
{ "documents": 0, "facets": [], "note": "…" }. Auparavant la réponse listait toutes les facettes déclarées avecminetmaxànull, ce qui se lisait comme une absence de donnée alors que c’est la valeur filtrée qui ne correspond à rien. - Un scope mal formé (JSON illisible, opérateur inconnu, valeur qui ne se lit pas, date dans un
autre format, facette qu’aucun enregistrement ne porte) renvoie une erreur
400. Auparavant il était écarté et la réponse portait sur toute la base. - Sans scope, la réponse est inchangée (pas de champ
documents).
À savoir
Section intitulée « À savoir »- Facturation : les opérations qui vectorisent (ajout de texte/chunk, modification de contenu, recherche) coûtent un embedding. Le reste du CRUD (lister, renommer, supprimer, créer une base, lister les valeurs de facettes) est gratuit.
- Sécurité : le texte des extraits est chiffré au repos (une clé par base) ; il n’est déchiffré que pour vous répondre.
- Codes :
401clé invalide ·404base/document/chunk introuvable ·400requête invalide.