Recherche WordPress : natif, BM25, embeddings et hybride
WordPress

Recherche WordPress : natif, BM25, embeddings et hybride

Ce que fait vraiment la recherche native de WordPress, quand passer à BM25, aux embeddings ou à l'hybride, où stocker les vecteurs, et à quel coût.

Mis à jour le 1er novembre 2026. Article réécrit : requête réelle de la recherche native sur WordPress 7.1.3, comparaison des moteurs, support vectoriel de MariaDB et MySQL, coût d’indexation aux tarifs du 1er novembre 2026, code de fusion testé.

Beaucoup de projets de « recherche IA » sur WordPress corrigent en réalité deux défauts du moteur intégré : il exige tous les mots saisis et ignore les synonymes. Cet article part de la requête SQL que WordPress génère, puis décrit les étapes possibles : un index lexical classé par BM25, une recherche vectorielle, puis la combinaison des deux.

Ce que fait la recherche native de WordPress

Nous avons fait générer la requête d’une recherche « veste pluie » par WP_Query sur notre installation de test en WordPress 7.1.3, sans rien modifier sur le site. Voici la requête obtenue, raccourcie :

SELECT wp_posts.ID FROM wp_posts
WHERE 1=1
  AND (((post_title LIKE '%veste%') OR (post_excerpt LIKE '%veste%') OR (post_content LIKE '%veste%'))
  AND ((post_title LIKE '%pluie%') OR (post_excerpt LIKE '%pluie%') OR (post_content LIKE '%pluie%')))
  AND (post_password = '')
  AND ((post_type = 'post' AND (post_status = 'publish')))
ORDER BY (CASE WHEN post_title LIKE '%veste pluie%' THEN 1
               WHEN post_title LIKE '%veste%' AND post_title LIKE '%pluie%' THEN 2
               WHEN post_title LIKE '%veste%' OR post_title LIKE '%pluie%' THEN 3
               WHEN post_excerpt LIKE '%veste pluie%' THEN 4
               WHEN post_content LIKE '%veste pluie%' THEN 5
               ELSE 6 END), post_date DESC
LIMIT 0, 10

La lecture du code de class-wp-query.php confirme ce que montre cette requête :

Les quatre niveaux de recherche

NiveauExemple d’outilCe qu’il corrigeCe qu’il coûte
NatifWP_Query et LIKERien à installerMots obligatoires, table parcourue
Index lexical en baseRelevanssi 4.28.8Pertinence tf*idf, correspondance partielle, champs personnalisésIndex d’environ trois fois la taille de wp_posts, selon l’éditeur
Moteur externe BM25ElasticPress 5.3.5 et ElasticsearchBM25, filtres, autocomplétionUn service à héberger, ou un abonnement
Vectoriel puis hybrideModèle d’embeddings et index vectorielSynonymes, langage naturelAppels au modèle, stockage, réindexation

Versions relevées sur wordpress.org le 1er novembre 2026 : Relevanssi 4.28.8 (30 octobre 2026), ElasticPress 5.3.5 (10 septembre 2026). Aucune des deux pages ne mentionne de recherche vectorielle.

BM25 : le score lexical de référence

BM25 attribue à chaque document un score qui augmente avec la fréquence des termes de la requête, plafonne cette fréquence (paramètre k1) et corrige l’effet de la longueur du document (paramètre b). Les termes rares pèsent plus que les termes courants. C’est l’algorithme de similarité par défaut d’Elasticsearch, avec k1 = 1,2 et b = 0,75.

BM25 excelle sur les références, les codes et les noms propres, mais ne voit pas les synonymes. Pour un blog de quelques centaines d’articles, un index lexical comme Relevanssi apporte souvent l’essentiel du gain, sans service externe.

Embeddings : le score sémantique

Un modèle d’embeddings transforme un texte en vecteur ; deux textes de sens voisin donnent des vecteurs proches, mesurés le plus souvent par la similarité cosinus. La recherche vectorielle retrouve « tenue de pluie » pour « veste imperméable », mais rate parfois une référence exacte comme « VX-210 ».

Deux règles d’exploitation : les vecteurs de deux modèles ne sont pas comparables, donc changer de modèle oblige à tout recalculer ; et un article long se découpe en passages, chacun vectorisé avec l’identifiant de l’article, qui reçoit le score de son meilleur passage.

Fusionner les deux classements

Pour démarrer, RRF est le choix le plus robuste.

Un exemple testé

Ce script n’a aucune dépendance. Les vecteurs sont fictifs ; en production, ils viennent de votre modèle d’embeddings.

import math, re
from collections import Counter

def jetons(texte):
    return re.findall(r"\w+", texte.lower())

class BM25:
    def __init__(self, documents, k1=1.2, b=0.75):
        self.docs = [jetons(d) for d in documents]
        self.k1, self.b, self.n = k1, b, len(self.docs)
        self.moy = sum(len(d) for d in self.docs) / self.n
        self.df = Counter(t for d in self.docs for t in set(d))

    def scores(self, requete):
        resultat = []
        for doc in self.docs:
            tf, s = Counter(doc), 0.0
            for t in jetons(requete):
                f = tf.get(t, 0)
                if f:
                    idf = math.log(1 + (self.n - self.df[t] + 0.5) / (self.df[t] + 0.5))
                    norme = 1 - self.b + self.b * len(doc) / self.moy
                    s += idf * f * (self.k1 + 1) / (f + self.k1 * norme)
            resultat.append(s)
        return resultat

def cosinus(a, b):
    return sum(x * y for x, y in zip(a, b)) / (math.hypot(*a) * math.hypot(*b))

def rrf(listes_de_scores, k=60):
    total = Counter()
    for scores in listes_de_scores:
        # Un document sans score positif ne figure pas dans la liste.
        ordre = sorted((i for i, s in enumerate(scores) if s > 0), key=lambda i: -scores[i])
        for rang, doc in enumerate(ordre, start=1):
            total[doc] += 1 / (k + rang)
    return total.most_common()

textes = ["Veste imperméable de randonnée, référence VX-210",
          "Tenue de pluie légère pour la marche",
          "T-shirt de running respirant"]
vecteurs = [[0.9, 0.1, 0.0], [0.8, 0.3, 0.1], [0.1, 0.9, 0.2]]
lexical = BM25(textes).scores("veste pluie")
semantique = [cosinus([0.85, 0.2, 0.05], v) for v in vecteurs]
print(rrf([lexical, semantique]))

Exécuté avec Python 3.12, il donne des scores BM25 de 0,94, 0,94 et 0 : chaque document ne contient qu’un des deux mots, et la recherche native n’en aurait renvoyé aucun. La fusion classe la veste, la tenue de pluie, puis le t-shirt, retenu par le seul signal sémantique.

Où stocker les vecteurs

OptionÉtat relevé le 1er novembre 2026À retenir
MariaDBType VECTOR et index VECTOR INDEX (HNSW modifié) stables depuis la version LTS 11.8, apparus en 11.7L’index n’est utilisé qu’avec ORDER BY VEC_DISTANCE_*(…) croissant et un LIMIT ; un simple seuil en WHERE parcourt toute la table
MySQL 9.7Type VECTOR (jusqu’à 16 383 dimensions) disponibleLa fonction DISTANCE() est réservée à HeatWave sur OCI et à MySQL AI : elle n’existe pas dans les éditions Community et Commercial
Elasticsearch ou OpenSearchChamps vectoriels et recherche hybride dans le même moteurBonne option si vous utilisez déjà ElasticPress
Fichier ou mémoire dans un service à partSuffisant pour quelques milliers de passagesSimple à prototyper, à reconstruire au démarrage

Le point MySQL compte pour la plupart des hébergements mutualisés : stocker des vecteurs y est possible, mais calculer la distance en SQL ne l’est pas. Si votre hébergeur fournit MariaDB 11.8 ou plus récent, la recherche vectorielle peut rester dans la base du site ; sinon, prévoyez un service séparé. Pour les besoins serveur d’un tel service, voir notre guide de performance serveur des CMS.

Ce que coûte l’indexation

Ordre de grandeur pour un blog de 2 000 articles d’environ 1 000 jetons chacun, soit 2 millions de jetons, avec les tarifs relevés le 1er novembre 2026 :

ServiceTarif publiéIndexation initiale
Google, gemini-embedding-001 (offre payante)0,15 $ par million de jetons, 0,075 $ en lotEnviron 0,30 $
Cloudflare Workers AI, @cf/baai/bge-m31 075 neurones par million de jetons ; 10 000 neurones gratuits par jourEnviron 2 150 neurones, dans l’allocation gratuite
Modèle exécuté sur votre serveurAucun tarif à l’appelTemps de calcul de votre machine, à mesurer

Le coût d’appel est rarement le problème ; les vrais coûts sont l’hébergement de l’index, la latence ajoutée à chaque recherche et la maintenance. Attention aux offres gratuites : selon les conditions de l’API Gemini, les données envoyées au niveau gratuit servent à améliorer les produits de Google, sauf pour les utilisateurs de l’Espace économique européen, de la Suisse et du Royaume-Uni, à qui s’appliquent les conditions de l’offre payante.

Brancher le résultat sur WordPress

Le plus simple : un service externe renvoie des identifiants classés, que WordPress affiche dans cet ordre. Ce code se place dans une extension maison :

function genisoft_ids_hybrides( $requete ) {
    $reponse = wp_remote_post( 'http://127.0.0.1:8081/recherche', array(
        'timeout' => 1.5,
        'headers' => array( 'Content-Type' => 'application/json' ),
        'body'    => wp_json_encode( array( 'q' => $requete, 'k' => 50 ) ),
    ) );
    if ( is_wp_error( $reponse ) || 200 !== wp_remote_retrieve_response_code( $reponse ) ) {
        return null; // Service indisponible : la recherche native reprend la main.
    }
    $ids = json_decode( wp_remote_retrieve_body( $reponse ), true );
    return is_array( $ids ) ? array_map( 'absint', $ids ) : null;
}

add_action( 'pre_get_posts', function ( $query ) {
    if ( is_admin() || ! $query->is_main_query() || ! $query->is_search() ) {
        return;
    }
    $ids = genisoft_ids_hybrides( $query->get( 's' ) );
    if ( null === $ids ) {
        return;
    }
    $query->set( 'post__in', $ids ? $ids : array( 0 ) );
    $query->set( 'orderby', 'post__in' );
    $query->set( 'genisoft_hybride', true );
} );

add_filter( 'posts_search', function ( $search, $query ) {
    return $query->get( 'genisoft_hybride' ) ? '' : $search;
}, 10, 2 );

Sur WordPress 7.1.3, la requête produite ne contient plus aucun LIKE, se termine par ORDER BY FIELD(wp_posts.ID, …) dans l’ordre fourni, et is_search() reste vrai, donc le gabarit de recherche du thème s’applique. Effet de bord à connaître : la condition post_password = '' disparaît avec le filtre. Votre index ne doit donc contenir ni brouillons ni contenus protégés.

Pour la synchronisation, les crochets save_post, wp_trash_post et before_delete_post couvrent l’enregistrement, la corbeille et la suppression ; écartez révisions et sauvegardes automatiques, et confiez la vectorisation à une file d’attente traitée par un cron système, jamais à la requête d’enregistrement.

Mesurer avant de choisir

  1. Exportez les recherches internes des trois derniers mois et calculez la part sans résultat.
  2. Constituez un jeu de 30 à 50 requêtes réelles, avec les contenus attendus.
  3. Comparez la recherche native, Relevanssi puis un prototype hybride : le contenu attendu figure-t-il dans les cinq premiers résultats ?
  4. Mesurez la latence du prototype sur votre serveur, cache vide.

Si Relevanssi règle la majorité des échecs, arrêtez-vous là. L’hybride se justifie pour les requêtes en langage naturel et les vocabulaires variés (catalogue technique, documentation).

Données personnelles et limites

Les requêtes contiennent parfois un nom, une adresse e-mail ou un numéro de commande : ne les envoyez pas à un tiers sans nécessité et limitez la conservation des journaux. Un vecteur n’est pas une anonymisation. Un modèle exécuté sur votre serveur, comme dans notre guide du RAG local sous WordPress, évite le transfert. La même démarche appliquée à un catalogue est détaillée dans notre comparatif recherche vectorielle et SQL pour PrestaShop.

Questions fréquentes

Pourquoi la recherche de WordPress ne trouve-t-elle rien alors que le contenu existe ?

Parce que tous les mots saisis doivent figurer dans le titre, l’extrait ou le contenu, et que les champs personnalisés ne sont pas lus. Une requête de trois mots dont un seul est absent ne renvoie rien. Un index lexical comme Relevanssi assouplit cette règle.

Faut-il des embeddings pour un blog de quelques centaines d’articles ?

Rarement. Commencez par mesurer les recherches sans résultat, puis testez un index lexical. Les embeddings deviennent utiles quand les échecs viennent de synonymes ou de formulations en langage naturel.

Peut-on faire de la recherche vectorielle avec MySQL ?

MySQL 9.7 sait stocker des vecteurs, mais la fonction DISTANCE() n’existe que dans HeatWave sur OCI et MySQL AI. MariaDB 11.8 LTS propose en revanche un type VECTOR, un index et les fonctions de distance.

Quelle méthode de fusion choisir pour débuter ?

La Reciprocal Rank Fusion, avec k = 60. Elle ne demande aucune normalisation des scores. La fusion pondérée peut donner de meilleurs résultats, mais seulement si vous réglez α sur un jeu de requêtes réelles.

Sources

Agence web PrestaShop & WordPress

Agence web spécialisée PrestaShop et WordPress : création, maintenance, sécurité, performance et référencement. Nos tutoriels sont issus de nos interventions sur de vraies boutiques.

L'agence Genisoft → LinkedIn →