Construire des systèmes de recherche qui comprennent à la fois l'apparence d'une image et le contenu de son texte constitue un bond en avant significatif en matière d'expérience utilisateur. Pour les développeurs intermédiaires à avancés, le défi réside souvent dans la combinaison de ces sources de données disparates en un seul pipeline de récupération cohérent. Bien que des modèles d'intégration multimodale spécialisés existent, il est d'une immense valeur de comprendre comment implémenter la recherche hybride en utilisant des bibliothèques de bas niveau comme FAISS (Facebook AI Similarity Search). Cet article explore comment construire un système qui récupère des résultats sur la base d'une combinaison pondérée de la similarité visuelle et textuelle, avec des métriques de distance personnalisées pour affiner la pertinence.
Pourquoi la recherche hybride ?
La recherche textuelle pure manque de contexte visuel, tandis que la recherche d'images pure (utilisant des intégrations de modèles comme CLIP) peut manquer les nuances sémantiques présentes dans les métadonnées ou les légendes. La recherche hybride résout ce problème en calculant des scores de similarité séparés pour les composants texte et image d'une requête et d'un document, puis en les fusionnant. Dans FAISS, cela nécessite de gérer deux index distincts — un pour les intégrations de texte et un pour les intégrations d'images — et d'implémenter une couche de logique qui combine leurs résultats.
Mise en place du pipeline multimodal
Pour commencer, nous supposons que vous avez des vecteurs précalculés pour votre jeu de données. Pour cet exemple, nous utilisons des intégrations de 512 dimensions, une taille de sortie courante pour des modèles comme CLIP ViT-B/32. Nous créerons deux index FAISS distincts :
- Index de texte : En utilisant
IndexFlatIP(Produit Intérieur) pour la similarité cosinus. - Index d'image : En utilisant également
IndexFlatIPpour la simplicité, bien queIndexIVFFlatsoit recommandé pour les jeux de données plus volumineux.
import faiss
import numpy as np
# Définir les dimensions des intégrations
dim = 512
# 1. Créer l'index de texte (Produit Intérieur)
index_text = faiss.IndexFlatIP(dim)
# 2. Créer l'index d'image (Produit Intérieur)
index_image = faiss.IndexFlatIP(dim)
# Génération de données d'exemple
num_docs = 1000
np.random.seed(42)
text_embeddings = np.random.rand(num_docs, dim).astype("float32")
image_embeddings = np.random.rand(num_docs, dim).astype("float32")
# Normaliser les vecteurs pour la similarité cosinus (requis pour que IP agisse comme Cosinus)
faiss.normalize_L2(text_embeddings)
faiss.normalize_L2(image_embeddings)
# Ajouter les vecteurs aux index
index_text.add(text_embeddings)
index_image.add(image_embeddings)
Implémentation de métriques de distance personnalisées et de fusion
Le cœur de la recherche hybride est la stratégie de fusion. Une approche courante est la Combinaison Linéaire, où le score final est une somme pondérée des scores individuels. Cependant, les scores bruts provenant de différents index peuvent ne pas être sur la même échelle. Nous avons besoin d'une fonction de Métrique de Distance Personnalisée qui normalise ces scores avant la combinaison.
Voici une fonction Python qui effectue la recherche hybride. Elle accepte les poids alpha (pour le texte) et beta (pour l'image), où alpha + beta = 1.0.
def hybrid_search(index_t, index_i, query_text_vec, query_img_vec, k=10, alpha=0.5):
"""
Effectue une recherche hybride en combinant les scores de similarité texte et image.
:param index_t: Index FAISS pour les intégrations de texte
:param index_i: Index FAISS pour les intégrations d'image
:param query_text_vec: Vecteur de requête texte normalisé (1, dim)
:param query_img_vec: Vecteur de requête image normalisé (1, dim)
:param k: Nombre de résultats à retourner
:param alpha: Poids pour la similarité texte (1-alpha est le poids pour l'image)
:return: Liste de tuples (index, score_combiné)
"""
# Normaliser les vecteurs de requête si ce n'est pas déjà fait
faiss.normalize_L2(query_text_vec)
faiss.normalize_L2(query_img_vec)
# Effectuer la recherche sur les deux index
# Note : FAISS retourne des index et des distances (scores)
D_text, I_text = index_t.search(query_text_vec, k)
D_image, I_image = index_i.search(query_img_vec, k)
# Nous devons fusionner les résultats. Comme I_text et I_image peuvent différer,
# nous récupérons le top-k des deux et les fusionnons.
candidates = {}
# Collecter les scores de texte
for idx, score in zip(I_text[0], D_text[0]):
if idx != -1:
candidates[idx] = {'text_score': float(score), 'image_score': 0.0}
# Collecter les scores d'image
for idx, score in zip(I_image[0], D_image[0]):
if idx != -1:
if idx in candidates:
candidates[idx]['image_score'] = float(score)
else:
candidates[idx] = {'text_score': 0.0, 'image_score': float(score)}
# Appliquer la Somme Pondérée Personnalisée (Combinaison Linéaire)
# Score Final = alpha * ScoreTexte + (1 - alpha) * ScoreImage
weighted_results = []
for doc_id, scores in candidates.items():
combined_score = (alpha * scores['text_score']) + ((1 - alpha) * scores['image_score'])
weighted_results.append((doc_id, combined_score))
# Trier par score combiné décroissant (FAISS retourne des scores plus élevés comme meilleurs pour IP)
weighted_results.sort(key=lambda x: x[1], reverse=True)
return weighted_results[:k]
# Exemple d'utilisation
query_text = np.random.rand(1, dim).astype("float32")
query_img = np.random.rand(1, dim).astype("float32")
results = hybrid_search(index_text, index_image, query_text, query_img, k=5, alpha=0.7)
for doc_id, score in results:
print(f"ID Doc : {doc_id}, Score Combiné : {score:.4f}")
Optimisation et stratégies avancées
La combinaison linéaire ci-dessus est simple mais peut être améliorée. Pour les systèmes de production, envisagez :
- Fusion de rang réciproque (RRF) : Au lieu de sommer les scores bruts, sommez les rangs inverses. C'est robuste aux différences d'échelle entre les modalités.
- Choix de l'index : Pour les jeux de données dépassant 100k vecteurs, remplacez
IndexFlatIPparIndexIVFPQ(Fichier inversé avec quantification de produit) pour réduire l'empreinte mémoire et augmenter la vitesse de recherche. Vous devrez réentraîner ces index sur votre distribution de données spécifique. - Pondération dynamique : Utilisez un classifieur pour déterminer si une requête est plus axée sur le texte ou sur l'image et ajustez
alphadynamiquement.
Conclusion
L'implémentation de la récupération multimodale avec FAISS offre un contrôle granulaire sur la façon dont votre moteur de recherche interprète l'intention de l'utilisateur. En découplant les index de texte et d'image et en appliquant une logique de fusion personnalisée, vous pouvez créer une expérience de recherche à la fois sémantiquement riche et visuellement intuitive. Commencez par la pondération linéaire, mesurez vos taux de réussite et itérez vers des stratégies de fusion plus complexes comme RRF ou des modèles d'apprentissage du classement. Grâce à la vitesse de FAISS, même la recherche hybride en temps réel sur des jeux de données à grande échelle est réalisable.