Busca facetada
Nesta seção, você aprenderá sobre um padrão derivado de filtros que é amplamente utilizado em implementações de pesquisa, chamado pesquisa facetada. A ideia é permitir que o usuário execute uma consulta e, em seguida, apresentar a ele uma lista de filtros sugeridos juntamente com os resultados.
A captura de tela a seguir mostra uma barra lateral esquerda com facetas para os dois filtros atualmente implementados no aplicativo.

Segue abaixo um detalhamento dos resultados da busca facetada. Observe como cada entrada é exibida como um link clicável que adiciona o filtro à pesquisa atual. Cada rosto também indica quantos resultados ele inclui.

Agregações de termos
No Elasticsearch, a pesquisa facetada é implementada usando o recurso de agregações . Uma das agregações suportadas divide os resultados da pesquisa em grupos, com base em alguns critérios. A lista de buckets, cada um incluindo o número de documentos que contém, será usada para renderizar a barra lateral de facetas.
O tipo mais simples de agregação de buckets é aquele em que os buckets são definidos para cada palavra-chave. Este tipo, que é chamado de agregação de termos, é perfeito para criar os buckets para o campo category . Aqui está a solicitação de pesquisa do aplicativo, expandida para solicitar agregações de categorias:
results = es.search(
query={
'bool': {
**search_query,
**filters
}
},
aggs={
'category-agg': {
'terms': {
'field': 'category.keyword',
}
},
},
size=5,
from_=from_
)A única alteração é a adição da opção aggs . Cada agregação recebe um nome, neste caso category-agg. A agregação terms indica que a filtragem deve ser feita por palavra-chave. Assim como nos filtros, o campo category deve ser fornecido como category.keyword, para que o subtipo de palavra-chave associado ao campo seja usado.
A resposta a uma solicitação com agregações tem um campo aggregations com os resultados agregados. Eis como a resposta para a solicitação de exemplo acima poderia ser:
{
"aggregations": {
"category-agg": {
"buckets": [
{ "key": "sharepoint", "doc_count": 7 },
{ "key": "teams", "doc_count": 3 },
{ "key": "github", "doc_count": 2 },
],
// other fields not used in this tutorial are omitted
}
}
}O modelo index.html incluído no aplicativo tutorial já está configurado para renderizar agregações na barra lateral esquerda, que até então estava vazia. Para manter a lógica do modelo simples, os dados da resposta acima devem ser transformados em um dicionário com a seguinte estrutura:
{
"Category": {
"sharepoint": 7,
"teams": 3,
"github": 2
}
}A próxima listagem mostra como converter o formato de agregação do Elasticsearch para o dicionário simplificado acima e também como enviar o dicionário convertido para o modelo para renderização:
results = es.search(
# ...
)
aggs = {
'Category': {
bucket['key']: bucket['doc_count']
for bucket in results['aggregations']['category-agg']['buckets']
},
}
return render_template('index.html', results=results['hits']['hits'],
query=query, from_=from_,
total=results['hits']['total']['value'],
aggs=aggs)Caso você esteja curioso, o arquivo index.html inclui a seguinte lógica para renderizar o dicionário aggs :
{% for agg in aggs %}
<h6 class="mt-3">{{ agg }}</h6>
{% for key, count in aggs[agg].items() %}
<form method="POST">
<input type="hidden" name="query" value="{{ agg|lower }}:{{key}} {{ query }}">
<button type="submit" class="btn btn-link btn-sm"{% if aggs[agg]|length == 1 %} disabled{% endif %}>{{ key }} ({{ count }})</button>
</form>
{% endfor %}
{% endfor %}Esta implementação utiliza ideias semelhantes às usadas para renderizar os botões de paginação "próximo" e "anterior". Cada faceta é apresentada como um formulário com um campo oculto que define a consulta com o filtro adicional correspondente. Por exemplo, uma faceta de categoria sharepoint adicionaria category:sharepoint à consulta atual.
Como um mero detalhe estético, o botão de envio em cada seção é renderizado no formato de um link.
Agregações Anuais
O termo "agregações" usado com as categorias não funciona para o filtro de ano criado na seção anterior, porque, como você deve se lembrar, o índice não armazena os anos individualmente como palavras-chave. Em vez disso, o ano em que cada artigo foi atualizado é definido pelo campo updated_at , que armazena uma data completa.
Dentre a longa lista de agregações de buckets disponíveis, o histograma de datas é o que melhor se adapta a este caso de uso. Segue abaixo a solicitação de agregações atualizada:
results = es.search(
query={
'bool': {
**search_query,
**filters
}
},
aggs={
'category-agg': {
'terms': {
'field': 'category.keyword',
}
},
'year-agg': {
'date_histogram': {
'field': 'updated_at',
'calendar_interval': 'year',
'format': 'yyyy',
},
},
},
size=5,
from_=from_
)Aqui você pode ver que uma segunda agregação foi adicionada ao campo aggs . O tipo desta agregação é date_histogram e o intervalo é definido como year para que os buckets criados representem cada um um ano. A opção format configura o formato a ser usado para o nome de cada bucket, que neste caso deve incluir apenas o ano.
O campo aggregations na resposta agora incluirá duas seções:
{
"aggregations": {
"category-agg": {
"buckets": [
{ "key": "sharepoint", "doc_count": 7 },
{ "key": "teams", "doc_count": 3 },
{ "key": "github", "doc_count": 2 },
],
// fields not used in this tutorial are omitted
},
"year-agg": {
"buckets": [
{ "key_as_string": "2018", "doc_count": 6 },
{ "key_as_string": "2019", "doc_count": 1 },
{ "key_as_string": "2020", "doc_count": 1 },
{ "key_as_string": "2021", "doc_count": 1 },
{ "key_as_string": "2022", "doc_count": 2 },
{ "key_as_string": "2023", "doc_count": 1 },
],
// fields not used in this tutorial are omitted
}
}
}Existe outra pequena complicação com essa segunda agregação. O campo key que está incluído em cada bucket não é útil, porque para agregações de intervalo de data ele está em unidades de milissegundos. Mas felizmente, a data renderizada no formato fornecido na opção format na agregação é fornecida em um campo key_as_string .
Eis como o dicionário aggs , incluindo todas as facetas, é calculado:
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
},
}Além de usar key_as_string em vez de key, para as facetas de ano, uma condição é adicionada para eliminar quaisquer buckets que tenham zero documentos, já que obviamente não há motivo para usá-los como filtros.
E com isso, a implementação da busca facetada está completa. Aqui está a implementação completa da função handle_search() :
@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 = {
'must': {
'multi_match': {
'query': parsed_query,
'fields': ['name', 'summary', 'content'],
}
}
}
else:
search_query = {
'must': {
'match_all': {}
}
}
results = es.search(
query={
'bool': {
**search_query,
**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)A implementação da pesquisa facetada incluída neste tutorial foi projetada com a simplicidade em mente. As agregações no Elasticsearch oferecem muitas possibilidades que ainda não foram abordadas, portanto, certifique-se de consultar a documentação para aprender tudo o que esse recurso tem a oferecer.
Parabéns, você chegou ao final da seção de Busca de Texto Completo deste tutorial! Clique aqui para rever o estado atual do aplicativo de busca de tutoriais.