Skip to content

Commit

Permalink
Initial support for dashboard search (#95)
Browse files Browse the repository at this point in the history
* Initial support for dashboard search

* fix lint

* Add tests

* fix lint

* fix mypy issues

* Address per comment

* revert it back

* update pypi

* Change default exception

* update doc
  • Loading branch information
feng-tao authored Apr 13, 2020
1 parent f368576 commit 8906568
Show file tree
Hide file tree
Showing 26 changed files with 479 additions and 46 deletions.
4 changes: 4 additions & 0 deletions search_service/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import Dict, Any # noqa: F401
from flasgger import Swagger

from search_service.api.dashboard import SearchDashboardAPI
from search_service.api.table import SearchTableAPI, SearchTableFieldAPI, SearchTableFilterAPI
from search_service.api.user import SearchUserAPI
from search_service.api.document import DocumentUserAPI, DocumentTableAPI, DocumentTablesAPI, DocumentUsersAPI
Expand Down Expand Up @@ -89,6 +90,9 @@ def create_app(*, config_module_class: str) -> Flask:
# User Search API
api.add_resource(SearchUserAPI, '/search_user')

# Dashboard Search API
api.add_resource(SearchDashboardAPI, '/search_dashboard')

# DocumentAPI
api.add_resource(DocumentTablesAPI, '/document_table')
api.add_resource(DocumentTableAPI, '/document_table/<document_id>')
Expand Down
77 changes: 77 additions & 0 deletions search_service/api/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import logging
from http import HTTPStatus
from typing import Iterable, Any

from flask_restful import fields, Resource, marshal_with, reqparse # noqa: I201
from flasgger import swag_from

from search_service.exception import NotFoundException
from search_service.proxy import get_proxy_client


DASHBOARD_INDEX = 'dashboard_search_index'

LOGGING = logging.getLogger(__name__)

# todo: Use common to produce this
dashboard_fields = {
"uri": fields.String,
"cluster": fields.String,
"group_name": fields.String,
"group_url": fields.String,
"product": fields.String,
"name": fields.String,
"url": fields.String,
"description": fields.String,
"last_successful_run_timestamp": fields.Integer
}

search_dashboard_results = {
"total_results": fields.Integer,
"results": fields.Nested(dashboard_fields, default=[])
}


class SearchDashboardAPI(Resource):
"""
Search Dashboard API
"""

def __init__(self) -> None:
self.proxy = get_proxy_client()

self.parser = reqparse.RequestParser(bundle_errors=True)

self.parser.add_argument('query_term', required=True, type=str)
self.parser.add_argument('page_index', required=False, default=0, type=int)
self.parser.add_argument('index', required=False, default=DASHBOARD_INDEX, type=str)

super(SearchDashboardAPI, self).__init__()

@marshal_with(search_dashboard_results)
@swag_from('swagger_doc/dashboard/search_dashboard.yml')
def get(self) -> Iterable[Any]:
"""
Fetch dashboard search results based on query_term.
:return: list of dashboard results. List can be empty if query
doesn't match any dashboards
"""
args = self.parser.parse_args(strict=True)
try:
results = self.proxy.fetch_dashboard_search_results(
query_term=args.get('query_term'),
page_index=args['page_index'],
index=args['index']
)

return results, HTTPStatus.OK

except NotFoundException:
return {'message': 'query_term does not exist'}, HTTPStatus.NOT_FOUND

except Exception:

err_msg = 'Exception encountered while processing search request'
LOGGING.exception(err_msg)
return {'message': err_msg}, HTTPStatus.INTERNAL_SERVER_ERROR
39 changes: 39 additions & 0 deletions search_service/api/swagger_doc/dashboard/search_dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Dashboard search
This is used by the frontend API to search dashboard information.
---
tags:
- 'search_dashboard'
parameters:
- name: query_term
in: query
type: string
schema:
type: string
required: true
- name: page_index
in: query
type: integer
schema:
type: integer
default: 0
required: false
- name: index
in: query
type: string
schema:
type: string
default: 'dashboard_search_index'
required: false
responses:
200:
description: dashboard result information
content:
application/json:
schema:
$ref: '#/components/schemas/SearchDashboardResults'
500:
description: Exception encountered while searching
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
46 changes: 46 additions & 0 deletions search_service/api/swagger_doc/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ components:
type: array
items:
$ref: '#/components/schemas/TableFields'
SearchDashboardResults:
type: object
properties:
total_results:
type: integer
description: 'number of results'
example: 10
results:
type: array
items:
$ref: '#/components/schemas/DashboardFields'
SearchUserResults:
type: object
properties:
Expand Down Expand Up @@ -81,6 +92,41 @@ components:
type: integer
description: 'table last updated time'
example: 1568814420
DashboardFields:
type: object
properties:
uri:
type: string
description: 'dashboard uri'
example: 'mode:product/name'
cluster:
type: string
description: 'dashboard cluster'
example: 'gold'
group_name:
type: string
description: 'name of dashboard group'
example: 'Mode dashboard group'
group_url:
type: string
description: 'url of the dashboard group'
example: 'Mode dashboard group://'
product:
type: string
description: 'product of the dashboard group'
example: 'mode'
url:
type: string
description: 'url of the dashboard'
example: 'mode ://report'
description:
type: string
description: 'dashboard description'
example: 'this dashboard has info about that metric'
last_successful_run_timestamp:
type: integer
description: 'dashboard last successful run time'
example: 1568814420
UserFields:
type: object
properties:
Expand Down
1 change: 1 addition & 0 deletions search_service/api/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def __init__(self) -> None:
def get(self) -> Iterable[Any]:
"""
Fetch search results based on query_term.
:return: list of table results. List can be empty if query
doesn't match any tables
"""
Expand Down
3 changes: 3 additions & 0 deletions search_service/exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class NotFoundException(Exception):
def __init__(self, message: str) -> None:
super().__init__(message)
42 changes: 42 additions & 0 deletions search_service/models/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import Set

import attr
from amundsen_common.models.dashboard import DashboardSummary, DashboardSummarySchema

from search_service.models.base import Base


@attr.s(auto_attribs=True, kw_only=True)
class Dashboard(Base,
DashboardSummary):
"""
This represents the part of a dashboard stored in the search proxy
"""

def get_id(self) -> str:
# uses the table key as the document id in ES
return self.name

@classmethod
def get_attrs(cls) -> Set:
return {
'uri',
'cluster',
'group_name',
'group_url',
'product',
'name',
'url',
'description',
'last_successful_run_timestamp'
}

@staticmethod
def get_type() -> str:
return 'dashboard'


class DashboardSchema(DashboardSummarySchema):
class Meta:
target = Dashboard
register_as_scheme = True
6 changes: 6 additions & 0 deletions search_service/proxy/atlas.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,9 @@ def fetch_table_search_results_with_filter(self, *,
page_index: int = 0,
index: str = '') -> SearchResult:
raise NotImplementedError()

def fetch_dashboard_search_results(self, *,
query_term: str,
page_index: int = 0,
index: str = '') -> SearchResult:
raise NotImplementedError()
7 changes: 7 additions & 0 deletions search_service/proxy/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,10 @@ def fetch_table_search_results_with_filter(self, *,
page_index: int = 0,
index: str = '') -> SearchResult:
pass

@abstractmethod
def fetch_dashboard_search_results(self, *,
query_term: str,
page_index: int = 0,
index: str = '') -> SearchResult:
pass
46 changes: 46 additions & 0 deletions search_service/proxy/elasticsearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from search_service.models.search_result import SearchResult
from search_service.models.table import Table
from search_service.models.user import User
from search_service.models.dashboard import Dashboard
from search_service.models.tag import Tag
from search_service.proxy.base import BaseProxy
from search_service.proxy.statsd_utilities import timer_with_counter
Expand Down Expand Up @@ -614,3 +615,48 @@ def delete_document(self, *, data: List[str], index: str) -> str:
return ''

return self._delete_document_helper(data=data, index=index)

@timer_with_counter
def fetch_dashboard_search_results(self, *,
query_term: str,
page_index: int = 0,
index: str = '') -> SearchResult:
"""
Fetch dashboard search result with fuzzy search
:param query_term:
:param page_index:
:param index:
:return:
"""
current_index = index if index else \
current_app.config.get(config.ELASTICSEARCH_INDEX_KEY, DEFAULT_ES_INDEX)

if not query_term:
# return empty result for blank query term
return SearchResult(total_results=0, results=[])
s = Search(using=self.elasticsearch, index=current_index)

query_name = {
"function_score": {
"query": {
"multi_match": {
"query": query_term,
"fields": ["name.raw^75",
"name^5",
"group_name.raw^5",
"description^3",
"query_names^3"]
}
},
"field_value_factor": {
"field": "total_usage",
"modifier": "log2p"
}
}
}

return self._search_helper(page_index=page_index,
client=s,
query_name=query_name,
model=Dashboard)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from setuptools import setup, find_packages

__version__ = '2.1.6'
__version__ = '2.2.0'

requirements_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'requirements.txt')
with open(requirements_path) as requirements_file:
Expand Down
Empty file added tests/unit/api/__init__.py
Empty file.
Empty file.
41 changes: 41 additions & 0 deletions tests/unit/api/dashboard/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from search_service.models.dashboard import Dashboard


def mock_proxy_results() -> Dashboard:
return Dashboard(uri='dashboard_uri',
cluster='gold',
group_name='mode_dashboard_group',
group_url='mode_dashboard_group_url',
product='mode',
name='mode_dashboard',
url='mode_dashboard_url',
description='test_dashboard',
last_successful_run_timestamp=1000)


def mock_json_response() -> dict:
return {
"uri": 'dashboard_uri',
"cluster": 'gold',
"group_name": 'mode_dashboard_group',
"group_url": 'mode_dashboard_group_url',
"product": 'mode',
"name": 'mode_dashboard',
"url": 'mode_dashboard_url',
"description": 'test_dashboard',
"last_successful_run_timestamp": 1000,
}


def default_json_response() -> dict:
return {
"uri": None,
"cluster": None,
"group_name": None,
"group_url": None,
"product": None,
"name": None,
"url": None,
"description": None,
"last_successful_run_timestamp": 0,
}
Loading

0 comments on commit 8906568

Please sign in to comment.