Cómo combinar la búsqueda vectorial con el filtrado en Elasticsearch - Python 8.x

Cada día se desarrollan grandes modelos de lenguaje (LLM), situación que facilita el escalamiento de la búsqueda semántica. LLM es bueno para analizar textos y revelar similitudes semánticas. Esta situación también se refleja en los motores de búsqueda, porque los motores de búsqueda semánticos pueden proporcionar a los usuarios resultados más satisfactorios.

Aunque los modelos de lenguaje grandes pueden capturar resultados semánticamente cercanos, implementar filtros en los resultados de búsqueda es fundamental para mejorar la experiencia del usuario. Por ejemplo, incorporar filtros basados ​​en fechas o categorías puede mejorar significativamente una experiencia de búsqueda más satisfactoria. Entonces, ¿cómo se puede combinar eficazmente la búsqueda semántica con el filtrado?

En la presentación de hoy, usaré el último Elastic Stack 8.9.0 para hacer una demostración. Para que todos puedan aprender,   descargue todos los datos en la dirección https://github.com/liu-xiao-guo/elasticsearch-vector-search/ .

Instalar

Si no ha instalado su propio Elasticsearch y Kibana, consulte mi artículo anterior:

Al instalar, elegimos el último Elastic Stack 8.x para instalar. De forma predeterminada, Elasticsearch viene con acceso seguro HTTPS. Cuando Elasticsearch se inicia por primera vez, registramos el nombre de usuario y la contraseña del superusuario elástico:

✅ Elasticsearch security features have been automatically configured!
✅ Authentication is enabled and cluster connections are encrypted.
 
ℹ️  Password for the elastic user (reset with `bin/elasticsearch-reset-password -u elastic`):
  p1k6cT4a4bF+pFYf37Xx
 
ℹ️  HTTP CA certificate SHA-256 fingerprint:
  633bf7f6e4bf264e6a05d488af3c686b858fa63592dc83999a0d77f7e9fe5940
 
ℹ️  Configure Kibana to use this cluster:
• Run Kibana and click the configuration link in the terminal when Kibana starts.
• Copy the following enrollment token and paste it into Kibana in your browser (valid for the next 30 minutes):
  eyJ2ZXIiOiI4LjkuMCIsImFkciI6WyIxOTIuMTY4LjAuMzo5MjAwIl0sImZnciI6IjYzM2JmN2Y2ZTRiZjI2NGU2YTA1ZDQ4OGFmM2M2ODZiODU4ZmE2MzU5MmRjODM5OTlhMGQ3N2Y3ZTlmZTU5NDAiLCJrZXkiOiJ3WEE3MDRrQkxxWTFWWGY0QWRHbDpCa0VZVXZmaFFidWNPOFUxdXJwXzZnIn0=
 
ℹ️  Configure other nodes to join this cluster:
• On this node:
  ⁃ Create an enrollment token with `bin/elasticsearch-create-enrollment-token -s node`.
  ⁃ Uncomment the transport.host setting at the end of config/elasticsearch.yml.
  ⁃ Restart Elasticsearch.
• On other nodes:
  ⁃ Start Elasticsearch with `bin/elasticsearch --enrollment-token <token>`, using the enrollment token that you generated.

Búsqueda de vocabulario - Búsqueda básica

Comencemos con una conexión Elasticsearch y una consulta de búsqueda básica. Usamos Python para demostración. Necesitamos instalar los paquetes de Python requeridos:

pip3 install elasticsearch
pip3 install Config

Para obtener un enlace a Elasticsearch, consulte " Elasticsearch: todo lo que necesita saber sobre el uso de Elasticsearch en Python - 8.x ". Modificamos el siguiente archivo simple.cfg en el código descargado:

simple.cfg

ES_PASSWORD: "p1k6cT4a4bF+pFYf37Xx"
ES_FINGERPRINT: "633bf7f6e4bf264e6a05d488af3c686b858fa63592dc83999a0d77f7e9fe5940"

ES_PASSWORD anterior es la contraseña que mostramos cuando se inició Elasticsearch por primera vez, y el valor de ES_FINGERPRINT es la huella digital de http_ca.crt. También podemos verlo cuando Elasticsearch se inicia por primera vez. Si ya no puede encontrar esta pantalla, puede consultar el artículo " Elasticsearch: todo lo que necesita saber sobre el uso de Elasticsearch en Python - 8.x " para aprender cómo obtenerlo. Otro método relativamente simple es abrir el archivo config/kibana.yml:

Usamos jupyter para abrir el archivo es-intro.ipynb:

from elasticsearch import Elasticsearch
from config import Config

with open('simple.cfg') as f:
    cfg = Config(f)

print(cfg['ES_FINGERPRINT'])
print(cfg['ES_PASSWORD'])

client = Elasticsearch(
    'https://localhost:9200',
    ssl_assert_fingerprint = cfg['ES_FINGERPRINT'],
    basic_auth=('elastic', cfg['ES_PASSWORD'])
)

client.info()

Claramente nuestro código se conecta exitosamente a Elasticsearch.

Usamos el siguiente código para leer el archivo:

import json
with open('data.json', 'r') as f:
    data = json.load(f)

for book in data:
    print(book)

El conjunto de datos que usaré en esta publicación fue generado por ChatGPT y sigue el formato anterior.

Primero verificamos si se ha creado el índice book_index. Si es así, elimine el índice:

INDEX_NAME = "book_index"
 
if(client.indices.exists(index=INDEX_NAME)):
    print("The index has already existed, going to remove it")
    client.options(ignore_status=404).indices.delete(index=INDEX_NAME)

 Usamos el siguiente código para escribir datos en Elasticsearch:

book_mappings = {
    "properties": {
        "title": {"type": "text"},
        "author": {"type": "text"},
        "date": {"type": "date"}
    }
}

client.indices.create(index = INDEX_NAME, mappings = book_mappings)

for each in data:
    client.index(index = INDEX_NAME, document = each)
client.indices.refresh()

Lo anterior muestra que se han escrito 14 documentos. Usamos el siguiente código para mostrar todos los documentos:

# GET ALL DOCUMENTS
resp = client.search(index='book_index', query={"match_all": {}})
for hit in resp['hits']['hits']:
    print(hit['_source'])

 Para aplicar filtrado a los documentos del índice, necesitamos modificar el parámetro "consulta". Para buscar palabras en el texto, usaremos la palabra clave "coincidencia":

# FILTERING - MATCH
resp = client.search(index='book_index', 
                     query={
                         "match":
                         {"title": "Data"}
                     })
for hit in resp['hits']['hits']:
    print(hit['_score'], hit['_source'])

Enumeramos los documentos en el índice que contienen la palabra "Datos" en el campo "título".

Si desea aplicar filtrado en varios campos, puede utilizar la operación "bool" para hacerlo. Si no desea que ciertos campos afecten la puntuación en la búsqueda, puede especificarlos en el "filtro".

# FILTERING - COMBINE FILTERS
resp = client.search(index='book_index', 
                     query={
                         "bool": {
                             "must": [
                                #  {"match": {"title": "data"}},
                                 {"match": {"author": "Smith"}},
                                 {"range": {"date": {"gte": "2023-08-01"}}}
                             ]
                         }
                     })
for hit in resp['hits']['hits']:
    print(hit)
Consulta de búsqueda de Elasticsearch utilizando operaciones bool

Para obtener más información sobre las consultas de Elasticsearch, puede consultar aquí .

Ahora, creemos el mismo índice que contiene vectores de documentos. En esta publicación, usaré la biblioteca Sentence-Transformers y el modelo "all-mpnet-base-v2". No hay restricciones en el uso del modelo, por lo que puede elegir el modelo que desee. Puedes explorar más modelos aquí .

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('all-mpnet-base-v2')
model

 Comprobamos el tamaño de la dimensión del modelo de la siguiente manera:

Usamos el siguiente código para verificar si el índice vector_index ya existe y lo eliminamos si ha existido durante tanto tiempo:

INDEX_NAME_VECTOR = "vector_index"
if(client.indices.exists(index = INDEX_NAME_VECTOR)):
    print("The index has already existed, going to remove it")
    client.options(ignore_status=404).indices.delete(index = INDEX_NAME_VECTOR)
vector_mapping = {
    "properties": {
        "title": {"type": "text"},
        "author": {"type": "text"},
        "date": {"type": "date"},
        "vector": {
            "type": "dense_vector",
            "dims": 768,
            "index": True,
            "similarity": "dot_product"
        }
    }
}

client.indices.create(index = INDEX_NAME_VECTOR, mappings = vector_mapping)

Al crear "vector_index" esta vez, agregamos un campo adicional de tipo "dense_vector" y especificamos los parámetros para la búsqueda de vectores: el parámetro "dims" indica la dimensionalidad del vector producido como salida por el modelo utilizado. La "similitud" determina cómo se mide la similitud del vector. Puede explorar diferentes valores de "similitud" aquí .

for each in data:
    each['vector'] = model.encode(each['title'])
    client.index(index='vector_index', document=each)
client.indices.refresh()

Carguemos el modelo usando la biblioteca Sentence-Transformers y extraigamos los vectores de la parte del "título" del conjunto de datos. Luego agregamos estos vectores a cada entrada de datos y continuamos agregando estos datos al índice "vector_index".

Para realizar una búsqueda vectorial en Elasticsearch, primero necesitamos un texto de consulta seguido de su representación vectorial correspondiente.

Importante : el modelo utilizado para obtener vectores de consulta debe ser el mismo modelo utilizado al indexar documentos; de lo contrario, será muy difícil obtener resultados precisos.

Podemos ejecutar el siguiente código para ver las incrustaciones generadas:

resp = client.search(index = INDEX_NAME_VECTOR, query={"match_all": {}})
for hit in resp['hits']['hits']:
    print(resp)

Para realizar una búsqueda vectorial, la función Elasticsearch.search() toma el parámetro "knn". La siguiente figura muestra un ejemplo de una consulta "knn". El valor "k" indica cuántos resultados recuperar, mientras que "num_candidates" especifica cuántos documentos candidatos se colocarán en el grupo para el cálculo. "query_vector" es una representación vectorial del texto de la consulta (" programación HTML y CSS " en nuestro caso ). Puede encontrar detalles sobre los parámetros de consulta knn aquí .

query_text = "HTML and CSS programming"
query_vector = model.encode(query_text)
query = {
    "field": "vector",
    "query_vector": query_vector,
    "k": 5,
    "num_candidates": 14
}

resp = client.search(index='vector_index', knn=query, source=False, fields=['title'])
for hit in resp['hits']['hits']:
    print(hit['_score'], hit['fields'])

El resultado que se muestra arriba es:

Los resultados devueltos por la consulta de ejemplo se muestran arriba. Aunque ninguno de los resultados devueltos contiene exactamente las mismas palabras, han capturado con éxito resultados semánticamente similares.

Entonces, ¿cómo deberíamos preparar consultas knn si también queremos utilizar estos resultados de búsqueda semántica con filtrado?

query = {
    "field": "vector",
    "query_vector": query_vector,
    "k": 5,
    "num_candidates": 14,
    "filter":[
        {"range": {"date": {"gte": "2023-07-01"}}},
        {"match": {"title": "Development"}}
    ]
}
resp = client.search(index='vector_index', knn=query, source=False, fields=['title'])
for hit in resp['hits']['hits']:
    print(hit['_score'], hit['fields'])

Cada filtro que aplicamos se proporciona como filtro en el parámetro knn. Puede agregar cualquier cantidad de filtros aquí y combinar los resultados en función de esos filtros. En el ejemplo anterior, se agregaron un filtro de fecha y un filtro de palabras clave para enumerar documentos semánticamente cercanos que contienen la palabra Desarrollo pero que tienen una fecha posterior al 1 de julio de 2023.

Nota importante : Elasticsearch realiza el filtrado después del proceso de búsqueda vectorial, por lo que puede haber casos en los que no devolverá exactamente k resultados. En la imagen de arriba, aunque el valor "k" está establecido en 5, la consulta devuelve 3 documentos como resultados. Esto se debe a que, en el conjunto de datos de ejemplo preparado, solo 3 documentos cumplen la condición especificada.

Para obtener más información sobre la búsqueda de vectores, consulte el capítulo " NLP: procesamiento de lenguaje natural y búsqueda de vectores " en el artículo " Elastic: una guía para desarrolladores ".

Supongo que te gusta

Origin blog.csdn.net/UbuntuTouch/article/details/132332809
Recomendado
Clasificación