Busca por k-vizinhos mais próximos (kNN)
O algoritmo k-vizinhos mais próximos (kNN) realiza uma busca de similaridade em campos do tipo dense_vector . Esse tipo de busca, mais apropriadamente chamada de "kNN aproximado", aceita um vetor ou incorporação como termo de busca e encontra entradas no índice que sejam próximas.
Nesta seção, você aprenderá como executar uma pesquisa kNN usando os embeddings de documentos criados na seção anterior.
A consulta knn
Na seção de pesquisa de texto completo do tutorial, você aprendeu sobre a opção de consulta passada para o método search() do cliente Elasticsearch. Ao pesquisar vetores, a opção knn é usada em vez disso.
Abaixo você pode ver uma nova versão da função handle_search() em app.py que executa uma busca kNN para a consulta inserida pelo usuário no formulário de busca.
@app.post('/')
def handle_search():
query = request.form.get('query', '')
filters, parsed_query = extract_filters(query)
from_ = request.form.get('from_', type=int, default=0)
results = es.search(
knn={
'field': 'embedding',
'query_vector': es.get_embedding(parsed_query),
'num_candidates': 50,
'k': 10,
},
size=5,
from_=from_
)
return render_template('index.html', results=results['hits']['hits'],
query=query, from_=from_,
total=results['hits']['total']['value'])Nesta versão da função, a opção query foi substituída por knn. As opções size e from_ para paginação permanecem as mesmas, e todo o resto na função e no modelo index.html também são iguais a antes.
A opção de pesquisa knn aceita vários parâmetros que configuram a pesquisa:
field: o campo no índice a ser pesquisado. O campo deve ter o tipodense_vector.query_vector: o elemento incorporado a ser pesquisado. Este deve ser um elemento incorporado gerado a partir do texto da pesquisa.num_candidates: o número de documentos candidatos a serem considerados em cada fragmento. O Elasticsearch recupera essa quantidade de candidatos de cada shard, combina-os em uma única lista e, em seguida, encontra o "k" mais próximo para retornar como resultado.k: o número de resultados a serem retornados. Esse número tem um efeito direto no desempenho, portanto, deve ser mantido o menor possível. O valor passado nesta opção deve ser menor quenum_candidates.
Com as configurações utilizadas no código acima, serão retornados os 10 resultados que melhor correspondem à descrição.
Fique à vontade para experimentar esta nova versão do aplicativo. Aqui estão dois bons exemplos para ilustrar a utilidade desse tipo de busca:
Ao pesquisar por "holiday", que é o equivalente em inglês britânico a "vacation" em inglês americano, a busca do kNN retorna o documento "Vacation Policy" como primeiro resultado, embora a palavra "holiday" em si não apareça no documento.
A busca por "cães e gatos" ou qualquer outro termo relacionado a animais de estimação traz o documento "Política de Animais de Estimação do Escritório" como primeiro resultado, embora o resumo do documento não mencione nenhum animal de estimação específico.
Utilizando filtros em consultas kNN
A consulta de pesquisa, conforme definida na seção de texto completo deste tutorial, permitiu ao usuário solicitar que uma categoria específica fosse usada, usando a sintaxe category:<category-name> em qualquer lugar do texto de pesquisa. A função extract_filters() em app.py é responsável por encontrar e separar essas expressões de filtro da consulta de pesquisa. Na versão da função handle_search() da seção anterior, a variável filters não é usada, portanto os filtros de categoria são ignorados.
Felizmente, a opção knn também suporta filtragem. A opção de filtro aceita, na verdade, o mesmo tipo de filtros, portanto, os filtros podem ser inseridos diretamente na consulta knn , exatamente como são retornados pela função extract_filters() :
@app.post('/')
def handle_search():
query = request.form.get('query', '')
filters, parsed_query = extract_filters(query)
from_ = request.form.get('from_', type=int, default=0)
results = es.search(
knn={
'field': 'embedding',
'query_vector': es.get_embedding(parsed_query),
'k': 10,
'num_candidates': 50,
**filters,
},
size=5,
from_=from_
)
return render_template('index.html', results=results['hits']['hits'],
query=query, from_=from_,
total=results['hits']['total']['value'])As agregações também funcionam bem em consultas kNN, portanto, podem ser adicionadas novamente:
@app.post('/')
def handle_search():
query = request.form.get('query', '')
filters, parsed_query = extract_filters(query)
from_ = request.form.get('from_', type=int, default=0)
results = es.search(
knn={
'field': 'embedding',
'query_vector': es.get_embedding(parsed_query),
'k': 10,
'num_candidates': 50,
**filters,
},
aggs={
'category-agg': {
'terms': {
'field': 'category.keyword',
}
},
'year-agg': {
'date_histogram': {
'field': 'updated_at',
'calendar_interval': 'year',
'format': 'yyyy',
},
},
},
size=5,
from_=from_
)
aggs = {
'Category': {
bucket['key']: bucket['doc_count']
for bucket in results['aggregations']['category-agg']['buckets']
},
'Year': {
bucket['key_as_string']: bucket['doc_count']
for bucket in results['aggregations']['year-agg']['buckets']
if bucket['doc_count'] > 0
},
}
return render_template('index.html', results=results['hits']['hits'],
query=query, from_=from_,
total=results['hits']['total']['value'], aggs=aggs)Esta versão da função handle_search() tem a mesma funcionalidade que a versão de pesquisa de texto completo, implementada usando pesquisa vetorial em vez de pesquisa baseada em palavras-chave.
Na próxima seção, você aprenderá como combinar os resultados desses dois métodos de pesquisa diferentes.