🚀 Essayez Zilliz Cloud, la version entièrement gérée de Milvus, gratuitement—découvrez des performances 10x plus rapides ! Essayez maintenant>>

milvus-logo
LFAI

HomeBlogsDémarrer avec la recherche hybride sémantique / plein texte avec Milvus 2.5

Démarrer avec la recherche hybride sémantique / plein texte avec Milvus 2.5

  • Engineering
December 17, 2024
Stefan Webb

Dans cet article, nous vous montrerons comment utiliser rapidement la nouvelle fonction de recherche en texte intégral et la combiner avec la recherche sémantique conventionnelle basée sur les vector embeddings.

Conditions requises

Tout d'abord, assurez-vous d'avoir installé Milvus 2.5 :

pip install -U pymilvus[model]

et que vous disposez d'une instance de Milvus Standalone en cours d'exécution (par exemple, sur votre machine locale) à l'aide des instructions d'installation figurant dans la documentation de Milvus.

Construction du schéma de données et des indices de recherche

Nous importons les classes et les fonctions nécessaires :

from pymilvus import MilvusClient, DataType, Function, FunctionType, model

Vous avez peut-être remarqué deux nouvelles entrées pour Milvus 2.5, Function et FunctionType, que nous expliquerons bientôt.

Ensuite, nous ouvrons la base de données avec Milvus Standalone, c'est-à-dire localement, et nous créons le schéma de données. Le schéma comprend une clé primaire entière, une chaîne de texte, un vecteur dense de dimension 384 et un vecteur clairsemé (de dimensionnalité illimitée). Notez que Milvus Lite ne prend pas actuellement en charge la recherche plein texte, mais seulement Milvus Standalone et Milvus Distributed.

client = MilvusClient(uri="http://localhost:19530")

schema = client.create_schema()

schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True, auto_id=True)
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=1000, enable_analyzer=True)
schema.add_field(field_name="dense", datatype=DataType.FLOAT_VECTOR, dim=768),
schema.add_field(field_name="sparse", datatype=DataType.SPARSE_FLOAT_VECTOR)
{'auto_id': False, 'description': '', 'fields': [{'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'is_primary': True, 'auto_id': True}, {'name': 'text', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 1000, 'enable_analyzer': True}}, {'name': 'dense', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}}, {'name': 'sparse', 'description': '', 'type': <DataType.SPARSE_FLOAT_VECTOR: 104>}], 'enable_dynamic_field': False}

Vous avez peut-être remarqué le paramètre enable_analyzer=True. Il indique à Milvus 2.5 d'activer l'analyseur lexical sur ce champ et de construire une liste de tokens et de fréquences de tokens, qui sont nécessaires pour la recherche en texte intégral. Le champ sparse contiendra une représentation vectorielle de la documentation sous la forme d'un sac de mots produit à partir de l'analyse text.

Mais comment relier les champs text et sparse et indiquer à Milvus comment sparse doit être calculé à partir de text? C'est ici que nous devons invoquer l'objet Function et l'ajouter au schéma :

bm25_function = Function(
    name="text_bm25_emb", # Function name
    input_field_names=["text"], # Name of the VARCHAR field containing raw text data
    output_field_names=["sparse"], # Name of the SPARSE_FLOAT_VECTOR field reserved to store generated embeddings
    function_type=FunctionType.BM25,
)

schema.add_function(bm25_function)
{'auto_id': False, 'description': '', 'fields': [{'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'is_primary': True, 'auto_id': True}, {'name': 'text', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 1000, 'enable_analyzer': True}}, {'name': 'dense', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}}, {'name': 'sparse', 'description': '', 'type': <DataType.SPARSE_FLOAT_VECTOR: 104>, 'is_function_output': True}], 'enable_dynamic_field': False, 'functions': [{'name': 'text_bm25_emb', 'description': '', 'type': <FunctionType.BM25: 1>, 'input_field_names': ['text'], 'output_field_names': ['sparse'], 'params': {}}]}

L'abstraction de l'objet Function est plus générale que celle de l'application de la recherche en texte intégral. À l'avenir, il pourra être utilisé dans d'autres cas où un champ doit être une fonction d'un autre champ. Dans notre cas, nous spécifions que sparse est une fonction de text via la fonction FunctionType.BM25. BM25 fait référence à une métrique courante dans la recherche d'informations, utilisée pour calculer la similarité d'une requête avec un document (par rapport à une collection de documents).

Nous utilisons le modèle d'intégration par défaut dans Milvus, qui est paraphrase-albert-small-v2:

embedding_fn = model.DefaultEmbeddingFunction()

L'étape suivante consiste à ajouter nos index de recherche. Nous en avons un pour le vecteur dense et un autre pour le vecteur clairsemé. Le type d'index est SPARSE_INVERTED_INDEX avec BM25 car la recherche en texte intégral nécessite une méthode de recherche différente de celle des vecteurs denses standard.

index_params = client.prepare_index_params()

index_params.add_index(
    field_name="dense",
    index_type="AUTOINDEX", 
    metric_type="COSINE"
)

index_params.add_index(
    field_name="sparse",
    index_type="SPARSE_INVERTED_INDEX", 
    metric_type="BM25"
)

Enfin, nous créons notre collection :

client.drop_collection('demo')
client.list_collections()
[]
client.create_collection(
    collection_name='demo', 
    schema=schema, 
    index_params=index_params
)

client.list_collections()
['demo']

Nous disposons ainsi d'une base de données vide configurée pour accepter des documents textuels et effectuer des recherches sémantiques et en texte intégral !

L'insertion de données ne diffère pas des versions précédentes de Milvus :

docs = [
    'information retrieval is a field of study.',
    'information retrieval focuses on finding relevant information in large datasets.',
    'data mining and information retrieval overlap in research.'
]

embeddings = embedding_fn(docs)

client.insert('demo', [
    {'text': doc, 'dense': vec} for doc, vec in zip(docs, embeddings)
])
{'insert_count': 3, 'ids': [454387371651630485, 454387371651630486, 454387371651630487], 'cost': 0}

Illustrons d'abord une recherche en texte intégral avant de passer à la recherche hybride :

search_params = {
    'params': {'drop_ratio_search': 0.2},
}

results = client.search(
    collection_name='demo', 
    data=['whats the focus of information retrieval?'],
    output_fields=['text'],
    anns_field='sparse',
    limit=3,
    search_params=search_params
)

Le paramètre de recherche drop_ratio_search fait référence à la proportion de documents moins bien notés à abandonner au cours de l'algorithme de recherche.

Voyons les résultats :

for hit in results[0]:
    print(hit)
{'id': 454387371651630485, 'distance': 1.3352930545806885, 'entity': {'text': 'information retrieval is a field of study.'}}
{'id': 454387371651630486, 'distance': 0.29726022481918335, 'entity': {'text': 'information retrieval focuses on finding relevant information in large datasets.'}}
{'id': 454387371651630487, 'distance': 0.2715056240558624, 'entity': {'text': 'data mining and information retrieval overlap in research.'}}

Combinons maintenant ce que nous avons appris pour effectuer une recherche hybride qui combine des recherches sémantiques et en texte intégral distinctes avec un reranker :

from pymilvus import AnnSearchRequest, RRFRanker
query = 'whats the focus of information retrieval?'
query_dense_vector = embedding_fn([query])

search_param_1 = {
    "data": query_dense_vector,
    "anns_field": "dense",
    "param": {
        "metric_type": "COSINE",
    },
    "limit": 3
}
request_1 = AnnSearchRequest(**search_param_1)

search_param_2 = {
    "data": [query],
    "anns_field": "sparse",
    "param": {
        "metric_type": "BM25",
        "params": {"drop_ratio_build": 0.0}
    },
    "limit": 3
}
request_2 = AnnSearchRequest(**search_param_2)

reqs = [request_1, request_2]
ranker = RRFRanker()

res = client.hybrid_search(
    collection_name="demo",
    output_fields=['text'],
    reqs=reqs,
    ranker=ranker,
    limit=3
)
for hit in res[0]:
    print(hit)
{'id': 454387371651630485, 'distance': 0.032786883413791656, 'entity': {'text': 'information retrieval is a field of study.'}}
{'id': 454387371651630486, 'distance': 0.032258063554763794, 'entity': {'text': 'information retrieval focuses on finding relevant information in large datasets.'}}
{'id': 454387371651630487, 'distance': 0.0317460335791111, 'entity': {'text': 'data mining and information retrieval overlap in research.'}}

Comme vous l'avez peut-être remarqué, cela ne diffère pas d'une recherche hybride avec deux champs sémantiques distincts (disponible depuis Milvus 2.4). Les résultats sont identiques à ceux de la recherche en texte intégral dans cet exemple simple, mais pour les bases de données plus importantes et les recherches par mot-clé, la recherche hybride a généralement un taux de rappel plus élevé.

Résumé

Vous disposez désormais de toutes les connaissances nécessaires pour effectuer des recherches en texte intégral et des recherches hybrides sémantique/texte intégral avec Milvus 2.5. Voir les articles suivants pour plus de détails sur le fonctionnement de la recherche en texte intégral et la raison pour laquelle elle est complémentaire de la recherche sémantique :

Like the article? Spread the word

Continuer à Lire