diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CQLFields.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CQLFields.java index 0977febe..bbe37f7f 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CQLFields.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CQLFields.java @@ -187,7 +187,7 @@ public enum CQLFields implements CQLFieldsInterface { credit_contains( StacSummeries.Credits.searchField, StacSummeries.Credits.displayField, - (literal) -> MatchQuery.of(m -> m// We want the words exact so need to add space in front and end + (literal) -> MatchPhraseQuery.of(m -> m// We want the words exact so need to add space in front and end .field(StacSummeries.Credits.searchField) .query(literal))._toQuery(), null), diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/elastic/DuringImpl.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/elastic/DuringImpl.java index 017f785d..f1cb2b16 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/elastic/DuringImpl.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/elastic/DuringImpl.java @@ -4,6 +4,7 @@ import au.org.aodn.ogcapi.server.core.model.enumeration.CQLFieldsInterface; import au.org.aodn.ogcapi.server.core.model.enumeration.StacSummeries; import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.ExistsQuery; import co.elastic.clients.elasticsearch._types.query_dsl.NestedQuery; import co.elastic.clients.elasticsearch._types.query_dsl.Query; import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery; @@ -67,15 +68,23 @@ public DuringImpl(Expression expression1, Expression expression2, Class enumT if(type instanceof CQLFields cqlFields && cqlFields == CQLFields.temporal) { - Query gte = RangeQuery.of(r -> r + Query endAfterOrAtFilterStart = RangeQuery.of(r -> r .date(d -> d - .field(StacSummeries.TemporalStart.searchField) + .field(StacSummeries.TemporalEnd.searchField) .gte(dateFormatter.format(period.getBeginning().getPosition().getDate())) .format("strict_date_optional_time")))._toQuery(); - Query lte = RangeQuery.of(r -> r + Query missingEndDate = BoolQuery.of(b -> b + .mustNot(ExistsQuery.of(e -> e + .field(StacSummeries.TemporalEnd.searchField))._toQuery()))._toQuery(); + + Query endAfterFilterStartOrOngoing = BoolQuery.of(b -> b + .should(endAfterOrAtFilterStart, missingEndDate) + .minimumShouldMatch("1"))._toQuery(); + + Query startBeforeOrAtFilterEnd = RangeQuery.of(r -> r .date(d -> d - .field(StacSummeries.TemporalEnd.searchField) + .field(StacSummeries.TemporalStart.searchField) .lte(dateFormatter.format(period.getEnding().getPosition().getDate())) .format("strict_date_optional_time")))._toQuery(); @@ -83,7 +92,7 @@ public DuringImpl(Expression expression1, Expression expression2, Class enumT this.query = NestedQuery.of(n -> n .path(StacSummeries.Temporal.searchField) .query(BoolQuery.of(q -> q - .must(gte, lte))._toQuery() + .must(endAfterFilterStartOrOngoing, startBeforeOrAtFilterEnd))._toQuery() ) )._toQuery(); } diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/common/RestApiTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestApiTest.java index 7e623e07..a6573258 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/common/RestApiTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestApiTest.java @@ -218,7 +218,8 @@ public void verifyDateTimeBetweenBounds() throws IOException { // The time is slight off by 1 sec so only 1 document returned collections = testRestTemplate.getForEntity(getBasePath() + "/collections?datetime=1870-07-16T14:10:45Z/2013-06-16T14:00:00Z", Collections.class); // There are only 3 docs - assertEquals(1, Objects.requireNonNull(collections.getBody()).getCollections().size(), "hit 1"); + // matched records: 7709f541-fc0c-4318-b5b9-9053aa474e0e and 5c418118-2581-4936-b6fd-d6bedfe74f62 + assertEquals(2, Objects.requireNonNull(collections.getBody()).getCollections().size(), "hit 2"); assertEquals( "5c418118-2581-4936-b6fd-d6bedfe74f62", collections.getBody().getCollections().get(0).getId(), @@ -233,7 +234,7 @@ public void verifyDateTimeBetweenBounds() throws IOException { public void verifyDateTimeBoundsWithDiscreteTime() throws IOException { super.insertJsonToElasticRecordIndex( "516811d7-cd1e-207a-e0440003ba8c79dd.json", - "7709f541-fc0c-4318-b5b9-9053aa474e0e.json", + "5c418118-2581-4936-b6fd-d6bedfe74f62.json", "caf7220a-19e0-4a7f-9af6-eade6c79a47a.json" // This one have two start/end ); @@ -245,8 +246,8 @@ public void verifyDateTimeBoundsWithDiscreteTime() throws IOException { collections.getBody().getCollections().get(0).getId(), "Correct UUID - caf7220a-19e0-4a7f-9af6-eade6c79a47a"); - // The start datetime is 1 sec more then one of the start date in the record, however it still fit into the next start/end slot, so should return same result - collections = testRestTemplate.getForEntity(getBasePath() + "/collections?datetime=1991-12-31T13:00:01Z/2013-06-16T14:00:00Z", Collections.class); + // The query start time is the first end, the queried time range should overlap the next start/end slot, so should return same result + collections = testRestTemplate.getForEntity(getBasePath() + "/collections?datetime=1995-01-30T13:00:00Z/2013-06-16T14:00:00Z", Collections.class); // There are only 3 docs assertEquals(1, Objects.requireNonNull(collections.getBody()).getCollections().size(), "hit 1, this record have 2 start/end"); assertEquals( @@ -255,13 +256,13 @@ public void verifyDateTimeBoundsWithDiscreteTime() throws IOException { "Correct UUID - caf7220a-19e0-4a7f-9af6-eade6c79a47a"); // Now we check the before which should include the same record as it match one of the start/end time - collections = testRestTemplate.getForEntity(getBasePath() + "/collections?datetime=/1995-03-30T13:00:00Z", Collections.class); + collections = testRestTemplate.getForEntity(getBasePath() + "/collections?datetime=/1994-02-21T13:00:00Z", Collections.class); // There are only 3 docs - assertEquals(1, Objects.requireNonNull(collections.getBody()).getCollections().size(), "hit 1, this record have 2 start/end"); + assertEquals(1, Objects.requireNonNull(collections.getBody()).getCollections().size(), "hit 1"); assertEquals( - "caf7220a-19e0-4a7f-9af6-eade6c79a47a", + "5c418118-2581-4936-b6fd-d6bedfe74f62", collections.getBody().getCollections().get(0).getId(), - "Correct UUID - caf7220a-19e0-4a7f-9af6-eade6c79a47a"); + "Correct UUID - 5c418118-2581-4936-b6fd-d6bedfe74f62"); collections = testRestTemplate.getForEntity(getBasePath() + "/collections?datetime=/2013-06-16T14:00:00Z", Collections.class); // There are only 3 docs diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/elastic/CQLToElasticFilterFactoryTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/elastic/CQLToElasticFilterFactoryTest.java index 48eead43..448dfb4b 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/elastic/CQLToElasticFilterFactoryTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/elastic/CQLToElasticFilterFactoryTest.java @@ -3,12 +3,16 @@ import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLElasticSetting; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLFields; +import au.org.aodn.ogcapi.server.core.model.enumeration.StacSummeries; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; import org.geotools.filter.text.commons.CompilerUtil; import org.geotools.filter.text.commons.Language; import org.geotools.filter.text.cql2.CQLException; import org.junit.jupiter.api.Test; import org.opengis.filter.Filter; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -67,6 +71,41 @@ public void prioritySortMetadataIsCollectedAlongsideQuerySettings() throws CQLEx assertTrue(factory.isPlatformPrioritySort()); } + @Test + public void temporalDuringUsesOverlapRangeQueryAndIncludesOngoingRecords() throws CQLException { + Filter filter = CompilerUtil.parseFilter( + Language.ECQL, + "temporal DURING 2025-06-25T00:00:00Z/2026-06-25T00:00:00Z", + newFactory()); + + DuringImpl duringFilter = assertInstanceOf(DuringImpl.class, filter); + assertTrue(duringFilter.getQuery().isNested()); + + List must = duringFilter.getQuery().nested().query().bool().must(); + assertEquals(2, must.size()); + + Query startRange = findDateRange(must, StacSummeries.TemporalStart.searchField); + assertEquals("strict_date_optional_time", startRange.range().date().format()); + assertTrue(startRange.range().date().lte().contains("2026-06-25")); + + Query endAfterFilterStartOrOngoing = must.stream() + .filter(Query::isBool) + .findFirst() + .orElseThrow(); + assertEquals("1", endAfterFilterStartOrOngoing.bool().minimumShouldMatch()); + + List should = endAfterFilterStartOrOngoing.bool().should(); + Query endRange = findDateRange(should, StacSummeries.TemporalEnd.searchField); + assertEquals("strict_date_optional_time", endRange.range().date().format()); + assertTrue(endRange.range().date().gte().contains("2025-06-25")); + + assertTrue(should.stream() + .filter(Query::isBool) + .flatMap(query -> query.bool().mustNot().stream()) + .anyMatch(query -> query.isExists() + && StacSummeries.TemporalEnd.searchField.equals(query.exists().field()))); + } + @Test public void querySettingsCannotBeCombinedWithOr() { IllegalArgumentException settingFirst = assertThrows( @@ -94,4 +133,13 @@ private CQLToElasticFilterFactory parse(String cql) throws CQLExcepti private CQLToElasticFilterFactory newFactory() { return new CQLToElasticFilterFactory<>(CQLCrsType.EPSG4326, CQLFields.class); } + + private Query findDateRange(List queries, String field) { + return queries.stream() + .filter(Query::isRange) + .filter(query -> query.range().isDate()) + .filter(query -> field.equals(query.range().date().field())) + .findFirst() + .orElseThrow(); + } }