"""A drop-in chainable manager for providing models with basic search features such as +/- modifiers, quoted exact phrases and ordering by relevance. Simply assign the SearchableManager to your model and optionally supply the fields to search on in either the manager's constructor, as a separate attribute of the manager's model, or as an argument to the actual search method. If search fields aren't specified then all char-like fields are used.Usage: Class MyModel(models.Model): # Some fields to search on. title = models.CharField(max_length=100) description = models.TextField() # Set up the manager and set the searchable fields. Both methods are # demonstrated here, as a constructor and as a separate model attrribute # although only one is required. objects = SearchableManager(search_fields=("title", "description")) search_fields = ("title", "description")# This search query excludes 'how', requires 'cow' and uses 'now brown' as an # exact phrase. Also shown is the ability to optionally specify the fields to # use for this search overriding the fields specified above.MyModel.objects.search('-how "now brown" +cow', search_fields=("title",))Credits:--------Stephen McDonald <steve@jupo.org>License:--------Creative Commons Attribution-Share Alike 3.0 Licensehttp://creativecommons.org/licenses/by-sa/3.0/When attributing this work, you must maintain the Creditsparagraph above."""fromoperatorimportior,iandfromstringimportpunctuationfromdjango.db.modelsimportManager,Q,CharField,TextFieldfromdjango.db.models.queryimportQuerySetclassSearchableQuerySet(QuerySet):def__init__(self,*args,**kwargs):self._search_ordered=Falseself._search_terms=set()self._search_fields=set(kwargs.pop("search_fields",[]))super(SearchableQuerySet,self).__init__(*args,**kwargs)defsearch(self,query,search_fields=None):""" Build a queryset matching words in the given search query, treating quoted terms as exact phrases and taking into account + and - symbols as modifiers controlling which terms to require and exclude. """# Use fields arg if given, otherwise check internal list which if empty, # populate from model attr or char-like fields.ifsearch_fieldsisNone:search_fields=self._search_fieldsiflen(search_fields)==0:search_fields=getattr(self.model,"search_fields",[])iflen(search_fields)==0:search_fields=[f.nameforfinself.model._meta.fieldsifissubclass(f.__class__,CharField)orissubclass(f.__class__,TextField)]iflen(search_fields)==0:returnself.none()self._search_fields.update(search_fields)# Remove extra spaces, put modifiers inside quoted terms.terms=" ".join(query.split()).replace("+ ","+").replace('+"','"+').replace("- ","-").replace('-"','"-').split('"')# Strip punctuation other than modifiers from terms and create term # list first from quoted terms, and then remaining words.terms=[(""ift[0]notin"+-"elset[0])+t.strip(punctuation)fortinterms[1::2]+"".join(terms[::2]).split()]# Append terms to internal list for sorting when results are iterated.self._search_terms.update([t.lower().strip(punctuation)fortintermsift[0]!="-"])# Create the queryset combining each set of terms.excluded=[reduce(iand,[~Q(**{"%s__icontains"%f:t[1:]})forfinsearch_fields])fortintermsift[0]=="-"]required=[reduce(ior,[Q(**{"%s__icontains"%f:t[1:]})forfinsearch_fields])fortintermsift[0]=="+"]optional=[reduce(ior,[Q(**{"%s__icontains"%f:t})forfinsearch_fields])fortintermsift[0]notin"+-"]queryset=selfifexcluded:queryset=queryset.filter(reduce(iand,excluded))ifrequired:queryset=queryset.filter(reduce(iand,required))# Optional terms aren't relevant to the filter if there are terms# that are explicitly requiredelifoptional:queryset=queryset.filter(reduce(ior,optional))returnquerysetdef_clone(self,*args,**kwargs):""" Ensure attributes are copied to subsequent queries. """forattrin("_search_terms","_search_fields","_search_ordered"):kwargs[attr]=getattr(self,attr)returnsuper(SearchableQuerySet,self)._clone(*args,**kwargs)deforder_by(self,*field_names):""" Mark the filter as being ordered if search has occurred. """ifnotself._search_ordered:self._search_ordered=len(self._search_terms)>0returnsuper(SearchableQuerySet,self).order_by(*field_names)defiterator(self):""" If search has occured and no ordering has occurred, sort the results by number of occurrences of terms. """results=super(SearchableQuerySet,self).iterator()ifself._search_termsandnotself._search_ordered:sort_key=lambdaobj:sum([getattr(obj,f).lower().count(t.lower())forfinself._search_fieldsfortinself._search_termsifgetattr(obj,f)])returniter(sorted(results,key=sort_key,reverse=True))returnresultsclassSearchableManager(Manager):""" Manager providing a chainable queryset. Adapted from http://www.djangosnippets.org/snippets/562/ """def__init__(self,*args,**kwargs):self._search_fields=kwargs.pop("search_fields",[])super(SearchableManager,self).__init__(*args,**kwargs)defget_query_set(self):returnSearchableQuerySet(self.model,search_fields=self._search_fields)def__getattr__(self,attr,*args):try:returngetattr(self.__class__,attr,*args)exceptAttributeError:returngetattr(self.get_query_set(),attr,*args)