Change hsearch to consider unquoted parameter strings as prefix match (#4045)
* Change hsearch to consider unquoted parameter strings as prefix match * Adjust test according to spec (code fixed before). * Add :text handling to StringParam * Adjust StringParam tokenization to spec. Enable StringParam tests. Add changelog. * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_2_0/4034-align-text-query-syntax-with-hapi-string-search.yaml Co-authored-by: michaelabuckley <michael.buckley@smilecdr.com> * Avoid changing simple query parameters. Implement suggested tests. * Consider a few Lucene Simple Query Syntax special cases * Add search syntax changes to documentation and test to make sure samples are good. Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com> Co-authored-by: michaelabuckley <michael.buckley@smilecdr.com>
This commit is contained in:
parent
392bfb790f
commit
27ecba796b
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
type: add
|
||||||
|
issue: 4034
|
||||||
|
title: "Added support for the `:text` modifier on string search parameters. This corrects an issue when using Elastic/Lucene indexing enabled where prefix match was used instead."
|
|
@ -1,4 +1,4 @@
|
||||||
# HAPI FHIR JPA Lucene/Elasticsearch Indexing
|
**# HAPI FHIR JPA Lucene/Elasticsearch Indexing
|
||||||
|
|
||||||
The HAPI JPA Server supports optional indexing via Hibernate Search when configured to use Lucene or Elasticsearch.
|
The HAPI JPA Server supports optional indexing via Hibernate Search when configured to use Lucene or Elasticsearch.
|
||||||
This is required to support the `_content`, or `_text` search parameters.
|
This is required to support the `_content`, or `_text` search parameters.
|
||||||
|
@ -15,17 +15,36 @@ The Extended Lucene string search indexing supports the default search, as well
|
||||||
- Default searching matches by prefix, insensitive to case or accents
|
- Default searching matches by prefix, insensitive to case or accents
|
||||||
- `:exact` matches the entire string, matching case and accents
|
- `:exact` matches the entire string, matching case and accents
|
||||||
- `:contains` extends the default search to match any substring of the text
|
- `:contains` extends the default search to match any substring of the text
|
||||||
- `:text` provides a rich search syntax as using the Simple Query Syntax as defined by
|
- `:text` provides a rich search syntax as using a [modified Simple Query Syntax](#modified-simple-query-syntax).
|
||||||
[Lucene](https://lucene.apache.org/core/8_10_1/queryparser/org/apache/lucene/queryparser/simple/SimpleQueryParser.html) and
|
|
||||||
[Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html#simple-query-string-syntax).
|
|
||||||
|
|
||||||
## Token search
|
## Token search
|
||||||
|
|
||||||
The Extended Lucene Indexing supports the default token search by code, system, or system+code,
|
The Extended Lucene Indexing supports the default token search by code, system, or system+code,
|
||||||
as well as with the `:text` modifier.
|
as well as with the `:text` modifier.
|
||||||
The `:text` modifier provides the same Simple Query Syntax used by string `:text` searches.
|
The `:text` modifier provides the same modified Simple Query Syntax used by string `:text` searches.
|
||||||
See https://www.hl7.org/fhir/search.html#token.
|
See https://www.hl7.org/fhir/search.html#token.
|
||||||
|
|
||||||
|
## Modified Simple Query Syntax
|
||||||
|
|
||||||
|
The `:text` search for token and string, Hapi provides a modified version of the Simple Query Syntax provided by
|
||||||
|
[Lucene](https://lucene.apache.org/core/8_10_1/queryparser/org/apache/lucene/queryparser/simple/SimpleQueryParser.html) and
|
||||||
|
[Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html#simple-query-string-syntax).
|
||||||
|
Terms are delimited by whitespace, or query punctuation `"()|+'`. Literal uses of these characters must be escaped by '\'.
|
||||||
|
When the query only contains one or more bare terms, they are each converted to a prefix search to match the behaviour of a normal string search.
|
||||||
|
When multiple terms are present, they must all match (i.e. `AND`). For `OR` behaviour use the `|` operator between terms.
|
||||||
|
But if any special SQS syntax is active, the query is used as is.
|
||||||
|
To ensure that the query is used as-is, quote bare terms with the `"` or character. E.g. `without any special syntax characters
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
| Fhir Query String | Executed Query | Matches | No Match | Note |
|
||||||
|
|-----------------|------------------|-------------|----------------|--------------------------------------------|
|
||||||
|
| Smit | Smit* | John Smith | John Smi | |
|
||||||
|
| Jo Smit | Jo* Smit* | John Smith | John Frank | Multiple bare terms are `AND` |
|
||||||
|
| frank | john | frank | john | Frank Smith | Franklin Smith | SQS characters disable prefix wildcard |
|
||||||
|
| 'frank' | 'frank' | Frank Smith | Franklin Smith | Quoted terms are exact match |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Quantity search
|
## Quantity search
|
||||||
|
|
||||||
|
|
|
@ -221,7 +221,7 @@ public class ExtendedHSearchClauseBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (List<? extends IQueryParameterType> nextAnd : stringAndOrTerms) {
|
for (List<? extends IQueryParameterType> nextAnd : stringAndOrTerms) {
|
||||||
Set<String> terms = extractOrStringParams(nextAnd);
|
Set<String> terms = TermHelper.makePrefixSearchTerm(extractOrStringParams(nextAnd));
|
||||||
ourLog.debug("addStringTextSearch {}, {}", theSearchParamName, terms);
|
ourLog.debug("addStringTextSearch {}, {}", theSearchParamName, terms);
|
||||||
if (!terms.isEmpty()) {
|
if (!terms.isEmpty()) {
|
||||||
String query = terms.stream()
|
String query = terms.stream()
|
||||||
|
|
|
@ -113,7 +113,7 @@ public class ExtendedHSearchSearchBuilder {
|
||||||
} else if (param instanceof StringParam) {
|
} else if (param instanceof StringParam) {
|
||||||
switch (modifier) {
|
switch (modifier) {
|
||||||
// we support string:text, string:contains, string:exact, and unmodified string.
|
// we support string:text, string:contains, string:exact, and unmodified string.
|
||||||
case Constants.PARAMQUALIFIER_TOKEN_TEXT:
|
case Constants.PARAMQUALIFIER_STRING_TEXT:
|
||||||
case Constants.PARAMQUALIFIER_STRING_EXACT:
|
case Constants.PARAMQUALIFIER_STRING_EXACT:
|
||||||
case Constants.PARAMQUALIFIER_STRING_CONTAINS:
|
case Constants.PARAMQUALIFIER_STRING_CONTAINS:
|
||||||
case EMPTY_MODIFIER:
|
case EMPTY_MODIFIER:
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package ca.uhn.fhir.jpa.dao.search;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class TermHelper {
|
||||||
|
|
||||||
|
/** characters which indicate the string parameter is a simple query string */
|
||||||
|
private static final char[] simpleQuerySyntaxCharacters = new char[] { '+', '|', '"', '(', ')', '~' };
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each input set element is:
|
||||||
|
* _ copied to the output set unchanged if it contains a '*' character or is quoted
|
||||||
|
* _ trimmed, tokenized by spaces, and suffixed by ' *', and each resulting string copied to the output set
|
||||||
|
*/
|
||||||
|
public static Set<String> makePrefixSearchTerm(Set<String> theStringSet) {
|
||||||
|
return theStringSet.stream()
|
||||||
|
.map(s -> isToLeftUntouched(s) || isQuoted(s) ? s : suffixTokensWithStar(s) )
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static String suffixTokensWithStar(String theStr) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
Arrays.stream(theStr.trim().split(" "))
|
||||||
|
.forEach(s -> sb.append(s).append("* "));
|
||||||
|
|
||||||
|
return sb.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static boolean isQuoted(String theS) {
|
||||||
|
return ( theS.startsWith("\"") && theS.endsWith("\"") ) ||
|
||||||
|
( theS.startsWith("'") && theS.endsWith("'") );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the input string is recognized as Lucene Simple Query Syntax
|
||||||
|
* @see "https://lucene.apache.org/core/8_11_2/queryparser/org/apache/lucene/queryparser/simple/SimpleQueryParser.html"
|
||||||
|
*/
|
||||||
|
static boolean isToLeftUntouched(String theString) {
|
||||||
|
// remove backslashed * and - characters from string before checking, as those shouldn't be considered
|
||||||
|
if (theString.startsWith("-")) { return true; } // it is SimpleQuerySyntax
|
||||||
|
|
||||||
|
if (theString.endsWith("*")) { return true; } // it is SimpleQuerySyntax
|
||||||
|
|
||||||
|
return StringUtils.containsAny(theString, simpleQuerySyntaxCharacters);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package ca.uhn.fhir.jpa.dao.search;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class TermHelperTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void empty_returns_empty() {
|
||||||
|
assertEquals( Collections.emptySet(), TermHelper.makePrefixSearchTerm(Collections.emptySet()) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noQuotedSpcedOrStarElements_return_star_suffixed() {
|
||||||
|
Set<String> result = TermHelper.makePrefixSearchTerm(Set.of("abc", "def", "ghi"));
|
||||||
|
assertEquals( Set.of("abc*", "def*", "ghi*"), result );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void quotedElements_return_unchanged() {
|
||||||
|
Set<String> result = TermHelper.makePrefixSearchTerm(Set.of("'abc'", "\"def ghi\"", "\"jkl\""));
|
||||||
|
assertEquals( Set.of("'abc'", "\"def ghi\"", "\"jkl\""), result );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unquotedStarContainingElements_spaces_or_not_return_unchanged() {
|
||||||
|
Set<String> result = TermHelper.makePrefixSearchTerm(Set.of("abc*", "*cde", "ef*g", "hij* klm"));
|
||||||
|
assertEquals( TermHelper.makePrefixSearchTerm(Set.of("abc*", "*cde", "ef*g", "hij* klm")), result );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unquotedSpaceContainingElements_return_splitted_in_spaces_and_star_suffixed() {
|
||||||
|
Set<String> result = TermHelper.makePrefixSearchTerm(Set.of("abc", "cde", "hij klm"));
|
||||||
|
assertEquals( TermHelper.makePrefixSearchTerm(Set.of("abc*", "cde*", "hij* klm*")), result );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void multiSimpleTerm_hasSimpleTermsWildcarded() {
|
||||||
|
Set<String> result = TermHelper.makePrefixSearchTerm(Set.of("abc def"));
|
||||||
|
assertEquals( Set.of("abc* def*"), result );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void simpleQuerySyntax_mustBeLeftUnchanged() {
|
||||||
|
Set<String> result = TermHelper.makePrefixSearchTerm(Set.of("(def | efg)", "(def efg)", "ghi +(\"abc\" \"def\")"));
|
||||||
|
assertEquals( Set.of("(def | efg)", "(def efg)", "ghi +(\"abc\" \"def\")"), result );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isToLeftUntouchedRemovesbackslashedStarAndHypenBeforeChecking() {
|
||||||
|
assertTrue(TermHelper.isToLeftUntouched("-ab\\*cd\\-ef"), "When first char is a hyphen");
|
||||||
|
assertTrue(TermHelper.isToLeftUntouched("abcdef*"), "When last char is a star");
|
||||||
|
assertFalse(TermHelper.isToLeftUntouched("\\-ab\\*cd\\-ef"), "When all stars and hyphens are backslashed");
|
||||||
|
assertFalse(TermHelper.isToLeftUntouched("\\-ab*cd-ef"), "When all stars and hyphens are backslashed or internal");
|
||||||
|
assertFalse(TermHelper.isToLeftUntouched("\\-ab\\*c*d\\-ef"), "When all stars and hyphens are backslashed minus an internal star");
|
||||||
|
assertFalse(TermHelper.isToLeftUntouched("\\-ab\\*cd\\-e-f"), "When all stars and hyphens are backslashed minus an internal hyphen");
|
||||||
|
assertTrue(TermHelper.isToLeftUntouched("\\-ab\\*c+d\\-ef"), "When all stars and hyphens are backslashed but there is a plus");
|
||||||
|
assertFalse(TermHelper.isToLeftUntouched("\\ab cd\\fg"), "When only backslashes");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -34,14 +34,12 @@ import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
|
import ca.uhn.fhir.rest.api.SearchTotalModeEnum;
|
||||||
import ca.uhn.fhir.rest.api.SortSpec;
|
import ca.uhn.fhir.rest.api.SortSpec;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
|
||||||
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
|
||||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||||
import ca.uhn.fhir.rest.param.StringOrListParam;
|
import ca.uhn.fhir.rest.param.StringOrListParam;
|
||||||
import ca.uhn.fhir.rest.param.StringParam;
|
import ca.uhn.fhir.rest.param.StringParam;
|
||||||
import ca.uhn.fhir.rest.param.TokenParam;
|
import ca.uhn.fhir.rest.param.TokenParam;
|
||||||
import ca.uhn.fhir.rest.param.TokenParamModifier;
|
import ca.uhn.fhir.rest.param.TokenParamModifier;
|
||||||
import ca.uhn.fhir.rest.server.IPagingProvider;
|
|
||||||
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||||
import ca.uhn.fhir.storage.test.BaseDateSearchDaoTests;
|
import ca.uhn.fhir.storage.test.BaseDateSearchDaoTests;
|
||||||
|
@ -75,7 +73,6 @@ import org.hl7.fhir.r4.model.RiskAssessment;
|
||||||
import org.hl7.fhir.r4.model.StringType;
|
import org.hl7.fhir.r4.model.StringType;
|
||||||
import org.hl7.fhir.r4.model.ValueSet;
|
import org.hl7.fhir.r4.model.ValueSet;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Assertions;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
|
@ -122,6 +119,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.empty;
|
import static org.hamcrest.Matchers.empty;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.hasItem;
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.hamcrest.Matchers.hasItems;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.hamcrest.Matchers.not;
|
||||||
import static org.hamcrest.Matchers.notNullValue;
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
@ -130,9 +128,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.spy;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
@ExtendWith(SpringExtension.class)
|
@ExtendWith(SpringExtension.class)
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@ -151,9 +146,12 @@ import static org.mockito.Mockito.when;
|
||||||
,FhirResourceDaoR4SearchWithElasticSearchIT.TestDirtiesContextTestExecutionListener.class
|
,FhirResourceDaoR4SearchWithElasticSearchIT.TestDirtiesContextTestExecutionListener.class
|
||||||
})
|
})
|
||||||
public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest implements ITestDataBuilder {
|
public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest implements ITestDataBuilder {
|
||||||
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4SearchWithElasticSearchIT.class);
|
||||||
|
|
||||||
public static final String URL_MY_CODE_SYSTEM = "http://example.com/my_code_system";
|
public static final String URL_MY_CODE_SYSTEM = "http://example.com/my_code_system";
|
||||||
public static final String URL_MY_VALUE_SET = "http://example.com/my_value_set";
|
public static final String URL_MY_VALUE_SET = "http://example.com/my_value_set";
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR4SearchWithElasticSearchIT.class);
|
private static final String SPACE = "%20";
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
protected DaoConfig myDaoConfig;
|
protected DaoConfig myDaoConfig;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
@ -416,6 +414,185 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
public class StringTextSearch {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void secondWordFound() {
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "Cloudy, yellow") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?value-string:text=yellow");
|
||||||
|
assertThat(resourceIds, hasItem(id1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stringMatchesPrefixAndWhole() {
|
||||||
|
// smit - matches "smit" and "smith"
|
||||||
|
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "John Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "Carl Smit") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?value-string:text=smit");
|
||||||
|
assertThat(resourceIds, hasItems(id1, id2));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stringPlusStarMatchesPrefixAndWhole() {
|
||||||
|
// smit* - matches "smit" and "smith"
|
||||||
|
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "John Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "Carl Smit") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?_elements=valueString&value-string:text=smit*");
|
||||||
|
assertThat(resourceIds, hasItems(id1, id2));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void quotedStringMatchesIdenticalButNotAsPrefix() {
|
||||||
|
// "smit"- matches "smit", but not "smith"
|
||||||
|
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "John Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "Carl Smit") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?value-string:text=\"smit\"");
|
||||||
|
assertThat(resourceIds, contains(id2));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stringTokensAreAnded() {
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "John Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "Carl Smit") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?value-string:text=car%20smit");
|
||||||
|
assertThat(resourceIds, hasItems(id2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
public class DocumentationSamplesTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void line1() {
|
||||||
|
// | Fhir Query String | Executed Query | Matches | No Match
|
||||||
|
// | Smit | Smit* | John Smith | John Smi
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "John Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "John Smi") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?value-string:text=Smit");
|
||||||
|
assertThat(resourceIds, hasItems(id1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void line2() {
|
||||||
|
// | Fhir Query String | Executed Query | Matches | No Match | Note
|
||||||
|
// | Jo Smit | Jo* Smit* | John Smith | John Frank | Multiple bare terms are `AND`
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "John Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "John Frank") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?value-string:text=Jo%20Smit");
|
||||||
|
assertThat(resourceIds, hasItems(id1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void line3() {
|
||||||
|
// | Fhir Query String | Executed Query | Matches | No Match | Note
|
||||||
|
// | frank | john | frank | john | Frank Smith | Franklin Smith | SQS characters disable prefix wildcard
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "Frank Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "Franklin Smith") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?value-string:text=frank|john");
|
||||||
|
assertThat(resourceIds, hasItems(id1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void line4() {
|
||||||
|
// | Fhir Query String | Executed Query | Matches | No Match | Note
|
||||||
|
// | 'frank' | 'frank' | Frank Smith | Franklin Smith | Quoted terms are exact match
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "Frank Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withPrimitiveAttribute("valueString", "Franklin Smith") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?value-string:text='frank'");
|
||||||
|
assertThat(resourceIds, hasItems(id1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
public class TokenTextSearch {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void secondWordFound() {
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withObservationCode("http://example.com", "code-AA", "Cloudy, yellow") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?code:text=yellow");
|
||||||
|
assertThat(resourceIds, hasItem(id1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stringMatchesPrefixAndWhole() {
|
||||||
|
// smit - matches "smit" and "smith"
|
||||||
|
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withObservationCode("http://example.com", "code-AA", "John Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withObservationCode("http://example.com", "code-BB", "Carl Smit") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?code:text=smit");
|
||||||
|
assertThat(resourceIds, hasItems(id1, id2));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stringPlusStarMatchesPrefixAndWhole() {
|
||||||
|
// smit* - matches "smit" and "smith"
|
||||||
|
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withObservationCode("http://example.com", "code-AA", "Adam Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withObservationCode("http://example.com", "code-BB", "John Smit") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?code:text=smit*");
|
||||||
|
assertThat(resourceIds, hasItems(id1, id2));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void quotedStringMatchesIdenticalButNotAsPrefix() {
|
||||||
|
// "smit"- matches "smit", but not "smith"
|
||||||
|
|
||||||
|
String id1 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withObservationCode("http://example.com", "code-AA", "John Smith") )).getIdPart();
|
||||||
|
String id2 = myTestDataBuilder.createObservation(List.of(
|
||||||
|
myTestDataBuilder.withObservationCode("http://example.com", "code-BB", "Karl Smit") )).getIdPart();
|
||||||
|
|
||||||
|
List<String> resourceIds = myTestDaoSearch.searchForIds("/Observation?code:text=\"Smit\"");
|
||||||
|
assertThat(resourceIds, contains(id2));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testResourceCodeTextSearch() {
|
public void testResourceCodeTextSearch() {
|
||||||
IIdType id1, id2, id3, id4;
|
IIdType id1, id2, id3, id4;
|
||||||
|
@ -492,7 +669,14 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
|
||||||
// prefix
|
// prefix
|
||||||
SearchParameterMap map = new SearchParameterMap();
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
map.add("code", new TokenParam("Bod").setModifier(TokenParamModifier.TEXT));
|
map.add("code", new TokenParam("Bod").setModifier(TokenParamModifier.TEXT));
|
||||||
assertThat("Bare prefix does not match", toUnqualifiedVersionlessIdValues(myObservationDao.search(map)), Matchers.empty());
|
assertObservationSearchMatches("Bare prefix matches", map, id2);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// prefix
|
||||||
|
SearchParameterMap map = new SearchParameterMap();
|
||||||
|
map.add("code", new TokenParam("Bod").setModifier(TokenParamModifier.TEXT));
|
||||||
|
assertObservationSearchMatches("Bare prefix matches any word, not only first", map, id2);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -529,6 +713,7 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testResourceReferenceSearchForCanonicalReferences() {
|
public void testResourceReferenceSearchForCanonicalReferences() {
|
||||||
String questionnaireCanonicalUrl = "https://test.fhir.org/R4/Questionnaire/xl-5000-q";
|
String questionnaireCanonicalUrl = "https://test.fhir.org/R4/Questionnaire/xl-5000-q";
|
||||||
|
|
Loading…
Reference in New Issue