Lädt...


🔧 Full-Text-Search: Criando um Back-End de Filtro para o Django Rest-Framework


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

O texto Full-Text Search: Implementando com Postgres e Django [1] comenta sobre a implementação do sistema de Full-Text Search do Postgres, trazido pelo Leandro Proença no texto A powerful full-text search in PostgreSQL in less than 20 lines [2], utilizando o django.

O projeto está no GitHub [3] e, para complementá-lo, esse texto tem por objetivo, construir um back-end de filtro, i.e. um adapter de filtro, para lidar com o full-text search, como no algoritmo do texto anterior dentro do rest-framework.

Pra poder adicionar esse suporte, da melhor forma possível, podemos criar um filter back-end customizado. São utilizados, como referência, o SearchFilter original do django [4] e [5].

Mostre-me o código

O código desenvolvido nesse texto está disponível no repositório django-full-text-search no Github.

Implementando o BaseFilterBackend

Para criar o back-end de filtro, é preciso implementar a classe rest_framework.filters.BaseFilterBackend:

from rest_framework.filters import BaseFilterBackend

class FullTextSearchFilter(BaseFilterBackend):
    pass

Obtendo os parâmetros

Os primeiros métodos que serão implementados na classe acima são apenas métodos que buscam atributos na requisição, como o parâmetro ?search, ou no ModelViewSet como, por exemplo, o search_fields. Esse código é bem parecido com o da referência em [5]:

from rest_framework.filters import BaseFilterBackend
from rest_framework.settings import api_settings

class FullTextSearchFilter(BaseFilterBackend):
    search_param = api_settings.SEARCH_PARAM

    def get_config(self, view, request):
        return getattr(view, "search_config", None)

    def get_search_fields(self, view, request):
        return getattr(view, "search_fields", None)

    def get_similarity_threshold(self, view, request):
        return getattr(view, "similarity_threshold", 0)

    def get_search_term(self, request):
        params = request.query_params.get(self.search_param, '')
        params = params.replace('\x00', '')  # strip null characters
        params = params.replace(',', ' ')
        return params

Fazendo a Busca

O método mais importante dessa classe é, sem dúvidas, o filter_queryset que é o método que faz as alterações em um queryset para devolver a resposta da API.

É preciso, antes de tudo, obter os parâmetros para fazer nossa busca, por meio dos métodos implementados acima:

def filter_queryset(self, request, queryset, view):
    search_fields = self.get_search_fields(view, request)
    search_term = self.get_search_term(request)
    config = self.get_config(view, request)
    threshold = self.get_similarity_threshold(view, request)

Um primeiro ponto, que deve ser levado em consideração, é que, caso a variável search_fields ou a search_term não esteja preenchida, podemos retornar o queryset sem fazer alteração:

def filter_queryset(self, request, queryset, view):
    # ...

    if not search_term or not search_fields:
        return queryset

O restante do método é bem parecido com o que já implementamos no texto anterior:

def filter_queryset(self, request, queryset, view):
    # ...

    search_vector = SearchVector(*search_fields, config=config)
    search_query = SearchQuery(search_term, config=config)

    queryset = queryset.annotate(
        search=search_vector,
        rank=SearchRank(
            search_vector,
            search_query,
        ),
        similarity=TrigramSimilarity(*search_fields, search_term),
    ).filter(
        Q(search=search_query) | Q(similarity__gt=threshold)
    ).order_by("-rank", "-similarity")

    return queryset

Faz-se importante denotar que o search_fields aqui é usado como *search_fields para "desconstruir" o array. Assim, se search_fields = ["name", "description"], a criação da instância SearchVector seria feita como SearchVector("name", "description", config=config).

Por fim, a classe, completa, será:

class FullTextSearchFilter(BaseFilterBackend):
    search_param = api_settings.SEARCH_PARAM

    def get_config(self, view, request):
        return getattr(view, "search_config", None)

    def get_search_fields(self, view, request):
        return getattr(view, "search_fields", None)

    def get_similarity_threshold(self, view, request):
        return getattr(view, "similarity_threshold", 0)

    def get_search_term(self, request):
        params = request.query_params.get(self.search_param, '')
        params = params.replace('\x00', '')  # strip null characters
        params = params.replace(',', ' ')
        return params

    def filter_queryset(self, request, queryset, view):
        search_fields = self.get_search_fields(view, request)
        search_term = self.get_search_term(request)
        config = self.get_config(view, request)
        threshold = self.get_similarity_threshold(view, request)

        if not search_term or not search_fields:
            return queryset

        search_vector = SearchVector(*search_fields, config=config)
        search_query = SearchQuery(search_term, config=config)

        queryset = queryset.annotate(
            search=search_vector,
            rank=SearchRank(
                search_vector,
                search_query,
            ),
            similarity=TrigramSimilarity(*search_fields, search_term),
        ).filter(
            Q(search=search_query) | Q(similarity__gt=threshold)
        ).order_by("-rank", "-similarity")

        return queryset

Usando o FullTextSearchFilter

A classe FullTextSearchFilter pode ser utilizada nos filter_backends dos ModelViewSet do django-rest-framework. Simplificando:

from rest_framework import serializers
from rest_framework.viewsets import ModelViewSet
from texto.models import Singer
from core.filters import FullTextSearchFilter

class SingerSerializer(serializers.ModelSerializer):
    class Meta:
        model = Singer
        fields = "__all__"

class SingerViewSet(ModelViewSet):
    queryset = Singer.objects.all()
    serializer_class = SingerSerializer
    filter_backends = [FullTextSearchFilter]
    search_config = "portuguese"
    search_fields = ["name"]

Ao registrar o SingerViewSet nas urls do projeto já é possível fazer chamadas para o endpoint utilizando o ?search como full-text search:

from django.urls import path, include
from rest_framework.routers import SimpleRouter
from .viewsets import SingerViewSet

router = SimpleRouter()
router.register("singer", SingerViewSet, "Singer")

urlpatterns = [
    path('api/', include(router.urls))
]

Exemplo de chamada para a API com ?search=Marrone e mostrando os resultados filtrados e ordenados de modo correto

Mostrando o Rank e Similarity no retorno da API

É possível, inclusive, exibir os dados de rank e similarity no retorno da API. Como esses dados estão sendo anotados, i.e. acrescentados, na entidade, é possível, apenas, alterar o ModelSerializer:

class SingerSerializer(serializers.ModelSerializer):
    rank = serializers.FloatField(read_only=True)
    similarity = serializers.FloatField(read_only=True)

    class Meta:
        model = Singer
        fields = "__all__"

Exemplo de chamada para a API com ?search=Marrone e mostrando os resultados com os campos rank e similarity sendo exibidos

Mas, e sem a busca?

Acrescentar, apenas, o rank e similarity no ModelSerializer traz um problema: quando o endpoint é chamado sem o ?search os dados de rank e similarity não são retornados:

Exemplo de retorno da API sem utilizar o parâmetro ?search na URL e que os itens são retornados sem o campo rank e similarity

Isso pode ser resolvido, acrescentando, no construtor do FloatField, o parâmetro default=0:

class SingerSerializer(serializers.ModelSerializer):
    rank = serializers.FloatField(read_only=True, default=0)
    similarity = serializers.FloatField(read_only=True, default=0)

    class Meta:
        model = Singer
        fields = "__all__"

Filtrando por Similaridade

Por fim, para filtrar por similaridade, é possível definir a variável similarity_threshold no ModelViewSet:

class SingerViewSet(ModelViewSet):
    queryset = Singer.objects.all()
    serializer_class = SingerSerializer
    filter_backends = [FullTextSearchFilter]
    search_config = "portuguese"
    search_fields = ["name"]
    similarity_threshold = 0.3

Exemplo de chamada para a API com  raw `?search=Bruninho` endraw  exibindo apenas os itens com o campo "similarity" maior que 0.3

Referências

[1] Full-Text Search: Implementando com Postgres e Django

[2] A powerful full-text search in PostgreSQL in less than 20 lines

[3] django-full-text-search

[4] Filtering - SearchFilter

[5] rest_framework/filters.py

Foto de Capa por Douglas Lopes no Unsplash

...

🔧 Documentação técnica para iniciantes, parte 1: criando um bom README para o seu projeto


📈 47.61 Punkte
🔧 Programmierung

🕵️ CITSmart prior 9.1.2.28 Filtro de Autocomplete unknown vulnerability


📈 35.79 Punkte
🕵️ Sicherheitslücken

🔧 React: Criando um componente que transforma Json para Csv


📈 34.77 Punkte
🔧 Programmierung

🔧 Criando rotas dinâmicas para internacionalização (i18n) com Astro Build


📈 34.77 Punkte
🔧 Programmierung

🔧 Criando componentes para Web #01: Acessibilidade (a11y) na prática com WAI-ARIA


📈 34.77 Punkte
🔧 Programmierung

🔧 Docker para iniciantes: Criando Containers de Bancos de Dados


📈 34.77 Punkte
🔧 Programmierung

🔧 Criando meu próprio Github Actions para a área de AppSec


📈 34.77 Punkte
🔧 Programmierung

🔧 Announcing my new Django package: django-admin-export! #packaging #python #django


📈 33.61 Punkte
🔧 Programmierung

🔧 FullStack Next.js & Django Authentication: Django REST, TypeScript, JWT, Wretch & Djoser


📈 32.59 Punkte
🔧 Programmierung

📰 Nueve consejos para preparar al equipo de TI para el cambio


📈 25.7 Punkte
📰 IT Security Nachrichten

🔧 Redefinindo horizontes: Minha transição para a tecnologia e dicas para novos navegantes


📈 25.7 Punkte
🔧 Programmierung

🔧 Para quem é a sua homenagem para mulheres na tecnologia?


📈 25.7 Punkte
🔧 Programmierung

🔧 3 dicas para criar uma estratégia moderna de Testes para Microsserviços Spring Boot


📈 25.7 Punkte
🔧 Programmierung

🔧 Eligiendo la Arquitectura Correcta para Tu Aplicación Web: Un Enfoque Práctico para Startups


📈 25.7 Punkte
🔧 Programmierung

🔧 Comandos esenciales para la terminal: Guía práctica para principiantes


📈 25.7 Punkte
🔧 Programmierung

🔧 Comandos Linux para Redes: Um Guia Completo para DevOps


📈 25.7 Punkte
🔧 Programmierung

🔧 Dicas e truques: Ferramentas para produtividade para dev no Sistema operacional 🪟 Windows 11


📈 25.7 Punkte
🔧 Programmierung

🔧 Para te ajudar nesse caminho, preparei um guia completo com dicas valiosas para iniciantes na área:


📈 25.7 Punkte
🔧 Programmierung

🔧 Despliegue de aplicación de Django con Github Actions para un servidor propio


📈 24.05 Punkte
🔧 Programmierung

🔧 Django, Htmx e React: usando HTMX para além de TODO-Lists


📈 24.05 Punkte
🔧 Programmierung

🔧 GraphQL vs. REST: Qual é a Melhor Escolha para sua API?


📈 23.03 Punkte
🔧 Programmierung

🔧 REST vs GraphQL - Elegir la API adecuada para su proyecto


📈 23.03 Punkte
🔧 Programmierung

🔧 REST vs. GraphQL, Guía para Elegir la API Adecuada en tus Proyectos


📈 23.03 Punkte
🔧 Programmierung

🕵️ Vuln: Django 'django.views.static.serve()' Function Open Redirection Vulnerability


📈 22.41 Punkte
🕵️ Sicherheitslücken

🕵️ Django bis 1.8.17/1.9.12/1.10.6 django.utils.http.is_safe_url() Cross Site Scripting


📈 22.41 Punkte
🕵️ Sicherheitslücken

🔧 Django AllAuth Chapter 3 - Social login with Django AllAuth


📈 22.41 Punkte
🔧 Programmierung

🕵️ Django bis 1.8.17/1.9.12/1.10.6 django.views.static.serve() erweiterte Rechte


📈 22.41 Punkte
🕵️ Sicherheitslücken

🕵️ Django 1.4 up to 1.7 Redirect django.util.http.is_safe_url input validation


📈 22.41 Punkte
🕵️ Sicherheitslücken

matomo