Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
On Thursday, 7th July from 1 to 3 pm there will be a maintenance with a short downtime of GitLab.
Open sidebar
nomad-lab
nomad-FAIR
Commits
05b81af7
Commit
05b81af7
authored
Apr 29, 2020
by
Markus Scheidgen
Browse files
Added real suggestions to searchbar.
parent
37b771aa
Changes
6
Hide whitespace changes
Inline
Side-by-side
gui/src/components/api.js
View file @
05b81af7
...
...
@@ -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
}))
...
...
gui/src/components/search/SearchBarNew.js
View file @
05b81af7
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
}
=`
)
load
Value
s
(
quantity
)
load
Option
s
(
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
}
...
...
nomad/app/api/repo.py
View file @
05b81af7
...
...
@@ -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
(
...
...
nomad/search.py
View file @
05b81af7
...
...
@@ -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
)
...
...
tests/app/test_api.py
View file @
05b81af7
...
...
@@ -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'
),
...
...
tests/test_search.py
View file @
05b81af7
...
...
@@ -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
()
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment