From f7ca9df0bddf34ff78c998cf9d0c33ffde20c0f5 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Tue, 13 Oct 2026 12:09:01 +1100 Subject: [PATCH 1/5] add preference in Elasticsearch defaultSetting --- .../core/model/enumeration/CQLElasticSetting.java | 4 +++- .../parser/elastic/CQLToElasticFilterFactory.java | 1 + .../server/core/service/ElasticSearchBase.java | 13 ++++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CQLElasticSetting.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CQLElasticSetting.java index cfcaebd8..23b291fe 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CQLElasticSetting.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CQLElasticSetting.java @@ -6,7 +6,9 @@ public enum CQLElasticSetting { score("_score"), page_size("page_size"), - search_after("search_after"); + search_after("search_after"), + // Elasticsearch search preference, the same value routes identical searches to the same shard copies so scoring stays consistent + preference("preference"); private final String setting; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/elastic/CQLToElasticFilterFactory.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/elastic/CQLToElasticFilterFactory.java index 98c93635..315d77a9 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/elastic/CQLToElasticFilterFactory.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/parser/elastic/CQLToElasticFilterFactory.java @@ -96,6 +96,7 @@ public static Map getDefaultSetting() { defaultSetting.put(CQLElasticSetting.score, null); defaultSetting.put(CQLElasticSetting.page_size, null); defaultSetting.put(CQLElasticSetting.search_after, null); + defaultSetting.put(CQLElasticSetting.preference, null); return defaultSetting; } /** 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 b695f030..51e3dba1 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 @@ -180,6 +180,7 @@ protected SearchRequest buildSearchAsYouTypeRequest( * @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 + * @param preference - Elasticsearch search preference, a fixed value routes identical searches to the same shard copies so the score is consistent, null to leave routing to Elasticsearch * @return - The constructed builderSupplier */ protected Supplier buildCollectionSearchRequestSupplier(final List queries, @@ -189,9 +190,13 @@ protected Supplier buildCollectionSearchRequestSupplier(f final List searchAfter, final List sortOptions, final Double score, - final Long maxSize) { + final Long maxSize, + final String preference) { return () -> { SearchRequest.Builder builder = new SearchRequest.Builder(); + if (preference != null && !preference.isBlank()) { + builder.preference(preference); + } builder.index(indexName) // If user query request a page that is smaller then the internal default, then // we use the smaller one. The internal page size is used to get the result by @@ -344,6 +349,7 @@ else if (should != null && !should.isEmpty()) { * @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 + * @param preference - Elasticsearch search preference, see buildCollectionSearchRequestSupplier * @return - The search result from Elastic query and format in StacCollectionModel */ protected SearchResult searchCollectionBy(final List queries, @@ -353,9 +359,10 @@ protected SearchResult searchCollectionBy(final List final List searchAfter, final List sortOptions, final Double score, - final Long maxSize) { + final Long maxSize, + final String preference) { Supplier builderSupplier = buildCollectionSearchRequestSupplier( - queries, should, filters, properties, searchAfter, sortOptions, score, maxSize + queries, should, filters, properties, searchAfter, sortOptions, score, maxSize, preference ); List collections = new ArrayList<>(); From e4de1c4443203ecf3a4d6738e77f34de8bf916c7 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Tue, 13 Oct 2026 12:09:05 +1100 Subject: [PATCH 2/5] add preference in Elasticsearch defaultSetting --- .../org/aodn/ogcapi/server/core/service/CacheNoLandGeometry.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheNoLandGeometry.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheNoLandGeometry.java index ecb9e127..2c57f7af 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheNoLandGeometry.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheNoLandGeometry.java @@ -38,6 +38,7 @@ public Map getAllNoLandGeometry() { null, null, null, + null, null); return result.collections From e373add37c79e5c82ea2fa6220e0ca01e37e47ab Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Tue, 13 Oct 2026 12:09:10 +1100 Subject: [PATCH 3/5] add preference in Elasticsearch defaultSetting --- .../aodn/ogcapi/server/core/service/ElasticSearch.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 e7d42d77..4ad60a9c 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 @@ -394,6 +394,7 @@ protected ElasticSearchBase.SearchResult searchCollectionsB null, createSortOptions(sortBy, CQLFields.class), null, + null, null); } @@ -494,6 +495,7 @@ protected Supplier buildParameterSearchRequestSupplier( null, createSortOptions(sortBy, CQLFields.class), null, + null, null); } @@ -569,7 +571,8 @@ protected Supplier buildParameterSearchRequestSupplier( searchAfter, sortOptions, score, - maxSize); + maxSize, + setting.get(CQLElasticSetting.preference)); } @Override @@ -739,7 +742,8 @@ public ElasticSearchBase.SearchResult searchByParameters(Li searchAfter, sortOptions, score, - maxSize + maxSize, + setting.get(CQLElasticSetting.preference) ); } } From 9ee04ac348bf8cd750c77e78d9a482cdd6e97a12 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Tue, 13 Oct 2026 12:09:33 +1100 Subject: [PATCH 4/5] add tests --- .../CQLToElasticFilterFactoryTest.java | 11 ++++ .../server/service/ElasticSearchTest.java | 58 +++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) 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 8ccd385b..0dce6d78 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 @@ -73,6 +73,17 @@ public void prioritySortMetadataIsCollectedAlongsideQuerySettings() throws CQLEx assertTrue(factory.isPlatformPrioritySort()); } + @Test + public void preferenceIsCollectedAsQuerySetting() throws CQLException { + CQLToElasticFilterFactory factory = parse( + "preference='search-ranking-test' AND page_size=11 AND " + + "(BBOX(geometry,104,-43,163,-8) OR geometry IS NULL)"); + + assertEquals("search-ranking-test", factory.getQuerySetting().get(CQLElasticSetting.preference), + "quoted literal arrives without its quotes"); + assertEquals("11", factory.getQuerySetting().get(CQLElasticSetting.page_size)); + } + @Test public void temporalDuringUsesOverlapRangeQueryAndIncludesOngoingRecords() throws CQLException { Filter filter = CompilerUtil.parseFilter( 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 f6c51eca..d05f624b 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 @@ -294,6 +294,52 @@ public void cqlPageSizeStillCapsLightweightSearch() throws Exception { "CQL page_size remains the upper bound even for lightweight property lists"); } + @Test + public void cqlPreferenceIsSetOnNormalAndExplainRequests() throws Exception { + CapturingElasticSearch capturingSearch = new CapturingElasticSearch(mockClient); + List keywords = List.of("temperature"); + String cql = "preference='search-ranking-test' AND page_size=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, + false); + SearchRequest explainRequest = capturingSearch.explainRequest; + + assertEquals("search-ranking-test", normalRequest.preference(), + "the same preference routes identical searches to the same shard copies"); + assertEquals(normalRequest.toString(), explainRequest.toString(), + "explain must be routed the same way as the search it explains"); + assertTrue(normalRequest.query().scriptScore().query().bool().filter().isEmpty(), + "preference is a setting, not a filter"); + } + + @Test + public void noPreferenceUnlessCqlSetsIt() throws Exception { + CapturingElasticSearch capturingSearch = new CapturingElasticSearch(mockClient); + + capturingSearch.searchByParameters( + List.of("temperature"), + "page_size=3", + List.of("title"), + "-score,-rank", + CQLCrsType.EPSG4326); + + assertNull(capturingSearch.normalRequest.preference(), + "routing is left to Elasticsearch when the caller does not opt in"); + } + private record SearchArguments( List queries, List should, @@ -302,7 +348,8 @@ private record SearchArguments( List searchAfter, List sortOptions, Double score, - Long maxSize) { + Long maxSize, + String preference) { } private static class CapturingElasticSearch extends ElasticSearch { @@ -325,7 +372,8 @@ protected SearchResult searchCollect List searchAfter, List sortOptions, Double score, - Long maxSize) { + Long maxSize, + String preference) { this.should = should; this.arguments = new SearchArguments( queries, @@ -335,7 +383,8 @@ protected SearchResult searchCollect searchAfter, sortOptions, score, - maxSize); + maxSize, + preference); this.normalRequest = buildCollectionSearchRequestSupplier( queries, should, @@ -344,7 +393,8 @@ protected SearchResult searchCollect searchAfter, sortOptions, score, - maxSize).get().build(); + maxSize, + preference).get().build(); SearchResult result = new SearchResult<>(); result.setCollections(List.of()); return result; From aa0e7ead7d199ec45fbd02a7d87975ea3d817df1 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Tue, 13 Oct 2026 13:24:24 +1100 Subject: [PATCH 5/5] add preference for explainCollectionById --- .../org/aodn/ogcapi/server/core/service/ElasticSearchBase.java | 2 ++ 1 file changed, 2 insertions(+) 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 51e3dba1..d3a2ba41 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 @@ -493,6 +493,8 @@ protected JsonNode explainCollectionById(String id, Supplier s.fetch(false)) .build();