Django ORM Model Search Ordered By Search Term Relevance

Use this simple method of Django ORM search for a quick and easy site search.

Search Function

This function uses stem_phrase from a common Python Porter Stemmer implementation.


from django.db.models import Q, When, ExpressionWrapper, F, Value, FloatField, Case
from porter_stemmer import stem_phrase

def search(query_set, query):
    keywords = stem_phrase(query).split()

    if len(keywords) == 0:
        return query_set.none()

    def build_q(_keyword):
        return Q(keywords_stemmed__icontains=_keyword)

    q = build_q(keywords[0])
    for keyword in keywords[1:]:
        q = q | build_q(keyword)

    query_set = query_set.filter(q)
    cases = []
    
    length = len(keywords)
    if length > 4:
        length = 4

    for words in range(1, length):
        for subset in itertools.combinations(keywords, words):
            if len(subset) != 0:
                q = build_q(subset[0])
                for item in subset[1:]:
                    q = q & build_q(item)

                cases.append(When(
                    q,
                    then=ExpressionWrapper(
                        Value(words),
                        output_field=FloatField()
                    )
                ))

    cases.reverse()

    query_set = query_set.annotate(value=Case(*cases, default=Value(0), output_field=FloatField()))
    results = []
    for r in query_set:
        for keyword in keywords:
            r.value += r.keywords_stemmed.count(' {} '.format(keyword)) * .01
        results.append(r)
    results = sorted(results, key=lambda result: -result.value)
    return results

Model Setup


class Post(models.Model):
    title = models.CharField(max_length=80)
    content = models.TextField()
    keywords_stemmed = models.TextField(default='', blank=True)

    def save(self, *args, **kwargs):
        # Add all fields you would like to pull keywords from here
        self.keywords_stemmed = ' ' + ' '.join([
            stem_phrase(self.title.lower()),
            stem_phrase(self.content.lower()),
        ])
        return super().save(*args, **kwargs)

Usage


def view_func(request, **kwargs):
    results = search(Post.objects.all(), request.GET.get('query'))