Skip to content

Commit 0e5cfdc

Browse files
tmctoastdriven
authored andcommitted
Changed how you query for facets and how how they are presented in the facet counts. Allows customization of facet field names in indexes.
Lightly backward-incompatible (git only).
1 parent 9c4a691 commit 0e5cfdc

File tree

7 files changed

+122
-41
lines changed

7 files changed

+122
-41
lines changed

docs/faceting.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ might look like this::
190190
{% endif %}
191191

192192
Displaying the facets is a matter of looping through the facets you want and
193-
providing the UI to suit. The ``author_exact.0`` is the facet text from the backend
194-
and the ``author_exact.1`` is the facet count.
193+
providing the UI to suit. The ``author.0`` is the facet text from the backend
194+
and the ``author.1`` is the facet count.
195195

196196
4. Narrowing The Search
197197
-----------------------

haystack/backends/__init__.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# -*- coding: utf-8 -*-
2-
import re
32
from copy import deepcopy
43
from time import time
54
from django.conf import settings
@@ -10,7 +9,6 @@
109
from django.utils.encoding import force_unicode
1110
from haystack.constants import VALID_FILTERS, FILTER_SEPARATOR
1211
from haystack.exceptions import SearchBackendError, MoreLikeThisError, FacetingError
13-
from haystack.utils import get_facet_field_name
1412
try:
1513
set
1614
except NameError:
@@ -631,7 +629,7 @@ def add_highlight(self):
631629

632630
def add_field_facet(self, field):
633631
"""Adds a regular facet on a field."""
634-
self.facets.add(get_facet_field_name(field))
632+
self.facets.add(self.backend.site.get_facet_field_name(field))
635633

636634
def add_date_facet(self, field, start_date, end_date, gap_by, gap_amount=1):
637635
"""Adds a date-based facet on a field."""
@@ -644,11 +642,11 @@ def add_date_facet(self, field, start_date, end_date, gap_by, gap_amount=1):
644642
'gap_by': gap_by,
645643
'gap_amount': gap_amount,
646644
}
647-
self.date_facets[get_facet_field_name(field)] = details
645+
self.date_facets[self.backend.site.get_facet_field_name(field)] = details
648646

649647
def add_query_facet(self, field, query):
650648
"""Adds a query facet on a field."""
651-
self.query_facets.append((get_facet_field_name(field), query))
649+
self.query_facets.append((self.backend.site.get_facet_field_name(field), query))
652650

653651
def add_narrow_query(self, query):
654652
"""
@@ -661,15 +659,15 @@ def add_narrow_query(self, query):
661659
def post_process_facets(self, results):
662660
# Handle renaming the facet fields. Undecorate and all that.
663661
revised_facets = {}
662+
field_data = self.backend.site.all_searchfields()
664663

665664
for facet_type, field_details in results.get('facets', {}).items():
666665
temp_facets = {}
667666

668667
for field, field_facets in field_details.items():
669668
fieldname = field
670-
671-
if fieldname.endswith('_exact'):
672-
fieldname = fieldname[:-6]
669+
if field in field_data and hasattr(field_data[field], 'get_facet_for_name'):
670+
fieldname = field_data[field].get_facet_for_name()
673671

674672
temp_facets[fieldname] = field_facets
675673

haystack/fields.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,6 @@ def __init__(self, **kwargs):
245245
super(FacetField, self).__init__(**kwargs)
246246
# Make sure the field is nullable.
247247
self.null = True
248+
249+
def get_facet_for_name(self):
250+
return self.facet_for or self.instance_name

haystack/sites.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class SearchSite(object):
1919

2020
def __init__(self):
2121
self._registry = {}
22-
self._field_mapping = None
22+
self._cached_field_mapping = None
2323

2424
def register(self, model, index_class=None):
2525
"""
@@ -140,25 +140,51 @@ def get_index_fieldname(self, fieldname):
140140
``SearchIndex`` instead of having to remember & use the overridden
141141
name.
142142
"""
143-
if self._field_mapping is None:
144-
self._field_mapping = self._build_field_mapping()
145-
146-
# Return what was provided as a fallback instead of an IndexError.
147-
return self._field_mapping.get(fieldname, fieldname)
143+
if fieldname in self._field_mapping():
144+
return self._field_mapping()[fieldname]['index_fieldname']
145+
else:
146+
return fieldname
148147

149-
def _build_field_mapping(self):
148+
def get_facet_field_name(self, fieldname):
149+
"""
150+
Returns the actual name of the facet field in the index.
151+
152+
If not found, returns the fieldname provided.
153+
"""
154+
facet_fieldname = None
155+
if fieldname in self._field_mapping():
156+
facet_fieldname = self._field_mapping()[fieldname]['facet_fieldname']
157+
158+
if facet_fieldname:
159+
return self.get_index_fieldname(facet_fieldname)
160+
else:
161+
return fieldname
162+
163+
def _field_mapping(self):
150164
mapping = {}
151165

166+
if self._cached_field_mapping:
167+
return self._cached_field_mapping
168+
152169
for model, index in self.get_indexes().items():
153170
for field_name, field_object in index.fields.items():
154-
if field_name in mapping:
155-
# We've already seen this field in the list. Check to ensure
156-
# it uses the same index_fieldname as the previous mention.
157-
if field_object.index_fieldname != mapping[field_name]:
158-
raise SearchFieldError("All uses of the '%s' field need to use the same 'index_fieldname' attribute." % field_name)
171+
if field_name in mapping and field_object.index_fieldname != mapping[field_name]['index_fieldname']:
172+
# We've already seen this field in the list. Raise an exception if index_fieldname differs.
173+
raise SearchFieldError("All uses of the '%s' field need to use the same 'index_fieldname' attribute." % field_name)
174+
175+
facet_fieldname = None
176+
if hasattr(field_object, 'facet_for'):
177+
if field_object.facet_for:
178+
facet_fieldname = field_object.facet_for
179+
else:
180+
facet_fieldname = field_object.instance_name
159181

160-
mapping[field_name] = field_object.index_fieldname
182+
mapping[field_name] = {
183+
'index_fieldname': field_object.index_fieldname,
184+
'facet_fieldname': facet_fieldname,
185+
}
161186

187+
self._cached_field_mapping = mapping
162188
return mapping
163189

164190
def update_object(self, instance):

tests/core/tests/mocks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ class MockSearchBackend(BaseSearchBackend):
1919
mock_search_results = MOCK_SEARCH_RESULTS
2020

2121
def __init__(self, site=None):
22+
super(MockSearchBackend, self).__init__(site)
2223
self.docs = {}
23-
self.site = site
2424

2525
def update(self, index, iterable, commit=True):
2626
for obj in iterable:

tests/core/tests/query.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -151,34 +151,34 @@ def test_more_like_this(self):
151151

152152
def test_add_field_facet(self):
153153
self.bsq.add_field_facet('foo')
154-
self.assertEqual(self.bsq.facets, set(['foo_exact']))
154+
self.assertEqual(self.bsq.facets, set(['foo']))
155155

156156
self.bsq.add_field_facet('bar')
157-
self.assertEqual(self.bsq.facets, set(['foo_exact', 'bar_exact']))
157+
self.assertEqual(self.bsq.facets, set(['foo', 'bar']))
158158

159159
def test_add_date_facet(self):
160160
self.bsq.add_date_facet('foo', start_date=datetime.date(2009, 2, 25), end_date=datetime.date(2009, 3, 25), gap_by='day')
161-
self.assertEqual(self.bsq.date_facets, {'foo_exact': {'gap_by': 'day', 'start_date': datetime.date(2009, 2, 25), 'end_date': datetime.date(2009, 3, 25), 'gap_amount': 1}})
161+
self.assertEqual(self.bsq.date_facets, {'foo': {'gap_by': 'day', 'start_date': datetime.date(2009, 2, 25), 'end_date': datetime.date(2009, 3, 25), 'gap_amount': 1}})
162162

163163
self.bsq.add_date_facet('bar', start_date=datetime.date(2008, 1, 1), end_date=datetime.date(2009, 12, 1), gap_by='month')
164-
self.assertEqual(self.bsq.date_facets, {'foo_exact': {'gap_by': 'day', 'start_date': datetime.date(2009, 2, 25), 'end_date': datetime.date(2009, 3, 25), 'gap_amount': 1}, 'bar_exact': {'gap_by': 'month', 'start_date': datetime.date(2008, 1, 1), 'end_date': datetime.date(2009, 12, 1), 'gap_amount': 1}})
164+
self.assertEqual(self.bsq.date_facets, {'foo': {'gap_by': 'day', 'start_date': datetime.date(2009, 2, 25), 'end_date': datetime.date(2009, 3, 25), 'gap_amount': 1}, 'bar': {'gap_by': 'month', 'start_date': datetime.date(2008, 1, 1), 'end_date': datetime.date(2009, 12, 1), 'gap_amount': 1}})
165165

166166
def test_add_query_facet(self):
167167
self.bsq.add_query_facet('foo', 'bar')
168-
self.assertEqual(self.bsq.query_facets, [('foo_exact', 'bar')])
168+
self.assertEqual(self.bsq.query_facets, [('foo', 'bar')])
169169

170170
self.bsq.add_query_facet('moof', 'baz')
171-
self.assertEqual(self.bsq.query_facets, [('foo_exact', 'bar'), ('moof_exact', 'baz')])
171+
self.assertEqual(self.bsq.query_facets, [('foo', 'bar'), ('moof', 'baz')])
172172

173173
self.bsq.add_query_facet('foo', 'baz')
174-
self.assertEqual(self.bsq.query_facets, [('foo_exact', 'bar'), ('moof_exact', 'baz'), ('foo_exact', 'baz')])
174+
self.assertEqual(self.bsq.query_facets, [('foo', 'bar'), ('moof', 'baz'), ('foo', 'baz')])
175175

176176
def test_add_narrow_query(self):
177-
self.bsq.add_narrow_query('foo_exact:bar')
178-
self.assertEqual(self.bsq.narrow_queries, set(['foo_exact:bar']))
177+
self.bsq.add_narrow_query('foo:bar')
178+
self.assertEqual(self.bsq.narrow_queries, set(['foo:bar']))
179179

180-
self.bsq.add_narrow_query('moof_exact:baz')
181-
self.assertEqual(self.bsq.narrow_queries, set(['foo_exact:bar', 'moof_exact:baz']))
180+
self.bsq.add_narrow_query('moof:baz')
181+
self.assertEqual(self.bsq.narrow_queries, set(['foo:bar', 'moof:baz']))
182182

183183
def test_run(self):
184184
# Stow.

tests/core/tests/sites.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.test import TestCase
33
from haystack.indexes import *
44
from haystack.exceptions import SearchFieldError
5-
from haystack.fields import CharField
5+
from haystack.fields import CharField, FacetField
66
from haystack.sites import SearchSite, AlreadyRegistered, NotRegistered
77
from core.models import MockModel, AnotherMockModel
88

@@ -40,6 +40,13 @@ class AlternateValidSearchIndex(SearchIndex):
4040
author = CharField(faceted=True)
4141
title = CharField(faceted=True)
4242

43+
class ExplicitFacetSearchIndex(SearchIndex):
44+
text = CharField(document=True)
45+
author = CharField(faceted=True)
46+
title = CharField()
47+
title_facet = FacetField(facet_for='title')
48+
bare_facet = FacetField()
49+
4350

4451
class MultiValueValidSearchIndex(SearchIndex):
4552
text = CharField(document=True)
@@ -187,23 +194,70 @@ def test_all_searchfields(self):
187194
self.assertRaises(SearchFieldError, self.site.all_searchfields)
188195

189196
def test_get_index_fieldname(self):
190-
self.assertEqual(self.site._field_mapping, None)
197+
self.assertEqual(self.site._cached_field_mapping, None)
191198

192199
self.site.register(MockModel, ValidSearchIndex)
193200
self.site.register(AnotherMockModel)
194-
field = self.site.get_index_fieldname('text')
195-
self.assertEqual(self.site._field_mapping, {'text': 'text', 'title': 'title', 'author': 'name'})
201+
self.site.get_index_fieldname('text')
202+
self.assertEqual(self.site._cached_field_mapping, {
203+
'text': {'index_fieldname': 'text', 'facet_fieldname': None},
204+
'title': {'index_fieldname': 'title', 'facet_fieldname': None},
205+
'author': {'index_fieldname': 'name', 'facet_fieldname': None},
206+
})
196207
self.assertEqual(self.site.get_index_fieldname('text'), 'text')
197208
self.assertEqual(self.site.get_index_fieldname('author'), 'name')
198209
self.assertEqual(self.site.get_index_fieldname('title'), 'title')
199210

200211
# Reset the internal state to test the invalid case.
201-
self.site._field_mapping = None
202-
self.assertEqual(self.site._field_mapping, None)
212+
self.site._cached_field_mapping = None
213+
self.assertEqual(self.site._cached_field_mapping, None)
203214

204215
self.site.unregister(AnotherMockModel)
205216
self.site.register(AnotherMockModel, AlternateValidSearchIndex)
206217
self.assertRaises(SearchFieldError, self.site.get_index_fieldname, 'text')
218+
219+
def test_basic_get_facet_fieldname(self):
220+
self.assertEqual(self.site._cached_field_mapping, None)
221+
222+
self.site.register(MockModel, AlternateValidSearchIndex)
223+
self.site.register(AnotherMockModel)
224+
self.site.get_facet_field_name('text')
225+
self.assertEqual(self.site._cached_field_mapping, {
226+
'author': {'facet_fieldname': None, 'index_fieldname': 'author'},
227+
'author_exact': {'facet_fieldname': 'author',
228+
'index_fieldname': 'author_exact'},
229+
'text': {'facet_fieldname': None, 'index_fieldname': 'text'},
230+
'title': {'facet_fieldname': None, 'index_fieldname': 'title'},
231+
'title_exact': {'facet_fieldname': 'title', 'index_fieldname': 'title_exact'},
232+
})
233+
self.assertEqual(self.site.get_index_fieldname('text'), 'text')
234+
self.assertEqual(self.site.get_index_fieldname('author'), 'author')
235+
self.assertEqual(self.site.get_index_fieldname('title'), 'title')
236+
237+
self.assertEqual(self.site.get_facet_field_name('text'), 'text')
238+
self.assertEqual(self.site.get_facet_field_name('author'), 'author')
239+
self.assertEqual(self.site.get_facet_field_name('title'), 'title')
240+
241+
def test_more_advanced_get_facet_fieldname(self):
242+
self.assertEqual(self.site._cached_field_mapping, None)
243+
244+
self.site.register(MockModel, ExplicitFacetSearchIndex)
245+
self.site.register(AnotherMockModel)
246+
247+
self.site.get_facet_field_name('text')
248+
self.assertEqual(self.site._cached_field_mapping, {
249+
'author': {'facet_fieldname': None, 'index_fieldname': 'author'},
250+
'author_exact': {'facet_fieldname': 'author', 'index_fieldname': 'author_exact'},
251+
'bare_facet': {'facet_fieldname': 'bare_facet', 'index_fieldname': 'bare_facet'},
252+
'text': {'facet_fieldname': None, 'index_fieldname': 'text'},
253+
'title': {'facet_fieldname': None, 'index_fieldname': 'title'},
254+
'title_facet': {'facet_fieldname': 'title', 'index_fieldname': 'title_facet'},
255+
})
256+
self.assertEqual(self.site.get_facet_field_name('title'), 'title')
257+
258+
self.assertEqual(self.site.get_facet_field_name('title'), 'title')
259+
self.assertEqual(self.site.get_facet_field_name('bare_facet'), 'bare_facet')
260+
207261

208262
def test_update_object(self):
209263
self.site.register(MockModel, FakeSearchIndex)

0 commit comments

Comments
 (0)