This content originally appeared on DEV Community and was authored by Eduardo Oliveira
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))
]
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__"
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:
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
Referências
[1] Full-Text Search: Implementando com Postgres e Django
[2] A powerful full-text search in PostgreSQL in less than 20 lines
Foto de Capa por Douglas Lopes no Unsplash
This content originally appeared on DEV Community and was authored by Eduardo Oliveira
Eduardo Oliveira | Sciencx (2023-04-22T13:43:53+00:00) Full-Text-Search: Criando um Back-End de Filtro para o Django Rest-Framework. Retrieved from https://www.scien.cx/2023/04/22/full-text-search-criando-um-back-end-de-filtro-para-o-django-rest-framework/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.



