I am trying to order results of a query with parameters by number of matches.
For example, let’s say we have a Model:
class Template(models.Model):
headline = CharField(max_length=300)
text = TextField()
image_text = TextField(max_length=500, blank=True, null=True)
tags = TaggableManager(through=TaggedItem)
With a Serializer:
class TemplateSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Template
fields = ( )
And a ViewSet:
class TemplateViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows Templates to be viewed or edited.
"""
queryset = Template.objects.all()
serializer_class = TemplateSerializer
def get_queryset(self):
queryset = Template.objects.all()
tags = self.request.query_params.getlist('tags', None)
search_text = self.request.query_params.getlist('search_text', None)
if tags is not None:
queries = [Q(groost_tags__name__iexact=tag) for tag in tags]
query = queries.pop()
for item in queries:
query |= item
queryset = queryset.filter(query).distinct()
if search_tags is not None:
queries = [Q(image_text__icontains=string) |
Q(text__icontains=string) |
Q(headline__icontains=string) for string in search_tags]
query = queries.pop()
for item in queries:
query |= item
queryset = queryset.filter(query).distinct()
What I need to do is count every match the filter finds and then order the queryset by that number of matches for each template. For example:
I want to find all the templates that have “hello” and “world” strings in their text, image_text or headline. So I set the query parameter “search_text” to hello,world. Template with headline=”World“ and text=”Hello, everyone.” would have 2 matches. Another one with headline=”Hello“ would have 1 match. The template with 2 matches would be the first in the queryset. The same behaviour should work for tags and tags with search_text combined.
I tried to calculate these numbers right in the ViewSet and then return a sorted(queryset, key=attrgetter(‘matches’)) but encountered several issues with the DRF, like Template has no attribute ‘matches’. Or 404 when directly accessing a Template instance through API.
Any ideas?
Advertisement
Answer
Give a try to annotation
where each matching pair returns 1 or 0 that are summarized into rank:
from django.db.models import Avg, Case, F, FloatField, Value, When
Template.objects.annotate(
k1=Case(
When(image_text__icontains=string, then=Value(1.0)),
default=Value(0.0),
output_field=FloatField(),
),
k2=Case(
When(text__icontains=string, then=Value(1.0)),
default=Value(0.0),
output_field=FloatField(),
),
k3=Case(
When(headline__icontains=string, then=Value(1.0)),
default=Value(0.0),
output_field=FloatField(),
),
rank=F("k1") + F("k2") + F("k3"),
).order_by("-rank")