Skip to content

Commit 83167b2

Browse files
authored
Search: refactor API view (#9613)
1 parent 3ed160b commit 83167b2

File tree

1 file changed

+53
-44
lines changed

1 file changed

+53
-44
lines changed

readthedocs/search/api.py

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,16 @@ def paginate_queryset(self, queryset, request, view=None):
120120
class PageSearchAPIView(CDNCacheTagsMixin, GenericAPIView):
121121

122122
"""
123-
Main entry point to perform a search using Elasticsearch.
123+
Server side search API.
124124
125-
Required query params:
125+
Required query parameters:
126126
127-
- q (search term)
128-
- project
129-
- version
127+
- **q**: Search term.
128+
- **project**: Project to search.
129+
- **version**: Version to search.
130130
131-
.. note::
132-
133-
The methods `_get_project` and `_get_version`
134-
are called many times, so a basic cache is implemented.
135-
"""
131+
Check our [docs](https://docs.readthedocs.io/en/stable/server-side-search.html#api) for more information.
132+
""" # noqa
136133

137134
http_method_names = ['get']
138135
permission_classes = [IsAuthorizedToViewVersion]
@@ -208,55 +205,64 @@ def _get_all_projects_data(self):
208205
main_version = self._get_version()
209206
main_project = self._get_project()
210207

208+
if not self._has_permission(self.request.user, main_version):
209+
return {}
210+
211211
projects_data = {
212-
main_project.slug: ProjectData(
213-
alias=None,
214-
version=VersionData(
215-
slug=main_version.slug,
216-
docs_url=main_project.get_docs_url(version_slug=main_version.slug),
217-
),
218-
)
212+
main_project.slug: self._get_project_data(main_project, main_version),
219213
}
220214

221-
subprojects = Project.objects.filter(
222-
superprojects__parent_id=main_project.id,
223-
)
215+
subprojects = Project.objects.filter(superprojects__parent_id=main_project.id)
224216
for subproject in subprojects:
225-
version = self._get_subproject_version(
217+
version = self._get_project_version(
218+
project=subproject,
226219
version_slug=main_version.slug,
227-
subproject=subproject,
220+
include_hidden=False,
228221
)
229222

230223
# Fallback to the default version of the subproject.
231224
if not version and subproject.default_version:
232-
version = self._get_subproject_version(
225+
version = self._get_project_version(
226+
project=subproject,
233227
version_slug=subproject.default_version,
234-
subproject=subproject,
228+
include_hidden=False,
235229
)
236230

237231
if version and self._has_permission(self.request.user, version):
238-
url = subproject.get_docs_url(version_slug=version.slug)
239-
project_alias = subproject.superprojects.values_list('alias', flat=True).first()
240-
version_data = VersionData(
241-
slug=version.slug,
242-
docs_url=url,
243-
)
244-
projects_data[subproject.slug] = ProjectData(
245-
alias=project_alias,
246-
version=version_data,
232+
projects_data[subproject.slug] = self._get_project_data(
233+
subproject, version
247234
)
248235

249236
return projects_data
250237

251-
def _get_subproject_version(self, version_slug, subproject):
252-
"""Get a version from the subproject."""
238+
def _get_project_data(self, project, version):
239+
"""Build a `ProjectData` object given a project and its version."""
240+
url = project.get_docs_url(version_slug=version.slug)
241+
project_alias = project.superprojects.values_list("alias", flat=True).first()
242+
version_data = VersionData(
243+
slug=version.slug,
244+
docs_url=url,
245+
)
246+
return ProjectData(
247+
alias=project_alias,
248+
version=version_data,
249+
)
250+
251+
def _get_project_version(self, project, version_slug, include_hidden=True):
252+
"""
253+
Get a version from a given project.
254+
255+
:param project: A `Project` object.
256+
:param version_slug: The version slug.
257+
:param include_hidden: If hidden versions should be considered.
258+
"""
253259
return (
254260
Version.internal
255261
.public(
256262
user=self.request.user,
257-
project=subproject,
258-
include_hidden=False,
263+
project=project,
259264
only_built=True,
265+
include_hidden=include_hidden,
260266
)
261267
.filter(slug=version_slug)
262268
.first()
@@ -272,14 +278,16 @@ def _has_permission(self, user, version):
272278
"""
273279
return True
274280

281+
def _get_search_query(self):
282+
return self.request.query_params["q"]
283+
275284
def _record_query(self, response):
276285
project_slug = self._get_project().slug
277286
version_slug = self._get_version().slug
278287
total_results = response.data.get('count', 0)
279288
time = timezone.now()
280289

281-
query = self.request.query_params['q']
282-
query = query.lower().strip()
290+
query = self._get_search_query().lower().strip()
283291

284292
# Record the query with a celery task
285293
tasks.record_search_query.delay(
@@ -290,6 +298,10 @@ def _record_query(self, response):
290298
time.isoformat(),
291299
)
292300

301+
def _use_advanced_query(self):
302+
main_project = self._get_project()
303+
return not main_project.has_feature(Feature.DEFAULT_TO_FUZZY_SEARCH)
304+
293305
def get_queryset(self):
294306
"""
295307
Returns an Elasticsearch DSL search object or an iterator.
@@ -300,9 +312,6 @@ def get_queryset(self):
300312
calling ``search.execute().hits``. This is why an DSL search object
301313
is compatible with DRF's paginator.
302314
"""
303-
main_project = self._get_project()
304-
projects = {}
305-
306315
projects = {
307316
project: project_data.version.slug
308317
for project, project_data in self._get_all_projects_data().items()
@@ -312,12 +321,12 @@ def get_queryset(self):
312321
log.info('Unable to find a version to search')
313322
return []
314323

315-
query = self.request.query_params['q']
324+
query = self._get_search_query()
316325
queryset = PageSearch(
317326
query=query,
318327
projects=projects,
319328
aggregate_results=False,
320-
use_advanced_query=not main_project.has_feature(Feature.DEFAULT_TO_FUZZY_SEARCH),
329+
use_advanced_query=self._use_advanced_query(),
321330
)
322331
return queryset
323332

0 commit comments

Comments
 (0)