From f6b3755a1aff4289bf9a4e5fd75573f8b7c7883c Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Thu, 25 Jun 2026 15:22:37 +1000 Subject: [PATCH 1/5] change duringImpl logic to adapt overlapped date range --- .../core/parser/elastic/DuringImpl.java | 19 ++++++-- .../CQLToElasticFilterFactoryTest.java | 48 +++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) 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/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(); + } } From 5b83b133e58cdf75193b755814bbe2e9276b1d9e Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Thu, 25 Jun 2026 16:47:46 +1000 Subject: [PATCH 2/5] fix test --- .../java/au/org/aodn/ogcapi/server/common/RestApiTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 5557b349..0dd587fc 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 @@ -239,7 +239,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(), @@ -254,7 +255,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 ); From 6404d73327b7dc157f5e88b116141def708eeb83 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Thu, 25 Jun 2026 17:00:30 +1000 Subject: [PATCH 3/5] fix test --- .../java/au/org/aodn/ogcapi/server/common/RestApiTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0dd587fc..14026713 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 @@ -267,8 +267,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( From b6b3966b35df1abbb3a9ebe53e9a6912f22c85af Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 26 Jun 2026 14:21:25 +1000 Subject: [PATCH 4/5] fix test --- .../au/org/aodn/ogcapi/server/common/RestApiTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 14026713..e47e19b5 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 @@ -267,7 +267,7 @@ public void verifyDateTimeBoundsWithDiscreteTime() throws IOException { collections.getBody().getCollections().get(0).getId(), "Correct UUID - caf7220a-19e0-4a7f-9af6-eade6c79a47a"); - // The query start time is the first end , the queried time range should overlap the next start/end slot, so should return same result + // 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"); @@ -277,13 +277,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 From 31e05faefb172ae170ed4ca1bd3b7545dca4e785 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 26 Jun 2026 15:11:15 +1000 Subject: [PATCH 5/5] use matchphrase query for credit field --- .../aodn/ogcapi/server/core/model/enumeration/CQLFields.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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),