Chandra Shekhar Pandey
Chandra Shekhar Pandey

Reputation: 157

Django ManytoMany Field Queryset for Exact and Similar Match

I have the following models:

class Disease(models.Model):
    name = CICharField("Disease Name", max_length=200, unique=True)
    symptoms = models.ManyToManyField(Symptom, through='DiseaseSymptom', related_name='diseases')


class Symptom(models.Model):
    name = CICharField("Symptom Name", max_length=200, unique=True)

On the front-end, I have multiple select boxes where users can select multiple Symptoms to find disease and that will pass to Disease model as symptoms_selected params.

I have the following get_queryset on Disease > views.py


def get_queryset(self):
        params = self.request.query_params
        query_symptoms = self.request.GET.getlist('symptoms_selected')
        if query_symptoms:
            i = 0
            queryset = Disease.objects.all()
            while i < (len(query_symptoms)):
                symptom = [query_symptoms[i]]
                queryset = queryset.filter(symptoms__id__in=symptom)
                i=i+1
        else:
            queryset = Disease.objects.all()
    return queryset
serializer_class = DiseaseSerializer

I am using Django REST API to pass the result data.

class DiseaseSerializer(serializers.ModelSerializer):

    class Meta:
        model = Disease
        fields = ('id', 'name', 'symptoms')

For eg: Disease Data:

Disease A: Got Symptoms: A, B, C, D

Disease B: Got Symptoms: A, D, P, Q

Disease C: Got Symptoms: A, Q, X, Y

Currently, if Users select 3 symptoms: A, D, Y. It returns Null. Instead of Null, I want to show users some similar matches with 2 and 1 symptoms.

I want queryset will return:

Exact match diseases i.e. 3 Symptoms matched diseases

2 Symptoms matched diseases

1 symptom matched diseases

Can anyone tell me how can I achieve this?

Upvotes: 1

Views: 421

Answers (1)

Iain Shelvington
Iain Shelvington

Reputation: 32274

A single __in filter will get all diseases that have at least one matching symptom

Then you can annotate the query with the count of matches and then order by this annotation to get the diseases with the most matches first

from django.db.models import Count

Disease.objects.filter(
    symptoms__id__in=query_symptoms
).annotate(
    matches=Count('symptoms')
).order_by('-matches')

The regroup tag can be used to group the results by number of matches in your template

{% regroup diseases by matches as diseases_grouped %}
<ul>
{% for match_count in diseases_grouped %}
    <li>{{ match_count.grouper }} matches
    <ul>
        {% for disease in match_count.list %}
          <li>{{ disease }}</li>
        {% endfor %}
    </ul>
    </li>
{% endfor %}
</ul>

Use a SerializerMethodField to add a field to your serializer to access the annotation

class DiseaseSerializer(serializers.ModelSerializer):

    num_matches = serializers.SerializerMethodField()

    class Meta:
        model = Disease
        fields = ('id', 'name', 'symptoms')

    def get_num_matches(self, obj):
        return getattr(obj, 'matches', None)

Upvotes: 1

Related Questions