Use Haystack facets to provide multiple search fields - django

I am writing a simple library app (kind of just a book database at the moment) and I have a Haystack search page as the home page. I am trying to add multiple search fields to the page, so you can search specifically in the title, author etc. instead of just one generic "keyword" search. I have been trying to make this work with Haystack's "faceting" feature, but it seems to be more oriented around refining a search based on given, strict categories.
Can you use Haystack facets to provide multiple search fields? Or is Haystack just not cut out for this kind of job? If so, what should I be using instead?
If you need more context, the current project is available on GitHub.

Try ModelSearchForm. This form adds new fields to form. It iterates through all registered models for the current SearchSite and provides a checkbox for each one. If no models are selected, all types will show up in the results.
custom form example from documentation and can be converted to ModelSearchForm simply inherting from ModelSearcForm
from django import forms
from haystack.forms import SearchForm
class DateRangeSearchForm(SearchForm):
start_date = forms.DateField(required=False)
end_date = forms.DateField(required=False)
def search(self):
# First, store the SearchQuerySet received from other processing.
sqs = super(DateRangeSearchForm, self).search()
if not self.is_valid():
return self.no_query_found()
# Check to see if a start_date was chosen.
if self.cleaned_data['start_date']:
sqs = sqs.filter(pub_date__gte=self.cleaned_data['start_date'])
# Check to see if an end_date was chosen.
if self.cleaned_data['end_date']:
sqs = sqs.filter(pub_date__lte=self.cleaned_data['end_date'])
return sqs
DateRange SearchForm is a custom form and has more flexibility as it has more control on developer side. The simplest way to go about creating your own form is to inherit from SearchForm (or the desired parent) and extend the search method. By doing this, you save yourself most of the work of handling data correctly and stay API compatible with the SearchView.
More help can be from this question django haystack custom form

Related

Django-haystack: how do I select which index to use in a SearchQuerySet?

I've been looking through the Haystack documentation on multiple indexes, but I can't figure out exactly how to use them.
The main model in this example is Proposal. I want to have two search indexes that return a list of proposals: one that only searches in the proposals themselves, and one that searches in proposals along with their comments. I've set up search_indexes.py like this:
class ProposalIndexBase(indexes.SearchIndex, indexes.Indexable)
title = indexes.CharField(model_attr="title", boost=1.1)
text = indexes.NgramField(document=True, use_template=True)
date = indexes.DateTimeField(model_attr='createdAt')
def get_model(self):
return Proposal
class ProposalIndex(ProposalIndexBase):
comments = indexes.MultiValueField()
def prepare_comments(self, object):
return [comment.text for comment in object.comments.all()]
class SimilarProposalIndex(ProposalIndexBase):
pass
Here's my search in views.py:
def search(request):
if request.method == "GET":
if "q" in request.GET:
query = str(request.GET.get("q"))
results = SearchQuerySet().all().filter(content=query)
return render(request, "search/search.html", {"results": results})
How do I set up a separate view that gets a SearchQuerySet from a specific index?
The Haystack (and other auto-generated) documentation is not a good example of clarity and it's about as exciting as reading a phone book. I think the section you referred to on "Multiple Indexes" is actually about accessing different backend search engines (like whoosh, solr, etc.) for queries.
But your question seems to be about how to query the "SearchIndexes" for different models. In your example, you want to have one search query for "proposals" and another one for "proposals" + "comments," if I understand your question correctly.
I think you want to look at the SearchQuerySet API, which describes how to filter the queryset returned by the search. There is a method called models that allows you to supply a model class or a list of model classes to limit the queryset results.
For example, in your search view, you may want to have a query string parameter for, say, "content" that specifies whether the search is for "proposals" or for "everything" (proposals and comments). So your frontend needs to supply the extra content parameter when calling the view (or you can use separate views for the different searches).
Your query strings need to look something like:
/search/?q=python&content=proposal
/search/?q=python&content=everything
And your view should parse the content query string parameter to get the model(s) for filtering the search query results:
# import your model classes so you can use them in your search view
# (I'm just guessing these are what they are called in your project)
from proposals.models import Proposal
from comments.models import Comment
def search(request):
results = None
if request.method == "GET":
if "q" in request.GET:
query = str(request.GET.get("q"))
# Add extra code here to parse the "content" query string parameter...
# * Get content type to search for
content_type = request.GET.get("content")
# * Assign the model or models to a list for the "models" call
search_models = []
if content_type is "proposal":
search_models = [Proposal]
elif content_type is "everything":
search_models = [Proposal, Comment]
# * Add a "models" call to limit the search results to the particular models
results = SearchQuerySet().all().filter(content=query).models(*search_models)
return render(request, "search/search.html", {"results": results})
If you have a lot of search indexes (i.e. a lot of content from many models), you might not want to hardcode the models in your view but use the get_model function from django.db to fetch the model class dynamically.

Django-taggit prefetch_related

I'm building a basic time logging app right now and I have a todo model that uses django-taggit. My Todo model looks like this:
class Todo(models.Model):
project = models.ForeignKey(Project)
description = models.CharField(max_length=300)
is_done = models.BooleanField(default=False)
billable = models.BooleanField(default=True)
date_completed = models.DateTimeField(blank=True, null=True)
completed_by = models.ForeignKey(User, blank=True, null=True)
tags = TaggableManager()
def __unicode__(self):
return self.description
I'm trying to get a list of unique tags for all the Todos in a project and I have managed to get this to work using a set comprehension, however for every Todo in the project I have to query the database to get the tags. My set comprehension is:
unique_tags = { tag.name.lower() for todo in project.todo_set.all() for tag in todo.tags.all() }
This works just fine, however for every todo in the project it runs a separate query to grab all the tags. I was wondering if there is any way I can do something similar to prefetch_related in order to avoid these duplicate queries:
unique_tags = { tag.name.lower() for todo in project.todo_set.all().prefetch_related('tags') for tag in todo.tags.all() }
Running the previous code gives me the error:
'tags' does not resolve to a item that supports prefetching - this is an invalid parameter to prefetch_related().
I did see that someone asked a very similar question here: Optimize django query to pull foreign key and django-taggit relationship however it doesn't look like it ever got a definite answer. I was hoping someone could help me out. Thanks!
Taggit now supports prefetch_related directly on tag fields (in version 0.11.0 and later, released 2013-11-25).
This feature was introduced in this pull request. In the test case for it, notice that after prefetching tags using .prefetch_related('tags'), there are 0 additional queries for listing the tags.
Slightly hackish soution:
ct = ContentType.objects.get_for_model(Todo)
todo_pks = [each.pk for each in project.todo_set.all()]
tagged_items = TaggedItem.objects.filter(content_type=ct, object_id__in=todo_pks) #only one db query
unique_tags = set([each.tag for each in tagged_items])
Explanation
I say it is hackish because we had to use TaggedItem and ContentType which taggit uses internally.
Taggit doesn't provide any method for your particular use case. The reason is because it is generic. The intention for taggit is that any instance of any model can be tagged. So, it makes use of ContentType and GenericForeignKey for that.
The models used internally in taggit are Tag and TaggedItem. Model Tag only contains the string representation of the tag. TaggedItem is the model which is used to associate these tags with any object. Since the tags should be associatable with any object, TaggedItem uses model ContentType.
The apis provided by taggit like tags.all(), tags.add() etc internally make use of TaggedItem and filters on this model to give you the tags for a particular instance.
Since, your requirement is to get all the tags for a particular list of objects we had to make use of the internal classes used by taggit.
Use django-tagging and method usage_for_model
def usage_for_model(self, model, counts=False, min_count=None, filters=None):
"""
Obtain a list of tags associated with instances of the given
Model class.
If ``counts`` is True, a ``count`` attribute will be added to
each tag, indicating how many times it has been used against
the Model class in question.
If ``min_count`` is given, only tags which have a ``count``
greater than or equal to ``min_count`` will be returned.
Passing a value for ``min_count`` implies ``counts=True``.
To limit the tags (and counts, if specified) returned to those
used by a subset of the Model's instances, pass a dictionary
of field lookups to be applied to the given Model as the
``filters`` argument.
"""
A slightly less hackish answer than akshar's, but only slightly...
You can use prefetch_related as long as you traverse the tagged_item relations yourself, using the clause prefetch_related('tagged_items__tag'). Unfortunately, todo.tags.all() won't take advantage of that prefetch - the 'tags' manager will still end up doing its own query - so you have to step over the tagged_items relation there too. This should do the job:
unique_tags = { tagged_item.tag.name.lower()
for todo in project.todo_set.all().prefetch_related('tagged_items__tag')
for tagged_item in todo.tagged_items.all() }

how do i use a boolean field in django-haystack search query

I'm using django-haystack to power search for my application and I have boolean fields i would like to filter my models by. However, when I try doing this, my search query fails.
The search backend i'm using is elasticsearch
Posting some of your code here would be helpful in figuring out what is going wrong for you, as it's hard to troubleshoot with nothing.
How I implemented this (using whoosh rather than elasticsearch, but the django code should be the same/similar at any rate) was like so:
created my own searchform class (if you haven't already done this, look at Haystack Docs - Creating your own form
from django import forms
from haystack.forms import SearchForm
class PaidPropertySearchForm(SearchForm):
q = forms.CharField(required=False, label=('Town Area'))
#othersearchtermshere
furnished = forms.BooleanField(required=False)
def search(self):
sqs = super(PaidPropertySearchForm, self).search()
#other search filtering here
if self.is_valid() and self.cleaned_data['furnished']:
sqs = sqs.filter(furnished__contains=self.cleaned_data['furnished'])
return sqs
relevant model field is simply:
class Properties (models.Model):.
furnished = models.BooleanField()
and my searchindexes.py:
class PropertyIndex(indexes.SearchIndex, indexes.Indexable):
#other indexed terms
furnished = indexes.BooleanField(model_attr='furnished', default='false')
Here it's worth noting that there are some issues between haystack and the whoosh backend with boolean values, that required me to specify the defult index value as a string 'false' rather than the boolean value False. If i didn't do this or didn't set a default, for some reason ALL boolean values were indexed as True, regardless of what the model had stored. I don't know if that issue would affect elasticsearch, so you may not need
default='false'
in your search index
Hopefully this helps get you on the right track if you haven't already figured it out.
sqs = sqs.filter(boolean_field=True)
did not work for me either (using Haystack 2.1.0 and Elasticsearch 1.2.1) but
sqs = sqs.filter(boolean_field=1)
did.

Search multiple fields of django model without 3rd party app

I have a single model in my django app that I want to create a search form for. Is there a way to search all the fields in the model at once with the same search string? I've looked into xapian and solr but they seem like a lot of overhead for searching over 1 model. I want to be able to say something like:
results = Assignment.objects.filter(any_column = search_string)
I realize there might not be something that concise but right now the only option I can come up with other than using a search app is to check each field separately and concatenate the results together.
Once you have all the field names you can create Q objects using kwarg expansion and use reduce() along with operator.or_ to turn them into a single query.
qgroup = reduce(operator.or_, (Q(**{fieldname: value}) for fieldname in fieldnames))
asgns = Assignment.objects.filter(qgroup)
Old question, but for further reference I am adding this:
In django 1.10 SearchVector class was added.
Usage from the docs:
Searching against a single field is great but rather limiting. The Entry instances we’re searching belong to a Blog, which has a tagline field. To query against both fields, use a SearchVector:
>>> from django.contrib.postgres.search import SearchVector
>>> Entry.objects.annotate(
... search=SearchVector('body_text', 'blog__tagline'),
... ).filter(search='Cheese')
[<Entry: Cheese on Toast recipes>, <Entry: Pizza Recipes>]

django pagination - Remembering checked checkboxes across pages

I actually want to accomplish the same thing a user in
Remembering checked checkboxes across pages - what's the best way?
asked. But not for php, I want it for django. Since django is just great :-)
I`m using pagination and want to remember the checked checkbox while the user is navigating over the pages provided by pagination.
Now I'm searching for a best practice how I could accomplish this. I do not really have a good idea how i could accomplish this. My idea would include javascript, but I'm sure there is somewhere out there a solution without js.
I can't think of any way to do this with a paginator. But this is a good spot for a formwizard and dynamic forms. The general idea is to create a dynamic form for each "page" and then a formwizard to hold them all together. This allows for saving the checkboxes across multiple pages and lets you go backwards and forwards easily. The best thing is that it takes virtually no code!
Something like this should be able to deal with everything:
from django.contrib.formtools.wizard import FormWizard
from django import forms
from django.forms.extras.widgets import CheckboxSelectMultiple
from django.core.paginator import Paginator
# In your forms.py --------------
def MPageFormMaker(paginator):
"""
Create a "paginated" group of forms based on a queryset and num-per-page item.
"""
def Sform(this_q):
"""
Create this page's items
"""
class _PageForm(forms.Form):
items = forms.ModelMultipleChoiceField(queryset = this_q,
widget = forms.CheckboxSelectMultiple)
return _PageForm
for i in range(paginator.num_pages):
yield Sform(paginator.page(i).object_list)
class MpageForm(FormWizard):
def done(self, request, formlist):
#do something with the list of forms
#----- In your views.py
def MpageChecker(request):
qset = Item.objects.all()
paginator = Paginator(qset, 30)
formwizard = MPageForm(list(MPageFormMaker(paginator)))
#then deal with it like a normal formwizard
Essentially just instantiate the formwizard class and then let it take care of everything. Since it uses a paginator class to make the forms you can use any sort of personalization you'd like.
BTW: I haven't tested this code so it may have a few typos but it should be enough to get you on your way.
EDIT ... fix the ordering problem, now all ordering should be preserved correctly across pages!