🚀 Prueba Zilliz Cloud, el Milvus completamente gestionado, gratis—¡experimenta un rendimiento 10 veces más rápido! Prueba Ahora>>

milvus-logo
LFAI

HomeBlogsIntroducción a la búsqueda híbrida semántica/de texto completo con Milvus 2.5

Introducción a la búsqueda híbrida semántica/de texto completo con Milvus 2.5

  • Engineering
December 17, 2024
Stefan Webb

En este artículo, le mostraremos cómo poner en marcha rápidamente la nueva función de búsqueda de texto completo y combinarla con la búsqueda semántica convencional basada en incrustaciones vectoriales.

Requisitos

En primer lugar, asegúrese de haber instalado Milvus 2.5:

pip install -U pymilvus[model]

y tener una instancia en ejecución de Milvus Standalone (por ejemplo, en su máquina local) siguiendo las instrucciones de instalación de la documentación de Milvus.

Construir el esquema de datos y los índices de búsqueda

Importamos las clases y funciones necesarias:

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

Puede que haya notado dos nuevas entradas para Milvus 2.5, Function y FunctionType, que explicaremos en breve.

A continuación abrimos la base de datos con Milvus Standalone, es decir, localmente, y creamos el esquema de datos. El esquema comprende una clave primaria entera, una cadena de texto, un vector denso de dimensión 384, y un vector disperso (de dimensionalidad ilimitada). Observe que Milvus Lite no soporta actualmente la búsqueda de texto completo, sólo Milvus Standalone y 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}

Puede que haya notado el parámetro enable_analyzer=True. Esto indica a Milvus 2.5 que active el analizador léxico en este campo y construya una lista de tokens y frecuencias de tokens, que son necesarios para la búsqueda de texto completo. El campo sparse contendrá una representación vectorial de la documentación como una bolsa de palabras producida a partir del análisis text.

Pero, ¿cómo conectamos los campos text y sparse e indicamos a Milvus cómo debe calcularse sparse a partir de text? Aquí es donde tenemos que invocar el objeto Function y añadirlo al esquema:

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': {}}]}

La abstracción del objeto Function es más general que la de aplicar la búsqueda de texto completo. En el futuro, podrá utilizarse para otros casos en los que un campo deba ser función de otro. En nuestro caso, especificamos que sparse es una función de text a través de la función FunctionType.BM25. BM25 se refiere a una métrica común en la recuperación de información utilizada para calcular la similitud de una consulta con un documento (en relación con una colección de documentos).

Utilizamos el modelo de incrustación predeterminado en Milvus, que es paraphrase-albert-small-v2:

embedding_fn = model.DefaultEmbeddingFunction()

El siguiente paso es añadir nuestros índices de búsqueda. Tenemos uno para el vector denso y otro para el vector disperso. El tipo de índice es SPARSE_INVERTED_INDEX con BM25 ya que la búsqueda de texto completo requiere un método de búsqueda diferente al de los vectores densos estándar.

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"
)

Por último, creamos nuestra colección:

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

client.list_collections()
['demo']

Y ya tenemos una base de datos vacía preparada para aceptar documentos de texto y realizar búsquedas semánticas y de texto completo.

La inserción de datos no difiere de las versiones anteriores 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}

Ilustremos primero una búsqueda de texto completo antes de pasar a la búsqueda híbrida:

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
)

El parámetro de búsqueda drop_ratio_search se refiere a la proporción de documentos con puntuación más baja que deben descartarse durante el algoritmo de búsqueda.

Veamos los resultados:

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.'}}

Combinemos ahora lo que hemos aprendido para realizar una búsqueda híbrida que combine búsquedas semánticas y de texto completo por separado con 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.'}}

Como habrá notado, esto no difiere de una búsqueda híbrida con dos campos semánticos separados (disponible desde Milvus 2.4). Los resultados son idénticos a los de la búsqueda de texto completo en este sencillo ejemplo, pero para bases de datos más grandes y búsquedas específicas por palabra clave, la búsqueda híbrida suele tener una mayor recuperación.

Resumen

Ahora ya dispone de todos los conocimientos necesarios para realizar búsquedas de texto completo e híbridas semánticas/de texto completo con Milvus 2.5. Consulte los siguientes artículos para obtener más información sobre cómo funciona la búsqueda de texto completo y por qué es complementaria a la búsqueda semántica:

Like the article? Spread the word

Sigue Leyendo