diff --git a/nomad/app/flask/api/materialtransformer.py b/nomad/app/flask/api/materialtransformer.py index 3d50ca50352d0a879611893e68a0657561e31ebc..cbae16f292e0f9b8854c6fbb118f961290839555 100644 --- a/nomad/app/flask/api/materialtransformer.py +++ b/nomad/app/flask/api/materialtransformer.py @@ -1,6 +1,6 @@ from typing import Callable from lark import v_args -from elasticsearch_dsl import Q, Field +from elasticsearch_dsl import Q, Field, Text, Keyword, Integer, Boolean from optimade.filtertransformers.elasticsearch import Quantity from nomad.atomutils import get_hill_decomposition @@ -69,7 +69,8 @@ class MElasticTransformer(ElasticTransformer): A specialized Optimade/Lark transformer for handling material queries. Provides mostly the same functionality as optimade.filtertransformers.elasticsearch.ElasticTransformer, but has - additions that make nested queries and parameter conversions possible. + additions that make nested queries, parameter conversions and the use of + boolean values possible. Uses elasticsearch_dsl and will produce a :class:`Q` instance. @@ -77,6 +78,29 @@ class MElasticTransformer(ElasticTransformer): quantities: A list of :class:`MQuantity`s that describe how optimade (and other) quantities are mapped to the elasticsearch index. ''' + def _query_op(self, quantity, op, value, nested=None): + """ + Return a range, match, or term query for the given quantity, comparison + operator, and value + """ + field = self._field(quantity, nested=nested) + if op in _cmp_operators: + return Q("range", **{field: {_cmp_operators[op]: value}}) + + if quantity.elastic_mapping_type == Text: + query_type = "match" + elif quantity.elastic_mapping_type in [Keyword, Integer, Boolean]: + query_type = "term" + else: + raise NotImplementedError("Quantity has unsupported ES field type") + + if op in ["=", ""]: + return Q(query_type, **{field: value}) + + if op == "!=": + return ~Q( # pylint: disable=invalid-unary-operand-type + query_type, **{field: value} + ) def _has_query_op(self, quantities, op, predicate_zip_list): """ diff --git a/tests/app/flask/test_api_encyclopedia.py b/tests/app/flask/test_api_encyclopedia.py index b83554dfe62c18022d8622b15ee8e7b853e5310f..ab64ac54ed2948dd7c57b466e15c24e2c8b8b6f7 100644 --- a/tests/app/flask/test_api_encyclopedia.py +++ b/tests/app/flask/test_api_encyclopedia.py @@ -403,12 +403,33 @@ class TestEncyclopedia(): # Test that invalid query parameters raise code 400 def test_complex_search(self, enc_upload, elastic_infra, api, test_user_auth): + # Test an elaborate boolean query for elements + query = json.dumps({"query": """( + ( elements HAS ALL "Si", "O" OR elements HAS ALL "Ge", "O" ) OR + ( elements HAS ALL "Si", "N" OR elements HAS ALL "Ge", "N" ) + )"""}) + rv = api.post('/materials/', data=query, content_type='application/json') + assert rv.status_code == 200 + results = rv.json['results'] + assert len(results) == 0 + + # Test that there are no issues with the custom Optimade grammar + # containing boolean values. See discussion at + # https://github.com/Materials-Consortia/OPTIMADE/issues/345 + query = json.dumps({"query": 'has_band_structure=TRUE'}) + rv = api.post('/materials/', data=query, content_type='application/json') + assert rv.status_code == 200 + results = rv.json['results'] + assert len(results) == 1 + # Test that completely private materials only become visible after # authentication query = json.dumps({"query": 'elements HAS ALL "B"'}) rv = api.post('/materials/', data=query, content_type='application/json') results = rv.json['results'] + assert rv.status_code == 200 assert len(results) == 0 rv = api.post('/materials/', data=query, content_type='application/json', headers=test_user_auth) + assert rv.status_code == 200 results = rv.json['results'] assert len(results) == 1