Busca híbrida: resultados combinados de texto completo e ELSER
Assim como na busca vetorial da seção anterior, nesta seção você aprenderá como combinar os melhores resultados de busca de consultas de texto completo e semânticas usando o algoritmo Reciprocal Rank Fusion .
Introdução às Subbuscas
A solução para implementar uma pesquisa híbrida de texto completo e vetor denso foi enviar uma solicitação de pesquisa que incluía os argumentos query, knn para solicitar as duas pesquisas e o argumento rrf para combiná-las em uma única lista de resultados.
A complicação que surge ao tentar fazer o mesmo para combinar pedidos de pesquisa de texto completo e de vetor esparso é que ambos usam o argumento query . Para poder fornecer as duas consultas que precisam ser combinadas com o algoritmo RRF, é necessário incluir dois argumentos query , e a solução para fazer isso é fazê-lo com Sub-Buscas.
A funcionalidade de subbuscas encontra-se atualmente em fase de pré-visualização técnica. Por esse motivo, o cliente Elasticsearch em Python não oferece suporte nativo a isso. Para contornar essa limitação, o método search() da classe Search pode ser alterado para enviar a solicitação de pesquisa usando o argumento body . Abaixo você pode ver uma implementação nova, porém similar, que usa o argumento body do cliente para enviar uma solicitação de pesquisa:
class Search:
# ...
def search(self, **query_args):
# sub_searches is not currently supported in the client, so we send
# search requests using the body argument
if 'from_' in query_args:
query_args['from'] = query_args['from_']
del query_args['from_']
return self.es.search(
index='my_documents',
body=json.dumps(query_args),
)Essa implementação não requer nenhuma alteração no aplicativo, pois é funcionalmente equivalente. A única diferença é que o método search() valida todos os argumentos antes de enviar a solicitação, sendo body a única exceção. O servidor sempre valida as solicitações, independentemente de como o cliente as envia.
Nesta versão, o argumento sub_searches pode ser usado em Search.search() para enviar várias consultas de pesquisa da seguinte forma:
results = es.search(
sub_searches=[
{
'query': { ... }, # full-text search
},
{
'query': { ... }, # semantic search
},
],
rank={
'rrf': {}, # combine sub-search results
},
aggs={ ... },
size=5,
from_=from_,
)Implementação de Busca Híbrida
Para concluir esta seção, vamos retomar a lógica de texto completo e combiná-la com a consulta de busca semântica apresentada anteriormente neste capítulo.
Abaixo você pode ver o endpoint handle_search() atualizado:
@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)
if parsed_query:
search_query = {
'sub_searches': [
{
'query': {
'bool': {
'must': {
'multi_match': {
'query': parsed_query,
'fields': ['name', 'summary', 'content'],
}
},
**filters
}
}
},
{
'query': {
'bool': {
'must': [
{
'text_expansion': {
'elser_embedding': {
'model_id': '.elser_model_2',
'model_text': parsed_query,
}
},
}
],
**filters,
}
},
},
],
'rank': {
'rrf': {}
},
}
else:
search_query = {
'query': {
'bool': {
'must': {
'match_all': {}
},
**filters
}
}
}
results = es.search(
**search_query,
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)Como você deve se lembrar, a função extract_filters() procurava filtros de categoria inseridos pelo usuário no prompt de pesquisa e retornava a parte restante como parsed_query. Se parsed_query estiver vazio, significa que o usuário inseriu apenas um filtro de categoria e, nesse caso, a consulta deve ser um simples match_all com a categoria selecionada como filtro. Isso é implementado na parte else da grande condicional.
Quando há uma consulta de pesquisa, a opção sub_searches é usada conforme mostrado na seção anterior para incluir as consultas multi_match e text_expansion , com a opção rank solicitando que os resultados das duas subpesquisas sejam combinados em uma única lista de resultados classificados. Para completar a consulta, os argumentos size e from_ são fornecidos para manter o suporte à paginação.
Clique aqui para consultar esta versão do aplicativo.