Freetext search lastupdated param support (#3630)

* Initial implementation

* Implement job cancellation

* Handle uri, _tag, _security and _profile freetext search parameters

* Fix combined 'and' and 'or' clauses and add tests
Use collection instead of typed array to eliminate warnings

* Eliminate predicate nesting level

* Implement _source freetext search

* Implement _lastUpdated freetext search

* add ITestDataBuilder.withLastUpdated(String theIsoDate) method

Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
This commit is contained in:
jmarchionatto 2022-05-26 14:41:12 -04:00 committed by GitHub
parent 464a07e284
commit ad3294f8e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 155 additions and 7 deletions

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.search.ExtendedLuceneIndexData;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
@ -107,6 +108,12 @@ public class ExtendedLuceneIndexExtractor {
retVal.addUriIndexData("_source", source);
}
if (theResource.getMeta().getLastUpdated() != null) {
int ordinal = ResourceIndexedSearchParamDate.calculateOrdinalValue(theResource.getMeta().getLastUpdated()).intValue();
retVal.addDateIndexData("_lastUpdated", theResource.getMeta().getLastUpdated(), ordinal,
theResource.getMeta().getLastUpdated(), ordinal);
}
if (!theNewParams.myLinks.isEmpty()) {

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.dao.search;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.QuantityParam;
@ -56,6 +57,7 @@ public class ExtendedLuceneSearchBuilder {
*/
public boolean isSupportsSomeOf(SearchParameterMap myParams) {
return
myParams.getLastUpdated() != null ||
myParams.entrySet().stream()
.filter(e -> !ourUnsafeSearchParmeters.contains(e.getKey()))
// each and clause may have a different modifier, so split down to the ORs
@ -113,10 +115,8 @@ public class ExtendedLuceneSearchBuilder {
}
return false;
} else if (param instanceof UriParam) {
if (EMPTY_MODIFIER.equals(modifier)) {
return true;
}
return false;
return modifier.equals(EMPTY_MODIFIER);
} else {
return false;
}
@ -124,7 +124,7 @@ public class ExtendedLuceneSearchBuilder {
public void addAndConsumeAdvancedQueryClauses(ExtendedLuceneClauseBuilder builder, String theResourceType, SearchParameterMap theParams, ISearchParamRegistry theSearchParamRegistry) {
// copy the keys to avoid concurrent modification error
ArrayList<String> paramNames = Lists.newArrayList(theParams.keySet());
ArrayList<String> paramNames = compileParamNames(theParams);
for (String nextParam : paramNames) {
if (ourUnsafeSearchParmeters.contains(nextParam)) {
continue;
@ -170,7 +170,8 @@ public class ExtendedLuceneSearchBuilder {
break;
case DATE:
List<List<IQueryParameterType>> dateAndOrTerms = theParams.removeByNameUnmodified(nextParam);
List<List<IQueryParameterType>> dateAndOrTerms = nextParam.equalsIgnoreCase("_lastupdated")
? getLastUpdatedAndOrList(theParams) : theParams.removeByNameUnmodified(nextParam);
builder.addDateUnmodifiedSearch(nextParam, dateAndOrTerms);
break;
@ -183,4 +184,36 @@ public class ExtendedLuceneSearchBuilder {
}
}
}
private List<List<IQueryParameterType>> getLastUpdatedAndOrList(SearchParameterMap theParams) {
DateParam activeBound = theParams.getLastUpdated().getLowerBound() != null
? theParams.getLastUpdated().getLowerBound()
: theParams.getLastUpdated().getUpperBound();
TemporalPrecisionEnum precision = activeBound.getPrecision();
List<List<IQueryParameterType>> result = List.of( List.of(activeBound) );
// indicate parameter was processed
theParams.setLastUpdated(null);
return result;
}
/**
* Param name list is not only the params.keySet, but also the "special" parameters extracted from input
* (as _lastUpdated when the input myLastUpdated field is not null, etc).
*/
private ArrayList<String> compileParamNames(SearchParameterMap theParams) {
ArrayList<String> nameList = Lists.newArrayList(theParams.keySet());
if (theParams.getLastUpdated() != null) {
nameList.add("_lastUpdated");
}
return nameList;
}
}

View File

@ -91,6 +91,7 @@ import javax.persistence.EntityManager;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -1675,6 +1676,97 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest {
@Nested
public class LastUpdatedTests {
private String myOldObsId, myNewObsId;
private String myOldLastUpdatedDateTime = "2017-03-24T03:21:47";
@BeforeEach
public void enableResourceStorage() {
myDaoConfig.setStoreResourceInLuceneIndex(true);
myOldObsId = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCodeOld"),
myTestDataBuilder.withLastUpdated(myOldLastUpdatedDateTime) )).getIdPart();
myNewObsId = myTestDataBuilder.createObservation(List.of(
myTestDataBuilder.withObservationCode("http://example.com/", "theCodeNew"),
myTestDataBuilder.withLastUpdated(new Date()) )).getIdPart();
}
@AfterEach
public void resetResourceStorage() {
myDaoConfig.setStoreResourceInLuceneIndex(new DaoConfig().isStoreResourceInLuceneIndex());
}
@Test
public void eq() {
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=eq" + myOldLastUpdatedDateTime);
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(myOldObsId));
}
@Test
public void eqLessPrecisionRequest() {
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=eq2017-03-24");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(myOldObsId));
}
@Test
public void ne() {
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=ne" + myOldLastUpdatedDateTime);
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(myNewObsId));
}
@Test
void gt() {
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=gt2018-01-01");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(myNewObsId));
}
@Test
public void ge() {
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=ge" + myOldLastUpdatedDateTime);
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(myOldObsId, myNewObsId));
}
@Test
void lt() {
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=lt2018-01-01");
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(myOldObsId));
}
@Test
public void le() {
myCaptureQueriesListener.clear();
List<String> allIds = myTestDaoSearch.searchForIds("/Observation?_lastUpdated=le" + myOldLastUpdatedDateTime);
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size(), "we build the bundle with no sql");
assertThat(allIds, contains(myOldObsId));
}
}
@Nested
public class TotalParameter {

View File

@ -32,6 +32,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.r4</artifactId>
<version>${fhir_core_version}</version>
</dependency>
<!-- General -->
<dependency>
<groupId>org.eclipse.jetty</groupId>

View File

@ -33,9 +33,11 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.ICompositeType;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.InstantType;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Date;
import java.util.function.Consumer;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -159,6 +161,14 @@ public interface ITestDataBuilder {
return t -> MetaUtil.setSource(theContext, t.getMeta(), theSource);
}
default Consumer<IBaseResource> withLastUpdated(Date theLastUpdated) {
return t -> t.getMeta().setLastUpdated(theLastUpdated);
}
default Consumer<IBaseResource> withLastUpdated(String theIsoDate) {
return t -> t.getMeta().setLastUpdated(new InstantType(theIsoDate).getValue());
}
default IIdType createObservation(Consumer<IBaseResource>... theModifiers) {
return createResource("Observation", theModifiers);
}