feat: várias melhorias e evoluções no projeto
This commit is contained in:
@ -1,67 +1,733 @@
|
||||
from elasticsearch_dsl import Q, Search
|
||||
from .documents import DiarioOficialDocument
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any, List
|
||||
from elasticsearch import Elasticsearch, AsyncElasticsearch
|
||||
from .schemas import ResultadoSchema, PaginaSchema
|
||||
import unicodedata
|
||||
import asyncio
|
||||
from django.conf import settings
|
||||
|
||||
class DiarioOficialSearchService:
|
||||
@staticmethod
|
||||
def search(query, highlight=True, fuzziness=1, page=1, page_size=10, tipos=None, data_inicio=None, data_fim=None):
|
||||
# Configura a busca básica
|
||||
s = DiarioOficialDocument.search().source(excludes=['page_content.content'])
|
||||
|
||||
# Filtros
|
||||
if tipos:
|
||||
s = s.filter('terms', tipo_nome=tipos)
|
||||
if data_inicio and data_fim:
|
||||
s = s.filter('range', data={'gte': data_inicio, 'lte': data_fim})
|
||||
|
||||
# Query principal com fuzziness e sinônimos
|
||||
main_query = Q(
|
||||
'multi_match',
|
||||
query=query,
|
||||
fields=[
|
||||
'numero^3', # Maior peso para o número
|
||||
'tipo_nome^2', # Peso médio para o tipo
|
||||
'page_content.content' # Peso padrão para o conteúdo
|
||||
],
|
||||
fuzziness=fuzziness,
|
||||
analyzer='portuguese_synonyms'
|
||||
async def is_fuzzy_appropriate(term: str) -> bool:
|
||||
"""
|
||||
Determina se a fuzziness é apropriada para o termo de busca.
|
||||
|
||||
Args:
|
||||
term: Termo de busca a ser avaliado
|
||||
|
||||
Returns:
|
||||
bool: True se fuzziness é apropriada, False caso contrário
|
||||
"""
|
||||
return not re.match(r"^\d+/\d+$", term.strip())
|
||||
|
||||
|
||||
async def parse_date(date_str: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
Converte string de data para formato ISO para ElasticSearch.
|
||||
|
||||
Args:
|
||||
date_str: String de data no formato YYYY-MM-DD
|
||||
|
||||
Returns:
|
||||
Optional[str]: Data formatada ou None se inválida
|
||||
"""
|
||||
if not date_str:
|
||||
return None
|
||||
try:
|
||||
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
except ValueError:
|
||||
print(f"Alerta: Formato de data inválido recebido: {date_str}")
|
||||
return None
|
||||
|
||||
|
||||
async def buscar_diarios(
|
||||
query: Optional[str] = None,
|
||||
data_inicio: Optional[str] = None,
|
||||
data_fim: Optional[str] = None,
|
||||
tipo_diario: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Realiza busca nos diários oficiais com os parâmetros fornecidos.
|
||||
|
||||
Args:
|
||||
query: Termo de busca
|
||||
data_inicio: Data inicial no formato YYYY-MM-DD
|
||||
data_fim: Data final no formato YYYY-MM-DD
|
||||
tipo_diario: Tipo de diário a ser filtrado
|
||||
page: Número da página de resultados
|
||||
page_size: Quantidade de resultados por página
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dicionário com resultados da busca
|
||||
"""
|
||||
try:
|
||||
es = AsyncElasticsearch(
|
||||
"http://elasticsearch:9200",
|
||||
request_timeout=30,
|
||||
basic_auth=(
|
||||
settings.ELASTICSEARCH_USER,
|
||||
settings.ELASTICSEARCH_PASSWORD,
|
||||
),
|
||||
)
|
||||
s = s.query(main_query)
|
||||
if not await es.ping():
|
||||
raise ConnectionError("Não foi possível conectar ao Elasticsearch")
|
||||
except Exception as e:
|
||||
print(f"Erro ao conectar com Elasticsearch: {e}")
|
||||
return {"total": 0, "resultados": [], "pagina": page, "por_pagina": page_size}
|
||||
|
||||
# Highlighting
|
||||
if highlight:
|
||||
s = s.highlight(
|
||||
'page_content.content',
|
||||
fragment_size=150,
|
||||
number_of_fragments=3,
|
||||
pre_tags=['<mark>'],
|
||||
post_tags=['</mark>']
|
||||
)
|
||||
es_body = {
|
||||
"query": {"bool": {"must": [], "filter": []}},
|
||||
"size": page_size,
|
||||
"from": (page - 1) * page_size,
|
||||
"_source": [
|
||||
"numero",
|
||||
"data",
|
||||
"link",
|
||||
"tipo",
|
||||
"paginas.numero",
|
||||
"paginas.conteudo",
|
||||
],
|
||||
}
|
||||
|
||||
# Paginação
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
s = s[start:end]
|
||||
parsed_dt_inicio = await parse_date(data_inicio)
|
||||
parsed_dt_fim = await parse_date(data_fim)
|
||||
if parsed_dt_inicio or parsed_dt_fim:
|
||||
date_range_filter = {}
|
||||
if parsed_dt_inicio:
|
||||
date_range_filter["gte"] = parsed_dt_inicio
|
||||
if parsed_dt_fim:
|
||||
date_range_filter["lte"] = parsed_dt_fim
|
||||
es_body["query"]["bool"]["filter"].append(
|
||||
{"range": {"data": date_range_filter}}
|
||||
)
|
||||
|
||||
# Executa a busca
|
||||
response = s.execute()
|
||||
if tipo_diario:
|
||||
es_body["query"]["bool"]["filter"].append({"term": {"tipo.nome": tipo_diario}})
|
||||
|
||||
# Formata os resultados
|
||||
results = []
|
||||
for hit in response:
|
||||
result = {
|
||||
'id': hit.id,
|
||||
'numero': hit.numero,
|
||||
'data': hit.data,
|
||||
'link': hit.link,
|
||||
'tipo_nome': hit.tipo_nome,
|
||||
'score': hit.meta.score
|
||||
if query:
|
||||
aplicar_fuzziness = await is_fuzzy_appropriate(query)
|
||||
text_query_bool = {
|
||||
"bool": {
|
||||
"should": [
|
||||
{
|
||||
"match_phrase": {
|
||||
"paginas.conteudo.search": {
|
||||
"query": query,
|
||||
"slop": 4,
|
||||
"boost": 5.0,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"paginas.conteudo.search": {
|
||||
"query": query,
|
||||
"fuzziness": "AUTO",
|
||||
"operator": "and",
|
||||
"prefix_length": 3,
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
"minimum_should_match": "75%",
|
||||
}
|
||||
if highlight and hasattr(hit.meta, 'highlight'):
|
||||
result['highlights'] = hit.meta.highlight['page_content.content'].to_dict()
|
||||
results.append(result)
|
||||
|
||||
return {
|
||||
'total': response.hits.total.value,
|
||||
'results': results
|
||||
}
|
||||
|
||||
nested_query = {
|
||||
"nested": {
|
||||
"path": "paginas",
|
||||
"query": text_query_bool,
|
||||
"inner_hits": {
|
||||
"highlight": {
|
||||
"fields": {"paginas.conteudo.search": {}},
|
||||
"fragment_size": 500,
|
||||
"number_of_fragments": 1,
|
||||
"pre_tags": ["<mark>"],
|
||||
"post_tags": ["</mark>"],
|
||||
},
|
||||
"_source": ["paginas.numero"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
es_body["query"]["bool"]["must"].append(nested_query)
|
||||
else:
|
||||
es_body["query"]["bool"]["must"].append({"match_all": {}})
|
||||
|
||||
# Adicionar min_score
|
||||
if query:
|
||||
es_body["min_score"] = 2.5 * len(query.split())
|
||||
try:
|
||||
response = await es.search(index="diario_oficial", body=es_body)
|
||||
except Exception as e:
|
||||
print(f"Erro ao executar busca no Elasticsearch: {e}")
|
||||
return {"total": 0, "resultados": [], "pagina": page, "por_pagina": page_size}
|
||||
finally:
|
||||
await es.close()
|
||||
|
||||
hits = response.get("hits", {})
|
||||
total = hits.get("total", {}).get("value", 0)
|
||||
resultados_formatados = []
|
||||
|
||||
for hit in hits.get("hits", []):
|
||||
source = hit.get("_source", {})
|
||||
resultado_data = {
|
||||
"id": hit.get("_id"),
|
||||
"numero": source.get("numero", ""),
|
||||
"data": source.get("data"),
|
||||
"link": source.get("link", ""),
|
||||
"tipo": source.get("tipo", {}).get("nome", "Sem Tipo"),
|
||||
"score": hit.get("_score"),
|
||||
"paginas": [],
|
||||
}
|
||||
|
||||
pagina_match = None
|
||||
if query and "inner_hits" in hit and "paginas" in hit["inner_hits"]:
|
||||
paginas_inner_hits = hit["inner_hits"]["paginas"]["hits"]["hits"]
|
||||
if paginas_inner_hits:
|
||||
inner_hit = paginas_inner_hits[0]
|
||||
inner_source = inner_hit.get("_source", {})
|
||||
page_num = inner_source.get("numero", "N/A")
|
||||
highlights = inner_hit.get("highlight", {}).get(
|
||||
"paginas.conteudo.search", []
|
||||
)
|
||||
highlight_content = " ... ".join(highlights) if highlights else ""
|
||||
|
||||
if page_num != "N/A" and highlight_content:
|
||||
pagina_match = PaginaSchema(
|
||||
numero=page_num, conteudo=highlight_content
|
||||
)
|
||||
|
||||
if pagina_match:
|
||||
resultado_data["paginas"].append(pagina_match)
|
||||
elif not query and "paginas" in source and source["paginas"]:
|
||||
primeira_pagina_source = source["paginas"][0]
|
||||
conteudo_orig = primeira_pagina_source.get("conteudo", "")
|
||||
resultado_data["paginas"].append(
|
||||
PaginaSchema(
|
||||
numero=primeira_pagina_source.get("numero", 0),
|
||||
conteudo=conteudo_orig,
|
||||
)
|
||||
)
|
||||
|
||||
resultados_formatados.append(ResultadoSchema(**resultado_data))
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"resultados": resultados_formatados,
|
||||
"pagina": page,
|
||||
"por_pagina": page_size,
|
||||
}
|
||||
|
||||
|
||||
async def remover_acentos(texto: str) -> str:
|
||||
"""
|
||||
Remove acentos de uma string para comparações mais neutras.
|
||||
|
||||
Args:
|
||||
texto: Texto a ser normalizado
|
||||
|
||||
Returns:
|
||||
str: Texto sem acentos
|
||||
"""
|
||||
return "".join(
|
||||
c
|
||||
for c in unicodedata.normalize("NFD", texto)
|
||||
if unicodedata.category(c) != "Mn"
|
||||
)
|
||||
|
||||
|
||||
async def processar_query(query: str) -> str:
|
||||
"""
|
||||
Faz pré-processamento da query para separar termos como 'processonº404'.
|
||||
|
||||
Args:
|
||||
query: Texto da consulta original
|
||||
|
||||
Returns:
|
||||
str: Consulta processada
|
||||
"""
|
||||
return re.sub(r"([a-zA-Z]+)(nº|n°|no)(\d+)", r"\1 \2 \3", query)
|
||||
|
||||
|
||||
async def montar_suggest_body(query_processada: str) -> dict:
|
||||
"""
|
||||
Monta o corpo da sugestão para a requisição ao Elasticsearch.
|
||||
|
||||
Args:
|
||||
query_processada: Query processada para sugestão
|
||||
|
||||
Returns:
|
||||
dict: Corpo da requisição para suggest
|
||||
"""
|
||||
return {
|
||||
"suggest": {
|
||||
"text": query_processada,
|
||||
"correcao": {
|
||||
"phrase": {
|
||||
"field": "paginas.conteudo.search",
|
||||
"size": 3,
|
||||
"gram_size": 2,
|
||||
"confidence": 0.8,
|
||||
"max_errors": 4,
|
||||
"highlight": {"pre_tag": "**", "post_tag": "**"},
|
||||
"collate": {
|
||||
"query": {
|
||||
"source": {
|
||||
"nested": {
|
||||
"path": "paginas",
|
||||
"query": {
|
||||
"bool": {
|
||||
"should": [
|
||||
{
|
||||
"match_phrase": {
|
||||
"paginas.conteudo.search": {
|
||||
"query": "{{suggestion}}",
|
||||
"slop": 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"params": {"field_name": "paginas.conteudo.search"},
|
||||
"prune": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
}
|
||||
|
||||
|
||||
async def sugestao_termo(query: str) -> Optional[str]:
|
||||
"""
|
||||
Oferece sugestões de correção para a query, verificando se a sugestão
|
||||
realmente retorna resultados antes de apresentá-la ao usuário.
|
||||
|
||||
Args:
|
||||
query: Texto da consulta original
|
||||
|
||||
Returns:
|
||||
Optional[str]: Sugestão para a consulta ou None
|
||||
"""
|
||||
es = await conectar_elasticsearch()
|
||||
if not es:
|
||||
return None
|
||||
|
||||
query_processada = await processar_query(query)
|
||||
suggest_body = await montar_suggest_body(query_processada)
|
||||
|
||||
try:
|
||||
response = await es.search(index="diario_oficial", body=suggest_body)
|
||||
suggestions = response.get("suggest", {}).get("correcao", [])
|
||||
|
||||
for sug in suggestions:
|
||||
for option in sug.get("options", []):
|
||||
if option.get("collate_match", False):
|
||||
sugestao = option["text"]
|
||||
|
||||
# Verifica se a sugestão é idêntica à query (ignorando acentos e case)
|
||||
if sugestao.lower() == query.lower():
|
||||
continue
|
||||
if await remover_acentos(sugestao.lower()) == await remover_acentos(
|
||||
query.lower()
|
||||
):
|
||||
continue
|
||||
|
||||
return sugestao
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao buscar sugestão: {e}")
|
||||
return None
|
||||
finally:
|
||||
await es.close()
|
||||
|
||||
|
||||
async def conectar_elasticsearch() -> Optional[AsyncElasticsearch]:
|
||||
"""
|
||||
Conecta ao Elasticsearch e retorna o cliente.
|
||||
|
||||
Returns:
|
||||
Optional[AsyncElasticsearch]: Cliente Elasticsearch ou None se falhar
|
||||
"""
|
||||
try:
|
||||
es = AsyncElasticsearch(
|
||||
"http://elasticsearch:9200",
|
||||
request_timeout=30,
|
||||
basic_auth=(
|
||||
settings.ELASTICSEARCH_USER,
|
||||
settings.ELASTICSEARCH_PASSWORD,
|
||||
),
|
||||
)
|
||||
if not await es.ping():
|
||||
raise ConnectionError("Não foi possível conectar ao Elasticsearch")
|
||||
return es
|
||||
except Exception as e:
|
||||
print(f"Erro ao conectar com Elasticsearch: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def construir_ordenacao(ordenar_por: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Constrói a cláusula de ordenação para a consulta.
|
||||
|
||||
Args:
|
||||
ordenar_por: Critério de ordenação
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Lista de ordenação para Elasticsearch
|
||||
"""
|
||||
if ordenar_por == "data_asc":
|
||||
return [{"data": {"order": "asc"}}]
|
||||
elif ordenar_por == "data_desc":
|
||||
return [{"data": {"order": "desc"}}]
|
||||
else: # relevancia
|
||||
return ["_score"]
|
||||
|
||||
|
||||
async def preencher_numero_do_diario_com_zeros(numero_diario: str) -> str:
|
||||
"""
|
||||
Preenche o número do diário com zeros à esquerda.
|
||||
|
||||
Args:
|
||||
numero_diario: Número do diário
|
||||
|
||||
Returns:
|
||||
str: Número formatado com zeros à esquerda
|
||||
"""
|
||||
return numero_diario.zfill(4)
|
||||
|
||||
|
||||
async def construir_filtros(
|
||||
data_inicio: Optional[str],
|
||||
data_fim: Optional[str],
|
||||
tipo_diario: Optional[str],
|
||||
numero_diario: Optional[str],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Constrói os filtros de data, tipo e número do diário.
|
||||
|
||||
Args:
|
||||
data_inicio: Data inicial no formato YYYY-MM-DD
|
||||
data_fim: Data final no formato YYYY-MM-DD
|
||||
tipo_diario: Tipo de diário a ser filtrado
|
||||
numero_diario: Número do diário
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Lista de filtros para Elasticsearch
|
||||
"""
|
||||
filtros = []
|
||||
|
||||
parsed_dt_inicio = await parse_date(data_inicio)
|
||||
parsed_dt_fim = await parse_date(data_fim)
|
||||
if parsed_dt_inicio or parsed_dt_fim:
|
||||
date_range = {}
|
||||
if parsed_dt_inicio:
|
||||
date_range["gte"] = parsed_dt_inicio
|
||||
if parsed_dt_fim:
|
||||
date_range["lte"] = parsed_dt_fim
|
||||
filtros.append({"range": {"data": date_range}})
|
||||
|
||||
if tipo_diario:
|
||||
filtros.append({"term": {"tipo.nome": tipo_diario}})
|
||||
|
||||
if numero_diario:
|
||||
# numero_diario = await preencher_numero_do_diario_com_zeros(numero_diario)
|
||||
filtros.append({"wildcard": {"numero": f"*{numero_diario}*"}})
|
||||
|
||||
return filtros
|
||||
|
||||
|
||||
async def construir_query_busca(
|
||||
query: Optional[str], modo_busca: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Constrói a query de busca com base no termo e modo de busca.
|
||||
|
||||
Args:
|
||||
query: Termo de busca
|
||||
modo_busca: Modo de busca (exata ou qualquer)
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Query de busca para Elasticsearch
|
||||
"""
|
||||
if not query:
|
||||
return {
|
||||
"nested": {
|
||||
"path": "paginas",
|
||||
"query": {"match_all": {}},
|
||||
"inner_hits": {
|
||||
"_source": ["paginas.numero", "paginas.conteudo"],
|
||||
"size": 100,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
should_queries = []
|
||||
|
||||
# Busca exata (boost maior)
|
||||
should_queries.append(
|
||||
{
|
||||
"match_phrase": {
|
||||
"paginas.conteudo.search": {"query": query, "slop": 3, "boost": 5.0}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Busca mais leve (separando termos), se modo_busca permitir
|
||||
if modo_busca == "qualquer":
|
||||
should_queries.append(
|
||||
{
|
||||
"match": {
|
||||
"paginas.conteudo.search": {
|
||||
"query": query,
|
||||
"operator": "or",
|
||||
"fuzziness": "AUTO",
|
||||
"prefix_length": 3,
|
||||
"boost": 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"nested": {
|
||||
"path": "paginas",
|
||||
"query": {"bool": {"should": should_queries, "minimum_should_match": 1}},
|
||||
"inner_hits": {
|
||||
"highlight": {
|
||||
"fields": {"paginas.conteudo.search": {}},
|
||||
"fragment_size": 500,
|
||||
"number_of_fragments": 1,
|
||||
"pre_tags": ["<mark>"],
|
||||
"post_tags": ["</mark>"],
|
||||
},
|
||||
"_source": ["paginas.numero"],
|
||||
"size": 100, # Aumentar para retornar mais páginas correspondentes
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def construir_request_body(
|
||||
query: Optional[str],
|
||||
modo_busca: str,
|
||||
ordenar_por: str,
|
||||
data_inicio: Optional[str],
|
||||
data_fim: Optional[str],
|
||||
tipo_diario: Optional[str],
|
||||
numero_diario: Optional[str],
|
||||
page: int,
|
||||
page_size: int,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Constrói o corpo da requisição para o Elasticsearch.
|
||||
|
||||
Args:
|
||||
query: Termo de busca
|
||||
modo_busca: Modo de busca (exata ou qualquer)
|
||||
ordenar_por: Critério de ordenação
|
||||
data_inicio: Data inicial
|
||||
data_fim: Data final
|
||||
tipo_diario: Tipo de diário
|
||||
numero_diario: Número do diário
|
||||
page: Número da página
|
||||
page_size: Tamanho da página
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Corpo da requisição para Elasticsearch
|
||||
"""
|
||||
es_body = {
|
||||
"query": {"bool": {"must": [], "filter": []}},
|
||||
"size": page_size,
|
||||
"from": (page - 1) * page_size,
|
||||
"_source": [
|
||||
"numero",
|
||||
"data",
|
||||
"link",
|
||||
"tipo",
|
||||
"paginas.numero",
|
||||
"paginas.conteudo",
|
||||
],
|
||||
}
|
||||
|
||||
# Adicionar ordenação
|
||||
es_body["sort"] = await construir_ordenacao(ordenar_por)
|
||||
|
||||
# Adicionar filtros
|
||||
es_body["query"]["bool"]["filter"] = await construir_filtros(
|
||||
data_inicio, data_fim, tipo_diario, numero_diario
|
||||
)
|
||||
|
||||
# Adicionar query principal
|
||||
query_principal = await construir_query_busca(query, modo_busca)
|
||||
es_body["query"]["bool"]["must"].append(query_principal)
|
||||
|
||||
return es_body
|
||||
|
||||
|
||||
async def processar_paginas_encontradas(
|
||||
hit: Dict[str, Any], query: Optional[str]
|
||||
) -> List[PaginaSchema]:
|
||||
"""
|
||||
Processa as páginas encontradas em um hit, retornando todas as correspondências.
|
||||
|
||||
Args:
|
||||
hit: Item de resultado do Elasticsearch
|
||||
query: Termo de busca original
|
||||
|
||||
Returns:
|
||||
List[PaginaSchema]: Lista de páginas encontradas
|
||||
"""
|
||||
paginas = []
|
||||
source = hit.get("_source", {})
|
||||
|
||||
if not query and "paginas" in source:
|
||||
for pagina in source["paginas"]:
|
||||
paginas.append(
|
||||
PaginaSchema(
|
||||
numero=pagina.get("numero", 0), conteudo=pagina.get("conteudo", "")
|
||||
)
|
||||
)
|
||||
# Se temos uma query e há inner_hits, processamos todas as páginas correspondentes
|
||||
if query and "inner_hits" in hit and "paginas" in hit["inner_hits"]:
|
||||
paginas_inner_hits = hit["inner_hits"]["paginas"]["hits"]["hits"]
|
||||
|
||||
# Processar todas as páginas encontradas, não apenas a primeira
|
||||
for inner_hit in paginas_inner_hits:
|
||||
inner_source = inner_hit.get("_source", {})
|
||||
page_num = inner_source.get("numero", "N/A")
|
||||
highlights = inner_hit.get("highlight", {}).get(
|
||||
"paginas.conteudo.search", []
|
||||
)
|
||||
highlight_content = " ... ".join(highlights) if highlights else ""
|
||||
|
||||
if page_num != "N/A" and highlight_content:
|
||||
paginas.append(
|
||||
PaginaSchema(numero=page_num, conteudo=highlight_content)
|
||||
)
|
||||
|
||||
# Se não há query ou não encontramos páginas nos inner_hits, usamos a primeira página do documento
|
||||
if not paginas and "paginas" in source and source["paginas"]:
|
||||
primeira_pagina_source = source["paginas"][0]
|
||||
conteudo_orig = primeira_pagina_source.get("conteudo", "")
|
||||
paginas.append(
|
||||
PaginaSchema(
|
||||
numero=primeira_pagina_source.get("numero", 0),
|
||||
conteudo=conteudo_orig,
|
||||
)
|
||||
)
|
||||
|
||||
return paginas
|
||||
|
||||
|
||||
async def processar_resultados(
|
||||
response: Dict[str, Any], query: Optional[str], page: int, page_size: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Processa os resultados da busca no Elasticsearch.
|
||||
|
||||
Args:
|
||||
response: Resposta do Elasticsearch
|
||||
query: Termo de busca original
|
||||
page: Número da página atual
|
||||
page_size: Tamanho da página
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Resultados processados
|
||||
"""
|
||||
hits = response.get("hits", {})
|
||||
total = hits.get("total", {}).get("value", 0)
|
||||
resultados_formatados = []
|
||||
|
||||
for hit in hits.get("hits", []):
|
||||
source = hit.get("_source", {})
|
||||
resultado_data = {
|
||||
"id": hit.get("_id"),
|
||||
"numero": source.get("numero", ""),
|
||||
"data": source.get("data"),
|
||||
"link": source.get("link", ""),
|
||||
"tipo": source.get("tipo", {}).get("nome", "Sem Tipo"),
|
||||
"score": hit.get("_score"),
|
||||
"paginas": [],
|
||||
}
|
||||
|
||||
# Processar todas as páginas encontradas
|
||||
resultado_data["paginas"] = await processar_paginas_encontradas(hit, query)
|
||||
|
||||
resultados_formatados.append(ResultadoSchema(**resultado_data))
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"resultados": resultados_formatados,
|
||||
"pagina": page,
|
||||
"por_pagina": page_size,
|
||||
}
|
||||
|
||||
|
||||
async def buscar_diarios_simples(
|
||||
query: Optional[str] = None,
|
||||
numero_diario: Optional[str] = None,
|
||||
modo_busca: str = "exata", # "exata" ou "qualquer"
|
||||
ordenar_por: str = "data_asc", # "relevancia" ou "data"
|
||||
data_inicio: Optional[str] = None,
|
||||
data_fim: Optional[str] = None,
|
||||
tipo_diario: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Função principal para buscar diários oficiais.
|
||||
|
||||
Args:
|
||||
query: Termo de busca
|
||||
numero_diario: Número do diário oficial
|
||||
modo_busca: Modo de busca (exata ou qualquer)
|
||||
ordenar_por: Critério de ordenação
|
||||
data_inicio: Data inicial
|
||||
data_fim: Data final
|
||||
tipo_diario: Tipo de diário
|
||||
page: Número da página
|
||||
page_size: Tamanho da página
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dicionário com os resultados da busca
|
||||
"""
|
||||
# Conectar ao Elasticsearch
|
||||
es = await conectar_elasticsearch()
|
||||
if not es:
|
||||
return {"total": 0, "resultados": [], "pagina": page, "por_pagina": page_size}
|
||||
|
||||
# Construir o corpo da requisição
|
||||
es_body = await construir_request_body(
|
||||
query,
|
||||
modo_busca,
|
||||
ordenar_por,
|
||||
data_inicio,
|
||||
data_fim,
|
||||
tipo_diario,
|
||||
numero_diario,
|
||||
page,
|
||||
page_size,
|
||||
)
|
||||
|
||||
# Executar a busca
|
||||
try:
|
||||
response = await es.search(index="diario_oficial", body=es_body)
|
||||
except Exception as e:
|
||||
print(f"Erro ao executar busca no Elasticsearch: {e}")
|
||||
return {"total": 0, "resultados": [], "pagina": page, "por_pagina": page_size}
|
||||
finally:
|
||||
await es.close()
|
||||
|
||||
# Processar e retornar os resultados
|
||||
return await processar_resultados(response, query, page, page_size)
|
||||
|
||||
Reference in New Issue
Block a user