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