Commit 05b81af7 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Added real suggestions to searchbar.

parent 37b771aa
...@@ -474,13 +474,14 @@ class Api { ...@@ -474,13 +474,14 @@ class Api {
.finally(this.onFinishLoading) .finally(this.onFinishLoading)
} }
async quantity_search(quantity, search, size, noLoadingIndicator) { async suggestions_search(quantity, search, include, size, noLoadingIndicator) {
if (!noLoadingIndicator) { if (!noLoadingIndicator) {
this.onStartLoading() this.onStartLoading()
} }
return this.swagger() return this.swagger()
.then(client => client.apis.repo.quantity_search({ .then(client => client.apis.repo.suggestions_search({
size: size || 20, size: size || 20,
include: include,
quantity: quantity, quantity: quantity,
...search ...search
})) }))
......
import React from 'react' import React, { useRef, useState, useContext, useCallback, useMemo } from 'react'
import {searchContext} from './SearchContext' import {searchContext} from './SearchContext'
import Autocomplete from '@material-ui/lab/Autocomplete' import Autocomplete from '@material-ui/lab/Autocomplete'
import TextField from '@material-ui/core/TextField' import TextField from '@material-ui/core/TextField'
...@@ -6,15 +6,21 @@ import { CircularProgress } from '@material-ui/core' ...@@ -6,15 +6,21 @@ import { CircularProgress } from '@material-ui/core'
import * as searchQuantities from '../../searchQuantities.json' import * as searchQuantities from '../../searchQuantities.json'
import { apiContext } from '../api' import { apiContext } from '../api'
const defaultOptions = Object.keys(searchQuantities).map(quantity => searchQuantities[quantity].name)
export default function SearchBar() { export default function SearchBar() {
const [open, setOpen] = React.useState(false) const suggestionsTimerRef = useRef(null)
const [options, setOptions] = React.useState(defaultOptions) const {response: {statistics, pagination}, domain, query, setQuery} = useContext(searchContext)
const [loading, setLoading] = React.useState(false) const defaultOptions = useMemo(() => {
const [inputValue, setInputValue] = React.useState('') return Object.keys(searchQuantities)
const {response: {statistics, pagination}, domain, query, setQuery} = React.useContext(searchContext) .map(quantity => searchQuantities[quantity].name)
const {api} = React.useContext(apiContext) .filter(quantity => !quantity.includes('.') || quantity.startsWith(domain.key + '.'))
}, [domain.key])
const [open, setOpen] = useState(false)
const [options, setOptions] = useState(defaultOptions)
const [loading, setLoading] = useState(false)
const [inputValue, setInputValue] = useState('')
const {api} = useContext(apiContext)
const autocompleteValue = Object.keys(query).map(quantity => `${quantity}=${query[quantity]}`) const autocompleteValue = Object.keys(query).map(quantity => `${quantity}=${query[quantity]}`)
...@@ -33,36 +39,52 @@ export default function SearchBar() { ...@@ -33,36 +39,52 @@ export default function SearchBar() {
} }
} }
const loadValues = (quantity, value) => { const filterOptions = useCallback((options, params) => {
setLoading(true) const [quantity, value] = params.inputValue.split('=')
const size = searchQuantities[quantity].statistic_size || 20 const filteredOptions = options.filter(option => {
api.quantity_search(quantity, query, size, true) const [optionQuantity, optionValue] = option.split('=')
.then(response => { if (!value) {
setLoading(false) return optionQuantity.includes(quantity) || optionQuantity === quantity
const options = Object.keys(response.quantity.values).map(value => `${quantity}=${value}`) } else {
setOptions(options) return optionValue.includes(value) || optionValue === value
setOpen(true) }
}) })
.catch(() => { return filteredOptions
setLoading(false) }, [])
})
}
const handleInputChange = (event, value, reason) => { const loadOptions = useCallback((quantity, value) => {
if (suggestionsTimerRef.current !== null) {
clearTimeout(suggestionsTimerRef.current)
}
suggestionsTimerRef.current = setTimeout(() => {
setLoading(true)
api.suggestions_search(quantity, query, value, 20, true)
.then(response => {
setLoading(false)
const options = response.suggestions.map(value => `${quantity}=${value}`)
setOptions(options)
setOpen(true)
})
.catch(() => {
setLoading(false)
})
}, 200)
}, [api, suggestionsTimerRef])
const handleInputChange = useCallback((event, value, reason) => {
if (reason === 'input') { if (reason === 'input') {
setInputValue(value) setInputValue(value)
const [quantity, quantityValue] = value.split('=') const [quantity, quantityValue] = value.split('=')
if (!quantityValue) {
if (searchQuantities[quantity]) { if (searchQuantities[quantity]) {
loadValues(quantity, quantityValue) loadOptions(quantity, quantityValue)
} else { } else {
setOptions(defaultOptions) setOptions(defaultOptions)
}
} }
} }
} }, [loadOptions])
const handleChange = (event, entries, reason) => { const handleChange = (event, entries) => {
const newQuery = entries.reduce((query, entry) => { const newQuery = entries.reduce((query, entry) => {
if (entry) { if (entry) {
const [quantity, value] = entry.split('=') const [quantity, value] = entry.split('=')
...@@ -91,7 +113,7 @@ export default function SearchBar() { ...@@ -91,7 +113,7 @@ export default function SearchBar() {
setInputValue('') setInputValue('')
} else { } else {
setInputValue(`${entry}=`) setInputValue(`${entry}=`)
loadValues(quantity) loadOptions(quantity)
} }
} }
} }
...@@ -119,9 +141,9 @@ export default function SearchBar() { ...@@ -119,9 +141,9 @@ export default function SearchBar() {
onChange={handleChange} onChange={handleChange}
onInputChange={handleInputChange} onInputChange={handleInputChange}
getOptionSelected={(option, value) => option === value} getOptionSelected={(option, value) => option === value}
getOptionLabel={(option) => option}
options={options} options={options}
loading={loading} loading={loading}
filterOptions={filterOptions}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
......
...@@ -566,6 +566,8 @@ _repo_quantity_search_request_parser.add_argument( ...@@ -566,6 +566,8 @@ _repo_quantity_search_request_parser.add_argument(
'after', type=str, help='The after value to use for "scrolling".') 'after', type=str, help='The after value to use for "scrolling".')
_repo_quantity_search_request_parser.add_argument( _repo_quantity_search_request_parser.add_argument(
'size', type=int, help='The max size of the returned values.') 'size', type=int, help='The max size of the returned values.')
_repo_quantity_search_request_parser.add_argument(
'value', type=str, help='A partial value. Only values that include this will be returned')
_repo_quantity_model = api.model('RepoQuantity', { _repo_quantity_model = api.model('RepoQuantity', {
'after': fields.String(description='The after value that can be used to retrieve the next set of values.'), 'after': fields.String(description='The after value that can be used to retrieve the next set of values.'),
...@@ -631,6 +633,62 @@ class RepoQuantityResource(Resource): ...@@ -631,6 +633,62 @@ class RepoQuantityResource(Resource):
abort(400, 'Given quantity does not exist: %s' % str(e)) abort(400, 'Given quantity does not exist: %s' % str(e))
_repo_suggestions_search_request_parser = api.parser()
add_search_parameters(_repo_suggestions_search_request_parser)
_repo_suggestions_search_request_parser.add_argument(
'size', type=int, help='The max size of the returned values.')
_repo_suggestions_search_request_parser.add_argument(
'include', type=str, help='A substring that all values need to include.')
_repo_suggestions_model = api.model('RepoSuggestionsValues', {
'suggestions': fields.List(fields.String, description='A list with the suggested values.')
})
@ns.route('/suggestions/<string:quantity>')
class RepoSuggestionsResource(Resource):
@api.doc('suggestions_search')
@api.response(400, 'Invalid requests, e.g. wrong owner type, bad quantity, bad search parameters')
@api.expect(_repo_suggestions_search_request_parser, validate=True)
@api.marshal_with(_repo_suggestions_model, skip_none=True, code=200, description='Suggestions send')
@authenticate()
def get(self, quantity: str):
'''
Retrieve the top values for the given quantity from entries matching the search.
Values can be filtered by to include a given value.
There is no ordering, no pagination, and no scroll interface.
The result will contain a 'suggestions' key with values. There will be upto 'size' many values.
'''
search_request = search.SearchRequest()
args = {
key: value
for key, value in _repo_suggestions_search_request_parser.parse_args().items()
if value is not None}
apply_search_parameters(search_request, args)
size = args.get('size', 20)
include = args.get('include', None)
try:
assert size >= 0
except AssertionError:
abort(400, message='invalid size')
try:
search_request.statistic(quantity, size=size, include=include)
results = search_request.execute()
results['suggestions'] = list(results['statistics'][quantity].keys())
return results, 200
except KeyError as e:
import traceback
traceback.print_exc()
abort(400, 'Given quantity does not exist: %s' % str(e))
_repo_quantities_search_request_parser = api.parser() _repo_quantities_search_request_parser = api.parser()
add_search_parameters(_repo_quantities_search_request_parser) add_search_parameters(_repo_quantities_search_request_parser)
_repo_quantities_search_request_parser.add_argument( _repo_quantities_search_request_parser.add_argument(
......
...@@ -297,7 +297,7 @@ class SearchRequest: ...@@ -297,7 +297,7 @@ class SearchRequest:
def statistic( def statistic(
self, quantity_name: str, size: int, metrics_to_use: List[str] = [], self, quantity_name: str, size: int, metrics_to_use: List[str] = [],
order: Dict[str, str] = dict(_key='asc')): order: Dict[str, str] = dict(_key='asc'), include: str = None):
''' '''
This can be used to display statistics over the searched entries and allows to This can be used to display statistics over the searched entries and allows to
implement faceted search on the top values for each quantity. implement faceted search on the top values for each quantity.
...@@ -322,9 +322,15 @@ class SearchRequest: ...@@ -322,9 +322,15 @@ class SearchRequest:
``unique_code_runs``, ``datasets``, other domain specific metrics. ``unique_code_runs``, ``datasets``, other domain specific metrics.
The basic doc_count metric ``code_runs`` is always given. The basic doc_count metric ``code_runs`` is always given.
order: The order dictionary is passed to the elastic search aggregation. order: The order dictionary is passed to the elastic search aggregation.
include:
Uses an regular expression in ES to only return values that include
the given substring.
''' '''
quantity = search_quantities[quantity_name] quantity = search_quantities[quantity_name]
terms = A('terms', field=quantity.search_field, size=size, order=order) terms_kwargs = {}
if include is not None:
terms_kwargs['include'] = '.*%s.*' % include
terms = A('terms', field=quantity.search_field, size=size, order=order, **terms_kwargs)
buckets = self._search.aggs.bucket('statistics:%s' % quantity_name, terms) buckets = self._search.aggs.bucket('statistics:%s' % quantity_name, terms)
self._add_metrics(buckets, metrics_to_use) self._add_metrics(buckets, metrics_to_use)
......
...@@ -1048,6 +1048,23 @@ class TestRepo(): ...@@ -1048,6 +1048,23 @@ class TestRepo():
rv = api.get('/repo/?owner=user') rv = api.get('/repo/?owner=user')
assert rv.status_code == 401 assert rv.status_code == 401
@pytest.mark.parametrize('suggestions, quantity, value', [
(1, 'dft.system', 'bulk'),
(1, 'dft.system', 'ulk'),
(1, 'dft.system', 'ul'),
(0, 'dft.system', 'notbulk'),
(1, 'dft.system', None)
])
def test_suggestions_search(self, api, example_elastic_calcs, no_warn, test_user_auth, suggestions, quantity, value):
url = '/repo/suggestions/%s' % quantity
if value is not None:
url = url + '?include=%s' % value
rv = api.get(url, headers=test_user_auth)
assert rv.status_code == 200
data = json.loads(rv.data)
values = data['suggestions']
assert len(values) == suggestions
@pytest.mark.parametrize('calcs, quantity, value', [ @pytest.mark.parametrize('calcs, quantity, value', [
(2, 'dft.system', 'bulk'), (2, 'dft.system', 'bulk'),
(0, 'dft.system', 'atom'), (0, 'dft.system', 'atom'),
......
...@@ -170,6 +170,14 @@ def test_search_statistics(elastic, example_search_data): ...@@ -170,6 +170,14 @@ def test_search_statistics(elastic, example_search_data):
assert 'quantities' not in results assert 'quantities' not in results
def test_suggest_statistics(elastic, example_search_data):
results = SearchRequest(domain='dft').statistic('dft.system', include='ulk', size=2).execute()
assert len(results['statistics']['dft.system']) == 1
results = SearchRequest(domain='dft').statistic('dft.system', include='not_ulk', size=2).execute()
assert len(results['statistics']['dft.system']) == 0
def test_search_totals(elastic, example_search_data): def test_search_totals(elastic, example_search_data):
use_metrics = search_extension.metrics.keys() use_metrics = search_extension.metrics.keys()
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment