Search code examples
pythondjangosearchpartial-matches

how to split words that user inserts in input field in Django app


I have a search bar that is searching in 2 models columns title, body, short_description. I am.using MySQL database. Right now, I am using Q lookups but there are some search limitations that I'd like to 'improve'.

One of them is that Q lookups find only results based only on phrase results that are exactly the same as in field so for instance, I have the title why python is so amazing? and I must write why or python or python is in order to get results. What I'd like to get is to extend the search bar to work in the following way:

A user inserts a question in the search bar: python language and search lookup is splitting each word and returning all objects that contain python or language. In the end the result would return object with why python is so amazing?, no matter it user puts python language or amazing python.

I am posting my current code below:

views.py

def search_items(request):
    query = request.GET.get('q')
    article_list= Article.objects.filter(title__icontains=query)
    qa_list = QA.objects.filter(title__icontains=query)

    if query is not None:
        lookups = Q(title__icontains=query) | Q(short_description__icontains=query) | Q(body__icontains=query)
        article_list= Article.objects.filter(lookups, status=1).distinct()
        qa_list = QA.objects.filter(lookups, status=1).distinct()

    context = {
        'query_name': query,
        'article_list': article_list,
        'qa_list': qa_list,
    }
    return render(request, 'search/search_items.html', context)

I've checked this solution and this one but the results are not satisfactory because when I put python language to find an object with the title why python is so amazing I get no result.

Question

I'd appreciate any advice on how to achieve the result where I get objects with a list of all objects based on words that the user puts into the input field.


Solution

  • I ran into the same issue, and solved it by adding a custom search manager in models.py, above my model. The manager has two methods, one for single word searches, another for multi-word. The query string is split into a list of words using the .split(), (see the view) below.

    models.py

    class MyModelSearchManager(models.QuerySet):
        def search(self, query=None):
            qs = self
            if query is not None:
                or_lookup = (Q(some_field__icontains=query))
                qs = qs.filter(or_lookup).distinct()
            return qs
    
        def search_and(self, query=None):
            qs = self
            if query is not None:
                or_lookup = reduce(lambda x, y: x & y, [Q(some_field__icontains=word) for word in query])
                qs = qs.filter(or_lookup).distinct()
            return qs
    
    class MyModelManager(models.Manager):
        def get_queryset(self):
            return MyModelSearchManager(self.model, using=self._db)
    
        def search(self, query=None):
            return self.get_queryset().search(query=query)
    
        def search_and(self, query=None):
            return self.get_queryset().search_and(query=query)
    
    

    and of course, declare the custom manager below your model fields:

    objects = MyModelManager()
    

    Then, in your view, split the search string and distinguish between single word searches and multi-word searches:

    class SearchView(ListView):
        template_name = 'my_app_templates/search_results.html'
        count = 0
        
        def get_context_data(self, *args, **kwargs):
            context = super().get_context_data(*args, **kwargs)
            context['count'] = self.count or 0
            context['query'] = self.request.GET.get('q')
            return context
    
        def get_queryset(self):
            request = self.request
            query_list = request.GET.get('q', None).split()
            query_list_count = len(query_list)
            
            if query_list is not None:
                if query_list_count == 1:
                    qs = MyModel.objects.search(query=query_list[0]).order_by('-date_added')
                    self.count = len(qs)
                elif query_list_count > 1:
                    qs = MyModel.objects.search_and(query=query_list).order_by('-date_added')
                    self.count = len(qs)
                result_count = len(qs)
                create_search_record(self, request, query_list, query_list_count, result_count)
                return qs
    

    For multi-word strings, the magic is in the reduce function that attempts all keywords against the given model fields. Much of the credit for this manager pattern goes to Justin Mitchel's excellent writeup on multi-model search.