Skip to content

Commit

Permalink
✨ Issue: #1 add support for ´:contains´ modifier
Browse files Browse the repository at this point in the history
tests coverage added
  • Loading branch information
nazrulworld committed Feb 21, 2020
1 parent 1aa7de9 commit f308958
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 14 deletions.
2 changes: 2 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ History

Improvements

- Add support for important FHIR search modifier ``:contains``. See https://github.com/nazrulworld/fhirpath/issues/1

- Add support for ``:above``FHIR search modifier and `èb``prefix. See https://github.com/nazrulworld/fhirpath/issues/2
- Add support for ``:bellow`` FHIR search modifier and ``sa`` prefix. See https://github.com/nazrulworld/fhirpath/issues/2
Expand Down
70 changes: 57 additions & 13 deletions src/fhirpath/dialects/elasticsearch.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@
}


def escape_star(v):
""" """
if "*" in v:
v = v.replace("*", "\\*")
return v


def escape_all(v):
""" """
v = escape_star(v)
v = (
v.replace(".", "\\.")
.replace("?", "\\?")
.replace(":", "\\:")
.replace("[", "\\[")
)
return v


class ElasticSearchDialect(DialectBase):
""" """

Expand Down Expand Up @@ -106,26 +125,38 @@ def _create_sa_term(self, path, value):

return q

def _create_eb_term(self, path, value):
"""Create ES Prefix Query"""
def _create_contains_term(self, path, value):
"""Create ES Regex Query"""

def _check(v):
if "*" in v:
raise NotImplementedError
if isinstance(value, (list, tuple)):
if len(value) == 1:
value = value[0]
else:
q = {"bool": {"should": [], "minimum_should_match": 1}}
for val in value:
q["bool"]["should"].append(
{"prefix": {path: {"value": ".*{0}.*".format(escape_all(val))}}}
)
return q

q = {"regexp": {path: {"value": ".*{0}.*".format(escape_all(value))}}}

return q

def _create_eb_term(self, path, value):
"""Create ES Prefix Query"""
if isinstance(value, (list, tuple)):
if len(value) == 1:
value = value[0]
else:
q = {"bool": {"should": [], "minimum_should_match": 1}}
for val in value:
_check(val)
q["bool"]["should"].append(
{"wildcard": {path: {"value": "*" + val}}}
{"wildcard": {path: {"value": "*" + escape_star(val)}}}
)
return q
_check(value)
q = {"wildcard": {path: {"value": "*" + value}}}

q = {"wildcard": {path: {"value": "*{0}".format(escape_star(value))}}}

return q

Expand Down Expand Up @@ -325,7 +356,8 @@ def resolve_term(self, term, mapping, root_replacer):
q = self._create_sa_term(dotted_path, value)
elif term.comparison_operator == OPERATOR.eb:
q = self._create_eb_term(dotted_path, value)

elif term.comparison_operator == OPERATOR.contains:
q = self._create_contains_term(dotted_path, value)
else:
q = self._create_term(dotted_path, value, multiple=multiple)
resolved = q, term.unary_operator
Expand Down Expand Up @@ -474,15 +506,27 @@ def resolve_string_term(self, term, map_info, root_replacer=None):
fulltext_analyzers = ("standard",)
value = term.get_real_value()
if map_info.get("analyzer", "standard") in fulltext_analyzers:
# xxx: should handle exact match

if term.match_type == TermMatchType.EXACT:
qr = {"match_phrase": {path_: value}}
elif term.comparison_operator == OPERATOR.sa:
qr = {"match_phrase_prefix": {path_: value}}
elif term.comparison_operator == OPERATOR.eb:
qr = {"match": {path_: {"query": value, "operator": "AND"}}}
qr = {
"query_string": {
"fields": [path_],
"query": "*{0}".format(escape_star(value)),
}
}
elif term.comparison_operator == OPERATOR.contains:
qr = {
"query_string": {
"fields": [path_],
"query": "*{0}*".format(escape_star(value)),
}
}
else:
qr = {"match": {path_: value}}
qr = {"match": {path_: {"query": value, "fuzziness": "AUTO"}}}
elif ("/" in value or URI_SCHEME.match(value)) and ".reference" in path_:
qr = {"match_phrase": {path_: value}}
else:
Expand Down
2 changes: 2 additions & 0 deletions src/fhirpath/fql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .expressions import T_
from .expressions import V_
from .expressions import and_
from .expressions import contains_
from .expressions import eb_
from .expressions import exists_
from .expressions import in_
Expand Down Expand Up @@ -32,5 +33,6 @@
"sort_",
"sa_",
"eb_",
"contains_",
"ElementPath"
]
7 changes: 7 additions & 0 deletions src/fhirpath/fql/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@ def eb_(path, value=EMPTY_VALUE):
return term_or_group


def contains_(path, value=EMPTY_VALUE):
""" """
term_or_group = _prepare_term_or_group(path, value)
term_or_group.comparison_operator = OPERATOR.contains
return term_or_group


def sort_(path, order=EMPTY_VALUE):
""" """
sort_term = SortTerm(path)
Expand Down
8 changes: 7 additions & 1 deletion src/fhirpath/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from fhirpath.fql import exists_
from fhirpath.fql import not_
from fhirpath.fql import not_exists_
from fhirpath.fql import contains_
from fhirpath.fql import sa_
from fhirpath.fql import sort_
from fhirpath.fql.types import ElementPath
Expand Down Expand Up @@ -863,6 +864,8 @@ def validate_pre_term(self, operator_, path_, value, modifier):
raise ValidationError(
"You cannot use modifier (above,below) and prefix (sa,eb) at a time"
)
if modifier == "contains" and operator_ != "eq":
raise NotImplementedError("In case of :contains modifier, only eq prefix is supported")

def create_term(self, path_, value, modifier):
""" """
Expand Down Expand Up @@ -894,7 +897,10 @@ def create_term(self, path_, value, modifier):
val = V_(original_value)

if operator_ == "eq":
term = term == val
if modifier == "contains":
term = contains_(term, val)
else:
term = term == val
elif operator_ == "ne":
term = term != val
elif operator_ == "lt":
Expand Down
11 changes: 11 additions & 0 deletions tests/test_fql.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from fhirpath.fql.expressions import eb_
from fhirpath.fql.expressions import exists_
from fhirpath.fql.expressions import in_
from fhirpath.fql.expressions import contains_
from fhirpath.fql.expressions import not_exists_
from fhirpath.fql.expressions import not_in_
from fhirpath.fql.expressions import or_
Expand Down Expand Up @@ -201,6 +202,16 @@ def test_eb_expression(engine):
assert term.comparison_operator == OPERATOR.eb


def test_contains_expression(engine):
""" """
term = T_("Organization.id")
value = V_("01")
term = contains_(term, value)
term.finalize(engine)

assert term.comparison_operator == OPERATOR.contains


def test_query_builder(engine):
""" """
builder = (
Expand Down
21 changes: 21 additions & 0 deletions tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,3 +511,24 @@ def test_search_result_with_above_modifier(es_data, engine):
fhir_search = Search(search_context, params=params)
bundle = fhir_search()
assert bundle.total == 1


def test_search_result_with_contains_modifier(es_data, engine):
""" """
# little bit complex
search_context = SearchContext(engine, "Patient")
params = (("identifier:contains", "|365"),)
fhir_search = Search(search_context, params=params)
bundle = fhir_search()
assert bundle.total == 1

params = (("given:contains", "ect"),)
fhir_search = Search(search_context, params=params)
bundle = fhir_search()
assert bundle.total == 1

search_context = SearchContext(engine, "Organization")
params = (("name:contains", "Medical"),)
fhir_search = Search(search_context, params=params)
bundle = fhir_search()
assert bundle.total == 1

0 comments on commit f308958

Please sign in to comment.