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.
CodePudding user response:
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.