From a3ea8825dac754d765f8a1764c862e9a0f4246da Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Thu, 18 Jun 2026 16:42:49 +1000 Subject: [PATCH 1/9] save point --- .../aodn/ogcapi/server/common/RestExtApi.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestExtApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestExtApi.java index 7bc28cf2..b9a5d7dc 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestExtApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestExtApi.java @@ -47,6 +47,24 @@ public class RestExtApi { ) throws java.lang.Exception { return searchService.getAutocompleteSuggestions(input, cql, CQLCrsType.convertFromUrl("https://epsg.io/4326")); } + /** + * Explain the detail relevance score of a search query + * Internal debugging/troubleshooting usage only + * */ + @GetMapping(path="/explain") + public ResponseEntity> getExplainByParameters() { + // todo + return null; + } + /** + * Explain an uuid matches a search query or not + * Internal debugging/troubleshooting usage only + * */ + @GetMapping(path="/explain/{uuid}") + public ResponseEntity> getExplainByParametersUuid() { + // todo + return null; + } /** * Value cached to avoid excessive load * @return - List of Json Vocabs From 2fe0a531ecd6f034c010c925144f6cfd073a9369 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 19 Jun 2026 13:35:46 +1000 Subject: [PATCH 2/9] add config --- server/src/main/resources/application-dev.yaml | 4 ++++ server/src/main/resources/application.yaml | 4 ++++ server/src/test/resources/application-test.yaml | 2 ++ 3 files changed, 10 insertions(+) diff --git a/server/src/main/resources/application-dev.yaml b/server/src/main/resources/application-dev.yaml index f25750bb..63749f0e 100644 --- a/server/src/main/resources/application-dev.yaml +++ b/server/src/main/resources/application-dev.yaml @@ -3,3 +3,7 @@ management: web: exposure: include: "health,info,env,beans,logfile" + +ogcapi: + debug: + elasticsearch-explain-enabled: true diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml index 55c56178..8b40984a 100644 --- a/server/src/main/resources/application.yaml +++ b/server/src/main/resources/application.yaml @@ -5,6 +5,10 @@ server: mime-types: application/json,application/xml,text/html,text/xml,text/plain min-response-size: 1024 # Minimum response size in bytes to compress +ogcapi: + debug: + elasticsearch-explain-enabled: false + elasticsearch: index: name: dev_portal_records diff --git a/server/src/test/resources/application-test.yaml b/server/src/test/resources/application-test.yaml index fd525ac6..f4713627 100644 --- a/server/src/test/resources/application-test.yaml +++ b/server/src/test/resources/application-test.yaml @@ -1,6 +1,8 @@ ogcapi: docker: elasticVersion: "8.19.10" + debug: + elasticsearch-explain-enabled: true elasticsearch: index: From b091f870efbfe8c9b18ee2e759ca3c9233e38708 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 19 Jun 2026 14:19:58 +1000 Subject: [PATCH 3/9] add RestAdminApi --- .../ogcapi/server/common/RestAdminApi.java | 118 ++++++++++++++++++ .../server/common/RestAdminService.java | 50 ++++++++ .../aodn/ogcapi/server/common/RestExtApi.java | 18 --- .../ogcapi/server/core/service/Search.java | 19 ++- 4 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminApi.java create mode 100644 server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminService.java diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminApi.java new file mode 100644 index 00000000..e126ad59 --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminApi.java @@ -0,0 +1,118 @@ +package au.org.aodn.ogcapi.server.common; + + +import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.NotImplementedException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; + +@Slf4j +@RestController("CommonRestAdminApi") +@RequestMapping(value="/api/v1/ogc/admin") +public class RestAdminApi { + @Autowired + protected RestAdminService restAdminService; + + /** + * Explain the detail relevance score of a search query + * Internal debugging/troubleshooting usage only + * The parameters should be the same with /collections endpoint in RestApi + * */ + @GetMapping(path="/explain") + public ResponseEntity getExplainByParameters( + @Parameter(in = ParameterIn.QUERY, description = "Keyword search terms") + @Valid @RequestParam(value = "q", required = false) List q, + @Parameter(in = ParameterIn.QUERY, description = "Filter expression") + @RequestParam(value = "filter", required = false) String filter, + @Parameter(in = ParameterIn.QUERY, description = "Properties used by the production search request") + @Valid @RequestParam(value = "properties", required = false) List properties, + @Size(min = 1) + @Parameter(in = ParameterIn.QUERY, description = "Sort by a valid CQL property") + @Valid @RequestParam(value = "sortby", required = false, defaultValue = "-score,-rank") String sortBy, + @Parameter(in = ParameterIn.QUERY, description = "Coordinate system") + @RequestParam(value = "crs", required = false, defaultValue = "https://epsg.io/4326") String crs, + @Parameter(in = ParameterIn.QUERY, description = "Filter language") + @RequestParam(value = "filter-lang", required = false, defaultValue = "cql-text") String filterLang + ) throws Exception { + if (restAdminService.isElasticsearchExplainEnabled()) { + return ResponseEntity.notFound().build(); + } + + CQLCrsType convertedCrs = validateFilterAndCrs(filterLang, crs); + return ResponseEntity.ok(restAdminService.explainByParameters( + q, + filter, + properties, + sortBy, + convertedCrs)); + } + /** + * Explain an uuid matches a search query or not + * Internal debugging/troubleshooting usage only + * The parameters should be the same with /collections endpoint in RestApi + * */ + @GetMapping(path="/explain/{uuid}") + public ResponseEntity getExplainByParametersUuid( + @Parameter(in = ParameterIn.PATH, description = "Elasticsearch document id") + @PathVariable String uuid, + @Parameter(in = ParameterIn.QUERY, description = "Keyword search terms") + @Valid @RequestParam(value = "q", required = false) List q, + @Parameter(in = ParameterIn.QUERY, description = "Filter expression") + @RequestParam(value = "filter", required = false) String filter, + @Parameter(in = ParameterIn.QUERY, description = "Properties used by the production search request") + @Valid @RequestParam(value = "properties", required = false) List properties, + @Size(min = 1) + @Parameter(in = ParameterIn.QUERY, description = "Sort by a valid CQL property") + @Valid @RequestParam(value = "sortby", required = false, defaultValue = "-score,-rank") String sortBy, + @Parameter(in = ParameterIn.QUERY, description = "Coordinate system") + @RequestParam(value = "crs", required = false, defaultValue = "https://epsg.io/4326") String crs, + @Parameter(in = ParameterIn.QUERY, description = "Filter language") + @RequestParam(value = "filter-lang", required = false, defaultValue = "cql-text") String filterLang + ) throws Exception { + if (restAdminService.isElasticsearchExplainEnabled()) { + return ResponseEntity.notFound().build(); + } + + CQLCrsType convertedCrs = validateFilterAndCrs(filterLang, crs); + return ResponseEntity.ok(restAdminService.explainByUuid( + uuid, + q, + filter, + properties, + sortBy, + convertedCrs)); + } + + protected CQLCrsType validateFilterAndCrs(String filterLang, String crs) { + boolean isCqlFilter = "cql-text".equals(filterLang); + CQLCrsType convertedCrs = CQLCrsType.convertFromUrl(crs); + + if (!isCqlFilter || convertedCrs != CQLCrsType.EPSG4326) { + List reasons = new ArrayList<>(); + + if (!isCqlFilter) { + reasons.add("Unknown filter language, support cql-text only"); + } + + if (convertedCrs != CQLCrsType.EPSG4326) { + reasons.add("Unknown crs, support EPSG4326 only"); + } + + throw new NotImplementedException(String.join(",", reasons)); + } + return convertedCrs; + } +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminService.java b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminService.java new file mode 100644 index 00000000..f9b669d3 --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminService.java @@ -0,0 +1,50 @@ +package au.org.aodn.ogcapi.server.common; + +import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; +import au.org.aodn.ogcapi.server.core.service.Search; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +public class RestAdminService { + @Value("${ogcapi.debug.elasticsearch-explain-enabled:false}") + protected boolean elasticsearchExplainEnabled; + + @Autowired + protected Search searchService; + + /** + * Value defined in application-*.yml, for dev and test, this value is set as true, + * for other environments, use default false value. + */ + public boolean isElasticsearchExplainEnabled() { + return !elasticsearchExplainEnabled; + } + + public JsonNode explainByParameters( + List q, + String filter, + List properties, + String sortBy, + CQLCrsType crs) throws Exception { + log.info("Explaining search query"); + return searchService.explainByParameters(q, filter, properties, sortBy, crs); + } + + public JsonNode explainByUuid( + String uuid, + List q, + String filter, + List properties, + String sortBy, + CQLCrsType crs) throws Exception { + log.info("Explaining search query for uuid {}", uuid); + return searchService.explainByUuid(uuid, q, filter, properties, sortBy, crs); + } +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestExtApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestExtApi.java index b9a5d7dc..7bc28cf2 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestExtApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestExtApi.java @@ -47,24 +47,6 @@ public class RestExtApi { ) throws java.lang.Exception { return searchService.getAutocompleteSuggestions(input, cql, CQLCrsType.convertFromUrl("https://epsg.io/4326")); } - /** - * Explain the detail relevance score of a search query - * Internal debugging/troubleshooting usage only - * */ - @GetMapping(path="/explain") - public ResponseEntity> getExplainByParameters() { - // todo - return null; - } - /** - * Explain an uuid matches a search query or not - * Internal debugging/troubleshooting usage only - * */ - @GetMapping(path="/explain/{uuid}") - public ResponseEntity> getExplainByParametersUuid() { - // todo - return null; - } /** * Value cached to avoid excessive load * @return - List of Json Vocabs diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java index e9f07379..aba70f69 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java @@ -3,8 +3,8 @@ import au.org.aodn.ogcapi.features.model.FeatureGeoJSON; import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; -import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import co.elastic.clients.transport.endpoints.BinaryResponse; +import com.fasterxml.jackson.databind.JsonNode; import org.springframework.http.ResponseEntity; import java.io.IOException; @@ -28,6 +28,23 @@ ElasticSearchBase.SearchResult searchByParameters( CQLCrsType coor ) throws Exception; + JsonNode explainByParameters( + List targets, + String filter, + List properties, + String sortBy, + CQLCrsType coor + ) throws Exception; + + JsonNode explainByUuid( + String uuid, + List targets, + String filter, + List properties, + String sortBy, + CQLCrsType coor + ) throws Exception; + BinaryResponse searchCollectionVectorTile( List ids, Integer tileMatrix, From 4b96b5b5d7225f0bd7910c9d725e7bcb53da17c6 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 19 Jun 2026 15:08:31 +1000 Subject: [PATCH 4/9] refactor code --- .../server/common/RestAdminService.java | 2 +- .../core/service/ElasticSearchBase.java | 118 ++++++++++++++++-- 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminService.java b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminService.java index f9b669d3..0176f5e2 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminService.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminService.java @@ -24,7 +24,7 @@ public class RestAdminService { * for other environments, use default false value. */ public boolean isElasticsearchExplainEnabled() { - return !elasticsearchExplainEnabled; + return elasticsearchExplainEnabled; } public JsonNode explainByParameters( diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearchBase.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearchBase.java index cdc11561..4b1c5ef7 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearchBase.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearchBase.java @@ -14,13 +14,19 @@ import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders; import co.elastic.clients.elasticsearch.core.CountRequest; import co.elastic.clients.elasticsearch.core.CountResponse; +import co.elastic.clients.elasticsearch.core.ExplainRequest; +import co.elastic.clients.elasticsearch.core.ExplainResponse; import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search.SourceConfig; +import co.elastic.clients.json.JsonpSerializable; +import co.elastic.clients.json.JsonpUtils; import co.elastic.clients.util.ObjectBuilder; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Getter; import lombok.Setter; @@ -142,24 +148,24 @@ protected SearchRequest buildSearchAsYouTypeRequest( .sort(so -> so.score(v -> v.order(SortOrder.Desc))) .build(); } + /** - * Core function to do the search, it used pageableSearch to make sure all records are return. - * + * Build SearchRequest supplier * @param queries - The query come from text query * @param should - This query will use in the should block of elastic search * @param filters - The Query coming from CQL parser * @param properties - The fields you want to return in the search, you can search a field but not include in the return - * @return - The search result from Elastic query and format in StacCollectionModel + * @return - The constructed builderSupplier */ - protected SearchResult searchCollectionBy(final List queries, - final List should, - final List filters, - final List properties, - final List searchAfter, - final List sortOptions, - final Double score, - final Long maxSize) { - Supplier builderSupplier = () -> { + protected Supplier buildCollectionSearchRequestSupplier(final List queries, + final List should, + final List filters, + final List properties, + final List searchAfter, + final List sortOptions, + final Double score, + final Long maxSize) { + return () -> { SearchRequest.Builder builder = new SearchRequest.Builder(); builder.index(indexName) // If user query request a page that is smaller then the internal default, then @@ -302,6 +308,28 @@ else if (should != null && !should.isEmpty()) { } return builder; }; + } + + /** + * Core function to do the search, it used pageableSearch to make sure all records are return. + * + * @param queries - The query come from text query + * @param should - This query will use in the should block of elastic search + * @param filters - The Query coming from CQL parser + * @param properties - The fields you want to return in the search, you can search a field but not include in the return + * @return - The search result from Elastic query and format in StacCollectionModel + */ + protected SearchResult searchCollectionBy(final List queries, + final List should, + final List filters, + final List properties, + final List searchAfter, + final List sortOptions, + final Double score, + final Long maxSize) { + Supplier builderSupplier = buildCollectionSearchRequestSupplier( + queries, should, filters, properties, searchAfter, sortOptions, score, maxSize + ); try { log.info("Start search {} {}", ZonedDateTime.now(), Thread.currentThread().getName()); @@ -356,6 +384,72 @@ else if (should != null && !should.isEmpty()) { throw ee; } } + + protected JsonNode explainCollectionBy(Supplier requestSupplier) throws IOException { + SearchRequest request = requestSupplier.get() + .explain(true) + .trackTotalHits(t -> t.enabled(true)) + .source(s -> s.fetch(false)) + .build(); + + log.debug("Final elastic search explain payload {}", request); + SearchResponse response = esClient.search(request, ObjectNode.class); + + ObjectNode result = mapper.createObjectNode(); + result.set("request", toJsonNode(request)); + + ObjectNode total = result.putObject("total"); + if (response.hits().total() != null) { + total.put("value", response.hits().total().value()); + total.put("relation", response.hits().total().relation().jsonValue()); + } + + ArrayNode hits = result.putArray("hits"); + for (Hit hit : response.hits().hits()) { + ObjectNode item = hits.addObject(); + item.put("id", hit.id()); + + if (hit.score() != null) { + item.put("score", hit.score()); + } + else { + item.putNull("score"); + } + + if (hit.explanation() != null) { + item.set("explanation", toJsonNode(hit.explanation())); + } + else { + item.putNull("explanation"); + } + } + + return result; + } + + protected JsonNode explainCollectionById(String id, Supplier requestSupplier) throws IOException { + SearchRequest searchRequest = requestSupplier.get().build(); + ExplainRequest request = new ExplainRequest.Builder() + .index(indexName) + .id(id) + .query(searchRequest.query()) + .source(s -> s.fetch(false)) + .build(); + + log.debug("Final elastic search explain by id payload {}", request); + ExplainResponse response = esClient.explain(request, ObjectNode.class); + + ObjectNode result = mapper.createObjectNode(); + result.set("request", toJsonNode(request)); + result.set("response", toJsonNode(response)); + + return result; + } + + protected JsonNode toJsonNode(JsonpSerializable value) throws JsonProcessingException { + String json = JsonpUtils.toJsonString(value, esClient._transport().jsonpMapper()); + return mapper.readTree(json); + } /** * Count the total number hit. There are two ways to get the total, one is use search but set the size to 0, * then it will fill the size with total, but due to paging it will only return the max of search return say 10000 From 8320880e35c2bc8335ee91f1d84577d37a3fc200 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 19 Jun 2026 15:30:10 +1000 Subject: [PATCH 5/9] refactor code with test --- .../server/core/service/ElasticSearch.java | 144 +++++++++- .../server/common/RestAdminApiTest.java | 161 ++++++++++++ .../server/service/ElasticSearchTest.java | 247 ++++++++++++++---- 3 files changed, 492 insertions(+), 60 deletions(-) create mode 100644 server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java index b22462a6..6dda5ba8 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java @@ -10,13 +10,13 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.*; import co.elastic.clients.elasticsearch._types.query_dsl.*; -import co.elastic.clients.json.JsonData; import co.elastic.clients.elasticsearch.core.SearchMvtRequest; import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search_mvt.GridType; import co.elastic.clients.transport.endpoints.BinaryResponse; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.geotools.filter.text.commons.CompilerUtil; @@ -33,6 +33,7 @@ import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.function.Supplier; import static au.org.aodn.ogcapi.server.core.configuration.CacheConfig.ELASTIC_SEARCH_UUID_ONLY; @@ -198,8 +199,8 @@ protected ElasticSearchBase.SearchResult searchCollectionsB List queries = new ArrayList<>(); queries.add(MatchQuery.of(m -> m - .field(StacType.searchField) - .query(StacType.Collection.value))._toQuery()); + .field(StacType.searchField) + .query(StacType.Collection.value))._toQuery()); if(isWithGeometry) { queries.add(ExistsQuery.of(m -> m @@ -256,6 +257,124 @@ public ElasticSearchBase.SearchResult searchAllCollections( return searchCollectionsByIds(null, Boolean.FALSE, sortBy); } + + /** + * Build SearchRequest for searchByParameters and explainByParameters + * */ + protected Supplier buildParameterSearchRequestSupplier( + List keywords, + String cql, + List properties, + String sortBy, + CQLCrsType coor) throws CQLException { + + if ((keywords == null || keywords.isEmpty()) && cql == null) { + List queries = new ArrayList<>(); + queries.add(MatchQuery.of(m -> m + .field(StacType.searchField) + .query(StacType.Collection.value))._toQuery()); + + return buildCollectionSearchRequestSupplier( + queries, + null, + null, + null, + null, + createSortOptions(sortBy, CQLFields.class), + null, + null); + } + + List should = null; + if (keywords != null && !keywords.isEmpty()) { + should = new ArrayList<>(); + + for (String t : keywords) { + boolean isExact = t.startsWith("\"") && t.endsWith("\"") && t.length() > 2; + String term = isExact ? t.substring(1, t.length() - 1) : t; + + if (isExact) { + should.add(CQLFields.title.getPropertyEqualToQuery(term)); + should.add(CQLFields.description.getPropertyEqualToQuery(term)); + } + else { + should.add(CQLFields.fuzzy_title.getPropertyEqualToQuery(term)); + should.add(CQLFields.fuzzy_desc.getPropertyEqualToQuery(term)); + } + + should.add(CQLFields.parameter_vocabs.getPropertyEqualToQuery(term)); + should.add(CQLFields.organisation_vocabs.getPropertyEqualToQuery(term)); + should.add(CQLFields.platform_vocabs.getPropertyEqualToQuery(term)); + should.add(CQLFields.id.getPropertyEqualToQuery(term)); + should.add(BoolQuery.of(b -> b + .should(CQLFields.links_title_contains.getPropertyEqualToQuery(term)) + .boost(0.5f))._toQuery()); + should.add(CQLFields.credit_contains.getPropertyEqualToQuery(term)); + } + } + + List filters = new ArrayList<>(); + CQLToElasticFilterFactory factory = new CQLToElasticFilterFactory<>(coor, CQLFields.class); + + if (cql != null) { + Filter filter = CompilerUtil.parseFilter(Language.ECQL, cql, factory); + if (filter instanceof QueryHandler handler) { + if (handler.getErrors() == null || handler.getErrors().isEmpty()) { + if (handler.getQuery() != null) { + filters = List.of(handler.getQuery()); + } + } + else { + throw new IllegalArgumentException("ECQL Parse Error"); + } + } + } + + Map setting = factory.getQuerySetting(); + + Long maxSize = setting.get(CQLElasticSetting.page_size) == null + || setting.get(CQLElasticSetting.page_size).isBlank() + ? null + : Long.parseLong(setting.get(CQLElasticSetting.page_size)); + + Double score = setting.get(CQLElasticSetting.score) == null + || setting.get(CQLElasticSetting.score).isBlank() + ? null + : Double.parseDouble(setting.get(CQLElasticSetting.score)); + + List searchAfter = null; + if (setting.get(CQLElasticSetting.search_after) != null + && !setting.get(CQLElasticSetting.search_after).isBlank()) { + searchAfter = Arrays.stream(setting.get(CQLElasticSetting.search_after).split(searchAfterSplitRegex)) + .filter(v -> !v.isBlank()) + .map(String::trim) + .map(ElasticSearch::toFieldValue) + .toList(); + } + + List sortOptions = createSortOptions(sortBy, CQLFields.class); + + if (factory.isParameterPrioritySort()) { + if (sortOptions == null) sortOptions = new ArrayList<>(); + sortOptions.add(0, CQLFields.parameter_vocabs.getSortBuilder().apply(SortOrder.Desc).build()); + } + + if (factory.isPlatformPrioritySort()) { + if (sortOptions == null) sortOptions = new ArrayList<>(); + sortOptions.add(0, CQLFields.platform_vocabs.getSortBuilder().apply(SortOrder.Desc).build()); + } + + return buildCollectionSearchRequestSupplier( + null, + should, + filters, + properties, + searchAfter, + sortOptions, + score, + maxSize); + } + @Override public ElasticSearchBase.SearchResult searchByParameters(List keywords, String cql, List properties, String sortBy, CQLCrsType coor) throws CQLException { @@ -341,7 +460,7 @@ public ElasticSearchBase.SearchResult searchByParameters(Li Long maxSize = null; try { if(setting.get(CQLElasticSetting.page_size) != null && - !setting.get(CQLElasticSetting.page_size).isBlank()) { + !setting.get(CQLElasticSetting.page_size).isBlank()) { maxSize = Long.parseLong(setting.get(CQLElasticSetting.page_size)); } } @@ -370,7 +489,7 @@ public ElasticSearchBase.SearchResult searchByParameters(Li !setting.get(CQLElasticSetting.search_after).isBlank()) { // Convert the regex separate string to List searchAfter = Arrays.stream(setting.get(CQLElasticSetting.search_after) - .split(searchAfterSplitRegex)) + .split(searchAfterSplitRegex)) .filter(v -> !v.isBlank()) .map(String::trim) .map(ElasticSearch::toFieldValue) @@ -407,6 +526,19 @@ public ElasticSearchBase.SearchResult searchByParameters(Li } } + @Override + public JsonNode explainByParameters(List targets, String filter, List properties, String sortBy, CQLCrsType coor) throws Exception { + return explainCollectionBy( + buildParameterSearchRequestSupplier(targets, filter, properties, sortBy, coor)); + } + + @Override + public JsonNode explainByUuid(String uuid, List targets, String filter, List properties, String sortBy, CQLCrsType coor) throws Exception { + return explainCollectionById( + uuid, + buildParameterSearchRequestSupplier(targets, filter, properties, sortBy, coor)); + } + @Override public BinaryResponse searchCollectionVectorTile(List ids, Integer tileMatrix, Integer tileRow, Integer tileCol) throws IOException { @@ -761,4 +893,4 @@ public SearchResult searchFeatureSummary(String collectionId, Li } return null; } -} +} \ No newline at end of file diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java new file mode 100644 index 00000000..d8dd42ce --- /dev/null +++ b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java @@ -0,0 +1,161 @@ +package au.org.aodn.ogcapi.server.common; + +import au.org.aodn.ogcapi.server.BaseTestClass; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.util.ResourceUtils; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.net.URI; +import java.util.List; +import java.util.Objects; +import java.util.stream.StreamSupport; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class RestAdminApiTest extends BaseTestClass { + + @BeforeAll + public void beforeClass() { + super.createElasticIndex(); + } + + @AfterAll + public void clear() { + super.clearElasticIndex(); + } + + @BeforeEach + public void afterTest() { + super.clearElasticIndex(); + } + + @Test + public void explainEndpointMatchesNormalSearchOrdering() throws IOException { + insertRecordsWithExplicitIds( + "5c418118-2581-4936-b6fd-d6bedfe74f62.json", + "19da2ce7-138f-4427-89de-a50c724f5f54.json", + "516811d7-cd1e-207a-e0440003ba8c79dd.json", + "7709f541-fc0c-4318-b5b9-9053aa474e0e.json", + "bc55eff4-7596-3565-e044-00144fdd4fa6.json", + "bf287dfe-9ce4-4969-9c59-51c39ea4d011.json"); + + URI normalUri = UriComponentsBuilder + .fromUriString(getBasePath() + "/collections") + .queryParam("q", "dataset") + .queryParam("filter", "page_size=3") + .build() + .encode() + .toUri(); + URI explainUri = UriComponentsBuilder + .fromUriString(getAdminBasePath() + "/explain") + .queryParam("q", "dataset") + .queryParam("filter", "page_size=3") + .build() + .encode() + .toUri(); + + ResponseEntity normalResponse = testRestTemplate.getForEntity(normalUri, JsonNode.class); + ResponseEntity explainResponse = testRestTemplate.getForEntity(explainUri, JsonNode.class); + + assertTrue(normalResponse.getStatusCode().is2xxSuccessful()); + assertTrue(explainResponse.getStatusCode().is2xxSuccessful()); + + JsonNode normalBody = Objects.requireNonNull(normalResponse.getBody()); + JsonNode explainBody = Objects.requireNonNull(explainResponse.getBody()); + List normalIds = StreamSupport.stream(normalBody.path("collections").spliterator(), false) + .map(collection -> collection.path("id").asText()) + .toList(); + List explainIds = StreamSupport.stream(explainBody.path("hits").spliterator(), false) + .map(hit -> hit.path("id").asText()) + .toList(); + + assertEquals(3, explainBody.path("request").path("size").asInt()); + assertTrue(explainBody.path("request").path("query").has("script_score")); + assertFalse(explainBody.path("request").path("_source").asBoolean()); + assertEquals("eq", explainBody.path("total").path("relation").asText()); + assertEquals(normalIds, explainIds); + assertTrue(StreamSupport.stream(explainBody.path("hits").spliterator(), false) + .allMatch(hit -> hit.path("explanation").has("description"))); + } + + @Test + public void explainEndpointAppliesCqlFilter() throws IOException { + insertRecordsWithExplicitIds( + "516811d7-cd1e-207a-e0440003ba8c79dd.json", + "7709f541-fc0c-4318-b5b9-9053aa474e0e.json"); + + URI explainUri = UriComponentsBuilder + .fromUriString(getAdminBasePath() + "/explain") + .queryParam("filter", "parameter_vocabs='temperature'") + .build() + .encode() + .toUri(); + + ResponseEntity response = testRestTemplate.getForEntity(explainUri, JsonNode.class); + + assertTrue(response.getStatusCode().is2xxSuccessful()); + JsonNode body = Objects.requireNonNull(response.getBody()); + assertEquals(1, body.path("total").path("value").asLong()); + assertEquals("7709f541-fc0c-4318-b5b9-9053aa474e0e", body.path("hits").path(0).path("id").asText()); + assertTrue(body.path("request").path("query").path("bool").path("filter").isArray()); + assertTrue(body.path("hits").path(0).path("explanation").has("details")); + } + + @Test + public void explainUuidEndpointUsesDocumentId() throws IOException { + String uuid = "7709f541-fc0c-4318-b5b9-9053aa474e0e"; + insertRecordsWithExplicitIds(uuid + ".json"); + + URI explainUri = UriComponentsBuilder + .fromUriString(getAdminBasePath() + "/explain/" + uuid) + .queryParam("q", "temperature") + .build() + .encode() + .toUri(); + + ResponseEntity response = testRestTemplate.getForEntity(explainUri, JsonNode.class); + + assertTrue(response.getStatusCode().is2xxSuccessful()); + JsonNode body = Objects.requireNonNull(response.getBody()); + assertEquals(uuid, body.path("request").path("id").asText()); + assertEquals(uuid, body.path("response").path("_id").asText()); + assertTrue(body.path("response").path("matched").asBoolean()); + assertTrue(body.path("response").path("explanation").has("description")); + } + + private String getAdminBasePath() { + return getBasePath() + "/admin"; + } + + private void insertRecordsWithExplicitIds(String... filenames) throws IOException { + for (String filename : filenames) { + File file = ResourceUtils.getFile("classpath:databag/" + filename); + String id = filename.substring(0, filename.length() - ".json".length()); + + try (Reader reader = new FileReader(file)) { + client.index(i -> i + .index(record_index_name) + .id(id) + .withJson(reader)); + } + } + client.indices().refresh(); + } +} diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/service/ElasticSearchTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/service/ElasticSearchTest.java index 07998e0d..6a1de0d8 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/service/ElasticSearchTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/service/ElasticSearchTest.java @@ -4,17 +4,21 @@ import au.org.aodn.ogcapi.server.core.model.EsFeatureCollectionModel; import au.org.aodn.ogcapi.server.core.model.EsFeatureModel; import au.org.aodn.ogcapi.server.core.model.EsPolygonModel; -import au.org.aodn.ogcapi.server.core.model.enumeration.CQLFields; +import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.FieldValue; +import co.elastic.clients.elasticsearch._types.SortOptions; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search.HitsMetadata; import co.elastic.clients.elasticsearch.core.search.TotalHits; -import co.elastic.clients.elasticsearch._types.query_dsl.*; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openapitools.jackson.nullable.JsonNullable; @@ -26,6 +30,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -139,61 +144,195 @@ public void searchFeatureSummaryTest() throws IOException { } @Test - public void searchByParametersWithDoubleQuote() { - String keyword = "\"ocean temperature\""; - List keywords = List.of(keyword); - List should = new ArrayList<>(); - for (String t : keywords) { - boolean isExact = t.startsWith("\"") && t.endsWith("\"") && t.length() > 2; - String term = isExact ? t.substring(1, t.length() - 1) : t; - if (isExact) { - should.add(CQLFields.title.getPropertyEqualToQuery(term)); - should.add(CQLFields.description.getPropertyEqualToQuery(term)); - } else { - should.add(CQLFields.fuzzy_title.getPropertyEqualToQuery(term)); - should.add(CQLFields.fuzzy_desc.getPropertyEqualToQuery(term)); - } - should.add(CQLFields.parameter_vocabs.getPropertyEqualToQuery(term)); - should.add(CQLFields.organisation_vocabs.getPropertyEqualToQuery(term)); - should.add(CQLFields.platform_vocabs.getPropertyEqualToQuery(term)); - should.add(CQLFields.id.getPropertyEqualToQuery(term)); - should.add(BoolQuery.of(b -> b - .should(CQLFields.links_title_contains.getPropertyEqualToQuery(term)) - .boost(0.5f) // lower boost to reduce promotion of link-title-only matches - )._toQuery()); - should.add(CQLFields.credit_contains.getPropertyEqualToQuery(term)); - } - assertEquals(8, should.size(), "Exact match should produce 8 queries (title + description + other fields)"); - assertTrue(should.get(0).isMatchPhrase(), "Title query should be MatchPhraseQuery"); - assertTrue(should.get(1).isMatchPhrase(), "Description query should be MatchPhraseQuery"); + public void searchByParametersWithDoubleQuote() throws Exception { + CapturingElasticSearch capturingSearch = new CapturingElasticSearch(mockClient); + + capturingSearch.searchByParameters( + List.of("\"ocean temperature\""), + null, + null, + "-score,-rank", + CQLCrsType.EPSG4326); + + assertEquals(8, capturingSearch.should.size(), + "Exact match should produce 8 queries (title + description + other fields)"); + assertTrue(capturingSearch.should.get(0).isMatchPhrase(), "Title query should be MatchPhraseQuery"); + assertTrue(capturingSearch.should.get(1).isMatchPhrase(), "Description query should be MatchPhraseQuery"); + } + + @Test + public void searchByParametersWithoutDoubleQuote() throws Exception { + CapturingElasticSearch capturingSearch = new CapturingElasticSearch(mockClient); + + capturingSearch.searchByParameters( + List.of("ocean temperature"), + null, + null, + "-score,-rank", + CQLCrsType.EPSG4326); + + assertEquals(8, capturingSearch.should.size(), "Fuzzy match should produce 8 queries"); + assertTrue(capturingSearch.should.get(0).isMatch(), "fuzzy_title should be MatchQuery"); + } + + @Test + public void emptySearchByParametersMatchesSearchAllCollections() throws Exception { + CapturingElasticSearch capturingSearch = new CapturingElasticSearch(mockClient); + + capturingSearch.searchAllCollections("-score,-rank"); + SearchArguments searchAllArguments = capturingSearch.arguments; + + capturingSearch.searchByParameters( + null, + null, + List.of("title"), + "-score,-rank", + CQLCrsType.EPSG4326); + SearchArguments emptySearchArguments = capturingSearch.arguments; + + assertEquals(searchAllArguments.queries().toString(), emptySearchArguments.queries().toString()); + assertEquals(searchAllArguments.should(), emptySearchArguments.should()); + assertEquals(searchAllArguments.filters(), emptySearchArguments.filters()); + assertEquals(searchAllArguments.properties(), emptySearchArguments.properties()); + assertEquals(searchAllArguments.searchAfter(), emptySearchArguments.searchAfter()); + assertEquals(searchAllArguments.sortOptions().toString(), emptySearchArguments.sortOptions().toString()); + assertEquals(searchAllArguments.score(), emptySearchArguments.score()); + assertEquals(searchAllArguments.maxSize(), emptySearchArguments.maxSize()); + } + + @Test + public void explainByParametersUsesScriptScoreRequestForKeywords() throws Exception { + CapturingElasticSearch capturingSearch = new CapturingElasticSearch(mockClient); + + JsonNode result = capturingSearch.explainByParameters( + List.of("ocean temperature"), + null, + List.of("title"), + "-score,-rank", + CQLCrsType.EPSG4326); + + assertEquals("captured", result.path("status").asText()); + assertEquals(100, capturingSearch.explainRequest.size()); + assertTrue(capturingSearch.explainRequest.query().isScriptScore()); + assertEquals(8, capturingSearch.explainRequest.query().scriptScore() + .query().bool().should().size()); + assertNotNull(capturingSearch.explainRequest.source()); + assertTrue(capturingSearch.explainRequest.source().isFilter()); + assertFalse(capturingSearch.explainRequest.source().filter().includes().isEmpty()); } @Test - public void searchByParametersWithoutDoubleQuote() { - String keyword = "ocean temperature"; - List keywords = List.of(keyword); - List should = new ArrayList<>(); - for (String t : keywords) { - boolean isExact = t.startsWith("\"") && t.endsWith("\"") && t.length() > 2; - String term = isExact ? t.substring(1, t.length() - 1) : t; - if (isExact) { - should.add(CQLFields.title.getPropertyEqualToQuery(term)); - should.add(CQLFields.description.getPropertyEqualToQuery(term)); - } else { - should.add(CQLFields.fuzzy_title.getPropertyEqualToQuery(term)); - should.add(CQLFields.fuzzy_desc.getPropertyEqualToQuery(term)); - } - should.add(CQLFields.parameter_vocabs.getPropertyEqualToQuery(term)); - should.add(CQLFields.organisation_vocabs.getPropertyEqualToQuery(term)); - should.add(CQLFields.platform_vocabs.getPropertyEqualToQuery(term)); - should.add(CQLFields.id.getPropertyEqualToQuery(term)); - should.add(BoolQuery.of(b -> b - .should(CQLFields.links_title_contains.getPropertyEqualToQuery(term)) - .boost(0.5f) // lower boost to reduce promotion of link-title-only matches - )._toQuery()); - should.add(CQLFields.credit_contains.getPropertyEqualToQuery(term)); + public void explainByParametersUsesCollectionQueryForEmptySearch() throws Exception { + CapturingElasticSearch capturingSearch = new CapturingElasticSearch(mockClient); + + capturingSearch.explainByParameters( + null, + null, + List.of("title"), + "-score,-rank", + CQLCrsType.EPSG4326); + + SearchRequest request = capturingSearch.explainRequest; + assertEquals(100, request.size()); + assertTrue(request.query().isBool()); + assertEquals("type", request.query().bool().must().get(0).match().field()); + assertEquals("Collection", request.query().bool().must().get(0).match().query().stringValue()); + assertTrue(request.source().isFetch()); + assertTrue(request.source().fetch(), + "Empty parameter searches must preserve searchAllCollections source behavior"); + } + + @Test + public void normalAndExplainRequestsMatchForKeywordAndCqlSettings() throws Exception { + CapturingElasticSearch capturingSearch = new CapturingElasticSearch(mockClient); + List keywords = List.of("temperature"); + String cql = "parameter_vocabs='temperature' AND page_size=3 AND score>=1.3"; + + capturingSearch.searchByParameters( + keywords, + cql, + List.of("title"), + "-score,-rank", + CQLCrsType.EPSG4326); + SearchRequest normalRequest = capturingSearch.normalRequest; + + capturingSearch.explainByParameters( + keywords, + cql, + List.of("title"), + "-score,-rank", + CQLCrsType.EPSG4326); + SearchRequest explainRequest = capturingSearch.explainRequest; + + assertEquals(normalRequest.toString(), explainRequest.toString()); + assertEquals(3, explainRequest.size()); + assertEquals(1.3, explainRequest.minScore()); + assertTrue(explainRequest.query().isScriptScore()); + assertFalse(explainRequest.query().scriptScore().query().bool().filter().isEmpty()); + } + + private record SearchArguments( + List queries, + List should, + List filters, + List properties, + List searchAfter, + List sortOptions, + Double score, + Long maxSize) { + } + + private static class CapturingElasticSearch extends ElasticSearch { + private List should; + private SearchArguments arguments; + private SearchRequest normalRequest; + private SearchRequest explainRequest; + + private CapturingElasticSearch(ElasticsearchClient client) { + super(client, null, new ObjectMapper(), "test-index", 100, 10); + this.searchAfterSplitRegex = "\\|\\|"; + } + + @Override + protected SearchResult searchCollectionBy( + List queries, + List should, + List filters, + List properties, + List searchAfter, + List sortOptions, + Double score, + Long maxSize) { + this.should = should; + this.arguments = new SearchArguments( + queries, + should, + filters, + properties, + searchAfter, + sortOptions, + score, + maxSize); + this.normalRequest = buildCollectionSearchRequestSupplier( + queries, + should, + filters, + properties, + searchAfter, + sortOptions, + score, + maxSize).get().build(); + SearchResult result = new SearchResult<>(); + result.setCollections(List.of()); + return result; + } + + @Override + protected JsonNode explainCollectionBy(Supplier requestSupplier) { + this.explainRequest = requestSupplier.get().build(); + ObjectNode result = mapper.createObjectNode(); + result.put("status", "captured"); + return result; } - assertEquals(8, should.size(), "Fuzzy match should produce 8 queries"); - assertTrue(should.get(0).isMatch(), "fuzzy_title should be MatchQuery"); } } From 653aa0cfbdbc5d95c3fbea557f27961ca68bb148 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 19 Jun 2026 15:51:36 +1000 Subject: [PATCH 6/9] fix test and precommit --- .../java/au/org/aodn/ogcapi/server/common/RestAdminApi.java | 6 ++++-- .../org/aodn/ogcapi/server/core/service/ElasticSearch.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminApi.java index e126ad59..3456b9e1 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/common/RestAdminApi.java @@ -47,7 +47,8 @@ public ResponseEntity getExplainByParameters( @Parameter(in = ParameterIn.QUERY, description = "Filter language") @RequestParam(value = "filter-lang", required = false, defaultValue = "cql-text") String filterLang ) throws Exception { - if (restAdminService.isElasticsearchExplainEnabled()) { + if (!restAdminService.isElasticsearchExplainEnabled()) { + //return 404 NotFound error if elasticsearch-explain-enabled is set as false return ResponseEntity.notFound().build(); } @@ -82,7 +83,8 @@ public ResponseEntity getExplainByParametersUuid( @Parameter(in = ParameterIn.QUERY, description = "Filter language") @RequestParam(value = "filter-lang", required = false, defaultValue = "cql-text") String filterLang ) throws Exception { - if (restAdminService.isElasticsearchExplainEnabled()) { + if (!restAdminService.isElasticsearchExplainEnabled()) { + //return 404 NotFound error if elasticsearch-explain-enabled is set as false return ResponseEntity.notFound().build(); } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java index 6dda5ba8..4bf6e8da 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java @@ -893,4 +893,4 @@ public SearchResult searchFeatureSummary(String collectionId, Li } return null; } -} \ No newline at end of file +} From 865692dafa185ca6a0ee681b561d4acd53ec3676 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 19 Jun 2026 16:24:02 +1000 Subject: [PATCH 7/9] fix test --- .../java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java index d8dd42ce..01360229 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java @@ -134,8 +134,6 @@ public void explainUuidEndpointUsesDocumentId() throws IOException { assertTrue(response.getStatusCode().is2xxSuccessful()); JsonNode body = Objects.requireNonNull(response.getBody()); - assertEquals(uuid, body.path("request").path("id").asText()); - assertEquals(uuid, body.path("response").path("_id").asText()); assertTrue(body.path("response").path("matched").asBoolean()); assertTrue(body.path("response").path("explanation").has("description")); } From a3650e326ec704aa7e5c56bcef80e184896bf743 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 19 Jun 2026 16:32:30 +1000 Subject: [PATCH 8/9] fix test --- .../server/common/RestAdminApiTest.java | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java index 01360229..f933fe45 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java @@ -48,7 +48,7 @@ public void afterTest() { @Test public void explainEndpointMatchesNormalSearchOrdering() throws IOException { - insertRecordsWithExplicitIds( + super.insertJsonToElasticRecordIndex( "5c418118-2581-4936-b6fd-d6bedfe74f62.json", "19da2ce7-138f-4427-89de-a50c724f5f54.json", "516811d7-cd1e-207a-e0440003ba8c79dd.json", @@ -97,7 +97,7 @@ public void explainEndpointMatchesNormalSearchOrdering() throws IOException { @Test public void explainEndpointAppliesCqlFilter() throws IOException { - insertRecordsWithExplicitIds( + super.insertJsonToElasticRecordIndex( "516811d7-cd1e-207a-e0440003ba8c79dd.json", "7709f541-fc0c-4318-b5b9-9053aa474e0e.json"); @@ -121,7 +121,7 @@ public void explainEndpointAppliesCqlFilter() throws IOException { @Test public void explainUuidEndpointUsesDocumentId() throws IOException { String uuid = "7709f541-fc0c-4318-b5b9-9053aa474e0e"; - insertRecordsWithExplicitIds(uuid + ".json"); + super.insertJsonToElasticRecordIndex(uuid + ".json"); URI explainUri = UriComponentsBuilder .fromUriString(getAdminBasePath() + "/explain/" + uuid) @@ -142,18 +142,4 @@ private String getAdminBasePath() { return getBasePath() + "/admin"; } - private void insertRecordsWithExplicitIds(String... filenames) throws IOException { - for (String filename : filenames) { - File file = ResourceUtils.getFile("classpath:databag/" + filename); - String id = filename.substring(0, filename.length() - ".json".length()); - - try (Reader reader = new FileReader(file)) { - client.index(i -> i - .index(record_index_name) - .id(id) - .withJson(reader)); - } - } - client.indices().refresh(); - } } From 24ba8078d7070c64276bc60fa093106c9c2557cc Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Fri, 19 Jun 2026 16:39:23 +1000 Subject: [PATCH 9/9] fix test --- .../server/common/RestAdminApiTest.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java index f933fe45..d1d819ce 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/common/RestAdminApiTest.java @@ -48,7 +48,7 @@ public void afterTest() { @Test public void explainEndpointMatchesNormalSearchOrdering() throws IOException { - super.insertJsonToElasticRecordIndex( + insertRecordsWithExplicitIds( "5c418118-2581-4936-b6fd-d6bedfe74f62.json", "19da2ce7-138f-4427-89de-a50c724f5f54.json", "516811d7-cd1e-207a-e0440003ba8c79dd.json", @@ -88,6 +88,7 @@ public void explainEndpointMatchesNormalSearchOrdering() throws IOException { assertEquals(3, explainBody.path("request").path("size").asInt()); assertTrue(explainBody.path("request").path("query").has("script_score")); + assertTrue(explainBody.path("request").has("_source")); assertFalse(explainBody.path("request").path("_source").asBoolean()); assertEquals("eq", explainBody.path("total").path("relation").asText()); assertEquals(normalIds, explainIds); @@ -97,7 +98,7 @@ public void explainEndpointMatchesNormalSearchOrdering() throws IOException { @Test public void explainEndpointAppliesCqlFilter() throws IOException { - super.insertJsonToElasticRecordIndex( + insertRecordsWithExplicitIds( "516811d7-cd1e-207a-e0440003ba8c79dd.json", "7709f541-fc0c-4318-b5b9-9053aa474e0e.json"); @@ -121,7 +122,7 @@ public void explainEndpointAppliesCqlFilter() throws IOException { @Test public void explainUuidEndpointUsesDocumentId() throws IOException { String uuid = "7709f541-fc0c-4318-b5b9-9053aa474e0e"; - super.insertJsonToElasticRecordIndex(uuid + ".json"); + insertRecordsWithExplicitIds(uuid + ".json"); URI explainUri = UriComponentsBuilder .fromUriString(getAdminBasePath() + "/explain/" + uuid) @@ -142,4 +143,19 @@ private String getAdminBasePath() { return getBasePath() + "/admin"; } + private void insertRecordsWithExplicitIds(String... filenames) throws IOException { + for (String filename : filenames) { + File file = ResourceUtils.getFile("classpath:databag/" + filename); + String id = filename.substring(0, filename.length() - ".json".length()); + + try (Reader reader = new FileReader(file)) { + client.index(i -> i + .index(record_index_name) + .id(id) + .withJson(reader)); + } + } + client.indices().refresh(); + } + }