async def featured_result_query(self, request: ONSRequest): """ Executes the ONS featured result query using the given SearchEngine class :param request: :return: """ engine: AbstractSearchEngine = self.get_search_engine_instance() # Perform the query search_term = request.get_search_term() try: engine: AbstractSearchEngine = engine.featured_result_query( search_term) logger.debug(request.request_id, "Executing featured result query", extra={"query": engine.to_dict()}) response: ONSResponse = await engine.execute() except ConnectionError as e: message = "Unable to connect to Elasticsearch cluster to perform featured result query request" logger.error(request.request_id, message, exc_info=e) raise ServerError(message) except RequestSizeExceededException as e: # Log and raise a 400 BAD_REQUEST message = "Requested page size exceeds max allowed: '{0}'".format( e) logger.error(request.request_id, message, exc_info=e) raise InvalidUsage(message) search_result: SearchResult = response.to_featured_result_query_search_result( ) return search_result
def get_type_filters(self, list_type: ListType) -> List[TypeFilter]: """ Returns requested type filters or the defaults for the desired ListType :param list_type: :return: """ if hasattr(self, "json") and isinstance(self.json, dict): type_filters_raw = self.json.get("filter", None) if type_filters_raw is not None: if isinstance(type_filters_raw, str): type_filters_raw = loads(type_filters_raw) if not isinstance(type_filters_raw, list): type_filters_raw = [type_filters_raw] try: type_filters: List[ TypeFilter] = AvailableTypeFilters.from_string_list( type_filters_raw) return type_filters except UnknownTypeFilter as e: # Import logger here to prevent circular dependency on module import message = "Received unknown type filter: '{0}'".format( e.unknown_type_filter) logger.error(self.request_id, message, exc_info=e) raise InvalidUsage(message) return list_type.to_type_filters()
async def departments_query(self, request: ONSRequest) -> SearchResult: """ Executes the ONS departments query using the given SearchEngine class :param request: :return: """ # Initialise the search engine engine: AbstractSearchEngine = self.get_search_engine_instance() # Perform the query search_term = request.get_search_term() page = request.get_current_page() page_size = request.get_page_size() try: engine: AbstractSearchEngine = engine.departments_query( search_term, page, page_size) logger.debug(request.request_id, "Executing departments query", extra={"query": engine.to_dict()}) response: ONSResponse = await engine.execute() except ConnectionError as e: message = "Unable to connect to Elasticsearch cluster to perform departments query request" logger.error(request.request_id, message, exc_info=e) raise ServerError(message) search_result: SearchResult = response.to_departments_query_search_result( page, page_size) return search_result
async def health_check(request: ONSRequest): """ API to check health of the app (i.e connection status to Elasticsearch) :param request: :return: """ # Ping elasticsearch to get cluster health app: SearchApp = request.app client: Elasticsearch = app.elasticsearch.client # Send the request try: health = client.cluster.health() if isawaitable(health): health = await health code = 200 if 'status' in health and health['status'] in [ 'yellow', 'green' ] else 500 if code != 200: logger.error(request.request_id, "Healthcheck results in non-200 response", extra={"health": health}) return json(request, health, code) except Exception as e: logger.error(request.request_id, "Unable to get Elasticsearch cluster health", exc_info=e) body = {"elasticsearch": "unavailable"} return json(request, body, 500)
async def spell_check(request: ONSRequest): """ API for spell checking :param request: :return: """ search_term = request.get_search_term() # Get spell checker app: SearchApp = request.app spell_checker: SpellChecker = app.spell_checker # Generate the tokens tokens = search_term.split() if len(tokens) > 0: # Get the result result = spell_checker.correct_spelling(tokens) # Return the json response return json(request, result, 200) # No input tokens - raise a 400 BAD_REQUEST message = "Found no input tokens in query: %s" % search_term logger.error(request.request_id, message) return json(request, message, 400)
async def ons_content_query(request: ONSRequest, list_type: str) -> HTTPResponse: """ Handles content queries to the <list_type> API. :param request: :param list_type: The list_type to query against (i.e ons, onsdata or onspublications; see api.search.list_type.py) :return: """ # Parse list_type to enum if ListType.is_list_type(list_type): list_type_enum: ListType = ListType.from_str(list_type) # Initialise the search engine sanic_search_engine = SanicSearchEngine(request.app, SearchEngine, Index.ONS) # Perform the request search_result: SearchResult = await sanic_search_engine.content_query( request, list_type_enum) return json(request, search_result.to_dict(), 200) else: # Log and return 404 message = "Received content query request for unknown list type: '{0}'".format( list_type) logger.error(request.request_id, message) return json(request, message, 404)
def get_search_term(self) -> str: """ Parses the request to extract a search term :return: """ search_term = self.args.get("q", None) if search_term is None: logger.error(self.request_id, "Search term not specified", extra={"status": 400}) raise InvalidUsage("Search term not specified") return search_term
async def content_query(self, request: ONSRequest, list_type: ListType) -> SearchResult: """ Executes the ONS content query using the given SearchEngine class :param request: :param list_type: :return: """ # Initialise the search engine engine: AbstractSearchEngine = self.get_search_engine_instance() # Perform the query search_term = request.get_search_term() page = request.get_current_page() page_size = request.get_page_size() sort_by: SortField = request.get_sort_by() type_filters: List[TypeFilter] = request.get_type_filters(list_type) # Build filter functions filter_functions: List[AvailableContentTypes] = [] for type_filter in type_filters: filter_functions.extend(type_filter.get_content_types()) try: engine: AbstractSearchEngine = engine.content_query( search_term, page, page_size, sort_by=sort_by, filter_functions=filter_functions, type_filters=type_filters) logger.debug(request.request_id, "Executing content query", extra={"query": engine.to_dict()}) response: ONSResponse = await engine.execute() except ConnectionError as e: message = "Unable to connect to Elasticsearch cluster to perform content query request" logger.error(request.request_id, message, exc_info=e) raise ServerError(message) except RequestSizeExceededException as e: # Log and raise a 400 BAD_REQUEST message = "Requested page size exceeds max allowed: '{0}'".format( e) logger.error(request.request_id, message, exc_info=e) raise InvalidUsage(message) search_result: SearchResult = response.to_content_query_search_result( page, page_size, sort_by) return search_result
def get_elasticsearch_query(self) -> dict: """ Parse the request body for Elasticsearch query JSON and return as dict :return: """ body = self.json if body is not None and isinstance(body, dict) and "query" in body: return body else: # Raise InvalidUsage (400) and log error # Import logger here to prevent circular dependency on module import message = "Invalid request body whilst trying to parse for Elasticsearch query" logger.error(self.request_id, message, extra={"body": body}) raise InvalidUsage(message)
def get_page_size(self) -> int: """ Returns the requested page size (min of 1). Defaults to the value set by the paginator. :return: """ page_size = self.args.get("size", SEARCH_CONFIG.results_per_page) if isinstance(page_size, str): if page_size.isdigit(): page_size = int(page_size) else: # Log error parsing param to int message = "Unable to convert size param to int" logger.error(self.request_id, message, extra={"size": page_size}) raise InvalidUsage(message) if isinstance(page_size, int) and page_size > 0: return page_size # Raise InvalidUsage (400) and log error message = "Invalid request [size={size}]".format(size=page_size) logger.error(self.request_id, message, extra={"size": page_size}) raise InvalidUsage(message)
async def proxy(self, request: ONSRequest) -> SearchResult: """ Proxy an Elasticsearch query over HTTP :param request: :return: """ # Initialise the search engine engine: AbstractSearchEngine = self.get_search_engine_instance() # Parse the request body for a valid Elasticsearch query body: dict = request.get_elasticsearch_query() # Parse query and filters query: dict = loads(body.get("query")) type_filters_raw = body.get("filter") # Update the search engine with the query JSON engine.update_from_dict(query) # Extract paginator params page = request.get_current_page() page_size = request.get_page_size() sort_by = request.get_sort_by() try: engine: AbstractSearchEngine = engine.paginate(page, page_size) except RequestSizeExceededException as e: # Log and raise a 400 BAD_REQUEST message = "Requested page size exceeds max allowed: '{0}'".format( e) logger.error(request.request_id, message, exc_info=e) raise InvalidUsage(message) # Add any type filters if type_filters_raw is not None: if not isinstance(type_filters_raw, list): type_filters_raw = [type_filters_raw] try: type_filters = AvailableTypeFilters.from_string_list( type_filters_raw) engine: AbstractSearchEngine = engine.type_filter(type_filters) except UnknownTypeFilter as e: message = "Received unknown type filter: '{0}'".format( e.unknown_type_filter) logger.error(request.request_id, message, exc_info=e) raise InvalidUsage(message) # Execute try: logger.debug(request.request_id, "Executing proxy query", extra={"query": engine.to_dict()}) response: ONSResponse = await engine.execute() except ConnectionError as e: message = "Unable to connect to Elasticsearch cluster to perform proxy query request" logger.error(request.request_id, message, e) raise ServerError(message) search_result: SearchResult = response.to_content_query_search_result( page, page_size, sort_by) return search_result