Skip to content

Commit cd233d6

Browse files
committed
Merge branch 'master' of github.com:toastdriven/django-haystack
2 parents b3023de + 6ad2976 commit cd233d6

File tree

12 files changed

+201
-14
lines changed

12 files changed

+201
-14
lines changed

docs/searchquery_api.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ to the rest of the ``SearchQuerySet``.
250250
Allows backends with support for "More Like This" to return results
251251
similar to the provided instance.
252252

253+
``add_stats_query``
254+
~~~~~~~~~~~~~~~~~~~
255+
.. method:: SearchQuery.add_stats_query(self,stats_field,stats_facets)
256+
257+
Adds stats and stats_facets queries for the Solr backend.
258+
253259
``add_highlight``
254260
~~~~~~~~~~~~~~~~~
255261

docs/searchqueryset_api.rst

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,40 @@ Spatial: Adds a distance-based search to the query.
360360

361361
See the :ref:`ref-spatial` docs for more information.
362362

363+
``stats``
364+
~~~~~~~~~
365+
366+
.. method:: SearchQuerySet.stats(self, field):
367+
368+
Adds stats to a query for the provided field. This is supported on
369+
Solr only. You provide the field (from one of the ``SearchIndex``
370+
classes) you would like stats on.
371+
372+
In the search results you get back, stats will be populated in the
373+
``SearchResult`` object. You can access them via the `` stats_results`` method.
374+
375+
Example::
376+
377+
# Get stats on the author field.
378+
SearchQuerySet().filter(content='foo').stats('author')
379+
380+
``stats_facet``
381+
~~~~~~~~~~~~~~~
382+
.. method:: SearchQuerySet.stats_facet(self, field,
383+
.. facet_fields=None):
384+
385+
Adds stats facet for the given field and facet_fields represents the
386+
faceted fields. This is supported on Solr only.
387+
388+
Example::
389+
390+
# Get stats on the author field, and stats on the author field
391+
faceted by bookstore.
392+
SearchQuerySet().filter(content='foo').stats_facet('author','bookstore')
393+
394+
363395
``distance``
364396
~~~~~~~~~~~~
365-
366397
.. method:: SearchQuerySet.distance(self, field, point):
367398

368399
Spatial: Denotes results must have distance measurements from the
@@ -632,6 +663,52 @@ Example::
632663
# 'queries': {}
633664
# }
634665

666+
``stats_results``
667+
~~~~~~~~~~~~~~~~~
668+
669+
.. method:: SearchQuerySet.stats_results(self):
670+
671+
Returns the stats results found by the query.
672+
673+
This will cause the query to
674+
execute and should generally be used when presenting the data (template-level).
675+
676+
You receive back a dictionary with three keys: ``fields``, ``dates`` and
677+
``queries``. Each contains the facet counts for whatever facets you specified
678+
within your ``SearchQuerySet``.
679+
680+
.. note::
681+
682+
The resulting dictionary may change before 1.0 release. It's fairly
683+
backend-specific at the time of writing. Standardizing is waiting on
684+
implementing other backends that support faceting and ensuring that the
685+
results presented will meet their needs as well.
686+
687+
Example::
688+
689+
# Count document hits for each author.
690+
sqs = SearchQuerySet().filter(content='foo').stats('price')
691+
692+
sqs.stats_results()
693+
694+
# Gives the following response
695+
# {
696+
# 'stats_fields':{
697+
# 'author:{
698+
# 'min': 0.0,
699+
# 'max': 2199.0,
700+
# 'sum': 5251.2699999999995,
701+
# 'count': 15,
702+
# 'missing': 11,
703+
# 'sumOfSquares': 6038619.160300001,
704+
# 'mean': 350.08466666666664,
705+
# 'stddev': 547.737557906113
706+
# }
707+
# }
708+
#
709+
# }
710+
711+
635712
``spelling_suggestion``
636713
~~~~~~~~~~~~~~~~~~~~~~~
637714

haystack/backends/__init__.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.utils import tree
88
from django.utils.encoding import force_unicode
99
from haystack.constants import VALID_FILTERS, FILTER_SEPARATOR, DEFAULT_ALIAS
10-
from haystack.exceptions import MoreLikeThisError, FacetingError
10+
from haystack.exceptions import MoreLikeThisError, FacetingError, StatsError
1111
from haystack.models import SearchResult
1212
from haystack.utils.loading import UnifiedIndex
1313

@@ -312,9 +312,10 @@ def __init__(self, using=DEFAULT_ALIAS):
312312
self._results = None
313313
self._hit_count = None
314314
self._facet_counts = None
315+
self._stats = None
315316
self._spelling_suggestion = None
316317
self.result_class = SearchResult
317-
318+
self.stats = {}
318319
from haystack import connections
319320
self._using = using
320321
self.backend = connections[self._using].get_backend()
@@ -497,6 +498,17 @@ def get_facet_counts(self):
497498

498499
return self._facet_counts
499500

501+
def get_stats(self):
502+
"""
503+
Returns the stats received from the backend.
504+
505+
If the query has not been run, this will execute the query and store
506+
the results
507+
"""
508+
if self._stats is None:
509+
self.run()
510+
return self._stats
511+
500512
def get_spelling_suggestion(self, preferred_query=None):
501513
"""
502514
Returns the spelling suggestion received from the backend.
@@ -693,6 +705,10 @@ def more_like_this(self, model_instance):
693705
self._more_like_this = True
694706
self._mlt_instance = model_instance
695707

708+
def add_stats_query(self,stats_field,stats_facets):
709+
"""Adds stats and stats_facets queries for the Solr backend."""
710+
self.stats[stats_field] = stats_facets
711+
696712
def add_highlight(self):
697713
"""Adds highlighting to the search results."""
698714
self.highlight = True
@@ -826,6 +842,7 @@ def _clone(self, klass=None, using=None):
826842
clone.models = self.models.copy()
827843
clone.boost = self.boost.copy()
828844
clone.highlight = self.highlight
845+
clone.stats = self.stats.copy()
829846
clone.facets = self.facets.copy()
830847
clone.date_facets = self.date_facets.copy()
831848
clone.query_facets = self.query_facets[:]
@@ -838,6 +855,7 @@ def _clone(self, klass=None, using=None):
838855
clone.distance_point = self.distance_point.copy()
839856
clone._raw_query = self._raw_query
840857
clone._raw_query_params = self._raw_query_params
858+
841859
return clone
842860

843861

haystack/backends/elasticsearch_backend.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,6 @@ def clear(self, models=[], commit=True):
222222
# a ``query`` root object. :/
223223
query = {'query_string': {'query': " OR ".join(models_to_delete)}}
224224
self.conn.delete_by_query(self.index_name, 'modelresult', query)
225-
226-
if commit:
227-
self.conn.refresh(index=self.index_name)
228225
except (requests.RequestException, pyelasticsearch.ElasticHttpError), e:
229226
if not self.silently_fail:
230227
raise

haystack/backends/solr_backend.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def build_search_kwargs(self, query_string, sort_by=None, start_offset=0, end_of
136136
narrow_queries=None, spelling_query=None,
137137
within=None, dwithin=None, distance_point=None,
138138
models=None, limit_to_registered_models=None,
139-
result_class=None):
139+
result_class=None, stats=None):
140140
kwargs = {'fl': '* score'}
141141

142142
if fields:
@@ -230,6 +230,15 @@ def build_search_kwargs(self, query_string, sort_by=None, start_offset=0, end_of
230230
if narrow_queries is not None:
231231
kwargs['fq'] = list(narrow_queries)
232232

233+
if stats:
234+
kwargs['stats'] = "true"
235+
236+
for k in stats.keys():
237+
kwargs['stats.field'] = k
238+
239+
for facet in stats[k]:
240+
kwargs['f.%s.stats.facet' % k] = facet
241+
233242
if within is not None:
234243
from haystack.utils.geo import generate_bounding_box
235244

@@ -324,11 +333,15 @@ def _process_results(self, raw_results, highlight=False, result_class=None, dist
324333
results = []
325334
hits = raw_results.hits
326335
facets = {}
336+
stats = {}
327337
spelling_suggestion = None
328338

329339
if result_class is None:
330340
result_class = SearchResult
331341

342+
if hasattr(raw_results,'stats'):
343+
stats = raw_results.stats.get('stats_fields',{})
344+
332345
if hasattr(raw_results, 'facets'):
333346
facets = {
334347
'fields': raw_results.facets.get('facet_fields', {}),
@@ -391,6 +404,7 @@ def _process_results(self, raw_results, highlight=False, result_class=None, dist
391404
return {
392405
'results': results,
393406
'hits': hits,
407+
'stats': stats,
394408
'facets': facets,
395409
'spelling_suggestion': spelling_suggestion,
396410
}
@@ -612,7 +626,7 @@ def build_params(self, spelling_query=None, **kwargs):
612626
search_kwargs = {
613627
'start_offset': self.start_offset,
614628
'result_class': self.result_class
615-
}
629+
}
616630
order_by_list = None
617631

618632
if self.order_by:
@@ -663,16 +677,21 @@ def build_params(self, spelling_query=None, **kwargs):
663677
if spelling_query:
664678
search_kwargs['spelling_query'] = spelling_query
665679

680+
if self.stats:
681+
search_kwargs['stats'] = self.stats
682+
666683
return search_kwargs
667-
684+
668685
def run(self, spelling_query=None, **kwargs):
669686
"""Builds and executes the query. Returns a list of search results."""
670687
final_query = self.build_query()
671688
search_kwargs = self.build_params(spelling_query, **kwargs)
689+
672690
results = self.backend.search(final_query, **search_kwargs)
673691
self._results = results.get('results', [])
674692
self._hit_count = results.get('hits', 0)
675693
self._facet_counts = self.post_process_facets(results)
694+
self._stats = results.get('stats',{})
676695
self._spelling_suggestion = results.get('spelling_suggestion', None)
677696

678697
def run_mlt(self, **kwargs):

haystack/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ class FacetingError(HaystackError):
2929
class SpatialError(HaystackError):
3030
"""Raised when incorrect arguments have been provided for spatial."""
3131
pass
32+
33+
class StatsError(HaystackError):
34+
"Raised when incorrect arguments have been provided for stats"
35+
pass

haystack/indexes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def remove_object(self, instance, using=None, **kwargs):
277277
backend = self._get_backend(using)
278278

279279
if backend is not None:
280-
backend.remove(instance)
280+
backend.remove(instance, **kwargs)
281281

282282
def clear(self, using=None):
283283
"""

haystack/query.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def __setstate__(self, data_dict):
7878

7979
def __repr__(self):
8080
data = list(self[:REPR_OUTPUT_SIZE])
81-
81+
8282
if len(self) > REPR_OUTPUT_SIZE:
8383
data[-1] = "...(remaining elements truncated)..."
8484

@@ -375,7 +375,23 @@ def dwithin(self, field, point, distance):
375375
clone = self._clone()
376376
clone.query.add_dwithin(field, point, distance)
377377
return clone
378-
378+
379+
def stats(self, field):
380+
"""Adds stats to a query for the provided field."""
381+
return self.stats_facet(field, facet_fields=None)
382+
383+
def stats_facet(self, field, facet_fields=None):
384+
"""Adds stats facet for the given field and facet_fields represents
385+
the faceted fields."""
386+
clone = self._clone()
387+
stats_facets = []
388+
try:
389+
stats_facets.append(sum(facet_fields,[]))
390+
except TypeError:
391+
if facet_fields: stats_facets.append(facet_fields)
392+
clone.query.add_stats_query(field,stats_facets)
393+
return clone
394+
379395
def distance(self, field, point):
380396
"""
381397
Spatial: Denotes results must have distance measurements from the
@@ -491,6 +507,16 @@ def facet_counts(self):
491507
clone = self._clone()
492508
return clone.query.get_facet_counts()
493509

510+
def stats_results(self):
511+
"""
512+
Returns the stats results found by the query.
513+
"""
514+
if self.query.has_run():
515+
return self.query.get_stats()
516+
else:
517+
clone = self._clone()
518+
return clone.query.get_stats()
519+
494520
def spelling_suggestion(self, preferred_query=None):
495521
"""
496522
Returns the spelling suggestion found by the query.

tests/core/tests/indexes.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,19 @@ def test_remove_object(self):
401401

402402
self.mi.remove_object(mock)
403403
self.assertEqual([(res.content_type(), res.pk) for res in self.sb.search('*')['results']], [(u'core.mockmodel', u'1'), (u'core.mockmodel', u'2'), (u'core.mockmodel', u'3')])
404+
405+
# Put it back so we can test passing kwargs.
406+
mock = MockModel()
407+
mock.pk = 20
408+
mock.author = 'daniel%s' % mock.id
409+
mock.pub_date = datetime.datetime(2009, 1, 31, 4, 19, 0)
410+
411+
self.mi.update_object(mock)
412+
self.assertEqual(self.sb.search('*')['hits'], 4)
413+
414+
self.mi.remove_object(mock, commit=False)
415+
self.assertEqual([(res.content_type(), res.pk) for res in self.sb.search('*')['results']], [(u'core.mockmodel', u'1'), (u'core.mockmodel', u'2'), (u'core.mockmodel', u'3'), (u'core.mockmodel', u'20')])
416+
404417
self.sb.clear()
405418

406419
def test_clear(self):
@@ -629,4 +642,4 @@ def test_float_integer_fields(self):
629642
self.assertTrue('average_delay' in self.yabmsi.fields)
630643
self.assertTrue(isinstance(self.yabmsi.fields['average_delay'], indexes.FloatField))
631644
self.assertEqual(self.yabmsi.fields['average_delay'].null, False)
632-
self.assertEqual(self.yabmsi.fields['average_delay'].index_fieldname, 'average_delay')
645+
self.assertEqual(self.yabmsi.fields['average_delay'].index_fieldname, 'average_delay')

tests/core/tests/mocks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ def update(self, index, iterable, commit=True):
4646

4747
def remove(self, obj, commit=True):
4848
global MOCK_INDEX_DATA
49-
del(MOCK_INDEX_DATA[get_identifier(obj)])
49+
if commit == True:
50+
del(MOCK_INDEX_DATA[get_identifier(obj)])
5051

5152
def clear(self, models=[], commit=True):
5253
global MOCK_INDEX_DATA

0 commit comments

Comments
 (0)