Introduction
Le Search API expose le moteur Heurix : un moteur de recherche pensé pour les catalogues techniques, où les requêtes et les fiches produits traversent le même pipeline — normalisation (casse, accents), synonymes métier, tolérance aux fautes de frappe, puis une cascade de règles qui transforme le texte en annotations structurées. C'est cette cascade qui permet à m8x20 de retrouver une fiche enregistrée « M8 x 20 — A2 ».
L'API est REST, en JSON, sur HTTPS uniquement. Toutes les réponses sont encodées en UTF-8.
Démarrage rapide
Trois appels suffisent pour une première recherche fonctionnelle.
1 Indexez vos produits (le catalogue est créé automatiquement au premier envoi) :
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/items \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{
"rulepack": "outillage",
"items": [
{"id": "V1", "name": "Vis tête hexagonale", "ref": "M8 x 20 — A2", "stock": 100},
{"id": "V2", "name": "Vis tête hexagonale", "ref": "M8 x 30 — A2", "stock": 50}
]
}'
2 Cherchez — les fautes de frappe sont tolérées. Modifiez les champs pour voir la commande changer :
3 Lisez la réponse — chaque résultat explique pourquoi il correspond :
{
"query": "m8x20 inox",
"total": 2,
"hits": [
{
"product": {"id": "V1", "name": "Vis tête hexagonale", "ref": "M8 x 20 — A2", "stock": 100},
"score": 37.5,
"in_stock": true,
"matched": ["annotation #VIS_M8X20", "annotation #DIAM_M8", "annotation #LONG_20"]
}
]
}
Authentification
Chaque appel doit porter votre clé API dans l'en-tête Authorization :
Authorization: Bearer VOTRE_CLE
Une clé manquante ou mal formée renvoie 401 ; une clé invalide ou hors de sa portée renvoie 403.
Deux types de clés
Le choix n'est pas cosmétique : il détermine ce qu'un tiers peut faire s'il met la main sur votre clé.
| Type | Préfixe | Portée | Où l'utiliser |
|---|---|---|---|
| Clé serveur | hx_ |
Accès complet : indexation, merchandising, statistiques, facturation | Côté serveur uniquement. Jamais dans une page web. |
| Clé publique | hxp_ |
Recherche, parcours de catalogue, événements de conversion — rien d'autre | Navigateur, applications mobiles, tout contexte où la clé est visible |
Une clé publique appelant un endpoint hors de sa portée reçoit un 403 explicite. Elle partage le quota de la clé serveur qui l'a générée — en créer plusieurs n'augmente donc pas vos plafonds.
Générer une clé publique
Depuis votre console (Mon compte → Clé API), ou par API avec votre clé serveur :
curl -X POST https://api.heurix.fr/v1/keys/public \
-H "Authorization: Bearer VOTRE_CLE_SERVEUR" \
-H "Content-Type: application/json" \
-d '{"allowed_origins": "monsite.fr,www.monsite.fr"}'
allowed_origins est optionnel mais recommandé : la clé n'est alors acceptée que depuis les domaines listés, ce qui la rend inutilisable si quelqu'un la recopie sur un autre site. GET /v1/keys/public les liste, DELETE /v1/keys/public/{clé} en révoque une.
Les clés existantes restent des clés serveur et continuent de fonctionner à l'identique — rien ne casse. Mais si vous en avez posé une dans le JavaScript de votre site (barre de recherche, tracker, widget Browse), remplacez-la par une clé publique : en l'état, n'importe quel visiteur peut la lire et accéder à votre facturation.
Comptage des requêtes
La facturation repose sur un compteur mensuel de requêtes, simple par principe :
| Recherche | 1 appel search = 1 requête |
| Indexation | jamais décomptée du quota de requêtes — mettez à jour votre catalogue aussi souvent que nécessaire, seul le nombre total de produits indexés reste plafonné par plan |
| Recherche fédérée | 1 requête par catalogue interrogé |
| Gestion | La plupart des appels de gestion ne sont pas décomptés — clés, comptes, abonnement, priorités de merchandising, analytics. Neuf points d'entrée font exception, listés juste en dessous |
| Ranking | 1 appel browse = 1 requête Browse, sur un compteur séparé de celui de la recherche |
| Dépassement (Scale) | Le surplus est facturé au tarif indiqué sur la page tarifs — jamais de coupure |
| Dépassement (autres plans) | Une marge de tolérance de 10 % absorbe un pic ponctuel ; au-delà, la requête est refusée avec un code 429 explicite |
Les appels de gestion qui décomptent
Ces neuf points d'entrée comptent chacun pour une requête. Aucune règle générale ne les distingue des autres appels de gestion — ni le verbe HTTP, ni la nature de l'opération : c'est pourquoi ils sont listés un par un plutôt que résumés en une phrase.
GET /v1/index/{catalog}/stats | Statistiques du catalogue |
GET /v1/index/{catalog}/synonyms | Lecture des synonymes |
PUT /v1/index/{catalog}/synonyms | Remplacement des synonymes |
GET /v1/index/{catalog}/synonym-suggestions | Suggestions de synonymes |
GET /v1/rulepacks | Liste des packs de règles |
PUT /v1/index/{catalog}/config | Changement de pack actif |
POST /v1/index/{catalog}/custom-rules | Création d'une règle personnalisée |
DELETE /v1/index/{catalog}/custom-rules/{id} | Suppression d'une règle personnalisée |
DELETE /v1/index/{catalog}/items/{id} | Suppression d'un produit |
À connaître si vous synchronisez un catalogue depuis une boutique : la suppression porte sur un produit et compte pour une requête. Retirer 300 produits un par un consomme donc 300 requêtes de votre quota mensuel.
Votre consommation en cours est consultable à tout moment via GET /v1/usage.
Plans et plafonds
Chaque clé API est rattachée à un plan qui définit son volume de requêtes mensuelles, son nombre de produits indexables, et son nombre de catalogues. Le détail des fonctionnalités de chaque plan est sur la page tarifs ; voici les plafonds bruts :
| Plan | Requêtes / mois | Produits max | Catalogues |
|---|---|---|---|
| Essai (14 jours) | 2 000 | 2 000 | 2 |
| Starter — 19 €/mois | 15 000 | 8 000 | 1 |
| Growth — 49 €/mois | 30 000 | 25 000 | 3 |
| Scale — 139 €/mois | 150 000 puis facturé | 50 000 | illimité |
Passé les 14 jours d'essai sans souscription à un plan payant, une clé repasse automatiquement aux plafonds de l'essai jusqu'à souscription — vos catalogues et leur configuration restent intacts, simplement moins accessibles en volume le temps de passer à un plan payant.
Un dépassement de plafond renvoie un code 429 avec le détail de la cause :
{
"detail": "Plafond de requêtes du plan 'starter' dépassé (5512/5000, marge de 10% également dépassée). Passez à un plan supérieur pour continuer.",
"plan": "starter",
"limit_type": "requests",
"upgrade_url": "https://heurix.fr/pricing.html"
}
Structure des produits
Un produit est un objet JSON libre. Quatre champs ont un rôle particulier :
| Champ | Type | Rôle |
|---|---|---|
id | string requis | Identifiant unique dans le catalogue. Ré-indexer le même id remplace le produit (upsert). |
ref | string | Référence produit (SKU, code fabricant). Indexée, poids le plus fort. |
name | string | Nom du produit. Indexé, poids fort. |
description | string | Description. Indexée, poids standard. |
stock | number | bool | string | Disponibilité — utilisée pour le tri à pertinence égale. 0, false, "rupture" = indisponible. |
lat / lon | number | string | Position, en degrés décimaux. Envoyés comme n'importe quel autre champ — aucune migration, aucune réindexation : un produit qui les reçoit au prochain envoi est géolocalisé le jour même. Les chaînes sont acceptées ("44.84" vaut 44.84) ; lat sans lon ne vaut rien — une position est un couple. Utilisés par la recherche par rayon, jamais indexés comme du texte. |
Tout autre champ (prix, image, URL, catégorie...) est stocké tel quel et restitué dans les résultats, sans être indexé. Un champ mérite une mention à part : compare_at_price — s'il est présent et supérieur à price, le widget de démo l'affiche automatiquement en prix barré avec le pourcentage de réduction, sans configuration supplémentaire.
/v1/index/{catalog}/searchRecherche
Lance une recherche sur un catalogue. Résultats triés par pertinence, puis disponibilité à score égal. Une requête vide (q: "") est acceptée : c'est le mode parcours, pour naviguer un catalogue par facettes seules, avant toute saisie texte.
Corps de la requête
| Paramètre | Type | Description |
|---|---|---|
q | string | La requête de recherche (0 à 500 caractères — vide autorisé, voir mode parcours ci-dessus). |
limit | integer | Nombre de résultats par page (1–100, défaut 10). |
offset | integer | Décalage de pagination (défaut 0). |
facets | array de string | Groupes d'annotations pour lesquels renvoyer un décompte (ex. ["FORMAT", "LANG"]). Voir Facettes et filtres. |
filters | array de string | Filtres exigés, combinés en ET. Deux formes acceptées, distinguées par le « : » :["FORMAT_POCHE", "LANG_FR"] — des annotations exactes.["brand:Makita", "color:bleu"] — des champs métier, les mêmes que côté Browse : tout champ texte non réservé devient filtrable dès l'indexation, sans configuration. Le tuyau exprime un OU à l'intérieur d'un champ (brand:Makita|Bosch).Les deux se mélangent dans la même liste. Aucune des annotations livrées par les packs ne contient de « : », et une règle personnalisée ne peut pas en produire : la distinction ne peut pas devenir ambiguë. |
latlonradius_km | number | Restreint les résultats à un rayon autour d'un point. Les trois ensemble ou aucun : un rayon partiel est une erreur d'intégration, pas une requête à interpréter, et l'API répond 422 en nommant ce qui manque.lat entre −90 et 90, lon entre −180 et 180, radius_km entre 0 (exclu) et 200. Au-delà de 200 km, « autour de moi » ne décrit plus un rayon mais une région — et une région se filtre par un champ (filters: ["region:Bretagne"]), ce qui coûte moins et se lit mieux.Un produit sans position est exclu du résultat filtré, et c'est délibéré : une agence dont vous n'avez pas saisi les coordonnées est invisible à toute recherche par rayon, y compris à 200 km. Le choix inverse — la garder — rendrait des résultats hors zone que rien ne distingue des bons, et il serait d'autant plus faux que vos données sont neuves. |
in_stock_only | bool | Exclut les produits en rupture (défaut false — certains catalogues affichent volontairement les ruptures pour signaler qu'un produit existe). |
lang | string | Filtre par langue si vos produits portent un champ lang (ex. "fr", "en"). Un produit sans ce champ reste visible quelle que soit la langue demandée — utile pour un catalogue majoritairement mono-langue avec quelques fiches bilingues. Omis, aucun filtrage n'est appliqué. |
exclude_description | bool | Omet le champ description de chaque produit dans les résultats (défaut false, rien ne change). Utile si vos descriptions sont longues : sur un catalogue réel, ce champ représente à lui seul 80%+ du poids de chaque produit dans la réponse — pertinent pour un widget mobile où chaque kilo-octet compte. |
visitor_id | string | Optionnel (64 caractères max). Transmis automatiquement par heurix-search.js quand le Tracker est chargé sur la même page — aucun code de liaison à écrire. Alimente le score d'intention, la segmentation, et re-classe légèrement les résultats selon l'historique d'achats et de clics de ce visiteur — jamais assez fort pour inverser un vrai écart de pertinence, seulement pour départager une quasi-égalité, même logique que la popularité. Sans lui, ces trois fonctionnalités restent vides pour ce visiteur, rien d'autre ne change. |
include_highlights | bool | Ajoute, pour chaque champ concerné (ref, name, description), les positions exactes des correspondances trouvées par cette requête précise — jamais toutes les caractéristiques que le produit possède par ailleurs. Défaut false : coût nul si non demandé, calculé une seule fois par requête, jamais par produit candidat. Voir Surlignage des résultats ci-dessous pour le format exact. |
Réponse
total (nombre de correspondances), hits — chaque hit contient le product complet tel qu'indexé, son score, in_stock, et matched : la liste lisible des raisons de la correspondance (termes trouvés, fautes corrigées, annotations partagées) — utile pour comprendre et régler la pertinence. Si facets a été demandé, un champ facets supplémentaire liste les décomptes par groupe. Si include_highlights a été demandé, un champ highlights supplémentaire apparaît sur chaque hit concerné.
Avec un rayon, chaque hit porte en plus distance_km — la distance au point demandé, arrondie à deux décimales. Elle est posée sur le hit et non dans product : c'est une propriété de votre requête, pas du produit, et la mettre dans product la ferait passer pour un champ de votre catalogue.
Deux clés de diagnostic, absentes quand elles n'ont rien à dire. Ne les traitez pas comme toujours présentes : leur absence est le cas normal.
filters_unknown— la liste des filtres qui ne désignent rien dans ce catalogue : un nom de champ inexistant comme une annotation mal orthographiée, la même clé pour les deux, parce que vous envoyez des chaînes dansfilterset voulez savoir lesquelles n'ont rien accroché. Le filtre s'applique quand même — un champ absent exclut tout, ettotalreste à 0. Seul le nom du champ est signalé, jamais la valeur :departement:Cantalest un filtre correct dont la réponse est « aucun résultat ».radius_no_positions—truequand un rayon a été demandé et qu'aucun résultat candidat ne porte de position. Sans elle, un rayon sur un catalogue non géolocalisé rend zéro sans rien dire, ce qui ne se distingue pas d'un rayon trop petit.
Surlignage des résultats (highlighting)
Avec include_highlights: true, chaque hit peut porter un champ highlights : un empan par champ concerné, chaque empan étant une paire [début, fin] à utiliser pour entourer le fragment correspondant dans votre propre rendu (une balise <mark>, par exemple).
{
"product": {
"ref": "VIS-M8X20-INOX-A2",
"name": "Vis à métaux M8x20 inox A2, lot de 50"
},
"highlights": {
"ref": [[0, 3], [4, 9], [10, 14]],
"name": [[0, 3], [13, 18], [19, 23]]
}
}
Les positions sont en points de code Unicode (codepoints), pas en octets ni en unités UTF-16 — le système natif de Python. Pour la quasi-totalité des titres produits réels, c'est identique à l'indexation native de votre langage ; un émoji ou un caractère hors du plan de base Unicode introduirait un décalage avec un langage qui indexe différemment (JavaScript, par exemple, itère nativement en UTF-16 — utilisez Array.from(texte) plutôt que texte[i] si vos titres peuvent en contenir).
Un champ sans correspondance pour cette requête n'apparaît simplement pas dans highlights — jamais un tableau vide. Un produit qui matche uniquement par sa référence n'aura donc pas de clé name dans son objet highlights, même si son titre existe bien dans la réponse.
Le highlighting reflète ce que la requête a réellement déclenché, jamais tout ce que le produit possède. Une caractéristique du produit sans rapport avec la recherche en cours (une matière, une norme, un type de tête de vis mentionnés dans une longue description) n'est jamais surlignée, même si elle est bien indexée et cherchable par ailleurs.
Deux empans peuvent se chevaucher partiellement sur un même champ (ex. [4, 7] et [4, 9] sur une même référence, si plusieurs règles matchent des fragments imbriqués du même terme) — fusionnez-les avant l'affichage plutôt que de les traiter comme des empans indépendants, sous peine de balises <mark> mal formées.
Simulation de règles côté Browse
/v1/browse/{catalog}/{category}/simulateLe pendant du champ simulate_overrides de la recherche, pour le merchandising de catégorie. Prévisualise un classement avec des règles non enregistrées :
{
"overrides": [{"product_id": "PER-BOSCH-18V", "action": "pin", "position": 1}],
"sort": "stock",
"limit": 20
}
Endpoint dédié en POST plutôt qu'un paramètre sur le GET existant : une liste de règles provisoires dépasse vite la longueur raisonnable d'une URL, et cela laisse le chemin de lecture normal totalement intact.
| Garantie | Comportement |
|---|---|
| Écriture en base | Aucune. Vos règles enregistrées sont inchangées. |
| Quota Browse | Non consommé. Ce sont vos tests, pas du trafic client. |
| Clé requise | Clé serveur. Une clé publique reçoit un 403. |
| Règles par attribut | Celles enregistrées continuent de s'appliquer — la simulation porte sur le merchandising par produit, pas sur la configuration du catalogue. |
overrides remplace l'ensemble persisté pour cet appel. Une liste vide prévisualise donc « sans aucune règle », ce qui permet de voir l'effet d'une suppression avant de la faire.
Simulation de règles
Tester une priorité de requête obligeait à l'enregistrer — donc à l'appliquer immédiatement à vos visiteurs. Le champ simulate_overrides permet de voir l'effet sans rien écrire :
{
"q": "vis",
"simulate_overrides": [
{"query": "vis", "product_id": "PER-BOSCH-18V", "action": "pin", "position": 1}
]
}
La réponse porte "simulated": true, et les résultats sont classés comme si ces règles étaient actives.
La liste remplace l'ensemble de vos priorités enregistrées pour cet appel — elle ne s'y ajoute pas. C'est ce qui permet de prévisualiser aussi bien un ajout qu'une modification ou une suppression : il suffit d'envoyer l'état voulu complet. Une liste vide prévisualise donc « sans aucune règle ».
| Garantie | Comportement |
|---|---|
| Écriture en base | Aucune. Vos priorités enregistrées sont inchangées. |
| Quota | Non consommé. Ce sont vos tests sur votre catalogue, pas des recherches de vos clients. |
| Statistiques de recherche | Non journalisé. Vos essais ne polluent pas vos propres analyses. |
| Clé requise | Clé serveur uniquement. Une clé publique reçoit un 403 — c'est un outil d'administration, pas du trafic visiteur. |
Prix par clé publique
Un distributeur B2B a des prix catalogue et des prix nets par population de clients, et beaucoup n'affichent aucun prix sans compte pro. Une clé publique porte donc quel champ du produit tient lieu de prix, et s'il est servi du tout.
Le marchand pousse ses prix comme des champs ordinaires du produit — price, price_pro, le nom qu'il veut — puis crée une clé publique par population et la sert depuis son back-office, seul endroit où l'identité du client existe.
curl -X PUT https://api.heurix.fr/v1/keys/public/hxp_xxx/pricing \
-H "Authorization: Bearer VOTRE_CLE_SERVEUR" \
-H "Content-Type: application/json" \
-d '{"catalog": "moncatalogue", "price_field": "price_pro"}'
| Champ | Type | Description |
|---|---|---|
catalog | string | Obligatoire. Le catalogue contre lequel valider le champ demandé — sans lui, une faute de frappe passerait et tous les produits paraîtraient sans prix. |
price_field | string | Le champ du produit servi comme prix. Défaut price. |
price_visible | bool | À false, aucun prix n'est servi. Défaut true. |
Réservé aux clés serveur. Une clé publique vit dans un navigateur : si elle pouvait déclarer son propre prix, n'importe quel visiteur lirait les prix pro en changeant un paramètre. Il n'y a pour la même raison aucun paramètre de prix dans une requête de recherche — la configuration vient de la clé, jamais de l'appel.
Un champ qu'aucun produit ne porte est refusé, et le message liste ceux qui existent :
{"detail": "Aucun des 5000 produits de « moncatalogue » ne porte le champ
« price_prro ». Champs de prix présents : price, price_pro."}
Une couverture partielle est acceptée et chiffrée. Les produits sans le champ n'ont pas de prix du tout — jamais un repli silencieux sur price, qui servirait le prix catalogue à un acheteur pro sans le dire.
Le masquage retire tout champ de prix, pas seulement price : « pas de prix sans compte » ne voudrait rien dire si price_pro continuait de sortir parce qu'il ne s'appelle pas price. Sont retirés les champs dont le nom contient price, prix ou tarif — et la réponse les liste, plutôt que de vous les faire découvrir :
{"price_visible": false,
"removed_fields": ["price", "price_pro", "tarif_livraison"]}
Ce que la recherche renvoie. Le produit porte un seul prix, sous la clef price quelle que soit sa source : votre front ne change pas. La réponse ajoute "custom_price": true quand ce n'est pas le prix catalogue, ou "price_visible": false quand aucun prix n'est servi. Le nom du champ ne descend jamais dans le navigateur — c'est une information sur votre structure de prix ; vous la relisez sur GET /v1/keys/public.
Une clé non configurée se comporte exactement comme avant : le champ price, servi, et aucune de ces deux clefs dans la réponse.
La limite, et elle est nette : un jeu de prix par clé publique, pas un tarif par client. Un distributeur crée une clé par population — visiteurs anonymes, comptes pro, grands comptes — et sert la bonne depuis son back-office, seul endroit où l'identité du client existe. Heurix ne porte pas de tarification individuelle : un tarif négocié par client demande un moteur de règles tarifaires, qui n'est pas ce que Heurix fait.
Contraintes de prix en langage naturel
Une contrainte de prix formulée en langage naturel est reconnue dans la requête et convertie en filtre — « vis inox moins de 5 € » filtre sur le prix au lieu de chercher les mots « moins » et « euros » dans votre catalogue. Le français, l'anglais, l'italien, l'espagnol, l'allemand et le portugais sont reconnus.
{"query": "vis inox", "total": 3, "hits": [...],
"price_filter": {"min": null, "max": 5.0}}
La requête renvoyée est débarrassée du fragment consommé (« vis inox », et non « vis inox moins de 5 € ») : les mots de la contrainte ne pèsent pas sur le score de pertinence. Le champ price_filter n'apparaît que si une contrainte a été détectée — utile pour l'afficher à l'acheteur et lui permettre de la retirer.
Formulations reconnues — liste volontairement courte et documentée plutôt qu'exhaustive :
| Effet | Tournures |
|---|---|
| Prix maximum | moins de X, inférieur à X, sous X €, en dessous de X, jusqu'à X €, max X € |
| Prix minimum | plus de X, supérieur à X, au-dessus de X, à partir de X €, min X € |
| Intervalle | entre X et Y € |
Le marqueur de monnaie est optionnel sur la plupart des tournures, et il peut se placer des deux côtés du nombre : moins de 5 €, moins de €5 et moins de 5 se lisent de la même façon, et $ comme £ valent €. Quatre tournures en exigent un — sous X, jusqu'à X, max X et min X — parce que « sous 5 » pourrait tout aussi bien désigner 5 mm. La virgule décimale française est acceptée (3,50 comme 3.50).
Le moteur lit un nombre, jamais une monnaie : il ne convertit pas. moins de $5 sur un catalogue en euros filtre sur 5, pas sur un montant converti.
La contrainte porte sur le prix que la clé appelante sert, pas nécessairement sur price — « moins de 5 € » filtre sur le prix pro pour une clé configurée ainsi (voir Prix par clé publique).
Un produit sans champ de prix est exclu dès qu'une contrainte est active : on ne peut pas affirmer qu'il la respecte, et le faire remonter serait trompeur. Il reste trouvable par une recherche sans contrainte de prix.
Recherche en train d'être tapée
Un mot de 3 caractères ou plus qui correspond au début d'un mot du vocabulaire remonte aussi des résultats — pas seulement une correspondance exacte ou une faute de frappe complète. Taper « per » retrouve déjà « perceuse », sans attendre le mot entier. Un résultat trouvé par ce biais compte toujours moins qu'une correspondance exacte ou une faute de frappe classique : dès que la requête est complète, le classement se réajuste vers les résultats les plus pertinents. En dessous de 3 caractères, aucune correspondance par préfixe n'est tentée — trop de mots du vocabulaire partageraient un début de 1 ou 2 lettres par pur hasard.
Secours sur zéro résultat
Une recherche qui ne trouve rien (total: 0) sur un catalogue contenant au moins un produit marqué featured: true à l'indexation renvoie jusqu'à 6 de ces produits dans hits, avec fallback: true — pour proposer une porte de sortie plutôt qu'une page vide. total reste à 0 : ce ne sont pas de vrais résultats de recherche, à vous de les présenter distinctement côté front (ex. « Aucun résultat pour "…" — nos incontournables » plutôt que de les mélanger à de vrais résultats). Sans produit featured dans le catalogue, hits reste vide et fallback vaut false, comme avant.
Catégorie suggérée
Si un mot de la requête (4 caractères ou plus) recoupe une catégorie Browse connue de ce catalogue, la réponse inclut un champ suggested_category — une piste pour proposer « vous cherchez sans doute dans telle catégorie ? » côté interface, jamais un filtre automatique : le tri et le contenu de hits restent inchangés.
{"query": "perceuse", "total": 4, "hits": [...],
"suggested_category": {"category": "perceuses-visseuses", "products": 12}}
La correspondance se fait par sous-chaîne, pas seulement une égalité stricte — « perceuse » (singulier) retrouve bien la catégorie « perceuses-visseuses » (pluriel, avec tiret). Si plusieurs catégories correspondent à la fois, la plus peuplée l'emporte. Absent de la réponse s'il n'y a aucune correspondance, ou en mode parcours (requête vide).
Mise en avant d'un pack ou d'un bundle
Un catalogue technique vend rarement que des produits seuls : un pack de 4 outils, un kit de câbles avec connecteurs, un lot de 100 vis — ces regroupements ont souvent un panier moyen bien plus élevé qu'un produit seul, mais les décrire en un seul long texte les désavantage naturellement au classement pur (un mot pertinent qui revient plusieurs fois dans une longue description accumule plus de poids qu'une fiche courte et précise — comportement voulu ailleurs, pas ici).
Plutôt que de trancher artificiellement « pack ou produit seul en premier » dans le score, la réponse inclut un champ séparé, highlighted_bundle : le meilleur pack correspondant à la requête, distinct de hits.
{"query": "perceuse 18V", "total": 47, "hits": [...],
"highlighted_bundle": {
"product": {"id": "pack-42", "name": "Pack 4 outils 18V Bosch", "category": "Pack 4 outils", "price": 1200},
"score": 35.66,
"in_stock": true
}}
hits ne change pas : c'est toujours le classement naturel, produits seuls compris — c'est à votre interface de choisir comment présenter les deux zones (par exemple, une mise en avant visuelle distincte au-dessus de la liste de résultats classique).
Détection par le champ category : la réponse considère un produit comme un bundle si sa catégorie contient « pack », « kit » ou « bundle » (insensible à la casse) — aucune donnée supplémentaire à fournir, ça fonctionne avec un catalogue déjà indexé. La contrepartie, assumée : ça dépend du vocabulaire déjà présent dans vos catégories, pas d'une déclaration explicite. Respecte le stock : un bundle épuisé n'est jamais choisi, même s'il est le mieux classé parmi les produits correspondants — la sélection continue vers le suivant. Si aucun produit correspondant et en stock ne contient l'un de ces mots, le champ est simplement absent de la réponse.
Popularité sur la recherche
Quand plusieurs produits répondent également bien à une requête, celui qui se vend vraiment remonte — pas un simple tri par pertinence texte. Le principe : jamais assez fort pour inverser un vrai écart de pertinence, seulement pour départager une quasi-égalité. Un produit avec un match exact sur le nom reste toujours devant, quelle que soit la popularité de l'autre.
score_final = score_texte × (1 + boost_max × popularité_normalisée)
popularité_normalisée compare chaque produit au plus populaire de tout le catalogue (pas seulement les résultats de cette requête) — une échelle stable, qui ne change pas d'une recherche à l'autre selon qui matche. boost_max vaut 10 % par défaut : même le produit le plus populaire du catalogue ne peut jamais faire remonter son score de plus d'un dixième.
La popularité elle-même combine clics depuis la recherche et achats, avec un poids plus fort pour l'achat (5) que pour le clic (1) — un signal agrégé sur 90 jours, recalculé toutes les heures, jamais à chaque recherche. Nécessite le Heurix Tracker : sans lui, ce comportement reste désactivé et le classement est identique à avant cette fonctionnalité.
/v1/index/{catalog}/search-overridesPriorités de requête
Épinglez ou reléguez un produit quand une recherche contient un mot précis — « si la recherche contient promo, tel produit apparaît en premier ». Indépendant de Browse & Discovery, qui classe des catégories de navigation, pas des requêtes de recherche.
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/search-overrides \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"query": "promo", "product_id": "sku-123", "action": "pin", "position": 1}'
Déclenchement par sous-séquence contiguë, pas seulement une correspondance exacte : une règle posée sur "promo" se déclenche pour "vis promo" ou "grosse promo inox", mais pas pour "promotionnel" (mot différent une fois normalisé). Un pin peut faire apparaître un produit qui ne matche pas textuellement la requête — c'est le point de cette fonctionnalité, pas un bug.
| Champ | Type | Description |
|---|---|---|
query | string | Le déclencheur, normalisé comme le reste du moteur (accents, casse) avant comparaison. |
product_id | string | Le produit concerné. |
action | string | pin (position exacte, requiert position) ou bury (fin de liste, ne s'applique qu'à un produit déjà présent naturellement). |
position | entier | Le rang final du produit, à partir de 1. position: 3 le place en 3e position ; les autres produits comblent les places restées libres dans leur ordre naturel. Deux produits demandant le même rang sont placés côte à côte. Un rang supérieur au nombre de résultats ramène le produit en fin de liste. |
GET /v1/index/{catalog}/search-overrides liste toutes les règles du catalogue (ou celles d'une requête précise avec ?query=...) ; DELETE /v1/index/{catalog}/search-overrides?query=...&product_id=... en retire une. Aucun de ces trois appels ne consomme le quota — ce sont des actions de configuration, pas des requêtes de recherche.
Facettes et filtres
Une facette regroupe les annotations qui partagent un même préfixe — FORMAT_POCHE et FORMAT_BROCHE appartiennent au groupe FORMAT. Le groupe est le préfixe, pas un nom déclaré : le moteur retient les annotations qui commencent par FORMAT_. Demandez un décompte par groupe avec facets, restreignez les résultats avec filters :
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/search \
-H "Authorization: Bearer VOTRE_CLE_API" \
-d '{"q": "polar", "facets": ["FORMAT", "LANG"], "filters": ["FORMAT_POCHE"]}'
{
"total": 17,
"hits": [ ... ],
"facets": {
"FORMAT": {"FORMAT_POCHE": 17, "FORMAT_BROCHE": 9, "FORMAT_GF": 4},
"LANG": {"LANG_FR": 12, "LANG_EN": 5}
}
}
Réponse relevée sur le moteur, pack livres, catalogue de 44 fiches dont 30 polars — 12 poche en français, 9 brochés en français, 5 poche en anglais, 4 grand format en français. Les cinq annotations citées ici sont celles que le pack pose vraiment ; aucune n'est un exemple de forme.
Le décompte et le filtre ne se comportent pas pareil, et c'est la distinction à retenir. Le décompte est disjonctif ; le filtre est cumulatif. Les deux répondent à deux questions différentes.
- Le décompte est disjonctif au sein d'un même groupe : filtrer sur
FORMAT_POCHEne fait pas disparaîtreFORMAT_BROCHEdu décompte. C'est ce qui permet d'afficher « Broché (9) » comme option alternative même une fois le poche sélectionné, exactement comme un filtre de boutique en ligne classique. Il répond à : « combien y en aurait-il si je cochais celle-ci ? » - Entre groupes, le filtre s'applique — c'est pourquoi
LANGaffiche 12 et 5 au lieu de 25 et 5 : les langues comptées sont celles des seuls polars au format poche. La disjonction ne vaut qu'à l'intérieur du groupe filtré. - Le filtre est cumulatif partout — entre deux groupes différents comme à l'intérieur d'un même groupe.
filters: ["FORMAT_POCHE", "FORMAT_BROCHE"]demande les produits qui portent les deux annotations, pas l'une ou l'autre : sur un catalogue où un livre n'a qu'un format, cela rend zéro résultat, pendant que le décompte continue d'afficher 17 et 9. Mesuré sur le même catalogue. Il répond à : « lesquels satisfont tout ce qui est coché ? »
Ce n'est donc pas une contradiction, mais il faut l'avoir en tête avant de câbler une barre latérale à cases à cocher : cocher deux valeurs d'un même groupe vide la liste alors que les compteurs affichent des nombres positifs.
Pour exprimer un OU, côté Browse uniquement : filters=langue:fr|en — le tuyau sépare des alternatives à l'intérieur d'un champ métier, celui que vous fournissez à l'indexation, la virgule sépare des champs qui restent cumulatifs. Voir Browse. Côté recherche, le tuyau vaut pour les filtres de champ — filters: ["brand:Makita|Bosch"] rend l'une ou l'autre. Les annotations, elles, restent toujours cumulatives entre elles : ["FORMAT_POCHE", "FORMAT_BROCHE"] exige les deux, et il n'existe pas de syntaxe pour en demander une seule.
/v1/browse/{catalog}/{category}Browse & Discovery
Classe tous les produits d'une catégorie sans requête — pour une page de listing (catégorie, rayon), pas une barre de recherche. Remplace un tri statique par un classement configurable, enrichi par vos propres priorités manuelles.
{category} est un champ que vous fournissez à l'indexation — exactement comme name ou stock, jamais dérivé d'un pack de règles. Ajoutez categories (une liste — un produit peut appartenir à plusieurs catégories à la fois, utile pour lister aussi bien la catégorie que ses ancêtres) ou category (une valeur unique) à vos produits :
Pas envie d'écrire l'appel fetch vous-même ? Téléchargez le snippet prêt à l'emploi — un guide pas à pas avec exemple complet est disponible sur le blog.
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/items \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"items": [{"id": "sku-123", "name": "Perceuse GDX 18V-285",
"categories": ["outillage-electroportatif", "perceuses-visseuses"],
"price": 139.90, "margin": 28.50, "stock": 8}]}'
curl "https://api.heurix.fr/v1/browse/moncatalogue/perceuses-visseuses?sort=price_asc" \
-H "Authorization: Bearer VOTRE_CLE_API"
| Paramètre | Type | Description |
|---|---|---|
sort | string | stock (défaut) · recent · alphabetical · price_asc / price_desc (lit le champ de prix de la clé appelante, price par défaut — voir Prix par clé publique) · margin (lit le champ margin, la plus forte d'abord) · popular (voir ci-dessous) |
filters | string | "champ:valeur,champ2:valeur2" — affine sur n'importe quel attribut libre du produit (marque, couleur…), en plus de la catégorie. Tout champ texte non réservé devient automatiquement filtrable dès l'indexation, sans configuration.La virgule est un ET, le tuyau est un OU. marque:Makita|Bosch rend l'une ou l'autre ; marque:Makita,couleur:bleu exige les deux. Deux paires du même champ restent cumulatives — tags:promo,tags:destockage ne rend que les produits portant les deux étiquettes, ce qui a un sens sur un champ à valeurs multiples. |
facets | string | "champ,champ2" — renvoie un décompte par valeur pour chaque champ demandé, disjonctif (filtrer sur une valeur d'un champ ne fait pas disparaître les autres valeurs de ce même champ du décompte, comme côté recherche). |
limit / offset | int | Pagination, comme pour la recherche. |
in_stock_only | bool | Exclut les produits en rupture (défaut false), avant le calcul des facettes — un produit masqué ne pèse pas dans les décomptes. |
lang | string | Même filtre que côté recherche — voir Recherche. |
curl "https://api.heurix.fr/v1/browse/moncatalogue/perceuses-visseuses?filters=brand:Makita&facets=brand,color" \
-H "Authorization: Bearer VOTRE_CLE_API"
Un produit sans le champ demandé (pas de price pour un tri price_asc, par exemple) n'est jamais exclu ni ne fait planter l'appel — il se retrouve simplement en fin de liste, quel que soit le sens du tri.
Consomme le quota Browse, sur un compteur séparé de celui de la recherche — un appel de rayon ne rogne jamais votre volume de recherche.
Construire une page de rayon côté navigateur
browse accepte une clé publique (hxp_) : vous pouvez donc bâtir une page de catégorie entièrement en JavaScript, sans proxy serveur. C'est ce que fait la boutique de démonstration, dont le code est lisible dans demo-boutique.js.
// Cle PUBLIQUE (hxp_), restreinte a vos domaines via allowed_origins.
// Une cle serveur ici serait lisible par n'importe quel visiteur.
const CLE = "hxp_votre_cle_publique";
const CATALOGUE = "moncatalogue";
fetch(`https://api.heurix.fr/v1/browse/${CATALOGUE}/visserie?sort=popularity&limit=8`, {
headers: { Authorization: "Bearer " + CLE },
})
.then(r => r.ok ? r.json() : Promise.reject(new Error("HTTP " + r.status)))
.then(d => {
// browse renvoie des hits : le produit est dans h.product.
document.getElementById("grille").innerHTML =
(d.hits || []).map(h => renduFiche(h.product)).join("");
})
.catch(e => {
// Message de VITRINE pour le visiteur, detail technique en console.
document.getElementById("grille").textContent = "Rayon momentanement indisponible.";
console.error("Heurix browse:", e.message);
});
d.total donne le nombre de références du rayon, utile pour un compteur ou une pagination. Un 403 depuis un domaine non listé dans allowed_origins porte un message explicite nommant les domaines autorisés — c'est une aide au diagnostic, pas une fuite.
Tri par popularité (clics + achats)
sort=popular s'appuie sur les événements réellement remontés par le Heurix Tracker — un achat compte pour 5, un clic sur un résultat de recherche pour 1. Un produit très cliqué mais jamais encore acheté reste donc visible dans le classement plutôt que totalement absent tant qu'aucune vente n'est remontée. Sans aucune donnée de tracking, tous les produits reviennent simplement dans l'ordre par défaut — l'appel ne plante jamais.
Merchandising manuel : épingler ou reléguer un produit
Indépendant du tri naturel : un produit épinglé (pin) apparaît à la position choisie en tête de liste quel que soit son stock, son prix ou sa popularité ; un produit relégué (bury) passe systématiquement en fin de liste. Utile pour mettre en avant une nouveauté ou masquer discrètement un produit en fin de vie sans le désindexer.
curl -X POST https://api.heurix.fr/v1/browse/moncatalogue/perceuses-visseuses/overrides \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"product_id": "sku-123", "action": "pin", "position": 1}'
curl -X DELETE https://api.heurix.fr/v1/browse/moncatalogue/perceuses-visseuses/overrides/sku-123 \
-H "Authorization: Bearer VOTRE_CLE_API"
Ne consomme pas le quota — c'est une action de configuration, pas une requête. Une priorité posée sur un produit qui sort ensuite de la catégorie devient simplement sans effet, pas une erreur.
Boost ou relégation par attribut
Même principe que le merchandising manuel, mais pour tous les produits partageant un attribut plutôt qu'un par un — « boostez toute la marque Makita » plutôt qu'épingler chaque référence individuellement. Un boost ou une relégation par attribut positionne les produits concernés comme un groupe, trié entre eux selon le sort choisi ; ce n'est pas une position fixe (voir pin ci-dessus pour ça).
curl -X POST https://api.heurix.fr/v1/browse/moncatalogue/perceuses-visseuses/attribute-rules \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"field": "brand", "value": "Makita", "action": "boost"}'
curl -X DELETE "https://api.heurix.fr/v1/browse/moncatalogue/perceuses-visseuses/attribute-rules?field=brand&value=Makita" \
-H "Authorization: Bearer VOTRE_CLE_API"
Ordre de priorité en cas de conflit, du plus fort au plus faible : épinglé (par produit) > relégué (par produit ou par attribut) > boosté (par attribut) > tri naturel. Une relégation, qu'elle soit posée par produit ou par attribut, l'emporte toujours sur un boost — un produit explicitement caché ne doit jamais être resurfacé par une règle de boost plus large qui le concernerait par ailleurs.
/v1/federated-searchRecherche fédérée
Interroge plusieurs catalogues en un seul appel et fusionne les résultats par pertinence — utile pour un site multilingue (un catalogue par langue) ou multi-marque. Les poids de score sont globaux au moteur, pas propres à un catalogue : les scores restent comparables entre catalogues, même avec des packs de règles différents.
Corps de la requête
| Paramètre | Type | Description |
|---|---|---|
catalogs | array de string requis | 1 à 10 noms de catalogues à interroger. |
q | string | La requête de recherche. |
limit / offset | integer | Pagination sur l'ensemble fusionné. |
filters / facets | array de string | Mêmes formes que sur la recherche — annotations et champs métier mélangés. Le filtre s'applique catalogue par catalogue ; les décomptes de facettes sont sommés entre catalogues, parce qu'une même valeur dans deux boutiques désigne la même chose. |
lat / lon / radius_km | number | Mêmes règles et mêmes bornes que sur la recherche, y compris le plafond de 200 km et l'exclusion des produits sans position. |
Réponse
Identique à une recherche classique, avec un champ catalog ajouté à chaque hit pour identifier sa provenance, et catalogs_searched / catalogs_not_found pour repérer un nom de catalogue invalide.
Les deux clés de diagnostic sont ici par catalogue, parce qu'un champ présent dans l'un et absent de l'autre est le cas normal d'une fédération, pas l'exception. filters_unknown devient un objet — {"agences": [], "produits": ["departement"]}, les catalogues sans problème listés avec un tableau vide pour que vous lisiez d'un coup où votre filtre a porté — et radius_no_positions une liste des catalogues sans aucune coordonnée. Toutes deux restent absentes quand elles n'ont rien à dire.
Un filtre qui ne désigne rien dans un catalogue en écarte tous les résultats, sans écarter le catalogue de catalogs_searched. C'est ce que ces clés rendent lisible : la réponse dit avoir cherché dans trois catalogues et n'en sert qu'un, et vous savez pourquoi.
curl -X POST https://api.heurix.fr/v1/federated-search \
-H "Authorization: Bearer VOTRE_CLE_API" \
-d '{"catalogs": ["boutique-fr", "boutique-en"], "q": "pull rouge"}'
Comptage : 1 requête par catalogue effectivement interrogé — une fédération sur 3 catalogues compte pour 3 requêtes.
/v1/index/{catalog}/itemsIndexation
Ajoute ou met à jour des produits par lot (upsert sur id). Le catalogue est créé automatiquement s'il n'existe pas. Champ optionnel featured (booléen) : marque un produit comme candidat au secours affiché quand une recherche ne trouve rien — voir Recherche, secours sur zéro résultat.
Regrouper les résultats par famille
Sur un catalogue technique, une recherche large renvoie souvent des centaines de produits qui ne diffèrent que par une dimension. Mesuré sur un catalogue de 10 000 références : vis M8 inox renvoie 6 582 résultats, dont les premiers sont des M8×30, M8×80, M8×35, M8×6.
Regroupés, ces 6 582 résultats deviennent 52 familles — « Vis hexagonale inox A2, 265 produits » — classées par pertinence.
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/search \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"q": "vis M8 inox", "limit": 6, "group_by": "auto"}'
{
"total": 6582,
"familles": 52,
"groupes": [
{
"famille": "vis inox hex",
"etiquettes": ["FAM_VIS", "MAT_INOX", "TETE_HEX"],
"produits": 265,
"score": 60.0,
"representant": { "id": "REF-001360", "name": "Vis hexagonale M8x30 inox A4" },
"ids": ["REF-001360", "REF-001789"]
}
]
}
| Paramètre | Effet |
|---|---|
group_by: "auto" | Regroupe sur les étiquettes standards du pack — famille, matière, type de tête, coloris |
group_by: "FAM,MAT" | Regroupe sur les préfixes que vous choisissez |
Les dimensions sont volontairement exclues de la clé de famille : ce sont elles qui varient à l'intérieur d'une famille. Les inclure produirait une famille par produit.
Les familles sont ordonnées par le meilleur score de leurs membres, pas par leur nombre. Une famille pertinente mais peu fournie passe donc devant une famille fournie mais marginale.
Sans group_by, la réponse est inchangée : vos intégrations existantes ne voient aucune différence.
Dans le widget JavaScript
Le regroupement se configure par un seuil, pas par un interrupteur. Un visiteur qui tape une référence précise veut son produit, pas une famille — grouper systématiquement transformerait une recherche exacte en détour.
Heurix.searchBox({
apiKey: "hxp_votre_cle_publique",
catalog: "moncatalogue",
containerId: "ma-recherche",
// Regroupe uniquement au-dela de 50 resultats
groupThreshold: 50,
// Optionnel : que faire au clic sur une famille.
// Par defaut, la recherche s'affine avec le nom de la famille.
onSelectGroup: function (famille, requete) {
window.location = "/recherche?q=" + encodeURIComponent(requete + " " + famille.famille);
}
});
Trois niveaux d'intégration
Le temps d'intégration ne dépend pas de Heurix mais de ce que vous remplacez. Ces trois niveaux vont du plus léger au plus complet — rien n'oblige à commencer par le dernier.
| Niveau | Ce que vous remplacez | Ordre de grandeur |
|---|---|---|
| 1 — Autocomplétion | Le menu déroulant de votre barre de recherche | 1 jour |
| 2 — Liste d'identifiants | Quels produits et dans quel ordre. Votre gabarit, vos fiches et vos facettes restent les vôtres | 3 à 5 jours |
| 3 — Remplacement complet | Toute la page de résultats, avec nos facettes et notre analytique | 2 à 3 semaines |
Niveau 2 — le paramètre ids_only
Heurix décide quels produits et dans quel ordre. Votre plateforme affiche ses propres fiches. La réponse ne contient que l'essentiel — sur 50 résultats, quelques centaines d'octets au lieu de plusieurs dizaines de kilo-octets.
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/search \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"q": "M8x20 inox", "limit": 24, "ids_only": true}'
# {"total": 47, "ids": ["SKU-1234", "SKU-5678", ...]}
Le classement, la pagination et les filtres s'appliquent exactement comme sur la réponse complète. Seule la forme change.
Appliquer cet ordre dans Magento
Sur une page de catégorie, la fonction FIELD() de MySQL impose l'ordre exact d'une liste à une collection de produits :
// Plugin sur la collection de produits
$ids = $this->heurix->getOrderedIds($query); // appel ids_only
$collection->addIdFilter($ids);
$collection->getSelect()->order(
new \Zend_Db_Expr('FIELD(e.entity_id,' . implode(',', $ids) . ')')
);
Point de vigilance. Depuis Magento 2.4, la page de résultats de recherche ne passe plus par une collection SQL : elle transite par Elasticsearch ou OpenSearch. Un plugin de collection n'y produit donc aucun effet, et il faut passer par un adaptateur de recherche personnalisé. Les pages de catégorie, elles, restent en collection SQL — c'est pourquoi l'option Ranking s'intègre nettement plus vite que Search.
Selon votre plateforme
Toutes n'accueillent pas une liste d'identifiants avec la même facilité. L'écart est important, et il détermine votre temps d'intégration bien plus que Heurix lui-même.
| Plateforme | Point d'accroche | Nature | Ordre de grandeur |
|---|---|---|---|
| PrestaShop 1.7+ | ProductSearchProviderInterface |
Interface prévue pour cela. Le cœur accepte un tableau d'identifiants et hydrate fiches, facettes, pagination et tri. | 2 à 3 jours |
| WooCommerce | pre_get_posts avec post__in et orderby => post__in |
Détournement de la requête WordPress. Fonctionne, mais ce n'est pas un point d'extension conçu pour cela. | 3 à 4 jours |
| Magento 2.4+ — pages de catégorie | FIELD(e.entity_id, …) sur la collection |
Fonctionne : la collection reste en SQL. | 3 à 5 jours |
| Magento 2.4+ — page de résultats | Adaptateur de recherche personnalisé | La recherche transite par Elasticsearch : un plugin de collection n'y produit aucun effet. | 2 à 3 semaines |
| Shopify | — | Non vérifié à ce jour. Écrivez-nous plutôt que de suivre une recette approximative. | — |
PrestaShop — module officiel
Un module prêt à installer existe. Il implémente ProductSearchProviderInterface et n'exige aucune modification de votre thème. Il est en bêta, fourni sur demande : écrivez-nous pour recevoir le dossier de l'étape 1.
| Compatibilité | PrestaShop 8.x et 9.x. La branche 1.7 n'est pas prise en charge : le module refuse de s'installer en deçà de 8.0.0 |
|---|---|
| Dépendances | Aucune — cURL natif |
| Ce qu'il remplace | Les résultats de recherche uniquement. Thème, gabarits et filtres restent les vôtres |
| Repli | Recherche native de PrestaShop si Heurix ne répond pas |
Installation
- Copiez le dossier
heurixsearchdansmodules/de votre boutique - Back-office → Modules → installer « Heurix Search »
- Renseignez votre clé serveur (
hx_) et le nom de votre catalogue - Activez
Un prérequis à connaître
PrestaShop identifie ses produits par un entier (id_product). Votre catalogue Heurix doit donc être indexé avec ces identifiants, et non avec vos références internes.
Concrètement, le champ id envoyé à Heurix doit contenir 1360, pas REF-001360. Vous pouvez conserver votre référence métier dans le champ ref, qui est indexé avec le poids le plus fort.
{
"items": [
{
"id": "1360", // id_product PrestaShop
"ref": "REF-001360", // votre reference metier
"name": "Vis tete hexagonale M8x20 inox A2",
"price": 1.24,
"stock": 2485
}
]
}
Si vos identifiants ne sont pas numériques, le module les écarte et l'inscrit dans les journaux PrestaShop — vous verrez alors « aucun résultat » alors que Heurix en a trouvé. C'est le symptôme à reconnaître.
PrestaShop — l'interface, si vous préférez votre propre module
Un module implémentant ProductSearchProviderInterface suffit. Il reçoit la requête, appelle Heurix avec ids_only, renvoie les identifiants. PrestaShop fait le reste.
public function runQuery(ProductSearchContext $context, ProductSearchQuery $query)
{
$ids = $this->heurix->rechercher($query->getSearchString()); // ids_only
$resultat = new ProductSearchResult();
// Le coeur de PrestaShop complete les donnees manquantes
$resultat->setProducts(array_map(fn($id) => ['id_product' => $id], $ids));
$resultat->setTotalProductsCount(count($ids));
return $resultat;
}
WooCommerce — un détail à connaître
Le mécanisme fonctionne, mais WordPress continue sa propre recherche tant que le paramètre s reste défini. Il faut le retirer — ce qui vide au passage le libellé « Résultats pour : … » affiché en haut de page. Prévoyez un filtre sur le titre pour le rétablir.
add_action('pre_get_posts', function ($query) {
if (!is_admin() && $query->is_main_query() && $query->is_search()) {
$ids = heurix_rechercher(get_search_query()); // ids_only
$query->set('post_type', 'product');
$query->set('post__in', $ids ?: [0]); // [0] = aucun resultat
$query->set('orderby', 'post__in'); // preserve NOTRE ordre
$query->set('s', ''); // sinon WP refiltre
}
});
Importer un fichier depuis la console
Si vous exportez votre catalogue en CSV ou en XML depuis un ERP ou un PIM, aucun développement n'est nécessaire. La console détecte le format, analyse le fichier et l'envoie pour vous.
| Formats acceptés | CSV et XML, détectés automatiquement — un seul bouton de dépôt |
|---|---|
| Détection — CSV | Séparateur (;, ,, tabulation) et encodage, y compris Latin-1, fréquent sur les exports français |
| Détection — XML | L'élément qui se répète une fois par produit, reconnu automatiquement ; encodage lu depuis le fichier lui-même. Les espaces de noms (flux Google Shopping, g:id, g:price) sont gérés sans configuration |
| Correspondance | Proposée d'après vos en-têtes de colonnes (CSV) ou vos éléments et attributs (XML), modifiable, avec aperçu des valeurs |
| Nombres | 1,24 et 1 234,56 sont compris ; en XML, in stock / out of stock sont reconnus pour le stock |
| Découpage | Automatique en lots de 5 000 |
| Pack de règles | Recommandé d'après le contenu de votre fichier, avant l'import |
Un contrôle affiche, avant l'envoi, combien de lignes seraient retenues — une correspondance fausse se voit immédiatement plutôt qu'après coup.
Un point à décider avant votre premier import
L'identifiant que vous indexez doit être celui que vous utiliserez partout ailleurs. C'est la seule décision difficile à revenir en arrière.
Trois systèmes s'appuient sur cet identifiant :
- Le tracker de conversion. Il enregistre le
product_idque votre site lui envoie. Si vous indexez par référence métier —VIS-M8-INOX— mais que votre site envoie l'identifiant de sa plateforme —1360—, les deux ne se rencontreront jamais et vos analyses de ventes resteront vides. - Les modules de plateforme. PrestaShop, WooCommerce et Magento identifient leurs produits par un entier. Le paramètre
id_fieldpermet de garder votre référence métier comme identifiant tout en fournissant cet entier — voir ci-dessous. - Le serveur MCP. Il renvoie vos identifiants tels quels. Une référence métier y est même plus lisible pour un agent conversationnel.
La solution la plus souple : indexez avec votre référence métier — c'est elle que vos acheteurs tapent, et elle est fortement pondérée au classement — et ajoutez un champ platform_id (une colonne en CSV, un élément ou attribut en XML) contenant l'identifiant de votre plateforme. Vous disposez alors des deux.
Code Article;id_prestashop;Désignation;Prix HT;Qté dispo
VIS-M8-INOX;1360;Vis tête hexagonale M8x20 inox A2;1,24;2485
Dans l'écran de correspondance, associez Code Article à Identifiant et id_prestashop à Identifiant plateforme. En XML, le principe est identique : associez l'élément ou l'attribut correspondant, quel que soit son nom dans votre flux.
Catalogues volumineux : indexez par lots
Un appel accepte 5 000 produits au maximum. Au-delà, la réponse est un 422 qui vous indique en combien d'envois découper.
Les appels successifs s'ajoutent au même catalogue : l'ordre n'a pas d'importance, et rien n'est écrasé. Un produit déjà présent est mis à jour, pas dupliqué.
# Découper un catalogue de 10 000 produits en deux lots
python3 -c "
import json
produits = json.load(open('catalogue.json'))['items']
for i in range(0, len(produits), 5000):
with open(f'lot-{i//5000}.json', 'w') as f:
json.dump({'rulepack': 'outillage', 'items': produits[i:i+5000]}, f)
"
# Puis envoyer chaque lot
for lot in lot-*.json; do
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/items \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
--data-binary @$lot
done
Ordre de grandeur. Comptez environ une seconde par tranche de 10 000 produits pour l'indexation elle-même. L'envoi réseau dépasse généralement ce temps.
Changer le pack de règles d'un catalogue existant
Le pack se déclare au moment de l'indexation, dans le champ rulepack. Il n'existe pas d'endpoint pour le changer après coup, et ce n'est pas un oubli : les annotations sont calculées à l'indexation, pas à la recherche. Changer le pack sans réindexer laisserait vos produits avec les annotations de l'ancien.
Pour changer de pack, réimportez votre catalogue avec le nouveau nom de pack. Les produits existants sont mis à jour, rien n'est perdu.
Comptez environ 6 secondes pour 10 000 produits, une minute pour 100 000. Pendant ce temps, les recherches sur l'ensemble de votre instance sont mises en attente — choisissez une heure creuse pour un gros catalogue.
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/items \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{
"rulepack": "mode",
"items": [ ... vos produits ... ]
}'
Corps de la requête
| Paramètre | Type | Description |
|---|---|---|
items | array requis | 1 à 5 000 produits par appel. Chaque produit doit avoir un id. |
rulepack | string | Pack de règles à associer au catalogue — onze disponibles, voir la liste complète (outillage, mode, industrie, etc.). Changer de pack ré-indexe le catalogue. |
Réponse
{"indexed": 2, "catalog": {"catalog": "moncatalogue", "products": 2, "terms": 11,
"annotations": 9, "rulepack": "outillage", "synonym_groups": 0}}
/v1/index/{catalog}/items/{id}Suppression
Retire un produit de l'index et du stockage. Renvoie 404 si l'identifiant est inconnu.
curl -X DELETE https://api.heurix.fr/v1/index/moncatalogue/items/V1 \
-H "Authorization: Bearer VOTRE_CLE_API"
/v1/index/{catalog}/synonymsSynonymes
Les synonymes métier étendent la recherche : « vis » peut retrouver « boulon ». GET lit les groupes en place, PUT les remplace intégralement. Les groupes qui partagent un terme sont fusionnés automatiquement.
curl -X PUT https://api.heurix.fr/v1/index/moncatalogue/synonyms \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"groups": [["vis", "boulon", "visserie"], ["ecrou", "nut"]]}'
/v1/index/{catalog}/synonym-suggestionsSuggestions de synonymes
Propose des rapprochements pour un terme qui n'a rien trouvé, à partir du vocabulaire réel de votre catalogue — même distance d'édition que la tolérance aux fautes de la recherche elle-même, jamais une IA. Les jetons déjà trouvés, les références chiffrées (rapprocher deux produits différents serait le pire faux synonyme possible) et les mots de moins de 4 lettres sont exclus d'office. Ne crée jamais rien automatiquement — vous validez chaque candidat avant qu'il n'atterrisse dans l'endpoint synonymes.
curl -X GET "https://api.heurix.fr/v1/index/moncatalogue/synonym-suggestions?q=rondele" \
-H "Authorization: Bearer VOTRE_CLE_API"
→ {"query": "rondele", "suggestions": [
{"jeton": "rondele", "candidats": [
{"terme": "rondelle", "distance": 1, "produits": 340}
]}
]}
/v1/index/{catalog}/statsStatistiques
État du catalogue : nombre de produits, de termes indexés, d'annotations produites par la cascade, pack de règles actif, groupes de synonymes.
/v1/usageConsommation
Compteur de requêtes du mois en cours pour la clé appelante.
{"month": "2026-07", "requests": 1284}
/v1/rulepacksPacks de règles
Liste les packs disponibles sur votre instance, avec leur nombre de niveaux et de règles. Onze packs standards sont fournis, couvrant les verticales où les références sont structurées : outillage (visserie, normes DIN/ISO, matières), automobile (références OEM Bosch/MANN/Valeo, motorisation, côté de montage), électricité (calibres, courbes, sections, désignations harmonisées), plomberie (filetages 15x21, diamètres nominaux, matières), industrie (roulements, références normalisées), électronique, mode (tailles, coloris, matières), vins, livres, finance et sport (volumes en litres, tailles normalisées, plans de cordage). Des packs sur mesure pour votre catalogue peuvent être conçus en mission Simulateur ROI.
/v1/index/{catalog}/rulepack-suggestionSuggestion de pack automatique
Recommande le pack le plus adapté d'après le contenu réel de votre catalogue, pas le secteur déclaré à l'inscription — un catalogue de vêtements resté sur le pack outillage ne le sait jamais tout seul autrement. Annote un échantillon du catalogue avec chaque pack disponible et compare la couverture obtenue. Ne modifie jamais rien : la suggestion est renvoyée, changer de pack reste une action volontaire via l'endpoint d'indexation. Ne compte pas dans le quota — une consultation de compte, comme /v1/usage.
curl -X GET https://api.heurix.fr/v1/index/moncatalogue/rulepack-suggestion \
-H "Authorization: Bearer VOTRE_CLE_API"
→ {"classement": [
{"pack": "mode", "annotations_distinctes": 42, "produits_annotes": 380, "couverture_pct": 95.0},
{"pack": "outillage", "annotations_distinctes": 3, "produits_annotes": 12, "couverture_pct": 3.0}
], "recommande": "mode", "raison": "...",
"marge": {"second": "outillage", "produits_annotes": 31.67,
"annotations_distinctes": 14.0, "critere": "produits_annotes"}}
De combien le premier gagne, et quand il ne gagne pas
recommande peut valoir null, et c'est un résultat, pas une panne : aucun pack ne reconnaît vos références de façon significative, le pack déjà en place est le meilleur, l'écart avec lui est trop faible pour justifier une réindexation, ou deux packs sont à égalité. raison dit lequel de ces cas s'applique. Un client qui affiche la recommandation doit prévoir cette valeur nulle plutôt que de la traiter comme une absence de réponse.
marge dit de combien le premier devance son meilleur concurrent. second n'est pas le deuxième du classement : c'est le meilleur pack qui reconnaît quelque chose — comparer à un pack qui n'annote rien rendrait une division par zéro. Sans concurrent, les trois champs valent null ; un premier sans rival n'a pas de marge, et ce n'est pas la même chose qu'une marge nulle.
Le classement se fait sur deux critères dans cet ordre — produits_annotes d'abord, annotations_distinctes pour départager — et les deux ratios sont donnés parce qu'aucun des deux ne suffit seul. critere est la seule autorité sur l'égalité : les ratios sont arrondis pour l'affichage, critere est calculé sur les entiers. Quand il vaut null, les deux packs annotent exactement la même chose, le tri n'a rien départagé — le moteur refuse alors de recommander, et recommande vaut null lui aussi. Aucun seuil n'est appliqué sur la marge elle-même : le nombre vous est rendu, la décision reste la vôtre.
Avant l'indexation, sur un simple échantillon
/v1/rulepacks/suggestMême comparaison, mais avant tout import : on envoie un échantillon de produits et on obtient le même objet de réponse, sans catalogue indexé. C'est ce qui permet de choisir son pack avant le premier import plutôt qu'après — corriger après coup impose de tout réimporter. Au plus 300 produits sont examinés ; au-delà, le reste du corps est ignoré. Un échantillon vide est refusé par un 422. Ne compte pas dans le quota, et n'écrit rien : aucun catalogue n'est créé.
pack_actuel vaut toujours null dans cette réponse — il n'y a pas encore de catalogue, donc pas de pack en place à comparer.
curl -X POST https://api.heurix.fr/v1/rulepacks/suggest \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"items": [{"id": "1", "ref": "VIS-M8X20", "name": "Vis tête hexagonale M8x20 inox A2"},
{"id": "2", "ref": "ECR-M8", "name": "Écrou hexagonal M8 zingué"}]}'
→ {"classement": [
{"pack": "outillage", "annotations_distinctes": 9, "produits_annotes": 2, "couverture_pct": 100.0},
{"pack": "plomberie", "annotations_distinctes": 1, "produits_annotes": 1, "couverture_pct": 50.0}
], "recommande": "outillage", "raison": "aucun pack n'est configuré",
"pack_actuel": null, "echantillon": 2,
"marge": {"second": "plomberie", "produits_annotes": 2.0,
"annotations_distinctes": 9.0, "critere": "produits_annotes"}}
/v1/index/{catalog}/custom-rulesCustom Rules
Étend le pack de règles actif d'un catalogue, sans y toucher — une Custom Rule est propre à un seul catalogue, jamais partagée avec un autre compte. Deux gabarits, sans regex à écrire : keyword (une liste de mots équivalents déclenche tous la même étiquette) et prefix_number (un préfixe suivi d'un nombre devient une étiquette avec la valeur capturée, ex. RAL reconnaît « RAL 9010 » et « RAL9010 »). Effet immédiat : tous les produits déjà indexés sont ré-annotés dès la création de la règle. Jusqu'à 30 règles personnalisées par catalogue.
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/custom-rules \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"rule_type": "keyword", "label": "Cheville",
"keywords": ["placo", "cheville", "molly", "plaquo"]}'
curl -X POST https://api.heurix.fr/v1/index/moncatalogue/custom-rules \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"rule_type": "prefix_number", "label": "Coloris RAL", "prefix": "RAL"}'
Gérable aussi bien par API que directement depuis la console, dans « Mon catalogue ».
/v1/eventsConversion & ROI
Remonte les clics et achats de votre site vers Heurix, pour mesurer le taux de clic sur vos recherches et le chiffre d'affaires — voire la marge — qui en découle. Deux types d'événement : search_click (un clic sur un résultat de recherche) et purchase (un ou plusieurs produits achetés, avec montant et marge optionnelle par produit).
curl -X POST https://api.heurix.fr/v1/events \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"event_type": "search_click", "catalog": "moncatalogue",
"query": "cheville placo", "product_id": "sku-123"}'
curl -X POST https://api.heurix.fr/v1/events \
-H "Authorization: Bearer VOTRE_CLE_API" \
-H "Content-Type: application/json" \
-d '{"event_type": "purchase", "catalog": "moncatalogue",
"products": [{"id": "sku-123", "amount": 29.90, "margin": 8.50}]}'
Cet endpoint est conçu pour être appelé depuis le navigateur du visiteur final, sur votre propre site — pas depuis heurix.fr — d'où un CORS ouvert à toutes origines. Trois façons de le poser, de la plus complète à la plus simple :
- Heurix Tracker (recommandé) — un identifiant visiteur persistant, pour relier un clic à un achat ultérieur du même visiteur plutôt qu'une simple corrélation agrégée. Un seul script à poser site-wide, expose les mêmes fonctions que le snippet ci-dessous.
- Modèle de balise Google Tag Manager (fichier
.tpl, à importer dans Modèles → Modèles de balises → Importer) — compatible avec le tracker via un champ « Identifiant visiteur » optionnel - Snippet JavaScript simple, sans identifiant persistant — pour une intégration minimale sans GTM ni tracker
Ne consomme pas votre quota de requêtes — ce n'est pas un appel moteur, c'est une remontée de données.
À savoir avant de lire vos chiffres : même avec le tracker, l'attribution recherche → achat n'est pas une garantie à 100 %. Sans identifiant visiteur, Heurix agrège les clics et achats reçus sur une même période et un même compte (total_revenue) — une corrélation, pas une preuve. Avec le tracker installé, un champ supplémentaire (attributed_revenue) ne compte que les achats où le même visiteur a cliqué depuis une recherche dans les 24 heures précédentes — un lien réel, nettement plus fiable, mais qui dépend toujours de la qualité de votre implémentation.
/v1/analytics/conversion-summaryTaux de clic, chiffre d'affaires et marge attribués sur la période (?days=30 par défaut). Renvoie aussi attributed_revenue et attributed_products — null si aucun événement de la période ne porte d'identifiant visiteur (le tracker n'est pas installé), un chiffre réel sinon.
/v1/analytics/top-productsProduits les plus achetés, triés par volume ou par marge (?sort_by=volume ou margin).
Vues de page de catégorie
Le tracker peut remonter les produits affichés sur une page de catégorie, ce qui permet de repérer ceux qui sont beaucoup montrés et peu cliqués — la population que l'épinglage et la relégation servent justement à corriger.
Heurix.trackCategoryView("visserie", ["sku-1", "sku-2", "sku-3"]);
Un seul appel par page vue, portant la liste des produits affichés — pas un appel par produit. Le moteur enregistre ensuite une impression par produit, ce qui rend l'agrégation possible en une requête. À savoir sur le volume : une page affichant 24 produits génère 24 lignes, purgées automatiquement au-delà de la période de rétention. La liste est plafonnée à 50 identifiants.
/v1/analytics/category-views/{catalog}Renvoie les catégories triées par nombre d'impressions, et dans chacune les produits les plus vus avec leur nombre de clics depuis la recherche. Ne consomme pas votre quota.
{"catalog": "moncatalogue", "categories": [
{"category": "visserie", "total_views": 512, "products": [
{"product_id": "sku-1", "views": 180, "search_clicks": 12},
{"product_id": "sku-2", "views": 175, "search_clicks": 0}
]}
]}
Renvoie une liste vide tant que le tracker ne remonte pas cet événement — c'est le cas de toute intégration antérieure à cette fonctionnalité. Rien ne casse, l'écran reste simplement vide.
Produits fréquemment achetés dans le même panier que {product_id} — une co-occurrence comptée, pas du machine learning. Utile pour une section « souvent acheté avec » sur une fiche produit.
{"product_id": "sku-123", "target_purchases": 47, "related": [
{"product_id": "sku-456", "co_purchases": 12, "name": "Rondelle plate M8 inox", "price": 0.12},
{"product_id": "sku-789", "co_purchases": 4, "name": "Clé plate 13mm", "price": 4.90}
]}
target_purchases donne le total d'achats du produit demandé sur la période — de quoi calculer un pourcentage plutôt que d'afficher un compte isolé (« 12 sur 47 achats », plus lisible que « 12 »). name et price sont résolus depuis votre catalogue actuel ; null si le produit associé a été retiré du catalogue depuis.
Liste vide si moins de 5 achats sont enregistrés pour ce produit précis — pas assez de signal pour être fiable, jamais une erreur. Nécessite le Heurix Tracker pour un résultat pertinent sur un site à trafic élevé : sans identifiant visiteur, deux achats de clients différents survenus presque au même instant peuvent, rarement, être confondus avec un seul panier.
/v1/analytics/intent-score/{catalog}/{visitor_id}Un score d'intention par visiteur — à distinguer de la popularité sur la recherche, qui est par produit. Explicable par conception : chaque composante est renvoyée séparément, jamais fondue dans un seul nombre qu'on ne pourrait pas justifier.
{"score": 0.72, "nb_recherches": 12, "nb_achats": 2,
"composantes": {
"precision_recherche": 0.83,
"taux_clic": 0.58,
"panier_moyen": 45.20
}}
precision_recherche : proportion des recherches de ce visiteur qui portent une référence chiffrée (« M8x20 ») plutôt qu'un mot générique (« vis »). taux_clic : proportion de ces mêmes recherches ayant mené à un clic sur un résultat. score est leur moyenne — les deux seuls signaux qui le composent, sur la même échelle 0 à 1. panier_moyen (en euros, null sans achat) est renvoyé à part, à titre de contexte : aucune échelle commune évidente avec les deux autres signaux, le fondre arbitrairement dans le score aurait été une décision impossible à justifier.
Visiteur jamais vu, ou sans recherche sur la période : score à 0.0, jamais une erreur. Ne consomme pas votre quota.
/v1/analytics/segmentation/{catalog}?days=30Répartit tous les visiteurs actifs d'un catalogue en trois tranches d'intention (même formule que le score d'intention ci-dessus), comparée à la période précédente de même durée.
{"period_days": 30,
"courant": {"total_visiteurs": 142, "repartition": {"fort": 38, "moyen": 71, "faible": 33}},
"precedent": {"total_visiteurs": 98, "repartition": {"fort": 19, "moyen": 58, "faible": 21}},
"variations": {"total_visiteurs": 44.9, "fort": 100.0, "moyen": 22.4, "faible": 57.1}}
variations vaut null plutôt qu'un pourcentage quand la période précédente était vide — passer de 0 à 5 n'est pas une progression de "500 %", c'est un démarrage ; mieux vaut ne rien afficher qu'un chiffre trompeur. Les comptes sont agrégés, jamais une liste nominative : Heurix ne connaît le visiteur que par un identifiant pseudonyme, jamais un nom ni un email. Recalculé au maximum une fois par heure — un signal de comportement agrégé qui n'a pas besoin d'être plus frais que ça.
heurix-search.jsBarre de recherche prête à l'emploi
Pas envie de coder l'appel fetch, le rendu des résultats et la gestion des facettes vous-même ? Une bibliothèque JavaScript autonome (zéro dépendance) affiche une barre de recherche complète — saisie, résultats en direct, facettes cliquables, navigation clavier — connectée à votre catalogue en quelques lignes.
Téléchargez heurix-search.js — un seul fichier, à poser où vous voulez sur votre site.
Utilisez une clé publique (hxp_) — ce script tourne dans le navigateur du visiteur, où la clé est lisible par tous. Une clé serveur donnerait accès à votre facturation.
Intégration minimale
<div id="ma-recherche"></div>
<script src="heurix-search.js"></script>
<script>
Heurix.searchBox({
apiKey: "VOTRE_CLE_PUBLIQUE",
catalog: "moncatalogue",
containerId: "ma-recherche"
});
</script>
C'est suffisant pour une barre de recherche fonctionnelle — saisie avec anti-rebond intégré, résultats affichés dès 2 caractères, tolérance aux fautes de frappe (gérée côté moteur, rien à faire ici), secours automatique sur zéro résultat si votre catalogue a des produits featured.
Options de configuration
| Option | Type | Description |
|---|---|---|
apiKey, catalog, containerId | string | Seules options obligatoires. |
facets | string[] | Champs à proposer en filtres cliquables (ex. ["brand", "color"]). Absent par défaut — aucune facette affichée tant que vous ne les listez pas explicitement. |
accentColor | string | Une couleur d'accent (ex. "#2952E3") pour le focus et les filtres actifs — personnalisation minimale par design, pas une refonte CSS complète. |
placeholder | string | Texte du champ de recherche. Défaut : « Rechercher… ». |
minChars | number | Nombre de caractères avant de déclencher une recherche. Défaut : 2. |
debounceMs | number | Délai d'anti-rebond en millisecondes. Défaut : 200. |
limit | number | Nombre maximal de résultats affichés. Défaut : 8. |
renderItem | function(hit) | Personnalise le HTML de chaque résultat. Reçoit un hit complet (voir la référence de recherche) ; par défaut affiche nom, référence, prix et rupture de stock. |
resultHref | function(hit) → string | Si fourni, chaque résultat devient un lien <a> vers l'URL renvoyée (ex. la fiche produit), plutôt qu'un simple élément cliquable. |
seeAllHref | function(query, total) → string | Le panneau reste volontairement compact (8 résultats par défaut). Si fourni, un lien « Voir les N résultats → » apparaît en pied de liste vers votre page de résultats complets. Sans cette option, le total s'affiche quand même, en texte simple. |
onSelect | function(hit) | Appelé au clic ou à la validation clavier (Entrée) sur un résultat — utile si vous gérez la navigation vous-même plutôt que via resultHref. |
timeoutMs | number | Délai au-delà duquel l'appel est abandonné, en millisecondes. Défaut : 3000. Un visiteur attend devant sa page : mieux vaut lui rendre la main que le laisser patienter. 0 désactive le délai. |
fallbackHref | function(query) → string | Si Heurix ne répond pas, le panneau propose au visiteur de poursuivre sur l'URL renvoyée — typiquement votre propre page de résultats. Sans cette option, le panneau affiche quand même un bouton « Réessayer » : il ne reste jamais un cul-de-sac. |
baseUrl | string | Défaut : https://api.heurix.fr. À ne changer que pour un environnement de test. |
Exemple complet, avec facettes et lien produit
Heurix.searchBox({
apiKey: "hxp_votre_cle_publique",
catalog: "moncatalogue",
containerId: "ma-recherche",
facets: ["brand"],
accentColor: "#C0392B",
resultHref: function (hit) {
return "/produits/" + hit.product.id;
}
});
Chaque recherche via ce widget consomme le quota normal de la clé utilisée, exactement comme un appel fetch direct au point d'entrée de recherche — ce n'est qu'une façade, pas un nouveau mécanisme de facturation.
Le style visuel fourni reste volontairement minimal — fonctionnel et lisible, pensé pour être facilement personnalisé (chaque élément a une classe CSS préfixée hx-) plutôt que pour correspondre à une charte graphique précise dès cette version.
@heurix-site/clientClient officiel TypeScript/JavaScript
Pour un projet Node.js, un build TypeScript, ou simplement préférer un module à un fetch écrit à la main : le client officiel couvre la recherche, Browse & Discovery, l'indexation et les synonymes. Zéro dépendance runtime (utilise fetch natif), types complets.
npm install @heurix-site/client
import { HeurixClient } from "@heurix-site/client";
const client = new HeurixClient({
apiKey: "hxp_votre_cle_publique",
catalog: "monsite",
});
const results = await client.search("vis m8 inox");
console.log(results.hits);
Sans étape de build, un import direct depuis un CDN fonctionne aussi bien :
<script type="module">
import { HeurixClient } from "https://cdn.jsdelivr.net/npm/@heurix-site/client@latest/dist/index.js";
</script>
Même règle que partout ailleurs côté navigateur : une clé publique (hxp_), jamais votre clé serveur, dans du code qui s'exécute chez le visiteur.
Un seul nom à retenir : @heurix-site/client. Le paquet heurix-client (sans organisation) a existé brièvement pendant la mise au point et est aujourd'hui déprécié — s'il apparaît dans un résultat de recherche npm plus ancien, ignorez-le.
Serveur Heurix pour agents IAInterroger Heurix depuis Claude Desktop, Cursor, ou un agent interne
Un serveur MCP (Model Context Protocol) qui expose la recherche et Browse comme des outils qu'un agent IA sait appeler nativement — un collaborateur peut demander « est-ce que j'ai des vis M8 en stock ? » en langage naturel, sans jamais toucher à l'API. Trois outils : heurix_search, heurix_browse, heurix_catalog_stats.
Prérequis : Python 3.10 ou plus, installé sur la machine où tourne l'agent (votre ordinateur, pas un serveur) — le serveur MCP se lance localement, à la demande, par Claude Desktop ou Cursor eux-mêmes.
Téléchargez le serveur MCP — installation et configuration détaillées (Claude Desktop et Cursor) sur le guide dédié.
pip install -r requirements.txt
HEURIX_API_KEY=hx_votre_cle python3 server.py
La clé API vit uniquement dans la configuration locale du client MCP (un fichier JSON sur la machine de l'utilisateur), jamais transmise en clair dans un appel d'outil. Chaque appel consomme le quota normal de la clé utilisée — heurix_search et heurix_browse comptent comme n'importe quel appel de recherche ou de Browse, heurix_catalog_stats non.
/healthSanté du service
Liveness, sans authentification — pratique pour vos sondes de supervision.
{"status": "ok", "catalogs": 3}
Glossaire
Le vocabulaire propre à Heurix, dans l'ordre où une requête le traverse : d'abord indexer un catalogue, puis comprendre ce qu'un visiteur tape, puis classer les résultats, enfin suivre et ajuster. Pour les termes plus généraux du secteur (index, facette, ranking...), voir aussi le glossaire du search e-commerce sur le blog.
Indexer un catalogue
Catalogue. Un ensemble de produits indexés sous un même nom, isolé des autres. Un compte peut posséder plusieurs catalogues — une langue, une marque, un environnement de test.
Règle. Un mécanisme de reconnaissance : une expression régulière, une liste de mots-clés, ou un patron préfixe + nombre qui repère un motif dans un texte. Une règle ne modifie rien — elle produit une annotation quand elle reconnaît quelque chose.
Annotation. L'étiquette posée sur un produit quand une règle a reconnu quelque chose dans son texte (référence, nom, description). DIAM_M8, FAM_VIS sont des annotations. Un même produit en accumule plusieurs, une par règle déclenchée.
Pack de règles. Un ensemble de règles prêtes à l'emploi pour un secteur (outillage, mode, électronique...). Choisi à l'indexation, il détermine quelles annotations un catalogue produit automatiquement.
Custom Rule (règle personnalisée). Une règle ajoutée par vous, propre à un seul catalogue, pour un vocabulaire qu'aucun pack ne pouvait deviner. Deux formats : mot-clé → étiquette, ou préfixe + nombre → étiquette.
Comprendre une requête
Tolérance aux fautes de frappe. La capacité à retrouver un produit malgré une faute d'une ou deux lettres. Le nombre de fautes tolérées dépend de la longueur du mot tapé : plus il est court, plus l'écart accepté est faible, pour éviter de confondre deux mots courts sans rapport.
Distance d'édition. La mesure derrière la tolérance aux fautes : le nombre minimal d'ajouts, de suppressions ou de substitutions de lettres pour transformer un mot tapé en un mot connu. « tshi » → « tshirt » : distance 2.
Correspondance floue (fuzzy matching). Le processus qui accepte un mot tapé comme correspondance d'un mot du catalogue tant que leur distance d'édition reste dans la tolérance autorisée.
Préfixe. Une requête encore en train d'être tapée. Contrairement à la tolérance aux fautes, qui suppose un mot complet et possiblement mal orthographié, la recherche par préfixe reconnaît qu'un mot incomplet mène sans ambiguïté vers un mot plus long du catalogue.
Synonyme. Une déclaration d'équivalence entre plusieurs mots libres (« vis » = « boulon » = « screw »). Contrairement à une annotation, un synonyme n'étiquette rien sur le produit : il élargit ce qu'une requête peut atteindre, au moment de la recherche.
Facette. Un filtre construit automatiquement à partir d'un champ du catalogue (marque, couleur, matière), pour affiner une liste de résultats sans retaper de requête.
Classer les résultats
Pertinence (score). La note attribuée à chaque résultat pour déterminer son rang. Elle combine la nature de la correspondance — exacte, floue, par préfixe, par synonyme — et son poids : un mot trouvé exactement pèse toujours plus qu'un mot retrouvé après tolérance aux fautes.
Ranking (classement). L'ordre final dans lequel les résultats sont présentés, une fois la pertinence calculée et les règles de priorité appliquées.
Priorité de requête (Search Override). Une règle manuelle qui épingle ou relègue un produit précis pour une requête précise (« sur "promo", toujours montrer X en premier »). Indépendante du classement naturel.
Browse & Discovery. Le classement des produits sur une page de catégorie, sans requête de recherche associée. Fonctionne avec ses propres priorités et boosts, indépendamment de la recherche.
Boost / relégation par attribut. Une règle qui met en avant ou recule tous les produits partageant un attribut commun (une marque, une couleur), sans les cibler un par un. Une relégation individuelle garde toujours la priorité sur un boost par attribut.
Regroupement par famille. Le rassemblement de résultats très proches (les mêmes vis en douze longueurs) sous une entrée représentative, pour qu'une requête large reste lisible plutôt que noyée sous des variantes.
Suivre et ajuster
Requête à zéro résultat. Une recherche qui n'a rien trouvé. Chaque occurrence est un signal direct : soit un synonyme manque, soit le produit cherché n'existe pas au catalogue.
Champ matched. La liste, incluse dans chaque résultat, des termes et annotations qui l'ont fait matcher. C'est l'outil de diagnostic : si un résultat surprend, ce champ montre pourquoi.
Clé API (serveur / publique). La clé serveur autorise tout, y compris l'indexation — elle ne doit jamais apparaître dans une page web. La clé publique ne peut que chercher et consulter le catalogue : c'est la seule à poser dans le code d'un site.
Bac à sable (sandbox). Un catalogue marqué comme test : il n'est pas facturé et ses recherches n'apparaissent pas dans les statistiques. Utile pour essayer une configuration sans toucher à de vraies données.
La cascade d'annotations
C'est le cœur du moteur. Un pack de règles est organisé en niveaux : le niveau 1 applique des expressions régulières sur le texte normalisé et produit des annotations ; les niveaux suivants s'appliquent sur le flux d'annotations et les composent. Documents et requêtes traversent la même cascade, la correspondance se fait donc dans un espace partagé :
« m8x20 inox » « M8 x 20 — A2 » (fiche produit)
│ │
▼ niveau 1 ▼ niveau 1
DIAM_M8, LONG_20, MAT_INOX DIAM_M8, LONG_20, MAT_INOX
│ │
▼ niveau 2 ▼ niveau 2
VIS_M8X20 ────────── correspondance ────── VIS_M8X20
Chaque annotation partagée pèse fortement dans le score — c'est ce qui fait passer la bonne référence en tête même quand la forme écrite diffère. Le champ matched de chaque résultat rend ces annotations visibles.
Bonnes pratiques
Remplissez ref, name et description distinctement. C'est la pondération entre ces trois champs qui fait la qualité du tri — tout concaténer dans name aplatit la pertinence.
Gardez les références telles quelles. N'essayez pas de « nettoyer » vos références produits avant indexation (retirer les espaces, les tirets...) : la normalisation et la cascade s'en chargent, et la forme d'origine reste disponible pour l'affichage.
Indexez par lots de 1 000 à 5 000. Un lot = un appel réseau ; trop petit multiplie les appels, la limite est à 5 000 par appel.
Envoyez le stock à chaque mise à jour. Le tri à pertinence égale repose dessus ; un stock à jour évite de mettre en avant des produits indisponibles.
Un catalogue par langue. Si votre boutique existe en français et en anglais, créez boutique-fr et boutique-en — les synonymes et les règles restent cohérents par langue.
Utilisez matched pour régler la pertinence. Chaque résultat explique pourquoi il sort : si un produit remonte à tort, ce champ montre quel terme ou quelle annotation l'a fait matcher — c'est votre outil de diagnostic.
Codes d'erreur
| Code | Signification | Que faire |
|---|---|---|
401 | En-tête Authorization absent ou mal formé | Envoyez Authorization: Bearer <clé> |
403 | Clé API invalide | Vérifiez la clé ; contactez-nous si elle a été révoquée |
404 | Catalogue ou produit introuvable | Vérifiez le nom du catalogue et l'id — le catalogue est créé au premier appel d'indexation, pas avant |
422 | Corps de requête invalide | Le détail de l'erreur indique le champ en cause (ex : produit sans id) |
5xx | Erreur côté serveur | Réessayez ; si l'erreur persiste, écrivez à contact@heurix.fr |
Les messages d'erreur sont en français. Le code HTTP et la structure de la réponse sont le contrat stable et documenté — branchez-vous dessus, pas sur le texte de detail, qui est une phrase écrite pour la personne qui lit la réponse brute et peut être reformulée. Sur un 422, detail est la liste des erreurs de validation : loc nomme le champ en cause et msg porte le message du validateur, en anglais pour tout ce que le schéma lui-même refuse.
Limites
| Requête de recherche | 500 caractères maximum |
| Résultats par page | 100 maximum (limit) |
| Indexation | 5 000 produits par appel — envoyez plusieurs lots pour un catalogue plus grand |
| Groupes de synonymes | 2 000 par catalogue |
| CORS | Ouverts aux clés publiques (hxp_) sur les endpoints de recherche, de parcours et de conversion — c'est le mode d'intégration prévu pour le widget. Restreignez-les à vos domaines avec allowed_origins. Les clés serveur (hx_) ne doivent jamais partir d'un navigateur : elles donnent accès à l'indexation et à la facturation. |
Un besoin au-delà de ces limites ? Parlons-en — c'est le rôle du plan Scale et des configurations dédiées.