← Tous les articles
Analyse · Septembre 2026

Ce qu'un agent IA trouve, et ne trouve pas, dans un catalogue technique

Un agent IA ne sait pas qu'un catalogue de visserie porte des diamètres, des longueurs et des normes. Il écrit une phrase, et il transmet ce qu'on lui a dit. Voici ce que notre serveur MCP lui rend, champ par champ, ce qu'il obtient sur 12 demandes en langage naturel, avec et sans pack de règles, et ce qu'il ne peut pas découvrir seul.

Meilisearch a publié le 9 septembre un guide pour construire un outil de recherche destiné aux agents. Cet article part d'un outil qui existe déjà, heurix-mcp-server, et mesure ce qu'il rend sur un catalogue technique. Les mesures ont été faites avec la version 0.1.0. La 0.1.1, celle que PyPI sert aujourd'hui, n'en change que les descriptions et les instructions que l'agent lit, pas le code des outils : c'est de ces textes-là, en 0.1.1, qu'il est question plus bas. L'installation dans Claude Desktop et Cursor est décrite dans le guide du serveur MCP ; elle n'est pas reprise ici.

Conditions de la mesure. Moteur Heurix en local, à la version servie en production le 15 septembre 2026. Catalogue de 10 000 références de quincaillerie produit par le générateur du moteur, avec son désordre voulu : quatre graphies de la même dimension (M8x30, M8 x 30, M8-30, M8*30), des abréviations métier, des fiches en anglais, des champs manquants. Ce catalogue est indexé deux fois, à l'identique : une fois avec le pack outillage, une fois sans pack. Les appels passent par les fonctions d'outil du serveur MCP, sans modification ; le transport MCP lui-même n'est pas en jeu dans les résultats.

Ce que l'agent reçoit

Le serveur expose trois outils. Chacun relaie un ou deux appels de l'API REST, sans logique propre.

OutilParamètresAppel relayéQuota de recherche
heurix_catalog_statscatalog (facultatif)Sans argument : GET /v1/index/catalogs. Avec un catalogue : GET /v1/index/{catalog}/stats puis /browse-categories0 sans argument, 1 requête avec un catalogue
heurix_searchcatalog, query, filters, limit (1 à 100, 10 par défaut)POST /v1/index/{catalog}/search1 requête
heurix_browsecatalog, category, sort, limitGET /v1/browse/{catalog}/{category}0 : compté sur le quota Browse

La colonne quota est mesurée sur le compteur de la clé, avant et après chaque appel.

Découvrir le catalogue

La description de heurix_catalog_stats demande de l'appeler en premier. Avec un nom de catalogue, l'agent reçoit :

{
  "catalog": "quincaillerie-outillage",
  "products": 10000,
  "terms": 5621,
  "annotations": 271,
  "rulepack": "outillage",
  "rulepacks": [
    "outillage"
  ],
  "synonym_groups": 0,
  "sandbox": false,
  "browse_categories": [
    {
      "category": "fixation",
      "products": 9239
    },
    {
      "category": "boulonnerie",
      "products": 4700
    },
    {
      "category": "visserie",
      "products": 4516
    },
    {
      "category": "accessoires",
      "products": 1080
    },
    {
      "category": "maconnerie",
      "products": 465
    }
  ]
}

Le nom du pack actif, des comptes, et les catégories avec leur nombre de produits. annotations: 271 est un nombre : la liste des 271 annotations n'y est pas. Les noms des champs du produit (famille, matiere, diametre, norme) n'y sont pas non plus. Les instructions du serveur le disent, et renvoient l'agent aux résultats de heurix_search pour les lire.

Sur une clé dont l'offre n'inclut pas Browse, la même réponse porte "browse_categories": []. La description de l'outil prévient que c'est le cas, mais la réponse elle-même ne distingue pas « pas d'accès » de « pas de catégorie » : l'agent ne le sait qu'en appelant heurix_browse, qui rend alors une erreur 403. Et un nom de catégorie mal cassé, Visserie au lieu de visserie, rend total: 0 sans erreur.

Chercher

heurix_search transmet quatre choses : le catalogue, le texte, les filtres et la limite. L'API de recherche en accepte davantage, que l'outil ne transmet pas : offset, facets, in_stock_only, exclude_description, group_by, lang. Un agent ne peut donc ni paginer, ni demander des facettes, ni écarter les ruptures. Voici un résultat, tel qu'il le reçoit :

{
  "query": "vis M8 x 30 inox",
  "tokens": [
    "vis",
    "m8",
    "x",
    "30",
    "inox"
  ],
  "total": 6733,
  "offset": 0,
  "limit": 1,
  "hits": [
    {
      "product": {
        "id": "REF-000004",
        "name": "Vis hexagonale M8 x 30 inox A4",
        "price": 1.79,
        "categories": [
          "visserie",
          "fixation"
        ],
        "famille": "Vis",
        "matiere": "inox A4",
        "diametre": "M8",
        "description": "ISO 4762 filetage métrique M8",
        "norme": "ISO 4762",
        "stock": 3643
      },
      "score": 77.4,
      "in_stock": true,
      "matched": [
        "terme 'vis' (depuis 'vis', x1.00)",
        "terme 'm8' (depuis 'm8', x1.00)",
        "terme '30' (depuis '30', x1.00)",
        "terme 'inox' (depuis 'inox', x1.00)",
        "annotation #DIAM_M8",
        "annotation #DIAM_M8_INOX",
        "annotation #FAM_VIS",
        "annotation #LONG_30"
      ],
      "pinned": false,
      "buried": false
    }
  ],
  "fallback": false
}
ChampCe que l'agent peut en faire
totalLe nombre de produits trouvés. Il compte ce qui contient un terme de la requête, pas ce qui répond à la demande : 6 582 pour « vis inox M8 » sur 10 000 références.
tokensLa requête telle que le moteur l'a découpée.
hits[].productLe produit tel qu'il a été indexé, description comprise. Les dix résultats de « vis inox M8 » pèsent environ 6 Ko.
hits[].matchedPourquoi ce produit est là : les termes trouvés et les annotations partagées avec la requête. C'est le champ qui permet à un agent d'expliquer un résultat plutôt que de le paraphraser.
hits[].score
in_stock
pinned
buried
La pertinence, la disponibilité, les priorités posées par le marchand.
fallbacktrue quand les résultats sont des produits mis en avant faute de correspondance.
price_filter
suggested_category
filters_unknown
Présents seulement quand ils ont quelque chose à dire. Voir plus bas.

Ce que l'agent tape, et ce qu'il obtient

Douze demandes, écrites comme un agent les transmet. Pour chacune, on compte les produits qui répondent à toute la demande, d'après les champs structurés du produit (famille, matière, diamètre, norme, prix) et la longueur écrite dans son nom. Le tableau donne le nombre de ces produits dans les dix premiers résultats, et le rang du premier.

RequêteProduits qui répondentAvec pack outillageSans pack
vis inox M8 de 30 mm de long40 dans les 10, premier au rang 360 dans les 10, premier au rang 63
vis de 8 par 30 en inox4aucun dans les 100 premiersaucun dans les 100 premiers
vis inox M8x3042 dans les 10, rang 1aucun dans les 100 premiers
vis M8 x 30 inox43 dans les 10, rang 10 dans les 10, rang 63
cheville nylon de 850 dans les 10, rang 190 dans les 10, rang 19
cheville nylon M853 dans les 10, rang 12 dans les 10, rang 1
stainless steel hex bolt M8115 dans les 10, rang 10 dans les 10, rang 11
vis à tête fraisée M5 en inox207 dans les 10, rang 13 dans les 10, rang 1
vis DIN 912 M12x401rang 2aucun dans les 100 premiers
écrou M6 inox A43410 dans les 1010 dans les 10
tige filetée M16 laiton228 dans les 108 dans les 10
vis inox moins de 1 euro16810 dans les 1010 dans les 10

Pour « hex bolt », seuls les boulons sont comptés. En comptant aussi les vis à tête hexagonale, 35 produits répondent : 10 parmi les 10 premiers avec le pack, 4 sans.

Une dimension écrite en phrase n'est pas lue comme une dimension

« de 30 mm de long » ne pèse rien, avec ou sans pack. Le premier résultat est une vis de 80 mm, et voici pourquoi il est là :

{
  "product": {
    "id": "REF-000148",
    "name": "Vis TF M8 x 80 inox A2",
    "price": 1.96,
    "categories": [
      "visserie",
      "fixation"
    ],
    "famille": "Vis",
    "matiere": "inox A2",
    "diametre": "M8",
    "ref": "VIS-8-80-INOX",
    "description": "ISO 4762 filetage métrique M8",
    "norme": "ISO 4762",
    "stock": 3978
  },
  "score": 57.0,
  "in_stock": true,
  "matched": [
    "terme 'vis' (depuis 'vis', x1.00)",
    "terme 'inox' (depuis 'inox', x1.00)",
    "terme 'm8' (depuis 'm8', x1.00)",
    "annotation #DIAM_M8",
    "annotation #DIAM_M8_INOX",
    "annotation #FAM_VIS",
    "annotation #MAT_INOX"
  ],
  "pinned": false,
  "buried": false
}

Diamètre, matière, famille : tout est reconnu, sauf la longueur. 30 est un terme isolé parmi de, mm et long. Le pack lit une dimension comme un catalogue l'écrit, M8x30 ou M8 x 30, et la réponse à « vis inox M8x30 » porte alors annotation #LONG_30 et annotation #VIS_M8X30. De même, « de 8 » n'est pas un diamètre : « cheville nylon M8 » place le bon produit en tête, « cheville nylon de 8 » au rang 19.

La description de l'outil dit que les formats différents sont tolérés. C'est exact pour les graphies d'une référence : avec M8x30 comme avec M8 x 30, le premier résultat répond à la demande. Ce ne l'est pas pour une phrase qui décrit la référence.

Là où le pack change le résultat

Sur les requêtes dont les mots figurent tels quels dans les noms de produits (écrou, rondelle, tige filetée, laiton), les deux catalogues rendent les mêmes 10 premiers résultats. Le pack change le résultat quand la requête et la fiche n'écrivent pas la même chose :

  • Une autre langue. Le catalogue ne contient pas le mot « bolt » ; le pack le range dans la même famille que « boulon ». Sans pack, « stainless steel hex bolt M8 » ne place aucun boulon inox M8 à tête hexagonale parmi les 10 premiers.
  • Une abréviation. Les fiches écrivent « TF » pour tête fraisée. Avec le pack, 7 des 10 premiers résultats sont des vis fraisées M5 inox ; sans, 3.
  • Une famille écrite autrement. Une partie des vis du catalogue s'appelle « Screw » : 111 des 225 vis M8. Le pack reconnaît les deux mots comme la même famille, et la requête « vis » rend 2 721 produits, contre 1 391 sans pack.
  • Une dimension collée. « vis inox M8x30 » : 4 produits sur 4 dans les cent premiers avec le pack, aucun sans. Sans pack, m8x30 reste un seul terme, et aucun des quatre produits ne l'écrit ainsi.

La contrainte de prix, elle, ne dépend pas du pack : « moins de 1 euro » est reconnu des deux côtés et la réponse porte "price_filter": {"min": null, "max": 1.0}.

Les filtres : ce que l'agent sait avant d'essayer

Le paramètre filters restreint la recherche. Sa description en donne deux formes, champ:valeur pour un champ du produit et une annotation seule pour une étiquette du pack, et demande à l'agent de chercher d'abord sans filtre pour lire les noms dans la réponse. Voici ce que rendent les filtres qu'un agent peut essayer, sur la requête « vis » :

filtersAvec packSans pack
["M8"]0, filters_unknown: ["M8"]0, filters_unknown: ["M8"]
["inox"]0, filters_unknown: ["inox"]0, filters_unknown: ["inox"]
["DIAM_M8"]2250, filters_unknown: ["DIAM_M8"]
["diametre:M8"]225114
["Diametre:M8"]0, filters_unknown: ["Diametre"]0, filters_unknown: ["Diametre"]
["matiere:inox A2"]588305
["matiere:inox"]0, sans signal0, sans signal

Trois choses se lisent dans ce tableau.

  • Une annotation n'existe que si le pack l'écrit. DIAM_M8 rend zéro sur un catalogue sans pack, et un mot seul comme M8 est lu comme une annotation, ce que la description dit. filters_unknown le signale, à condition que l'agent le lise.
  • Les champs du produit sont filtrables, avec ou sans pack, sous la forme champ:valeur, et le nom du champ est sensible à la casse : Diametre n'est pas diametre. La syntaxe complète est dans la référence de la recherche.
  • Une valeur fausse ne se signale pas. matiere:inox désigne un champ qui existe et une valeur qu'aucun produit ne porte : les valeurs sont « inox A2 » et « inox A4 ». La réponse est total: 0, identique à celle d'un filtre juste qui ne trouve rien. Le moteur vérifie le nom du champ, jamais la valeur, et c'est une décision : l'ensemble des valeurs d'un champ est ouvert. La description de l'outil le dit ; la réponse, elle, ne le dit pas.

Ce qui manque à un agent aujourd'hui

Pour filtrer juste, un agent doit connaître deux listes : les champs du catalogue avec leurs valeurs, et les annotations que son pack écrit. Aucun des trois outils ne les rend. Les instructions du serveur envoient l'agent les lire dans une première recherche : product porte les noms des champs, matched les annotations. C'est partiel : une recherche ne montre que les valeurs des produits qu'elle rend, et matched s'arrête à 8 entrées par résultat, limitées à ce qui a compté pour ce résultat.

L'API en porte une vue plus complète, sur deux routes que le serveur MCP n'appelle pas :

  • GET /v1/index/{catalog}/browse-attributes rend les champs filtrables et leurs vingt valeurs les plus fréquentes. Sur ce catalogue : diametre (12 valeurs), famille (8), matiere (6), norme (9). Elle exige une offre qui inclut Browse, et refuse en 403 sinon.
  • GET /v1/rulepacks/annotations rend, pour chaque pack, les modèles d'annotation qu'il peut écrire, comme DIAM_M{1} ou MAT_INOX. Ce sont des modèles, pas les annotations présentes dans un catalogue donné, et la réponse pèse environ 26 Ko pour les onze packs.

Exposer ces listes à l'agent serait un outil de plus. Il n'existe pas aujourd'hui, et cet article ne le date pas.

Ce que vous pouvez faire dès maintenant

Vous connaissez ce que l'agent ne découvre qu'en partie. Donnez-le-lui dans ses instructions :

  • Les champs et leurs valeurs exactes, sous la forme que filters accepte : diametre:M8, matiere:inox A2|inox A4. Le tuyau est un OU.
  • La grammaire de vos références : demandez-lui de transmettre la dimension comme votre catalogue l'écrit, M8x30 plutôt que « de 8 par 30 ».
  • Deux signaux à lire avant de conclure « rien trouvé » : filters_unknown, et fallback, qui signale une liste de produits mis en avant et non des résultats.
  • Les noms exacts de catégories, casse comprise, si vos clés ont accès à Browse.

Pour aller plus loin : l'installation du serveur dans le guide MCP, la syntaxe complète des filtres dans Recherche, et le détail de chaque champ de la réponse dans l'article sur la recherche headless.

Essai gratuit 14 jours

Testez Heurix sur votre catalogue, sans carte bancaire.

Démarrer l'essai gratuit