1.. _ref-autocomplete: 2 3============ 4Autocomplete 5============ 6 7Autocomplete is becoming increasingly common as an add-on to search. Haystack 8makes it relatively simple to implement. There are two steps in the process, 9one to prepare the data and one to implement the actual search. 10 11Step 1. Setup The Data 12====================== 13 14To do autocomplete effectively, the search backend uses n-grams (essentially 15a small window passed over the string). Because this alters the way your 16data needs to be stored, the best approach is to add a new field to your 17``SearchIndex`` that contains the text you want to autocomplete on. 18 19You have two choices: ``NgramField`` and ``EdgeNgramField``. Though very similar, 20the choice of field is somewhat important. 21 22* If you're working with standard text, ``EdgeNgramField`` tokenizes on 23 whitespace. This prevents incorrect matches when part of two different words 24 are mashed together as one n-gram. **This is what most users should use.** 25* If you're working with Asian languages or want to be able to autocomplete 26 across word boundaries, ``NgramField`` should be what you use. 27 28Example (continuing from the tutorial):: 29 30 import datetime 31 from haystack import indexes 32 from myapp.models import Note 33 34 35 class NoteIndex(indexes.SearchIndex, indexes.Indexable): 36 text = indexes.CharField(document=True, use_template=True) 37 author = indexes.CharField(model_attr='user') 38 pub_date = indexes.DateTimeField(model_attr='pub_date') 39 # We add this for autocomplete. 40 content_auto = indexes.EdgeNgramField(model_attr='content') 41 42 def get_model(self): 43 return Note 44 45 def index_queryset(self, using=None): 46 """Used when the entire index for model is updated.""" 47 return Note.objects.filter(pub_date__lte=datetime.datetime.now()) 48 49As with all schema changes, you'll need to rebuild/update your index after 50making this change. 51 52 53Step 2. Performing The Query 54============================ 55 56Haystack ships with a convenience method to perform most autocomplete searches. 57You simply provide a field and the query you wish to search on to the 58``SearchQuerySet.autocomplete`` method. Given the previous example, an example 59search would look like:: 60 61 from haystack.query import SearchQuerySet 62 63 SearchQuerySet().autocomplete(content_auto='old') 64 # Result match things like 'goldfish', 'cuckold' and 'older'. 65 66The results from the ``SearchQuerySet.autocomplete`` method are full search 67results, just like any regular filter. 68 69If you need more control over your results, you can use standard 70``SearchQuerySet.filter`` calls. For instance:: 71 72 from haystack.query import SearchQuerySet 73 74 sqs = SearchQuerySet().filter(content_auto=request.GET.get('q', '')) 75 76This can also be extended to use ``SQ`` for more complex queries (and is what's 77being done under the hood in the ``SearchQuerySet.autocomplete`` method). 78 79 80Example Implementation 81====================== 82 83The above is the low-level backend portion of how you implement autocomplete. 84To make it work in browser, you need both a view to run the autocomplete 85and some Javascript to fetch the results. 86 87Since it comes up often, here is an example implementation of those things. 88 89.. warning:: 90 91 This code comes with no warranty. Don't ask for support on it. If you 92 copy-paste it and it burns down your server room, I'm not liable for any 93 of it. 94 95 It worked this one time on my machine in a simulated environment. 96 97 And yeah, semicolon-less + 2 space + comma-first. Deal with it. 98 99A stripped-down view might look like:: 100 101 # views.py 102 import simplejson as json 103 from django.http import HttpResponse 104 from haystack.query import SearchQuerySet 105 106 107 def autocomplete(request): 108 sqs = SearchQuerySet().autocomplete(content_auto=request.GET.get('q', ''))[:5] 109 suggestions = [result.title for result in sqs] 110 # Make sure you return a JSON object, not a bare list. 111 # Otherwise, you could be vulnerable to an XSS attack. 112 the_data = json.dumps({ 113 'results': suggestions 114 }) 115 return HttpResponse(the_data, content_type='application/json') 116 117The template might look like:: 118 119 <!DOCTYPE html> 120 <html> 121 <head> 122 <meta charset="utf-8"> 123 <title>Autocomplete Example</title> 124 </head> 125 <body> 126 <h1>Autocomplete Example</h1> 127 128 <form method="post" action="/search/" class="autocomplete-me"> 129 <input type="text" id="id_q" name="q"> 130 <input type="submit" value="Search!"> 131 </form> 132 133 <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script> 134 <script type="text/javascript"> 135 // In a perfect world, this would be its own library file that got included 136 // on the page and only the ``$(document).ready(...)`` below would be present. 137 // But this is an example. 138 var Autocomplete = function(options) { 139 this.form_selector = options.form_selector 140 this.url = options.url || '/search/autocomplete/' 141 this.delay = parseInt(options.delay || 300) 142 this.minimum_length = parseInt(options.minimum_length || 3) 143 this.form_elem = null 144 this.query_box = null 145 } 146 147 Autocomplete.prototype.setup = function() { 148 var self = this 149 150 this.form_elem = $(this.form_selector) 151 this.query_box = this.form_elem.find('input[name=q]') 152 153 // Watch the input box. 154 this.query_box.on('keyup', function() { 155 var query = self.query_box.val() 156 157 if(query.length < self.minimum_length) { 158 return false 159 } 160 161 self.fetch(query) 162 }) 163 164 // On selecting a result, populate the search field. 165 this.form_elem.on('click', '.ac-result', function(ev) { 166 self.query_box.val($(this).text()) 167 $('.ac-results').remove() 168 return false 169 }) 170 } 171 172 Autocomplete.prototype.fetch = function(query) { 173 var self = this 174 175 $.ajax({ 176 url: this.url 177 , data: { 178 'q': query 179 } 180 , success: function(data) { 181 self.show_results(data) 182 } 183 }) 184 } 185 186 Autocomplete.prototype.show_results = function(data) { 187 // Remove any existing results. 188 $('.ac-results').remove() 189 190 var results = data.results || [] 191 var results_wrapper = $('<div class="ac-results"></div>') 192 var base_elem = $('<div class="result-wrapper"><a href="#" class="ac-result"></a></div>') 193 194 if(results.length > 0) { 195 for(var res_offset in results) { 196 var elem = base_elem.clone() 197 // Don't use .html(...) here, as you open yourself to XSS. 198 // Really, you should use some form of templating. 199 elem.find('.ac-result').text(results[res_offset]) 200 results_wrapper.append(elem) 201 } 202 } 203 else { 204 var elem = base_elem.clone() 205 elem.text("No results found.") 206 results_wrapper.append(elem) 207 } 208 209 this.query_box.after(results_wrapper) 210 } 211 212 $(document).ready(function() { 213 window.autocomplete = new Autocomplete({ 214 form_selector: '.autocomplete-me' 215 }) 216 window.autocomplete.setup() 217 }) 218 </script> 219 </body> 220 </html> 221