diff --git a/diarios/admin.py b/diarios/admin.py index 96074c4..08af8ce 100644 --- a/diarios/admin.py +++ b/diarios/admin.py @@ -1,18 +1,13 @@ from django.contrib import admin -from .models import PDFDocument, DiarioOficial, TipoDiarioOficial from django.db import models - -@admin.register(PDFDocument) -class PDFDocumentAdmin(admin.ModelAdmin): - pass +from .models import DiarioOficial, TipoDiarioOficial @admin.register(DiarioOficial) class DiarioOficialAdmin(admin.ModelAdmin): pass - @admin.register(TipoDiarioOficial) class TipoDiarioOficialAdmin(admin.ModelAdmin): pass diff --git a/diarios/documents.py b/diarios/documents.py index e4f7fbb..21351a7 100644 --- a/diarios/documents.py +++ b/diarios/documents.py @@ -1,7 +1,9 @@ from django_elasticsearch_dsl import Document, fields from django_elasticsearch_dsl.registries import registry + from .models import DiarioOficial + @registry.register_document class DiarioOficialDocument(Document): tipo = fields.ObjectField(properties={ @@ -46,7 +48,6 @@ class DiarioOficialDocument(Document): 'lei, legislação, norma', 'processo, procedimento, autos', 'contrato, acordo, convênio', - # Adicione mais sinônimos relevantes para o contexto legal ] } }, diff --git a/diarios/migrations/0005_delete_pdfdocument.py b/diarios/migrations/0005_delete_pdfdocument.py new file mode 100644 index 0000000..0e0679e --- /dev/null +++ b/diarios/migrations/0005_delete_pdfdocument.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.12 on 2025-03-15 15:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("diarios", "0004_remove_diariooficial_finalizado_and_more"), + ] + + operations = [ + migrations.DeleteModel( + name="PDFDocument", + ), + ] diff --git a/diarios/models.py b/diarios/models.py index 1d013a4..1bdcfe8 100644 --- a/diarios/models.py +++ b/diarios/models.py @@ -1,43 +1,13 @@ -import requests +import json 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 +import requests from babel.dates import format_date - - -class PDFDocument(models.Model): - title = models.CharField(max_length=255) - file = models.FileField(upload_to="pdfs/") - content = models.TextField(blank=True) - uploaded_at = models.DateTimeField(auto_now_add=True) - page_content = models.TextField(blank=True) - - def __str__(self): - return self.title - - def save(self, *args, **kwargs): - if self.file: - pdf = PyPDF2.PdfReader(self.file) - texto = [] - pages_data = [] - - for i, pagina in enumerate(pdf.pages): - page_text = pagina.extract_text() - pages_data.append( - { - "number": i + 1, - "content": page_text, - } - ) - texto.append(pagina.extract_text()) - self.content = "\n".join(texto) - self.page_content = json.dumps(pages_data) - - super().save(*args, **kwargs) +from django.core.files.base import ContentFile +from django.core.serializers.json import DjangoJSONEncoder +from django.db import models class TipoDiarioOficial(models.Model): diff --git a/diarios/templates/diarios/diarios_search.html b/diarios/templates/diarios/diarios_search.html index 956643b..f2e91e6 100644 --- a/diarios/templates/diarios/diarios_search.html +++ b/diarios/templates/diarios/diarios_search.html @@ -3,20 +3,73 @@ {% block content %}

Busca de Diários Oficiais

- +
-
- - +
+
+
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
- + + {% if error %} +
+ Erro na pesquisa: {{ error }} +
+ {% endif %} + {% if query %}

Resultados para "{{ query }}"

Encontrados {{ total }} resultados

+ + {% if did_you_mean %} +
+ Você quis dizer: {{ did_you_mean }}? +
+ {% endif %} + + {% if search_suggestions %} +
+
Pesquisas relacionadas:
+
+ {% for suggestion in search_suggestions %} + {{ suggestion }} + {% endfor %} +
+
+ {% endif %}
- + {% if results %}
{% for result in results %} @@ -24,6 +77,9 @@
{{ result.tipo }} nº {{ result.numero }}

Data: {{ result.data }}

+ {% if result.occurrences > 0 %} + {{ result.occurrences }} ocorrências encontradas + {% endif %}
{% if result.highlight %} @@ -32,7 +88,7 @@
{{ result.highlight|safe }}
{% endif %} - + {% if result.highlighted_pages %}
Páginas com o termo buscado:
@@ -40,12 +96,12 @@ {% for page in result.highlighted_pages %}

-

-
{{ page.content|safe }} @@ -56,7 +112,7 @@
{% endif %} - + {% endfor %}
- - + + {% if total_pages > 1 %} {% endif %} - + {% else %}
Nenhum resultado encontrado para a sua busca. + + {% if date_start or date_end %} +

Tente expandir o período de busca ou remover os filtros de data.

+ {% endif %} + + {% if match_type == 'exact' %} +

Tente usar a opção "Qualquer palavra" para resultados mais abrangentes.

+ {% endif %}
{% endif %} {% endif %} @@ -112,13 +197,30 @@ padding: 2px; border-radius: 2px; } - + .accordion-body em { background-color: #ffeeba; font-style: normal; padding: 2px; border-radius: 2px; } + + .badge { + font-size: 0.85rem; + } + + {% endblock %} diff --git a/diarios/views.py b/diarios/views.py index afc958e..2d99dee 100644 --- a/diarios/views.py +++ b/diarios/views.py @@ -1,80 +1,206 @@ from django.shortcuts import render from elasticsearch_dsl import Q +from datetime import datetime from .documents import DiarioOficialDocument +from elasticsearch.exceptions import RequestError def search_diarios(request): q = request.GET.get('q', '') page = int(request.GET.get('page', 1)) size = int(request.GET.get('size', 10)) + # Parâmetros de filtro de data + date_start = request.GET.get('date_start', '') + date_end = request.GET.get('date_end', '') + + # Tipo de correspondência (exata ou parcial) + match_type = request.GET.get('match_type', 'partial') # 'exact' ou 'partial' + start = (page - 1) * size end = start + size - + results = [] total = 0 + did_you_mean = None + search_suggestions = [] - 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) + try: + if q: + # Construir a consulta base + search = DiarioOficialDocument.search() + + # Determinar o tipo de consulta com base no match_type + if match_type == 'exact': + # Correspondência exata (frase exata) + query = Q( + 'multi_match', + query=q, + fields=['content^3', 'tipo.nome^2', 'numero', 'pages.content'], + type='phrase' + ) + else: + # Correspondência parcial (qualquer termo) + query = Q( + 'multi_match', + query=q, + fields=['content^3', 'tipo.nome^2', 'numero', 'pages.content'], + fuzziness='AUTO', + operator='or' # Pelo menos um termo deve corresponder + ) + + # Aplicar a consulta principal + search = search.query(query) + + # Aplicar filtros de data se fornecidos + date_filters = [] + if date_start: + try: + date_start_obj = datetime.strptime(date_start, '%Y-%m-%d') + date_filters.append(Q('range', data={'gte': date_start_obj})) + except ValueError: + pass # Ignorar datas inválidas + + if date_end: + try: + date_end_obj = datetime.strptime(date_end, '%Y-%m-%d') + date_filters.append(Q('range', data={'lte': date_end_obj})) + except ValueError: + pass # Ignorar datas inválidas + + if date_filters: + for date_filter in date_filters: + search = search.filter(date_filter) + + # Configuração do highlighting + 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 + total_search = search.count() + search = search[start:end] + + # Executar a pesquisa + response = search.execute() + + total = response.hits.total.value + + # "Você quis dizer" - sugestão para termos com erros de digitação + if total < 3 and q: # Se poucos resultados, sugira correções + suggestion_search = DiarioOficialDocument.search() + suggestion_search = suggestion_search.suggest( + 'phrase_suggestion', + q, + phrase={ + 'field': 'content', + 'size': 5, + 'highlight': { + 'pre_tag': '', + 'post_tag': '' + } + } + ) + suggestion_result = suggestion_search.execute() - # 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 - }) + # Processe as sugestões + if hasattr(suggestion_result, 'suggest') and 'phrase_suggestion' in suggestion_result.suggest: + suggestions = suggestion_result.suggest['phrase_suggestion'][0]['options'] + if suggestions: + for suggestion in suggestions: + if suggestion['text'].lower() != q.lower(): + did_you_mean = suggestion['text'] + break + # Gerar sugestões de pesquisa relacionadas + if q: + # Use a expansão de termos para sugerir pesquisas relacionadas + related_search = DiarioOficialDocument.search() + related_search = related_search.query( + 'more_like_this', + fields=['content'], + like=q, + min_term_freq=1, + max_query_terms=12 + ) + related_search = related_search[:5] # Limite para 5 sugestões + + try: + related_results = related_search.execute() + + # Extraia termos relevantes dos resultados relacionados + for hit in related_results: + if hasattr(hit, 'content') and hit.content: + # Extraia alguns termos significativos do conteúdo + content_terms = hit.content.split()[:10] # Primeiros 10 termos + suggestion = ' '.join(content_terms) + if suggestion not in search_suggestions and suggestion != q: + search_suggestions.append(suggestion) + if len(search_suggestions) >= 5: # Limite para 5 sugestões + break + except: + # Ignore erros de sugestões relacionadas + pass - # 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) - + # Processar resultados + for hit in response: + # Adicionar destaque + highlight = "" + if hasattr(hit.meta, 'highlight'): + if 'content' in hit.meta.highlight: + highlight = "...".join(hit.meta.highlight.content) + + # Processar páginas com destaque + highlighted_pages = [] + total_occurrences = 0 + + if hasattr(hit.meta, 'highlight') and 'pages.content' in hit.meta.highlight: + # Calcular o número total de ocorrências + for content in hit.meta.highlight['pages.content']: + # Contar o número de tags, que representam termos destacados + total_occurrences += content.count('') + + # Processar os destaques por página + 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, + 'occurrences': total_occurrences + } + + results.append(result) + except RequestError as e: + # Tratar erros de consulta do Elasticsearch + error_message = str(e) + return render(request, 'diarios/diarios_search.html', { + 'error': error_message, + 'query': q + }) + context = { 'query': q, + 'date_start': date_start, + 'date_end': date_end, + 'match_type': match_type, 'results': results, 'total': total, 'page': page, 'size': size, 'total_pages': (total + size - 1) // size if total > 0 else 0, + 'did_you_mean': did_you_mean, + 'search_suggestions': search_suggestions[:5] # Limite para 5 sugestões } - + return render(request, 'diarios/diarios_search.html', context) def diario_detail(request, pk):