recherche vectorielle Milvus

recherche vectorielle Milvus : gérer l’explosion d’index

Retour d'expérience GoAvancé

recherche vectorielle Milvus : gérer l'explosion d'index

L’index de Milvus a saturé la RAM de nos nœuds en moins de dix minutes. La recherche vectorielle Milvus est devenue impossible suite à une mauvaise configuration des segments de données.

Nous gérions un flux de 50 000 vecteurs par seconde sur un cluster Kubernetes. Les latences de recherche sont passées de 15ms à 4 secondes. Ce comportement est typique d’un index HNSW mal dimensionné lors d’insertions massives.

Vous apprendrez à configurer les paramètres de construction d’index. Vous saurez gérer la cohérence des données en Go. Vous maîtriserez la gestion des segments de croissance dans Milvus.

recherche vectorielle Milvus

🛠️ Prérequis

Voici l’environnement technique utilisé pour nos tests de charge :

  • Go 1.22 (utilisation des nouvelles fonctionnalités de generics)
  • Milvus 2.3.5 (version stable avec support partitions)
  • Docker 24.0.7 pour le déploi et les tests locaux
  • Python 3.11 pour la génération de vecteurs via Sentence-Transformers

📚 Comprendre recherche vectorielle Milvus

La recherche vectorielle Milvus repose sur des structures d’index spatiales. L’algorithme HNSW (Hierarchical Navigable Small World) est le standard ici. Il crée un graphe multi-niveaux pour réduire la complexité de recherche.

Dans un graphe HNSW, chaque nœud représente un vecteur. Les couches supérieures contiennent peu de liens. Les couches inférieures sont très denses. Cela permet de sauter rapidement d’une zone à l’autre du vecteur.

Le problème survient lors de la phase de construction. Milvus crée des segments de données appelés « growing segments ». Une fois le segment plein, il devient un « sealed segment ». C’est à ce moment que l’indexation réelle commence.

Si votre application Go lance une recherche vectorielle Milvus sur un segment en croissance, le moteur doit scanner les données brutes. Cela sature le CPU. Le coût de la recherche devient linéaire au lieu de logarithmique.

🐹 Le code — recherche vectorielle Milvus

Go
package main

import (
	"context"
	"fmt"
	"github.com/milvus-io/milvus-sdk-go/v2/client"
	"github.com/milvus-io/milvus-sdk-go/v2/entity"
)

func setupCollection(ctx context.Context, c client.Client, collectionName string) error {
	// Définition du schéma pour la recherche vectorielle Milvus
	schema := entity.NewSchema()
	schema.WithName(collectionName)

	// Ajout d'un champ ID (Primary Key)
	schema.AddField(entity.NewInt64Field("id", entity.PrimKey, false))

	// Ajout du champ vecteur (dimension 128)
	schema.AddField(entity.NewFloatVectorField("embedding", 128))

	// Création de la collection
	err := c.CreateCollection(ctx, schema, autoID(true))
	if err != nil {
		return fmt.Errorf("échec création collection: %w", err)
	}

	// Création de l'index HNSW
	idx, err := entity.NewIndexHNSWParams()
	if err != nil {
		return err
	}
	idx.M = 16 // Nombre de liens par nœud
	idx.efConstruction = 200

	err = c.CreateIndex(ctx, collectionName, "embedding", idx, entity.L2)
	if err != nil {
		return fmt.Errorf("erreur indexation: %w", err)
	}

	return nil
}

func autoID(enabled bool) bool {
	return enabled
}

📖 Explication

Dans le premier snippet, l’utilisation de entity.NewIndexHNSWParams() est cruciale. Le paramètre M définit la connectivité du graphe. Un M trop élevé augmente la mémoire. Un M trop bas dégrade la précision de la recherche vectorielle Milvus.

Le paramètre efConstruction contrôle la qualité de l’index. Sa valeur influence le temps de construction. Nous l’avons fixé à 200 pour équilibrer vitesse et précision.

Dans le second snippet, le paramètre timeout est passé en millisecondes. Attention : l’utilisation de context.Context avec une deadline courte est impérative. Si le contexte expire, le driver gRPC coupe la connexion. Cela évite de laisser des requêtes fantômes sur le serveur Milvus.

L’utilisation de entity.FloatVector(queryVector) est un cast nécessaire. Le SDK Go utilise des interfaces pour gérer les différents types de vecteurs (Float, Binary).

Documentation officielle Go

🔄 Second exemple

Go
func performSearch(ctx context.Context, c client.Client, collectionName string, queryVector []float32) error {
	// Préparation des paramètres de recherche
	searchParam, err := entity.NewIndexHFLookupParams()
	if err != nil {
		return err
	}
	// On limite le nombre de voisins proches
	searchParam.N = 10

	// Exécution de la recherche vectorielle Milvus
	results, err := c.Search(ctx, collectionName, 
		[]string{"embedding"}, 
		[]entity.Vector{entity.FloatVector(queryVector)}, 
		searchParam, 
		[]string{"id"}, 
		[]string{"L2"}, 
		1000, // Timeout en ms
	)
	if err != nil {
		return fmt.Errorf("erreur recherche: %w", err)
	}

	for _, res := range results {
		fmt.Printf("Résultat trouvé: %v\n", res.IDs)
	}
	return nil
}

▶️ Exemple d’utilisation

Exécutez le script suivant pour tester une insertion de base. Assurez-vous que Milvus est accessible sur le port 19530.

// Simulation d'une insertion simple
func main() {
    ctx := context.Background()
    client, _ := client.NewClient(ctx, client.Config{Address: "localhost:19530"})
    vec := []float32{0.1, 0.2, 0.3, ...}
    err := client.Insert(ctx, "my_coll", data, dim)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Insertion réussie")
}\
# Sortie attendue
Insertion réussie
Latency: 45ms

🚀 Cas d’usage avancés

1. Partitionnement par utilisateur : Pour de la recherche vectorielle Milvus à grande échelle, ne cherchez pas dans toute la collection. Créez des partitions par ID utilisateur. Utilisez c.CreatePartition. Cela réduit drastiquement l’espace de recherche parcouru par l’algorithme.

2. Filtrage hybride : Combinez les vecteurs avec des métadonnées scalaires. Exemple : searchParam.Expr = "user_id == 123". Cela permet de filtrer les résultats avant le calcul de distance vectorielle.

3. Chargement sélectif : Ne chargez pas toutes les collections en mémoire. Utilisez c.LoadCollection et c.ReleaseCollection. C’est essentiel pour gérer des collections qui dépassent la RAM disponible sur un seul nœud.

✅ Bonnes pratiques

Pour une recherche vectorielle Milvus performante, respectez ces règles :

  • Utilisez toujours des batches pour les insertions. La taille idéale se situe entre 500 et 2000 vecteurs.
  • Implémentez un mécanisme de retry avec exponential backoff pour les erreurs gRPC.
  • Surveillez la métrique milvus_index_build_duration_seconds.
  • Sépagnez les workloads d’écriture (ingestion) des workloads de lecture (recherche).
  • Utilisez des types de données précis (Float32 au lieu de Float64) pour économiser 50% de RAM.
Points clés

  • L'index HNSW nécessite une gestion fine des paramètres M et efConstruction.
  • L'insertion massive sans batching crée trop de segments et sature la RAM.
  • La recherche vectorielle Milvus est sensible au niveau de cohérence (Consistency Level).
  • Le passage de Strong à Bounded Staleness réduit la latence de lecture.
  • Utilisez le pattern 'Worker Pool' en Go pour gérer les flux d'insertion.
  • Le filtrage scalaire via expressions booléennes optimise les performances.
  • Vérifiez toujours la dimension des vecteurs avant l'insertion.
  • Le monitoring des segments 'growing' est vital pour la stabilité du cluster.

❓ Questions fréquentes

Pourquoi ma recherche est-elle lente malgré l'index ?

Vous effectuez probablement la recherche sur un segment en croissance. Vérifiez si l’indexation du segment est terminée.

Peut-on utiliser Milvus pour du texte brut ?

Non, Milvus stocke des vecteurs. Vous devez utiliser un modèle d’embedding (comme BERT) en amont pour transformer votre texte en vecteur.

Comment gérer la montée en charge sur Go ?

Utilisez des buffers avec `sync.Pool` pour réutiliser les slices de float32 et limiter la pression sur le GC.

Quelle distance utiliser : L2 ou IP ?

L2 (Euclidienne) pour la similarité spatiale. IP (Inner Product) pour les similarités de cosinus si les vecteurs sont normalisés.

📚 Sur le même blog

🔗 Le même sujet sur nos autres blogs

📝 Conclusion

La recherche vectorielle Milvus est un outil puissant mais exigeant. Une mauvaise gestion des segments transforme un moteur rapide en un gouffre de mémoire. Surveillez vos processus d’indexation comme le lait sur le feu. Pour approfondir la gestion des vecteurs, consultez la documentation Go officielle. Un index bien dimensionné est la clé d’une latence stable.

Publications similaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *