La recherche par similarité vectorielle est devenue la brique silencieuse de tout système documentaire moderne : retrouver, parmi des dizaines de milliers de passages, ceux qui parlent de la même chose qu'une requête, sans partager un seul mot avec elle. Les moteurs qui s'en chargent sont généralement des services lourds — serveurs dédiés, dépendances natives, orchestration. hazyhaar publie aujourd'hui horosvec, qui prend le parti inverse : un index de voisins approchés embarqué dans le processus, écrit en Go pur, dont tout l'état tient dans un fichier SQLite.
Dépôt : github.com/hazyhaar/horosvec (licence MIT, tag v0.2.0). Une seule dépendance : le portage Go pur de SQLite (modernc.org/sqlite) — pas de CGO, des binaires statiques.
Deux algorithmes en tandem
horosvec combine deux idées de la littérature récente des index approchés.
Vamana, le graphe de proximité popularisé par DiskANN : chaque vecteur devient un nœud relié à ses voisins pertinents, et la recherche navigue ce graphe de proche en proche au lieu de comparer la requête à toute la base. L'élagage « robuste » (alpha-RNG) garantit des raccourcis à longue portée qui évitent les minima locaux.
RaBitQ, une quantification binaire extrême : chaque coordonnée du vecteur est réduite à son signe — un bit par dimension, plus deux normes par vecteur. Les distances approximatives calculées sur ces codes sont grossières mais quasi gratuites, et suffisent à guider la navigation.
Le point d'architecture qui fait tenir l'ensemble est la recherche à deux étages : le parcours du graphe présélectionne des candidats aux distances RaBitQ, puis le classement final est recalculé en distance exacte sur les vrais vecteurs. L'estimateur peut être bruité : il n'a besoin que de placer les bons voisins dans le faisceau, le reclassement exact fait le reste.
Mesuré, pas promis
L'implémentation s'écarte sciemment du papier RaBitQ sur un point : la rotation aléatoire préalable, sur laquelle reposent les garanties théoriques de l'article, n'est pas implémentée. Plutôt que d'invoquer des bornes qui ne s'appliquent plus, le dépôt embarque des bancs de mesure déterministes et rejouables, et publie leurs chiffres.
Rappel à 10 voisins (recall@10) contre la vérité terrain exacte, 2 000 vecteurs de base, 50 requêtes, configuration par défaut :
| Jeu de données | dimension | rappel moyen | pire requête |
|---|---|---|---|
| synthétique uniforme | 128 | 1,000 | 1,000 |
| clusters gaussiens serrés | 128 | 0,982 | 0,900 |
| embeddings réels bge-m3 (textes de sessions de code) | 1024 | 1,000 | 1,000 |
La troisième ligne est celle qui compte : sur de la matière réelle — deux mille
messages de sessions de développement, vectorisés par un vrai modèle d'embedding en
dimension 1024 — l'index ne manque pas un voisin. La crainte théorique de l'anisotropie
des espaces d'embedding ne s'est pas matérialisée à cette échelle ; les limites
honnêtes (2×10³ vecteurs, requêtes tirées de la même distribution) sont documentées
dans le paquet, et le banc sur matière réelle est rejouable par quiconque
(HOROSVEC_REAL_VECS, un JSON de vecteurs).
Durci par l'usage, et par l'adversité
horosvec n'est pas un prototype de dépôt : c'est l'extraction du moteur qui sert en production dans l'écosystème horos55 la recherche de shards documentaires et les embeddings de cartes de code. Sa préparation à la publication est passée par un audit adversarial complet, dont les findings sont devenus des propriétés testées :
- la désérialisation est bornée : un blob binaire corrompu ou hostile échoue proprement, sans panique ni allocation démesurée ;
- les insertions sont transactionnelles jusque dans la mémoire : l'état interne (cache, compteurs, miroir plat) n'est appliqué qu'après le commit SQLite — un commit qui échoue ne laisse aucun voisin fantôme ;
- l'annulation est une erreur, jamais un silence : un contexte annulé en plein parcours du graphe remonte une erreur explicite au lieu de retourner un résultat vide indistinguable d'un « aucun voisin » ;
- 42 tests, 85,9 % de couverture, dont l'injection d'échec de commit, la corruption de blob, l'éviction du cache et la reconstruction déclenchée par dérive du centroïde.
Les limites connues sont assumées dans la documentation plutôt que passées sous silence : pas d'API de suppression (reconstruction complète), miroir mémoire non borné pour la force brute, et un contrat de dégradation gracieuse du reclassement externe appelé à évoluer.
Pour qui ?
Pour tout programme Go qui veut de la recherche sémantique sans service externe : un outil en ligne de commande qui indexe ses notes, un serveur qui cherche dans ses propres documents, un pipeline qui déduplique par similarité. Si l'état de l'art des bases vectorielles distribuées est votre besoin, ce n'est pas cet outil ; s'il vous faut un index qui tient dans le binaire et dans un fichier, il est fait pour cela.
go get github.com/hazyhaar/horosvec