From f2e5cd73b79a4e6bbadb654caafa74a5dc709aab Mon Sep 17 00:00:00 2001 From: root Date: Fri, 14 Mar 2025 17:36:14 +0100 Subject: [PATCH] arruma o processo de busca textual nos diarios --- config/settings/base.py | 34 ++ diarios/admin.py | 12 +- diarios/custom_filters.py | 8 + diarios/documents.py | 196 ++++------- .../management/commands/reindex_diarios.py | 24 ++ diarios/models.py | 60 +++- diarios/search_service.py | 67 ++++ diarios/signals.py | 26 +- diarios/templates/diarios/diarios_search.html | 124 +++++++ diarios/templates/diarios/search.html | 173 +++++++++ diarios/templates/diarios/search_results.html | 330 ------------------ diarios/urls.py | 7 +- diarios/views.py | 228 ++++-------- requirements/base.txt | 2 + sinonimos.txt | 4 + 15 files changed, 650 insertions(+), 645 deletions(-) create mode 100644 diarios/custom_filters.py create mode 100644 diarios/management/commands/reindex_diarios.py create mode 100644 diarios/search_service.py create mode 100644 diarios/templates/diarios/diarios_search.html create mode 100644 diarios/templates/diarios/search.html delete mode 100644 diarios/templates/diarios/search_results.html create mode 100644 sinonimos.txt diff --git a/config/settings/base.py b/config/settings/base.py index 25df074..9006d99 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -84,6 +84,7 @@ LOCAL_APPS = [ "diários_oficiais_alems.users", "diarios", "django_elasticsearch_dsl", + "rest_framework", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS @@ -293,3 +294,36 @@ ELASTICSEARCH_DSL = { "default": {"hosts": "http://elasticsearch:9200"}, # same as above } ELASTICSEARCH_HOSTS = "http://elasticsearch:9200" + +ELASTICSEARCH_INDEX_SETTINGS = { + 'number_of_shards': 1, + 'number_of_replicas': 0, + 'analysis': { + 'filter': { + 'portuguese_stop': { + 'type': 'stop', + 'stopwords': '_portuguese_' + }, + 'portuguese_stemmer': { + 'type': 'stemmer', + 'language': 'portuguese' + }, + 'synonym_filter': { + 'type': 'synonym', + 'synonyms_path': 'analysis/sinonimos.txt', + } + }, + 'analyzer': { + 'pt_analyzer': { + 'tokenizer': 'standard', + 'filter': [ + 'lowercase', + 'portuguese_stop', + 'portuguese_stemmer', + 'synonym_filter' + ] + } + } + } +} + diff --git a/diarios/admin.py b/diarios/admin.py index 4641b0b..96074c4 100644 --- a/diarios/admin.py +++ b/diarios/admin.py @@ -1,8 +1,18 @@ from django.contrib import admin -from .models import PDFDocument +from .models import PDFDocument, DiarioOficial, TipoDiarioOficial from django.db import models @admin.register(PDFDocument) class PDFDocumentAdmin(admin.ModelAdmin): pass + + +@admin.register(DiarioOficial) +class DiarioOficialAdmin(admin.ModelAdmin): + pass + + +@admin.register(TipoDiarioOficial) +class TipoDiarioOficialAdmin(admin.ModelAdmin): + pass diff --git a/diarios/custom_filters.py b/diarios/custom_filters.py new file mode 100644 index 0000000..7f0b7d1 --- /dev/null +++ b/diarios/custom_filters.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + +@register.filter +def get_range(value): + return range(value) + diff --git a/diarios/documents.py b/diarios/documents.py index a05ea81..e4f7fbb 100644 --- a/diarios/documents.py +++ b/diarios/documents.py @@ -1,157 +1,91 @@ -from django_elasticsearch_dsl import Document, fields -from django_elasticsearch_dsl.registries import registry -from .models import PDFDocument - - -@registry.register_document -class PDFDocumentDocument(Document): - title = fields.TextField() - content = fields.TextField(analyzer="portuguese") - pages = fields.NestedField( - properties={ - "number": fields.IntegerField(), - "content": fields.TextField(analyzer="portuguese"), - } - ) - - class Index: - name = "pdf_documents" - settings = { - "number_of_shards": 1, - "number_of_replicas": 0, - "analysis": { - "analyzer": { - "portuguese": { - "type": "custom", - "tokenizer": "standard", - "filter": [ - "lowercase", - "ascii_folding", - "portuguese_stemmer", - "stop", - "portuguese_synonyms", - ], - }, - "portuguese_search": { - "type": "custom", - "tokenizer": "standard", - "filter": [ - "lowercase", - "ascii_folding", - "portuguese_stemmer", - "stop", - "suggest_shingle", - ], - }, - }, - "filter": { - "suggest_shingle": { - "type": "shingle", - "min_shingle_size": 2, - "max_shingle_size": 3, - }, - "stop": {"type": "stop", "stopwords": "_portuguese_"}, - "ascii_folding": {"type": "asciifolding"}, - "portuguese_stemmer": {"type": "stemmer", "language": "portuguese"}, - "portuguese_synonyms": { - "type": "synonym", - "synonyms_path": "synonyms.txt", - "expand": True, - }, - }, - }, - } - - class Django: - model = PDFDocument - fields = ["uploaded_at", "file"] - from django_elasticsearch_dsl import Document, fields from django_elasticsearch_dsl.registries import registry from .models import DiarioOficial @registry.register_document class DiarioOficialDocument(Document): - # Campos principais - title = fields.TextField() - tipo = fields.KeywordField() - - # Campo para arquivo PDF (se aplicável) - arquivo = fields.TextField(attr="arquivo.url") - - # Nested field para páginas (usando o page_content) - pages = fields.NestedField( - properties={ - "number": fields.IntegerField(), - "content": fields.TextField(analyzer="portuguese") - } + tipo = fields.ObjectField(properties={ + 'nome': fields.TextField() + }) + + numero = fields.TextField() + data = fields.DateField() + link = fields.TextField() + + # Campo para armazenar todas as páginas para busca + content = fields.TextField( + analyzer='pt_analyzer', ) + + # Campo para armazenar páginas individualmente + pages = fields.NestedField(properties={ + 'number': fields.IntegerField(), + 'content': fields.TextField( + analyzer='pt_analyzer', + ) + }) class Index: - name = "diarios_oficiais" + name = 'diarios_oficiais' settings = { - "number_of_shards": 1, - "number_of_replicas": 0, - "analysis": { - "analyzer": { - "portuguese": { - "type": "custom", - "tokenizer": "standard", - "filter": [ - "lowercase", - "ascii_folding", - "portuguese_stemmer", - "stop", - "portuguese_synonyms", - ] + 'number_of_shards': 1, + 'number_of_replicas': 0, + 'analysis': { + 'filter': { + 'portuguese_stop': { + 'type': 'stop', + 'stopwords': '_portuguese_' }, - "portuguese_search": { - "type": "custom", - "tokenizer": "standard", - "filter": [ - "lowercase", - "ascii_folding", - "portuguese_stemmer", - "stop", - "suggest_shingle", + 'portuguese_stemmer': { + 'type': 'stemmer', + 'language': 'portuguese' + }, + 'synonym_filter': { + 'type': 'synonym', + 'synonyms': [ + 'lei, legislação, norma', + 'processo, procedimento, autos', + 'contrato, acordo, convênio', + # Adicione mais sinônimos relevantes para o contexto legal ] } }, - "filter": { - "suggest_shingle": { - "type": "shingle", - "min_shingle_size": 2, - "max_shingle_size": 3 - }, - "stop": {"type": "stop", "stopwords": "_portuguese_"}, - "ascii_folding": {"type": "asciifolding"}, - "portuguese_stemmer": {"type": "stemmer", "language": "portuguese"}, - "portuguese_synonyms": { - "type": "synonym", - "synonyms_path": "synonyms.txt", - "expand": True + 'analyzer': { + 'pt_analyzer': { + 'tokenizer': 'standard', + 'filter': [ + 'lowercase', + 'portuguese_stop', + 'portuguese_stemmer', + 'synonym_filter' + ] } } } } - + class Django: model = DiarioOficial fields = [ - "data", - "numero", - "link", + 'id' ] - + def prepare_tipo(self, instance): - return instance.tipo.nome if instance.tipo else None - - def prepare_title(self, instance): - return f"{instance.tipo.nome if instance.tipo else 'Diário'} {instance.numero}" - - def prepare_pages(self, instance): - # Prepara o campo pages usando o page_content + if instance.tipo: + return { + 'nome': instance.tipo.nome + } + return {} + + def prepare_content(self, instance): + """Concatena todo o conteúdo de todas as páginas em um único campo para busca""" if instance.page_content: - return instance.page_content # page_content já é uma lista de dicionários + return " ".join([page.get('content', '') for page in instance.page_content]) + return "" + + def prepare_pages(self, instance): + """Prepara o campo de páginas individuais para exibição e destaque""" + if instance.page_content: + return instance.page_content return [] diff --git a/diarios/management/commands/reindex_diarios.py b/diarios/management/commands/reindex_diarios.py new file mode 100644 index 0000000..6ccf046 --- /dev/null +++ b/diarios/management/commands/reindex_diarios.py @@ -0,0 +1,24 @@ +from django.core.management.base import BaseCommand +from django_elasticsearch_dsl.registries import registry + +class Command(BaseCommand): + help = 'Reindexar todos os Diários Oficiais no Elasticsearch' + + def handle(self, *args, **options): + self.stdout.write('Iniciando reindexação...') + + # Recria os índices + registry.delete_indices() + registry.create_indices() + + # Reindexar documentos + for index in registry.get_indices(): + self.stdout.write(f'Reindexando {index}...') + documents = [] + for doc in registry.get_documents(): + if index == doc._index._name: + self.stdout.write(f' + {doc.__name__}') + doc().update() + + self.stdout.write(self.style.SUCCESS('Reindexação concluída!')) + diff --git a/diarios/models.py b/diarios/models.py index 3afc642..1d013a4 100644 --- a/diarios/models.py +++ b/diarios/models.py @@ -1,7 +1,12 @@ +import requests +import os +from urllib.parse import urlparse +from django.core.files.base import ContentFile from django.db import models import PyPDF2 import json from django.core.serializers.json import DjangoJSONEncoder +from babel.dates import format_date class PDFDocument(models.Model): @@ -34,6 +39,7 @@ class PDFDocument(models.Model): super().save(*args, **kwargs) + class TipoDiarioOficial(models.Model): nome = models.CharField(max_length=100, unique=True) @@ -57,24 +63,51 @@ class DiarioOficial(models.Model): numero = models.CharField(max_length=20, unique=True) link = models.URLField(blank=True, null=True, unique=True) page_content = models.JSONField(encoder=DjangoJSONEncoder, blank=True, null=True) - + def save(self, *args, **kwargs): - if self.file: - pdf = PyPDF2.PdfReader(self.file) - pages_data = [] + # Se houver um link, baixa o PDF e extrai o conteúdo + if self.link and not self.arquivo: + try: + # Faz o download do PDF + response = requests.get(self.link) + response.raise_for_status() # Verifica se o download foi bem-sucedido - for i, pagina in enumerate(pdf.pages): - page_text = pagina.extract_text() - pages_data.append( - { - "number": i + 1, - "content": page_text, - } + # Define o nome do arquivo a partir do link + parsed_url = urlparse(self.link) + file_name = ( + os.path.basename(parsed_url.path) or f"diario_{self.numero}.pdf" ) - self.page_content = json.dumps(pages_data) + # Salva o arquivo no campo `arquivo` + self.arquivo.save(file_name, ContentFile(response.content), save=False) + + # Extrai o conteúdo do PDF + pdf = PyPDF2.PdfReader(self.arquivo) + pages_data = [] + + for i, pagina in enumerate(pdf.pages): + page_text = pagina.extract_text() + if page_text: # Ignora páginas sem conteúdo + pages_data.append( + { + "number": i + 1, + "content": page_text, + } + ) + + # Salva o conteúdo das páginas no campo `page_content` + self.page_content = pages_data + + except requests.RequestException as e: + print(f"Erro ao baixar o PDF: {e}") + except PyPDF2.PdfReadError as e: + print(f"Erro ao ler o PDF: {e}") + except Exception as e: + print(f"Erro inesperado: {e}") + + # Salva o modelo super().save(*args, **kwargs) - + @property def data_formatada(self): return format_date(self.data, format="long", locale="pt_BR") @@ -89,4 +122,3 @@ class DiarioOficial(models.Model): class Meta: constraints = [models.UniqueConstraint(fields=["numero"], name="unique_numero")] verbose_name_plural = "Diários Oficiais" - diff --git a/diarios/search_service.py b/diarios/search_service.py new file mode 100644 index 0000000..f72c755 --- /dev/null +++ b/diarios/search_service.py @@ -0,0 +1,67 @@ +from elasticsearch_dsl import Q, Search +from .documents import DiarioOficialDocument + +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' + ) + s = s.query(main_query) + + # Highlighting + if highlight: + s = s.highlight( + 'page_content.content', + fragment_size=150, + number_of_fragments=3, + pre_tags=[''], + post_tags=[''] + ) + + # Paginação + start = (page - 1) * page_size + end = start + page_size + s = s[start:end] + + # Executa a busca + response = s.execute() + + # 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 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 + } + diff --git a/diarios/signals.py b/diarios/signals.py index ff262d7..ec755b1 100644 --- a/diarios/signals.py +++ b/diarios/signals.py @@ -1,17 +1,15 @@ -from django.db.models.signals import post_save +from django.db.models.signals import post_save, post_delete from django.dispatch import receiver -import PyPDF2 -from io import BytesIO -from .models import PDFDocument -@receiver(post_save, sender=PDFDocument) -def extract_text(sender, instance, created, **kwargs): - print("Signal disparado!") # Teste se o Signal está funcionando - if created and instance.file: - pdf = PyPDF2.PdfReader(instance.file) - text = [] - for page in pdf.pages: - text.append(page.extract_text()) - instance.content = "\n".join(text) - instance.save(update_fields=["content"]) +@receiver(post_save, sender=DiarioOficial) +def update_document(sender, instance, **kwargs): + """Atualizar documento no Elasticsearch quando o objeto for salvo""" + DiarioOficialDocument.update_document(instance) + + +@receiver(post_delete, sender=DiarioOficial) +def delete_document(sender, instance, **kwargs): + """Deletar documento do Elasticsearch quando o objeto for deletado""" + document = DiarioOficialDocument.get(id=instance.id) + document.delete() diff --git a/diarios/templates/diarios/diarios_search.html b/diarios/templates/diarios/diarios_search.html new file mode 100644 index 0000000..956643b --- /dev/null +++ b/diarios/templates/diarios/diarios_search.html @@ -0,0 +1,124 @@ +{% extends "base.html" %} + +{% block content %} +
+

Busca de Diários Oficiais

+ +
+
+ + +
+
+ + {% if query %} +
+

Resultados para "{{ query }}"

+

Encontrados {{ total }} resultados

+
+ + {% if results %} +
+ {% for result in results %} +
+
+
{{ result.tipo }} nº {{ result.numero }}
+

Data: {{ result.data }}

+
+
+ {% if result.highlight %} +
+
Destaques:
+
{{ result.highlight|safe }}
+
+ {% endif %} + + {% if result.highlighted_pages %} +
+
Páginas com o termo buscado:
+
+ {% for page in result.highlighted_pages %} +
+

+ +

+
+
+ {{ page.content|safe }} +
+
+
+ {% endfor %} +
+
+ {% endif %} + + +
+
+ {% endfor %} +
+ + + {% if total_pages > 1 %} + + {% endif %} + + {% else %} +
+ Nenhum resultado encontrado para a sua busca. +
+ {% endif %} + {% endif %} +
+ + +{% endblock %} + diff --git a/diarios/templates/diarios/search.html b/diarios/templates/diarios/search.html new file mode 100644 index 0000000..c3d91e3 --- /dev/null +++ b/diarios/templates/diarios/search.html @@ -0,0 +1,173 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Busca de Diários Oficiais{% endblock %} + +{% block content %} +
+

Busca de Diários Oficiais

+ +
+
+
+
+
+ + +
+
+ +
+
+ + + +
+
+
+ +
+ {% for tipo in tipos_disponiveis %} +
+ + +
+ {% endfor %} +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+
+
+
+
+
+ + {% if query %} +
+
+

Resultados da busca

+ {{ total }} resultado(s) +
+ + {% if results %} + {% for result in results %} +
+
+
+ + {{ result.tipo_nome }} nº {{ result.numero }} - {{ result.data|date:"d/m/Y" }} + +
+ + {% if result.highlights %} +
+ {% for highlight in result.highlights %} +

...{{ highlight|safe }}...

+ {% endfor %} +
+ {% endif %} + +
+ + Relevância: {{ result.score|floatformat:2 }} + + {% if result.link %} + + Ver original + + {% endif %} +
+
+
+ {% endfor %} + + {% if pages > 1 %} + + {% endif %} + {% else %} +
+

Nenhum resultado encontrado

+

Não encontramos resultados para "{{ query }}". Tente ajustar seus termos de busca.

+
+ {% endif %} +
+ {% else %} +
+

Digite um termo de busca para encontrar diários oficiais

+
+ {% endif %} +
+{% endblock %} + diff --git a/diarios/templates/diarios/search_results.html b/diarios/templates/diarios/search_results.html deleted file mode 100644 index 3a5b304..0000000 --- a/diarios/templates/diarios/search_results.html +++ /dev/null @@ -1,330 +0,0 @@ - - - - - - {% if query %}{{ query }} - {% endif %}Pesquisa de Documentos - - - - - - - -
- -
-
- - - - - -
-
-
- -
-
- - -
-
- Use aspas duplas para buscar frases exatas, ex: "documento oficial" -
-
-
-
-
- -
- {% if query %} - -
- {% if total_hits > 0 %} -

Cerca de {{ total_hits }} resultados encontrados para "{{ query }}"

- {% else %} -

Nenhum resultado encontrado para "{{ query }}"

- {% endif %} -
- - - {% if spelling_correction %} -
-

Você quis dizer: {{ spelling_correction }}?

-
- {% endif %} - - - {% if suggestions %} -
-

Talvez você esteja procurando por: - {% for suggestion in suggestions %} - {{ suggestion }}{% if not forloop.last %}, {% endif %} - {% endfor %} -

-
- {% endif %} - - - {% if results %} -
- {% for result in results %} -
-
- {% if result.is_exact_match %} - Correspondência exata - {% endif %} - {% if result.is_related %} - Termo relacionado - {% endif %} -
-
- {{ result.highlighted_title|safe }} - - - -
-
{{ result.highlighted_content|safe }}
-
- {{ result.uploaded_at|date:"d/m/Y" }} - {% if result.matching_pages %} - - Páginas encontradas: - {% for page in result.matching_pages %} - {{ page }} - {% endfor %} - - {% endif %} -
-
- {% endfor %} -
- - - {% if total_pages > 1 %} - - {% endif %} - {% else %} - - {% endif %} - {% else %} - -
-

BuscaDocs

-
-
-
- - -
-
-

Pesquise em nossa biblioteca de documentos digitalizados

-
-
Dicas de pesquisa:
-
    -
  • Use aspas duplas para buscar frases exatas: "documento oficial"
  • -
  • Tente usar sinônimos se não encontrar resultados
  • -
  • Seja específico para encontrar documentos relevantes
  • -
-
-
-
- {% endif %} -
- - - - - - - - - - - - diff --git a/diarios/urls.py b/diarios/urls.py index 9b66c7c..60efb2a 100644 --- a/diarios/urls.py +++ b/diarios/urls.py @@ -1,7 +1,8 @@ from django.urls import path -from .views import search_view, spellcheck_view +from . import views urlpatterns = [ - path("pesquisa/", search_view, name="search_view"), - path("spellcheck/", spellcheck_view, name="spellcheck_view"), + path('diario//', views.diario_detail, name='diario_detail'), + path('diarios/search/', views.search_diarios, name='search_diarios'), ] + diff --git a/diarios/views.py b/diarios/views.py index edd3176..afc958e 100644 --- a/diarios/views.py +++ b/diarios/views.py @@ -1,159 +1,83 @@ -import json -import debugpy from django.shortcuts import render -from elasticsearch_dsl import Search, Q -from elasticsearch_dsl.connections import connections -from django.conf import settings -import re -from .documents import PDFDocument -from django.http import JsonResponse +from elasticsearch_dsl import Q +from .documents import DiarioOficialDocument - -# Configuração da conexão com o Elasticsearch -connections.create_connection(hosts=[settings.ELASTICSEARCH_HOSTS]) - - -def spellcheck_view(request): - query = request.GET.get("q", "") - suggestions = [] - - if query: - s = Search(index="pdf_documents") - s = s.suggest( - "auto_correct", - query, - phrase={ - "field": "suggest", - "size": 3, - "gram_size": 3, - "confidence": 2.0, - "direct_generator": [{"field": "suggest", "suggest_mode": "popular"}], - }, - ) - response = s.execute() - - if hasattr(response.suggest, "auto_correct"): - for option in response.suggest.auto_correct[0].options: - suggestions.append(option.text) - - return JsonResponse({"suggestions": suggestions}) - -def search_view(request): - query = request.GET.get('q', '') # Obtém o termo de pesquisa da URL +def search_diarios(request): + q = request.GET.get('q', '') page = int(request.GET.get('page', 1)) - + size = int(request.GET.get('size', 10)) + + start = (page - 1) * size + end = start + size + results = [] - suggestions = [] - spelling_correction = None - total_hits = 0 - per_page = 10 - - if query: - # Processamento especial para termos entre aspas - exact_phrases = re.findall(r'"([^"]*)"', query) - - # Remove os termos entre aspas da consulta principal - cleaned_query = query - for phrase in exact_phrases: - cleaned_query = cleaned_query.replace(f'"{phrase}"', '') - - # Remove espaços extras e pontuação desnecessária - cleaned_query = re.sub(r'\s+', ' ', cleaned_query).strip() - - # Cria uma consulta no Elasticsearch - search = Search(index='diarios_oficiais') - - # Lista para armazenar todas as consultas - queries = [] - - # Adiciona consulta para termos gerais (com fuzziness para tolerância a erros) - if cleaned_query: - queries.append( - Q('multi_match', - query=cleaned_query, - fields=['title^3', 'pages.content^2'], - fuzziness='AUTO', - boost=2) - ) - - # Adiciona consultas exatas para frases entre aspas (sem fuzziness) - for phrase in exact_phrases: - if phrase.strip(): - queries.append( - Q('match_phrase', - pages__content={ - 'query': phrase, - 'boost': 2, - 'slop': 0 # Sem flexibilidade na ordem das palavras - }) - ) - - # Combina as consultas com OR (se houver alguma) - if queries: - search = search.query( - Q('bool', should=queries, minimum_should_match=1) - ) - - # Configuração do highlight para mostrar mais contexto - search = search.highlight( - 'pages.content', - fragment_size=300, - number_of_fragments=2, - pre_tags=[''], - post_tags=[''] - ) - - # Paginação - search = search[(page-1)*per_page:page*per_page] - - # Executa a consulta - response = search.execute() - total_hits = response.hits.total.value - - # Processa os resultados - for hit in response: - # Extrai o conteúdo destacado ou usa o original - if hasattr(hit.meta, 'highlight') and hasattr(hit.meta.highlight, 'pages.content'): - highlighted_content = ' ... '.join(hit.meta.highlight['pages.content']) - else: - highlighted_content = "" - - # Extrai o título destacado ou usa o original - if hasattr(hit.meta, 'highlight') and hasattr(hit.meta.highlight, 'title'): - highlighted_title = hit.meta.highlight.title[0] - else: - highlighted_title = hit.title - - # Verifica se o resultado corresponde a uma frase exata - is_exact_match = any(phrase.lower() in hit.pages.content.lower() or - phrase.lower() in hit.title.lower() - for phrase in exact_phrases) - - results.append({ - 'id': hit.meta.id, - 'title': hit.title, - 'highlighted_title': highlighted_title, - 'highlighted_content': highlighted_content, - 'data': hit.data, - 'numero': hit.numero, - 'link': hit.link, - 'finalizado': hit.finalizado, - 'is_exact_match': is_exact_match - }) - - # Calcula a paginação - total_pages = (total_hits + per_page - 1) // per_page if total_hits > 0 else 0 - - # Renderiza o template com os resultados - return render(request, 'diarios/search_results.html', { - 'query': query, + total = 0 + + if q: + # Busca principal com boost para relevância + query = Q( + 'multi_match', + query=q, + fields=['content^3', 'tipo.nome^2', 'numero', 'pages.content'], + fuzziness='AUTO' + ) + + # Pesquisa com highlighting + search = DiarioOficialDocument.search() + search = search.query(query) + search = search.highlight('content', fragment_size=150, number_of_fragments=3) + search = search.highlight('pages.content', fragment_size=150, number_of_fragments=3) + + # Paginação + search = search[start:end] + + response = search.execute() + + total = response.hits.total.value + + for hit in response: + # Adicionar destaque + highlight = "" + if hasattr(hit.meta, 'highlight'): + if 'content' in hit.meta.highlight: + highlight = "...".join(hit.meta.highlight.content) + + # Processando páginas com destaque + highlighted_pages = [] + if hasattr(hit.meta, 'highlight') and 'pages.content' in hit.meta.highlight: + for i, content in enumerate(hit.meta.highlight['pages.content']): + # Encontre a página correspondente + page_number = i + 1 # Lógica simplificada, pode precisar de ajuste + highlighted_pages.append({ + 'number': page_number, + 'content': content + }) + + # Combine dados do documento com os destaques + result = { + 'id': hit.id, + 'tipo': hit.tipo.nome if hasattr(hit, 'tipo') and hit.tipo else '', + 'numero': hit.numero, + 'data': hit.data, + 'link': hit.link, + 'highlight': highlight, + 'highlighted_pages': highlighted_pages + } + + results.append(result) + + context = { + 'query': q, 'results': results, - 'suggestions': suggestions[:5], # Limita a 5 sugestões - 'spelling_correction': spelling_correction, - 'total_hits': total_hits, + 'total': total, 'page': page, - 'total_pages': total_pages, - 'page_range': range(max(1, page-2), min(total_pages+1, page+3)), - 'has_exact_phrases': bool(exact_phrases) - }) + 'size': size, + 'total_pages': (total + size - 1) // size if total > 0 else 0, + } + + return render(request, 'diarios/diarios_search.html', context) + +def diario_detail(request, pk): + diario = get_object_or_404(Diario, pk=pk) + return render(request, 'diarios/diario_detail.html', {'diario': diario}) diff --git a/requirements/base.txt b/requirements/base.txt index ce299c6..28863e1 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -16,7 +16,9 @@ django-crispy-forms==2.3 # https://github.com/django-crispy-forms/django-crispy crispy-bootstrap5==2024.10 # https://github.com/django-crispy-forms/crispy-bootstrap5 django-compressor==4.5.1 # https://github.com/django-compressor/django-compressor django-redis==5.4.0 # https://github.com/jazzband/django-redis +djangorestframework elasticsearch django-elasticsearch-dsl PyPDF2 +babel diff --git a/sinonimos.txt b/sinonimos.txt new file mode 100644 index 0000000..8cd9587 --- /dev/null +++ b/sinonimos.txt @@ -0,0 +1,4 @@ +lei, legislação, norma +processo, procedimento, autos +contrato, acordo, convênio +