En las aplicaciones web es una práctica común la realización de búsquedas en la base de datos a partir de entradas proporcionadas por el usuario. A continuación vamos a ver como introducir estas capacidades de búsqueda en la aplicación Blog.
El ORM de Django nos permite realizar búsquedas sencillas en la base de datos usando el filtro "contains" que no distingue por ejemplo entre mayúsculas o minúsculas. Por ejemplo podemos utilizarlo para ver los post que contienen la palabra "funciona" dentro del cuerpo del post.
from blog.models import Post
Post.objects.filter(contenido__contains='funciona')
Instalando PostgreSQL.
$ sudo apt-get install postgresql postgresql-contrib # Entramos en la consola de postgresql $ sudo su - postgres postgres@lenovo:~$ psql psql (14.5 (Ubuntu 14.5-0ubuntu0.22.04.1)) Type "help" for help. # Creamos un usuario y su contraseña. # En el ejemplo usuario = "tu_usuario" y contraseña="xxxxxxxx" postgres=# CREATE USER tu_usuario WITH PASSWORD 'xxxxxxxx';
Salida: CREATE ROLE # Creamos una base de datos de ejemplo. Se pude poner el nombre # que se quiera. # En el ejemplo aplicacion_db postgres=# CREATE DATABASE aplicacion_db OWNER tu_usuario ENCODING 'UTF8';
Salida: CREATE DATABASE # Para salir del terminal de postgresql y volver al terminal. postgres-# \q postgres@lenovo:~$ exit Salida: logout
Realizando una Copia de los datos existentes.
Reemplazando la base de datos en el proyecto.
PracticaDjango/PracticaDjango/settings.py
# Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'aplicacion_db', # nombre de la base de datos. 'USER': 'tu_usuario', # utiliza el usuario que hayas escogido. 'PASSWORD': 'xxxxxxxx', # Cambia las xxxxxxxx por la clave que hayas puesto. 'HOST': '127.0.0.1', 'DATABASE_PORT': '5432', } }
Reemplaza las 'xxxxxxxx' con la contraseña que usaste cuando creaste el usuario de la base de datos PostgreSQL. La nueva base de datos está vacia.
Ejecuta el siguiente comando para realizar las migraciones de la nueva base de datos PostgreSQL.
python manage.py migrate
Cargando los datos en la nueva base de datos.
from django.contrib.contenttypes.models import ContentType
ContentType.objects.all().delete()
Con esto deberíamos poder cargar sin problemas los datos de nuevo en la base nueva. Ejecuta de nuevo:
python manage.py loaddata mysite_data.json
Puede realizar, si quieres una copia de seguridad del contenido de tu ContentType, si quieres estar seguro de no perder nada usando:
python manage.py dumpdata contenttypes.ContentType
Una vez cambiado la base de datos y cargado los datos de la base antigua en la nueva vamos a ver que todo funcione. Ejecutamos el servidor de desarrollo y comprobamos que todo, sobre todo los post, este ahí y todo funcione correctamente.
Realizando búsquedas sencillas.
Edita el archivo settings.py del proyecto y añade django.contrib.postgres al grupo de aplicaciones instaladas:
PracticaDjango/PracticaDjango/settings.py
INSTALLED_APPS = [
# Nuestras aplicaciones
'Proyecto_web_app.apps.ProyectoWebAppConfig',
'Servicios.apps.ServiciosConfig',
'Blog.apps.BlogConfig',
'Contacto.apps.ContactoConfig',
# Aplicaciones de terceros
'django_bootstrap5',
# Mapa del Sitio
'django.contrib.sites', # add sites to installed_apps
'django.contrib.sitemaps', # add Django sitemaps to installed app
# PostgreSQL
'django.contrib.postgres',
# Aplicaciones por defecto
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
Buscando a través de varios campos.
Puede que quieras realizar una búsqueda a través de varios campos del modelo. En ese caso necesitaras crear un objeto SearchVector. Vamos a crear un vector para buscar en los campos 'titulo' y 'contenido' del modelo Post.
Ejecuta el siguiente código en el shell de Python:
>>> from django.contrib.postgres.search import SearchVector
>>> from Blog.models import Post
>>> Post.objects.annotate(search=SearchVector('titulo','contenido'),).filter(search='sitio')
<QuerySet [<Post: Este es el primer post de prueba>]>
>>>
Construyendo una vista de busqueda.
Vamos a construir una vista personalizada que permita a los usuario realizar búsquedas en los post que estén publicados. Lo primero que necesitaremos es crear un formulario de búsqueda. Edita el archivo forms.py de la aplicación Blog y añade el siguiente formulario:
PracticaDjango/Blog/forms.py
from django import forms
from .models import Comentario
class ComentarioForm(forms.ModelForm):
class Meta:
model = Comentario
class BusquedaForm(forms.Form):
consulta = forms.CharField()
/PracticaDjango/Blog/views.py
#...
# Importamos el formulario para los comentarios de cada post.
from .forms import ComentarioForm, BusquedaForm
from django.views.decorators.http import require_POST
# Para realizar busquedas en los campos de los post.
from django.contrib.postgres.search import SearchVector
#...
def busqueda_post(request):
formulario = BusquedaForm()
consulta = None
resultados = []
if "consulta" in request.GET:
formulario = BusquedaForm(request.GET)
if formulario.is_valid():
consulta = formulario.cleaned_data["consulta"]
resultados = Post.objects.annotate(
search=SearchVector("titulo", "contenido"),
).filter(search=consulta)
return render(
request,
"Blog/busqueda.html",
{"formulario": formulario, "consulta": consulta, "resultados": resultados},
)
PracticaDjango/Blog/templates/Blog/busqueda.html
<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}
<!-- Cargamos la etiqueta personalizada -->
{% load blog_tags %}
<!-- Establecemos el titulo de la página -->
{% block title %}Busqueda{% endblock %}
{% block content %}
<div class="container-fluid bg-white" style="margin-bottom: 50px;">
{% if consulta %}
<h1>Post que contienen la consulta "{{ consulta|truncatewords:15 }}"</h1>
<h3>
{% with resultados.count as total_results %}
Encontrados {{ total_results }} resultado{{total_results|pluralize}}
{% endwith %}
</h3>
{% for post in resultados %}
<h4>
<a href="{{ post.get_absolute_url }}">
{{ post.titulo }}
</a>
</h4>
{{ post.contenido|markdown|truncatewords_html:12 }}
{% empty %}
<p>No hay resultados para la consulta.</p>
{% endfor %}
<p><a href="{% url 'Blog:busqueda_post' %}">Realizar nueva búsqueda</a></p>
{% else %}
<h1>Busqueda en los Posts</h1>
<form method="get">
{{ formulario.as_p }}
<input type="submit" value="Busqueda">
</form>
{% endif %}
</div>
{% endblock %}
PracticaDjango/Blog/urls.py
from django.urls import path
from . import views
app_name = 'Blog'
urlpatterns = [
# post views
# path('', views.lista_post, name='lista_post'),
path('', views.lista_post.as_view(), name='lista_post'),
path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.detalle_post, name='detalle_post'),
path('categoria/<int:categoria_id>/', views.categoria, name='categoria'),
path('<int:post_id>/comentario/', views.post_comentario, name='post_comentario'),
path('busqueda/', views.busqueda_post, name='busqueda_post'),
]
Stemming y ordenación de los resultados por su relevancia.
Stemming, en el ámbito de la búsqueda de datos y procesamiento de texto en informática, se refiere a un proceso utilizado en el procesamiento del lenguaje natural (NLP) que busca reducir las palabras a su forma base o raíz, conocida como el "stem" en inglés. El objetivo principal es simplificar las palabras para que diferentes formas de la misma palabra se consideren como iguales, lo que facilita la búsqueda y el análisis de texto.
Por ejemplo, palabras como "correr", "corriendo" y "corrió" se reducirían al mismo stem, que en este caso sería "corr". Al hacer esto, se puede agrupar el contenido que contiene estas palabras en sus formas derivadas, facilitando el análisis y la recuperación de información.
El stemming no siempre produce palabras válidas o legibles, ya que se centra en la reducción de las palabras a su forma base sin considerar su significado contextual. Es una técnica útil en la recuperación de información y el procesamiento de texto, pero puede presentar limitaciones en ciertos contextos lingüísticos debido a las variaciones y excepciones en la estructura de las palabras en diferentes idiomas.
PracticaDjango/Blog/views.py
#... # Para realizar busquedas en los campos de los post. from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank #... def busqueda_post(request): formulario = BusquedaForm() consulta = None resultados = [] if "consulta" in request.GET: formulario = BusquedaForm(request.GET) if formulario.is_valid(): consulta = formulario.cleaned_data["consulta"] vector_busqueda = SearchVector("titulo","contenido", config='spanish') consulta_busqueda = SearchQuery(consulta, config='spanish') resultados = Post.objects.annotate( search = vector_busqueda, rank = SearchRank(vector_busqueda, consulta_busqueda) ).filter(search=consulta_busqueda).order_by('-rank') return render( request, "Blog/busqueda.html", {"formulario": formulario, "consulta": consulta, "resultados": resultados}, )
En el código superior, hemos creado un objeto SearchQuery, filtrando los resultados en base a él, y usando SearchRank para ordenar los resultados por su relevancia. Como verás hemos usado también el vector_busqueda y consulta_busqueda con el atributo config, lo que nos permite establecer diferentes configuraciones. Esto nos va a permitir trabajar con diferentes idiomas. Por ejemplo, en el código usamos el stemming y removemos las "stop words" usando el idioma Español.
Uso de ponderaciones en las consultas.
Podemos potenciar vectores específicos para que se les atribuya más peso cuando se ordenen los resultados por su relevancia. Por ejemplo, podemos utilizar esto para dar más relevancia a las publicaciones que coinciden por título en lugar de por contenido.
Edita el archivo views.py de la aplicación del blog y modifica la vista busqueda_post de la siguiente manera. El nuevo código está resaltado en negrita:
PracticaDjango/Blog/views.py
#... def busqueda_post(request): formulario = BusquedaForm() consulta = None resultados = [] if "consulta" in request.GET: formulario = BusquedaForm(request.GET) if formulario.is_valid(): consulta = formulario.cleaned_data["consulta"] vector_busqueda = SearchVector( "titulo", weight="A", config="spanish" ) + SearchVector("contenido", weight="B", config="spanish") consulta_busqueda = SearchQuery(consulta, config="spanish") resultados = ( Post.objects.annotate( search=vector_busqueda, rank=SearchRank(vector_busqueda, consulta_busqueda), ) .filter(rank__gte=0.3) .order_by("-rank") ) return render( request, "Blog/busqueda.html", {"formulario": formulario, "consulta": consulta, "resultados": resultados}, )
En el código anterior, aplicamos diferentes pesos a los vectores de búsqueda construidos utilizando los campos de título y cuerpo. Los pesos predeterminados son D, C, B y A, y se refieren a los números 0.1, 0.2, 0.4 y 1.0, respectivamente. Aplicamos un peso de 1.0 al vector de búsqueda del título (A) y un peso de 0.4 al vector del contenido (B). Las coincidencias en el título prevalecerán sobre las coincidencias en el contenido del cuerpo. Filtramos los resultados para mostrar solo aquellos con una clasificación superior a 0.3.