MxChat sur DuckDB (MotherDuck) : recherche vectorielle + BM25 en SQL pur

MxChat fait correctement le boulot côté intégration WordPress et UX : un chatbot qui se branche sans douleur, une interface propre, du RAG qui fonctionne. C’est sur la couche de stockage vectoriel que le plugin coince. Deux backends officiels, deux impasses.

Côté MySQL, le plugin sérialise les embeddings en PHP et les compare en mémoire à chaque requête. Ça tient sur un corpus de démo, puis s’effondre passé quelques milliers de vecteurs : la requête de similarité devient un full scan désérialisé, chaque ligne paye le coût d’un unserialize PHP suivi d’un produit scalaire interprété, et la latence part en vrille. Côté Pinecone, ça marche, c’est rapide, c’est cloud, et c’est facturé au vecteur stocké. La donnée part chez un tiers américain, et la note tombe tous les mois. Aucun des deux ne fait l’affaire : il me fallait un store local, ouvert, qui tienne la charge sans rente mensuelle ni dépendance externe.

DuckDB propose désormais l’extension VSS, qui ajoute la recherche par similarité vectorielle directement en SQL : index HNSW, distance cosinus, le tout sur un moteur OLAP qui sait déjà manipuler des millions de lignes en mémoire. Restait à brancher cette mécanique sur MxChat sans patcher l’upstream.

Le plugin compagnon que je publie aujourd’hui s’enregistre via les filtres que MxChat expose déjà : il se présente comme un backend Pinecone alternatif, et expose à son tour une API REST qui imite le wire protocol de Pinecone. MxChat croit parler à Pinecone. Le plugin traduit chaque appel en SQL DuckDB, et écrit dans un fichier .duckdb local ; ou, si on préfère le cloud sans retomber dans le vendor lock, sur MotherDuck via la connexion ATTACH native. Même code, deux modes de déploiement, zéro ligne touchée dans le plugin d’origine.


Au passage, un mot pour l’équipe MxChat, en anglais :

That said, this redirection is still a workaround. The cleanest fix would be a dedicated WordPress filter inside includes/class-mxchat-integrator.php, right before the wp_remote_post() call in find_relevant_content_pinecone() (around line 5287). Following WordPress core’s own pre_* short-circuit convention — the same pattern used by pre_get_posts, pre_user_query and pre_option_* — a mxchat_pre_vector_query filter would let a third-party plugin substitute the Pinecone HTTP call with a native implementation:

// Let companion plugins short-circuit the Pinecone HTTP call.
// Returning an array bypasses the network entirely; returning null
// falls through to the existing wp_remote_post() behavior.
$pre = apply_filters( 'mxchat_pre_vector_query', null, array(
    'vector'    => $user_embedding,
    'top_k'     => $request_body['topK'],
    'namespace' => $namespace,
    'bot_id'    => $bot_id,
) );

if ( is_array( $pre ) ) {
    $results = $pre; // Expected shape: ['matches' => [...]]
} else {
    $response = wp_remote_post( $api_endpoint, array(
        'headers' => array(
            'Api-Key'      => $api_key,
            'accept'       => 'application/json',
            'content-type' => 'application/json',
        ),
        'body'    => wp_json_encode( $request_body ),
        'timeout' => 30,
    ) );
    // ... existing is_wp_error / response_code / json_decode handling ...
    $results = json_decode( wp_remote_retrieve_body( $response ), true );
}Langage du code : PHP (php)

The contract is intentionally minimal: return null for current behavior, return a Pinecone-shaped ['matches' => [...]] array to bypass the network. The same hook (or sibling mxchat_pre_vector_fetch / mxchat_pre_vector_delete filters following the same convention) would naturally extend to the four admin-side POST /query call-sites in admin/class-pinecone-manager.phpmxchat_semantic_search_pinecone(), mxchat_text_search_fallback(), mxchat_query_based_list() and mxchat_get_recent_entries_safe() — covering the entire wire protocol with a handful of filter points.

PR ready to be submitted if there’s interest. Two open source vector storage backends would join the plugin’s ecosystem at no maintenance cost, and the proxy layer in mxchat-duckdb could be deprecated once a minimum supported MxChat version ships the hook.


La fonctionnalité dont je suis le plus content est la recherche hybride BM25 + vecteur. Sur un corpus WordPress typique (articles courts, vocabulaire spécifique, beaucoup de noms propres et de termes techniques) la similarité purement sémantique passe à côté de matches que BM25 attrape immédiatement, et inversement. La fusion des deux scores rattrape ce que le sémantique pur laisse filer, sans sacrifier sa capacité à comprendre les reformulations. C’est typiquement le genre de réglage qu’un SaaS propriétaire ne te laisse pas bricoler.

Le reste suit la même logique : cache de résultats, endpoint de santé, métriques de latence en fenêtre glissante. Une commande WP-CLI permet aussi de rapatrier un index Pinecone existant en réutilisant les vecteurs déjà calculés, ce qui transforme la migration en opération de quelques minutes plutôt qu’en re-facturation OpenAI. L’ensemble est sous GPL v2, testé et linté en CI.

C’est un projet ciblé, mais il s’inscrit dans une ligne que je pousse partout où je peux, chez mes clients et chez moi : sortir des SaaS où la donnée et la facture s’accumulent en silence. Le cloud propriétaire n’est jamais la seule réponse. Souvent, il n’est même pas la bonne.

Code et documentation complète sur GitHub.


Écrivez quelques éclats d'âme...

Dans l'ombre vacillante d'une chandelle, où les murmures du vent se mêlent aux secrets d'un vieux parchemin, je vous invite à tisser une toile de mots. Écrivez quelques éclats d'âme – rêve, étoile, abîme, étreinte, brume – et laissez-les danser sur la page, comme des lucioles dans une nuit d'encre. Que diriez-vous de les entrelacer dans une phrase, un souffle, une histoire ?

S’abonner
Notification pour
guest
0 Commentaires
Le plus ancien
Le plus récent Le plus populaire