Skip to content

Commit e037aad

Browse files
Merge branch '225-reduce-ES-hits-in-paginator-from-2-to-1'
2 parents bbf722b + 657f772 commit e037aad

File tree

9 files changed

+604
-57
lines changed

9 files changed

+604
-57
lines changed

examples/simple/search_indexes/urls.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
FrontAddressDocumentViewSet,
2929
LocationDocumentViewSet,
3030
PublisherDocumentViewSet,
31+
QueryFriendlyPaginationBookDocumentViewSet,
3132
TagDocumentViewSet,
3233
)
3334

@@ -77,6 +78,12 @@
7778
basename='bookdocument'
7879
)
7980

81+
router.register(
82+
r'books-query-friendly-pagination',
83+
QueryFriendlyPaginationBookDocumentViewSet,
84+
basename='bookdocument_query_friendly_pagination'
85+
)
86+
8087
router.register(
8188
r'books-ordered-by-score',
8289
BookOrderingByScoreDocumentViewSet,

examples/simple/search_indexes/viewsets/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
BookSimpleQueryStringBoostSearchFilterBackendDocumentViewSet,
2121
BookSimpleQueryStringSearchFilterBackendDocumentViewSet,
2222
BookSourceSearchBackendDocumentViewSet,
23+
QueryFriendlyPaginationBookDocumentViewSet,
2324
)
2425
from .city import CityDocumentViewSet, CityCompoundSearchBackendDocumentViewSet
2526
from .journal import JournalDocumentViewSet
@@ -55,5 +56,6 @@
5556
'FrontAddressDocumentViewSet',
5657
'LocationDocumentViewSet',
5758
'PublisherDocumentViewSet',
59+
'QueryFriendlyPaginationBookDocumentViewSet',
5860
'TagDocumentViewSet',
5961
)

examples/simple/search_indexes/viewsets/book/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .ordering_by_score import *
1313
from .ordering_by_score_compound_search import *
1414
from .permissions import *
15+
from .query_friendly_pagination import *
1516
from .simple_query_string import *
1617
from .simple_query_string_boost import *
1718
from .source import *
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django_elasticsearch_dsl_drf.pagination import (
2+
QueryFriendlyPageNumberPagination
3+
)
4+
from .default import BookDocumentViewSet
5+
6+
__all__ = (
7+
'QueryFriendlyPaginationBookDocumentViewSet',
8+
)
9+
10+
11+
class QueryFriendlyPaginationBookDocumentViewSet(BookDocumentViewSet):
12+
13+
pagination_class = QueryFriendlyPageNumberPagination

examples/simple/search_indexes/viewsets/journal.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ class JournalDocumentViewSet(BaseDocumentViewSet):
3838
"""JournalDocument ViewSet."""
3939

4040
document = JournalDocument
41-
# serializer_class = BookDocumentSerializer
4241
serializer_class = JournalDocumentSerializer
4342
lookup_field = 'isbn'
4443
document_uid_field = 'isbn.raw'
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
# coding: utf-8
2+
"""
3+
Pagination.
4+
"""
5+
6+
from __future__ import unicode_literals
7+
8+
from collections import OrderedDict
9+
10+
from django.core import paginator as django_paginator
11+
12+
from elasticsearch_dsl.utils import AttrDict
13+
14+
from rest_framework import pagination
15+
from rest_framework.exceptions import NotFound
16+
from rest_framework.response import Response
17+
18+
import six
19+
20+
from .versions import ELASTICSEARCH_GTE_6_0
21+
22+
__title__ = 'django_elasticsearch_dsl_drf.pagination'
23+
__author__ = 'Artur Barseghyan <[email protected]>'
24+
__copyright__ = '2017-2020 Artur Barseghyan'
25+
__license__ = 'GPL 2.0/LGPL 2.1'
26+
__all__ = (
27+
'LimitOffsetPagination',
28+
'Page',
29+
'PageNumberPagination',
30+
'Paginator',
31+
)
32+
33+
34+
class GetCountMixin:
35+
36+
def get_es_count(self, es_response):
37+
if isinstance(es_response.hits.total, AttrDict):
38+
return es_response.hits.total.value
39+
return es_response.hits.total
40+
41+
42+
class Page(django_paginator.Page, GetCountMixin):
43+
"""Page for Elasticsearch."""
44+
45+
def __init__(self, object_list, number, paginator, facets):
46+
self.facets = facets
47+
self.count = self.get_es_count(object_list)
48+
super(Page, self).__init__(object_list, number, paginator)
49+
50+
51+
class Paginator(django_paginator.Paginator):
52+
"""Paginator for Elasticsearch."""
53+
54+
def page(self, number):
55+
"""Returns a Page object for the given 1-based page number.
56+
57+
:param number:
58+
:return:
59+
"""
60+
number = self.validate_number(number)
61+
bottom = (number - 1) * self.per_page
62+
top = bottom + self.per_page
63+
if top + self.orphans >= self.count:
64+
top = self.count
65+
object_list = self.object_list[bottom:top].execute()
66+
__facets = getattr(object_list, 'aggregations', None)
67+
return self._get_page(object_list, number, self, facets=__facets)
68+
69+
def _get_page(self, *args, **kwargs):
70+
"""Get page.
71+
72+
Returns an instance of a single page.
73+
74+
This hook can be used by subclasses to use an alternative to the
75+
standard :cls:`Page` object.
76+
"""
77+
return Page(*args, **kwargs)
78+
79+
80+
class PageNumberPagination(pagination.PageNumberPagination, GetCountMixin):
81+
"""Page number pagination.
82+
83+
A simple page number based style that supports page numbers as
84+
query parameters.
85+
86+
Example:
87+
88+
http://api.example.org/accounts/?page=4
89+
http://api.example.org/accounts/?page=4&page_size=100
90+
"""
91+
92+
django_paginator_class = Paginator
93+
94+
def __init__(self, *args, **kwargs):
95+
"""Constructor.
96+
97+
:param args:
98+
:param kwargs:
99+
"""
100+
self.facets = None
101+
# self.page = None
102+
# self.request = None
103+
self.count = None
104+
super(PageNumberPagination, self).__init__(*args, **kwargs)
105+
106+
def get_facets(self, page=None):
107+
"""Get facets.
108+
109+
:param page:
110+
:return:
111+
"""
112+
if page is None:
113+
page = self.page
114+
115+
if hasattr(page, 'facets') and hasattr(page.facets, '_d_'):
116+
return page.facets._d_
117+
118+
def paginate_queryset(self, queryset, request, view=None):
119+
"""Paginate a queryset.
120+
121+
Paginate a queryset if required, either returning a page object,
122+
or `None` if pagination is not configured for this view.
123+
124+
:param queryset:
125+
:param request:
126+
:param view:
127+
:return:
128+
"""
129+
# TODO: It seems that paginator breaks things. If take out, queries
130+
# doo work.
131+
# Check if there are suggest queries in the queryset,
132+
# ``execute_suggest`` method shall be called, instead of the
133+
# ``execute`` method and results shall be returned back immediately.
134+
# Placing this code at the very start of ``paginate_queryset`` method
135+
# saves us unnecessary queries.
136+
is_suggest = getattr(queryset, '_suggest', False)
137+
if is_suggest:
138+
if ELASTICSEARCH_GTE_6_0:
139+
return queryset.execute().to_dict().get('suggest')
140+
return queryset.execute_suggest().to_dict()
141+
142+
# Check if we're using paginate queryset from `functional_suggest`
143+
# backend.
144+
if view.action == 'functional_suggest':
145+
return queryset
146+
147+
# If we got to this point, it means it's not a suggest or functional
148+
# suggest case.
149+
150+
page_size = self.get_page_size(request)
151+
if not page_size:
152+
return None
153+
154+
paginator = self.django_paginator_class(queryset, page_size)
155+
page_number = request.query_params.get(self.page_query_param, 1)
156+
if page_number in self.last_page_strings:
157+
page_number = paginator.num_pages
158+
159+
# Something weird is happening here. If None returned before the
160+
# following code, post_filter works. If None returned after this code
161+
# post_filter does not work. Obviously, something strange happens in
162+
# the paginator.page(page_number) and thus affects the lazy
163+
# queryset in such a way, that we get TransportError(400,
164+
# 'parsing_exception', 'request does not support [post_filter]')
165+
try:
166+
self.page = paginator.page(page_number)
167+
except django_paginator.InvalidPage as exc:
168+
msg = self.invalid_page_message.format(
169+
page_number=page_number, message=six.text_type(exc)
170+
)
171+
raise NotFound(msg)
172+
173+
if paginator.num_pages > 1 and self.template is not None:
174+
# The browsable API should display pagination controls.
175+
self.display_page_controls = True
176+
177+
self.request = request
178+
return list(self.page)
179+
180+
def get_paginated_response_context(self, data):
181+
"""Get paginated response data.
182+
183+
:param data:
184+
:return:
185+
"""
186+
__data = [
187+
('count', self.page.count),
188+
# ('count', self.count),
189+
('next', self.get_next_link()),
190+
('previous', self.get_previous_link()),
191+
]
192+
__facets = self.get_facets()
193+
if __facets is not None:
194+
__data.append(
195+
('facets', __facets),
196+
)
197+
__data.append(
198+
('results', data),
199+
)
200+
return __data
201+
202+
def get_paginated_response(self, data):
203+
"""Get paginated response.
204+
205+
:param data:
206+
:return:
207+
"""
208+
return Response(OrderedDict(self.get_paginated_response_context(data)))
209+
210+
211+
class LimitOffsetPagination(pagination.LimitOffsetPagination, GetCountMixin):
212+
"""A limit/offset pagination.
213+
214+
Example:
215+
216+
http://api.example.org/accounts/?limit=100
217+
http://api.example.org/accounts/?offset=400&limit=100
218+
"""
219+
220+
def __init__(self, *args, **kwargs):
221+
"""Constructor.
222+
223+
:param args:
224+
:param kwargs:
225+
"""
226+
self.facets = None
227+
self.count = None
228+
# self.limit = None
229+
# self.offset = None
230+
# self.request = None
231+
super(LimitOffsetPagination, self).__init__(*args, **kwargs)
232+
233+
def paginate_queryset(self, queryset, request, view=None):
234+
# Check if there are suggest queries in the queryset,
235+
# ``execute_suggest`` method shall be called, instead of the
236+
# ``execute`` method and results shall be returned back immediately.
237+
# Placing this code at the very start of ``paginate_queryset`` method
238+
# saves us unnecessary queries.
239+
is_suggest = getattr(queryset, '_suggest', False)
240+
if is_suggest:
241+
if ELASTICSEARCH_GTE_6_0:
242+
return queryset.execute().to_dict().get('suggest')
243+
return queryset.execute_suggest().to_dict()
244+
245+
# Check if we're using paginate queryset from `functional_suggest`
246+
# backend.
247+
if view.action == 'functional_suggest':
248+
return queryset
249+
250+
# If we got to this point, it means it's not a suggest or functional
251+
# suggest case.
252+
253+
# if hasattr(self, 'get_count'):
254+
# self.count = self.get_count(queryset)
255+
# else:
256+
# from rest_framework.pagination import _get_count
257+
# self.count = _get_count(queryset)
258+
259+
# self.count = get_count(self, queryset)
260+
261+
self.limit = self.get_limit(request)
262+
if self.limit is None:
263+
return None
264+
265+
self.offset = self.get_offset(request)
266+
self.request = request
267+
268+
resp = queryset[self.offset:self.offset + self.limit].execute()
269+
self.facets = getattr(resp, 'aggregations', None)
270+
271+
self.count = self.get_es_count(resp)
272+
273+
if self.count > self.limit and self.template is not None:
274+
self.display_page_controls = True
275+
276+
if self.count == 0 or self.offset > self.count:
277+
return []
278+
return list(resp)
279+
280+
def get_facets(self, facets=None):
281+
"""Get facets.
282+
283+
:param facets:
284+
:return:
285+
"""
286+
if facets is None:
287+
facets = self.facets
288+
289+
if facets is None:
290+
return None
291+
292+
if hasattr(facets, '_d_'):
293+
return facets._d_
294+
295+
def get_paginated_response_context(self, data):
296+
"""Get paginated response data.
297+
298+
:param data:
299+
:return:
300+
"""
301+
__data = [
302+
('count', self.count),
303+
('next', self.get_next_link()),
304+
('previous', self.get_previous_link()),
305+
]
306+
__facets = self.get_facets()
307+
if __facets is not None:
308+
__data.append(
309+
('facets', __facets),
310+
)
311+
__data.append(
312+
('results', data),
313+
)
314+
return __data
315+
316+
def get_paginated_response(self, data):
317+
"""Get paginated response.
318+
319+
:param data:
320+
:return:
321+
"""
322+
return Response(OrderedDict(self.get_paginated_response_context(data)))

0 commit comments

Comments
 (0)