Skip to content

Commit a2e41c7

Browse files
Merge pull request #19 from id13/master
Add highlight backend
2 parents 98c101b + c24bec3 commit a2e41c7

File tree

7 files changed

+267
-3
lines changed

7 files changed

+267
-3
lines changed

examples/simple/search_indexes/serializers/book.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class BookDocumentSimpleSerializer(DocumentSerializer):
8484

8585
# tags = serializers.SerializerMethodField()
8686
# authors = serializers.SerializerMethodField()
87+
highlight = serializers.SerializerMethodField()
8788

8889
class Meta(object):
8990
"""Meta options."""
@@ -103,5 +104,11 @@ class Meta(object):
103104
'pages',
104105
'stock_count',
105106
'tags',
107+
'highlight', # Used in highlight tests
106108
'null_field', # Used in testing of `isnull` functional filter.
107109
)
110+
111+
def get_highlight(self, obj):
112+
if hasattr(obj.meta, 'highlight'):
113+
return obj.meta.highlight.__dict__['_d_']
114+
return {}

examples/simple/search_indexes/viewsets/book.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
OrderingFilterBackend,
2020
SearchFilterBackend,
2121
SuggesterFilterBackend,
22+
HighlightBackend,
2223
)
2324
from django_elasticsearch_dsl_drf.views import BaseDocumentViewSet
2425

@@ -47,13 +48,31 @@ class BookDocumentViewSet(BaseDocumentViewSet):
4748
SearchFilterBackend,
4849
FacetedSearchFilterBackend,
4950
SuggesterFilterBackend,
51+
HighlightBackend,
5052
]
5153
# Define search fields
5254
search_fields = (
5355
'title',
5456
'description',
5557
'summary',
5658
)
59+
# Define highlight fields
60+
highlight_fields = {
61+
'title': {
62+
'enabled': True,
63+
'options': {
64+
'pre_tags': ["<b>"],
65+
'post_tags': ["</b>"],
66+
}
67+
},
68+
'summary': {
69+
'options': {
70+
'fragment_size': 50,
71+
'number_of_fragments': 3
72+
}
73+
},
74+
'description': {},
75+
}
5776
# Define filter fields
5877
filter_fields = {
5978
'id': {

src/django_elasticsearch_dsl_drf/filter_backends/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from .search import SearchFilterBackend
1717
from .suggester import SuggesterFilterBackend
18+
from .highlight import HighlightBackend
1819

1920
__title__ = 'django_elasticsearch_dsl_drf.filter_backends'
2021
__author__ = 'Artur Barseghyan <[email protected]>'
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""
2+
Faceted search backend.
3+
"""
4+
5+
from elasticsearch_dsl.query import Q
6+
7+
from rest_framework.filters import BaseFilterBackend
8+
9+
from six import string_types, iteritems
10+
11+
__title__ = 'django_elasticsearch_dsl_drf.highlight'
12+
__author__ = 'Artur Barseghyan <[email protected]>'
13+
__copyright__ = '2017 Artur Barseghyan'
14+
__license__ = 'GPL 2.0/LGPL 2.1'
15+
__all__ = ('HighlightBackend',)
16+
17+
18+
class HighlightBackend(BaseFilterBackend):
19+
"""Highlight backend.
20+
21+
Example:
22+
23+
>>> from django_elasticsearch_dsl_drf.filter_backends import (
24+
>>> HighlightBackend
25+
>>> )
26+
>>> from django_elasticsearch_dsl_drf.views import BaseDocumentViewSet
27+
>>>
28+
>>> # Local article document definition
29+
>>> from .documents import ArticleDocument
30+
>>>
31+
>>> # Local article document serializer
32+
>>> from .serializers import ArticleDocumentSerializer
33+
>>>
34+
>>> class ArticleDocumentView(BaseDocumentViewSet):
35+
>>>
36+
>>> document = ArticleDocument
37+
>>> serializer_class = ArticleDocumentSerializer
38+
>>> filter_backends = [HighlightBackend,]
39+
>>> highlight_fields = {
40+
>>> 'author.name': {
41+
>>> 'enabled': False,
42+
>>> 'options': {
43+
>>> 'fragment_size': 150,
44+
>>> 'number_of_fragments': 3
45+
>>> }
46+
>>> }
47+
>>> 'title': {
48+
>>> 'options': {
49+
>>> 'pre_tags' : ["<em>"],
50+
>>> 'post_tags' : ["</em>"]
51+
>>> },
52+
>>> 'enabled': True,
53+
>>> },
54+
>>> }
55+
56+
Highlight make queries to be more heavy. That's why by default all
57+
highlights are disabled and enabled only explicitly either in the filter
58+
options (`enabled` set to True) or via query params
59+
`?highlight=author.name&highlight=title`.
60+
"""
61+
62+
highlight_param = 'highlight'
63+
64+
@classmethod
65+
def prepare_highlight_fields(cls, view):
66+
"""Prepare faceted search fields.
67+
68+
Prepares the following structure:
69+
70+
>>> {
71+
>>> 'author.name': {
72+
>>> 'enabled': False,
73+
>>> 'options': {
74+
>>> 'fragment_size': 150,
75+
>>> 'number_of_fragments': 3
76+
>>> }
77+
>>> }
78+
>>> 'title': {
79+
>>> 'options': {
80+
>>> 'pre_tags' : ["<em>"],
81+
>>> 'post_tags' : ["</em>"]
82+
>>> },
83+
>>> 'enabled': True,
84+
>>> },
85+
>>> }
86+
87+
:param view:
88+
:type view: rest_framework.viewsets.ReadOnlyModelViewSet
89+
:return: Highlight fields options.
90+
:rtype: dict
91+
"""
92+
highlight_fields = view.highlight_fields
93+
94+
for field, options in highlight_fields.items():
95+
if 'enabled' not in highlight_fields[field]:
96+
highlight_fields[field]['enabled'] = False
97+
98+
if 'options' not in highlight_fields[field]:
99+
highlight_fields[field]['options'] = {}
100+
101+
return highlight_fields
102+
103+
def get_highlight_query_params(self, request):
104+
"""Get highlight query params.
105+
106+
:param request: Django REST framework request.
107+
:type request: rest_framework.request.Request
108+
:return: List of search query params.
109+
:rtype: list
110+
"""
111+
query_params = request.query_params.copy()
112+
return query_params.getlist(self.highlight_param, [])
113+
114+
def filter_queryset(self, request, queryset, view):
115+
"""Filter the queryset.
116+
117+
:param request: Django REST framework request.
118+
:param queryset: Base queryset.
119+
:param view: View.
120+
:type request: rest_framework.request.Request
121+
:type queryset: elasticsearch_dsl.search.Search
122+
:type view: rest_framework.viewsets.ReadOnlyModelViewSet
123+
:return: Updated queryset.
124+
:rtype: elasticsearch_dsl.search.Search
125+
"""
126+
highlight_query_params = self.get_highlight_query_params(request)
127+
highlight_fields = self.prepare_highlight_fields(view)
128+
for __field, __options in highlight_fields.items():
129+
if __field in highlight_query_params or __options['enabled']:
130+
queryset = queryset.highlight(__field, **__options['options'])
131+
132+
return queryset
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""
2+
Test faceted search backend.
3+
"""
4+
5+
from __future__ import absolute_import
6+
7+
import unittest
8+
9+
from django.core.management import call_command
10+
11+
from nine.versions import DJANGO_GTE_1_10
12+
13+
import pytest
14+
15+
from rest_framework import status
16+
17+
from books import constants
18+
import factories
19+
20+
from .base import BaseRestFrameworkTestCase
21+
22+
if DJANGO_GTE_1_10:
23+
from django.urls import reverse
24+
else:
25+
from django.core.urlresolvers import reverse
26+
27+
__title__ = 'django_elasticsearch_dsl_drf.tests.test_highlight'
28+
__author__ = 'Artur Barseghyan <[email protected]>'
29+
__copyright__ = '2017 Artur Barseghyan'
30+
__license__ = 'GPL 2.0/LGPL 2.1'
31+
__all__ = (
32+
'TestHighlight',
33+
)
34+
35+
36+
@pytest.mark.django_db
37+
class TestHighlight(BaseRestFrameworkTestCase):
38+
"""Test highlight."""
39+
40+
pytestmark = pytest.mark.django_db
41+
42+
@classmethod
43+
def setUp(cls):
44+
cls.books_count = 10
45+
cls.books = factories.BookFactory.create_batch(
46+
cls.books_count,
47+
)
48+
49+
cls.special_books_count = 10
50+
cls.special_books = factories.BookFactory.create_batch(
51+
cls.books_count,
52+
title='Twenty Thousand Leagues Under the Sea',
53+
description="""
54+
The title refers to the distance traveled while under the sea and
55+
not to a depth, as twenty thousand leagues is over six times the
56+
diameter, and nearly twice the circumference of the Earth
57+
""",
58+
summary="""
59+
Margaret Drabble argues that Twenty Thousand Leagues Under the Sea
60+
anticipated the ecology movement and shaped the French avant-garde
61+
""",
62+
)
63+
cls.all_books_count = cls.special_books_count + cls.books_count
64+
65+
call_command('search_index', '--rebuild', '-f')
66+
67+
def _list_results_with_highlights(self):
68+
"""List results with facets."""
69+
self.authenticate()
70+
71+
url = reverse('bookdocument-list', kwargs={}) + '?search=twenty'
72+
all_highlights_url = url + '&highlight=summary&highlight=description'
73+
74+
# Make request
75+
no_args_response = self.client.get(url, {})
76+
self.assertEqual(no_args_response.status_code, status.HTTP_200_OK)
77+
78+
# Should contain 10 results
79+
self.assertEqual(
80+
len(no_args_response.data['results']), self.special_books_count)
81+
82+
for result in no_args_response.data['results']:
83+
self.assertEqual(list(result['highlight'].keys()), ['title'])
84+
85+
# Make request
86+
all_highlights_response = self.client.get(all_highlights_url, {})
87+
self.assertEqual(
88+
all_highlights_response.status_code, status.HTTP_200_OK)
89+
90+
# Should contain 20 results
91+
self.assertEqual(
92+
len(all_highlights_response.data['results']),
93+
self.special_books_count,
94+
)
95+
for result in all_highlights_response.data['results']:
96+
self.assertEqual(set(['title', 'description', 'summary']),
97+
set(result['highlight'].keys()))
98+
99+
def test_list_results_with_highlights(self):
100+
"""Test list results with facets."""
101+
return self._list_results_with_highlights()
102+
103+
104+
if __name__ == '__main__':
105+
unittest.main()

src/django_elasticsearch_dsl_drf/tests/test_search.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def setUp(cls):
6565
cls.switz_cities = factories.CityFactory.create_batch(
6666
cls.switz_cities_count,
6767
country=cls.switzerland)
68-
cls.all_cities_cound = cls.cities_count + cls.switz_cities_count
68+
cls.all_cities_count = cls.cities_count + cls.switz_cities_count
6969

7070
call_command('search_index', '--rebuild', '-f')
7171

@@ -102,7 +102,7 @@ def _search_by_nested_field(self, search_term):
102102
# Should contain 20 results
103103
response = self.client.get(url, data)
104104
self.assertEqual(response.status_code, status.HTTP_200_OK)
105-
self.assertEqual(len(response.data['results']), self.all_cities_cound)
105+
self.assertEqual(len(response.data['results']), self.all_cities_count)
106106

107107
# Should contain only 10 results
108108
filtered_response = self.client.get(

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ deps =
1414
django110: -r{toxinidir}/examples/requirements/django_1_10.txt
1515
django111: -r{toxinidir}/examples/requirements/django_1_11.txt
1616
commands =
17-
{envpython} runtests.py
17+
{envpython} runtests.py {posargs}
1818
# {envpython} examples/simple/manage.py test {posargs:django_elasticsearch_dsl_drf} --settings=settings.testing --traceback -v 3

0 commit comments

Comments
 (0)