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 {
.finally(this.onFinishLoading)
}
async quantity_search(quantity, search, size, noLoadingIndicator) {
async suggestions_search(quantity, search, include, size, noLoadingIndicator) {
if (!noLoadingIndicator) {
this.onStartLoading()
}
return this.swagger()
.then(client => client.apis.repo.quantity_search({
.then(client => client.apis.repo.suggestions_search({
size: size || 20,
include: include,
quantity: quantity,
...search
}))
......
import React from 'react'
import React, { useRef, useState, useContext, useCallback, useMemo } from 'react'
import {searchContext} from './SearchContext'
import Autocomplete from '@material-ui/lab/Autocomplete'
import TextField from '@material-ui/core/TextField'
......@@ -6,15 +6,21 @@ import { CircularProgress } from '@material-ui/core'
import * as searchQuantities from '../../searchQuantities.json'
import { apiContext } from '../api'
const defaultOptions = Object.keys(searchQuantities).map(quantity => searchQuantities[quantity].name)
export default function SearchBar() {
const [open, setOpen] = React.useState(false)
const [options, setOptions] = React.useState(defaultOptions)
const [loading, setLoading] = React.useState(false)
const [inputValue, setInputValue] = React.useState('')
const {response: {statistics, pagination}, domain, query, setQuery} = React.useContext(searchContext)
const {api} = React.useContext(apiContext)
const suggestionsTimerRef = useRef(null)
const {response: {statistics, pagination}, domain, query, setQuery} = useContext(searchContext)
const defaultOptions = useMemo(() => {
return Object.keys(searchQuantities)
.map(quantity => searchQuantities[quantity].name)
.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]}`)
......@@ -33,36 +39,52 @@ export default function SearchBar() {
}
}
const loadValues = (quantity, value) => {
setLoading(true)
const size = searchQuantities[quantity].statistic_size || 20
api.quantity_search(quantity, query, size, true)
.then(response => {
setLoading(false)
const options = Object.keys(response.quantity.values).map(value => `${quantity}=${value}`)
setOptions(options)
setOpen(true)
})
.catch(() => {
setLoading(false)
})
}
const filterOptions = useCallback((options, params) => {
const [quantity, value] = params.inputValue.split('=')
const filteredOptions = options.filter(option => {
const [optionQuantity, optionValue] = option.split('=')
if (!value) {
return optionQuantity.includes(quantity) || optionQuantity === quantity
} else {
return optionValue.includes(value) || optionValue === value
}
})
return filteredOptions
}, [])
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') {
setInputValue(value)
const [quantity, quantityValue] = value.split('=')
if (!quantityValue) {
if (searchQuantities[quantity]) {
loadValues(quantity, quantityValue)
} else {
setOptions(defaultOptions)
}
if (searchQuantities[quantity]) {
loadOptions(quantity, quantityValue)
} else {
setOptions(defaultOptions)
}
}
}
}, [loadOptions])
const handleChange = (event, entries, reason) => {
const handleChange = (event, entries) => {
const newQuery = entries.reduce((query, entry) => {
if (entry) {
const [quantity, value] = entry.split('=')
......@@ -91,7 +113,7 @@ export default function SearchBar() {
setInputValue('')
} else {
setInputValue(`${entry}=`)
loadValues(quantity)
loadOptions(quantity)
}
}
}
......@@ -119,9 +141,9 @@ export default function SearchBar() {
onChange={handleChange}
onInputChange={handleInputChange}
getOptionSelected={(option, value) => option === value}
getOptionLabel={(option) => option}
options={options}
loading={loading}
filterOptions={filterOptions}
renderInput={(params) => (
<TextField
{...params}
......
......@@ -566,6 +566,8 @@ _repo_quantity_search_request_parser.add_argument(
'after', type=str, help='The after value to use for "scrolling".')
_repo_quantity_search_request_parser.add_argument(
'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', {
'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):
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()
add_search_parameters(_repo_quantities_search_request_parser)
_repo_quantities_search_request_parser.add_argument(
......
......@@ -297,7 +297,7 @@ class SearchRequest:
def statistic(
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
implement faceted search on the top values for each quantity.
......@@ -322,9 +322,15 @@ class SearchRequest:
``unique_code_runs``, ``datasets``, other domain specific metrics.
The basic doc_count metric ``code_runs`` is always given.
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]
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)
self._add_metrics(buckets, metrics_to_use)
......
......@@ -1048,6 +1048,23 @@ class TestRepo():
rv = api.get('/repo/?owner=user')
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', [
(2, 'dft.system', 'bulk'),
(0, 'dft.system', 'atom'),
......
......@@ -170,6 +170,14 @@ def test_search_statistics(elastic, example_search_data):
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):
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