From 8ce42bf9115ba6c23dfad77f7352f471408b1550 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Mon, 13 Apr 2026 16:31:01 +1000 Subject: [PATCH 1/3] add term query for text with double quote --- .../server/core/service/ElasticSearch.java | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 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 bcbf35bc..ace7d978 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 @@ -267,17 +267,28 @@ public ElasticSearchBase.SearchResult searchByParameters(Li should = new ArrayList<>(); for (String t : keywords) { - should.add(CQLFields.fuzzy_title.getPropertyEqualToQuery(t)); - should.add(CQLFields.fuzzy_desc.getPropertyEqualToQuery(t)); - should.add(CQLFields.parameter_vocabs.getPropertyEqualToQuery(t)); - should.add(CQLFields.organisation_vocabs.getPropertyEqualToQuery(t)); - should.add(CQLFields.platform_vocabs.getPropertyEqualToQuery(t)); - should.add(CQLFields.id.getPropertyEqualToQuery(t)); - // A request to not using acronym in title and description in metadata, hence these - // acronym moved to links, for example NRMN record is mentioned in the link title. - // This is a work-around to the requirement but still allow use of NRMN - should.add(CQLFields.links_title_contains.getPropertyEqualToQuery(t)); - should.add(CQLFields.credit_contains.getPropertyEqualToQuery(t)); + // check if the text is in double quote - if so, use term query instead of fuzzy match + boolean isExact = t.startsWith("\"") && t.endsWith("\"") && t.length() > 2; + String term = isExact ? t.substring(1, t.length() - 1) : t; + + if (isExact) { + // use exact term match, search in title and description + should.add(CQLFields.title.getPropertyEqualToQuery(term)); // match term in original title text + should.add(CQLFields.description.getPropertyEqualToQuery(term)); // match term in original description text + } + else { + should.add(CQLFields.fuzzy_title.getPropertyEqualToQuery(t)); + should.add(CQLFields.fuzzy_desc.getPropertyEqualToQuery(t)); + should.add(CQLFields.parameter_vocabs.getPropertyEqualToQuery(t)); + should.add(CQLFields.organisation_vocabs.getPropertyEqualToQuery(t)); + should.add(CQLFields.platform_vocabs.getPropertyEqualToQuery(t)); + should.add(CQLFields.id.getPropertyEqualToQuery(t)); + // A request to not using acronym in title and description in metadata, hence these + // acronym moved to links, for example NRMN record is mentioned in the link title. + // This is a work-around to the requirement but still allow use of NRMN + should.add(CQLFields.links_title_contains.getPropertyEqualToQuery(t)); + should.add(CQLFields.credit_contains.getPropertyEqualToQuery(t)); + } } } From b0b4ca16b7241993da39aa13b5a80385bf641209 Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Mon, 13 Apr 2026 16:46:43 +1000 Subject: [PATCH 2/3] add test --- .../server/service/ElasticSearchTest.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) 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 624182f5..6eb54421 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,6 +4,7 @@ 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.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; @@ -13,6 +14,7 @@ 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.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -116,5 +118,53 @@ public void searchFeatureSummaryTest() throws IOException { featureProps.get("key")); } + @Test + public void searchByParameters_withDoubleQuote_shouldUseExactMatch() throws Exception { + // text with double quote + 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)); + } + } + + // Assert + assertEquals(2, should.size(), "Exact match should only produce 2 queries (title + description)"); + assertTrue(should.get(0).isMatchPhrase(), "Title query should be MatchPhraseQuery"); + assertTrue(should.get(1).isMatchPhrase(), "Description query should be MatchPhraseQuery"); + } + + @Test + public void searchByParameters_withoutDoubleQuote_shouldUseFuzzyMatch() throws Exception { + 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; + + if (!isExact) { + should.add(CQLFields.fuzzy_title.getPropertyEqualToQuery(t)); + should.add(CQLFields.fuzzy_desc.getPropertyEqualToQuery(t)); + should.add(CQLFields.parameter_vocabs.getPropertyEqualToQuery(t)); + should.add(CQLFields.organisation_vocabs.getPropertyEqualToQuery(t)); + should.add(CQLFields.platform_vocabs.getPropertyEqualToQuery(t)); + should.add(CQLFields.id.getPropertyEqualToQuery(t)); + should.add(CQLFields.links_title_contains.getPropertyEqualToQuery(t)); + should.add(CQLFields.credit_contains.getPropertyEqualToQuery(t)); + } + } + + assertEquals(8, should.size(), "Fuzzy match should produce 8 queries"); + assertTrue(should.get(0).isMatch(), "fuzzy_title should be MatchQuery"); + } + } From 72efbcaceeb481e79a02927089dfc20a572d908a Mon Sep 17 00:00:00 2001 From: Yuxuan HU Date: Tue, 14 Apr 2026 10:31:17 +1000 Subject: [PATCH 3/3] explain logic in comment and update query according to review comment --- .../server/core/service/ElasticSearch.java | 37 +++++++++------ .../server/service/ElasticSearchTest.java | 47 ++++++++++--------- 2 files changed, 47 insertions(+), 37 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 ace7d978..3e8af415 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 @@ -267,28 +267,35 @@ public ElasticSearchBase.SearchResult searchByParameters(Li should = new ArrayList<>(); for (String t : keywords) { - // check if the text is in double quote - if so, use term query instead of fuzzy match + // If user's input (keywords) starts and ends with quote ", and the text is not empty + // treat the user intend to search with the exact term, + // instead of searching in fuzzy fields i.e., fuzzy_title and fuzzy_desc, + // search in the original title and description fields + // other fields are searched with the same term regardless of exact match or not, as they do not use fuzzy matching. boolean isExact = t.startsWith("\"") && t.endsWith("\"") && t.length() > 2; + // If search text with double quote, remove quotes, + // otherwise keeps same String term = isExact ? t.substring(1, t.length() - 1) : t; if (isExact) { - // use exact term match, search in title and description - should.add(CQLFields.title.getPropertyEqualToQuery(term)); // match term in original title text - should.add(CQLFields.description.getPropertyEqualToQuery(term)); // match term in original description text + // Match phrase in original title and description, not use fuzzy fields + should.add(CQLFields.title.getPropertyEqualToQuery(term)); + should.add(CQLFields.description.getPropertyEqualToQuery(term)); } else { - should.add(CQLFields.fuzzy_title.getPropertyEqualToQuery(t)); - should.add(CQLFields.fuzzy_desc.getPropertyEqualToQuery(t)); - should.add(CQLFields.parameter_vocabs.getPropertyEqualToQuery(t)); - should.add(CQLFields.organisation_vocabs.getPropertyEqualToQuery(t)); - should.add(CQLFields.platform_vocabs.getPropertyEqualToQuery(t)); - should.add(CQLFields.id.getPropertyEqualToQuery(t)); - // A request to not using acronym in title and description in metadata, hence these - // acronym moved to links, for example NRMN record is mentioned in the link title. - // This is a work-around to the requirement but still allow use of NRMN - should.add(CQLFields.links_title_contains.getPropertyEqualToQuery(t)); - should.add(CQLFields.credit_contains.getPropertyEqualToQuery(t)); + 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)); + // A request to not using acronym in title and description in metadata, hence these + // acronym moved to links, for example NRMN record is mentioned in the link title. + // This is a work-around to the requirement but still allow use of NRMN + // links_title_contains and credit_contains use match query by default, exact match is not applied here + should.add(CQLFields.links_title_contains.getPropertyEqualToQuery(term)); + should.add(CQLFields.credit_contains.getPropertyEqualToQuery(term)); } } 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 6eb54421..340536bc 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 @@ -119,52 +119,55 @@ public void searchFeatureSummaryTest() throws IOException { } @Test - public void searchByParameters_withDoubleQuote_shouldUseExactMatch() throws Exception { - // text with double quote + public void searchByParametersWithDoubleQuote() throws Exception { 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(CQLFields.links_title_contains.getPropertyEqualToQuery(term)); + should.add(CQLFields.credit_contains.getPropertyEqualToQuery(term)); } - - // Assert - assertEquals(2, should.size(), "Exact match should only produce 2 queries (title + description)"); + 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"); } @Test - public void searchByParameters_withoutDoubleQuote_shouldUseFuzzyMatch() throws Exception { + public void searchByParametersWithoutDoubleQuote() throws Exception { 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; - - if (!isExact) { - should.add(CQLFields.fuzzy_title.getPropertyEqualToQuery(t)); - should.add(CQLFields.fuzzy_desc.getPropertyEqualToQuery(t)); - should.add(CQLFields.parameter_vocabs.getPropertyEqualToQuery(t)); - should.add(CQLFields.organisation_vocabs.getPropertyEqualToQuery(t)); - should.add(CQLFields.platform_vocabs.getPropertyEqualToQuery(t)); - should.add(CQLFields.id.getPropertyEqualToQuery(t)); - should.add(CQLFields.links_title_contains.getPropertyEqualToQuery(t)); - should.add(CQLFields.credit_contains.getPropertyEqualToQuery(t)); + 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(CQLFields.links_title_contains.getPropertyEqualToQuery(term)); + should.add(CQLFields.credit_contains.getPropertyEqualToQuery(term)); } - assertEquals(8, should.size(), "Fuzzy match should produce 8 queries"); assertTrue(should.get(0).isMatch(), "fuzzy_title should be MatchQuery"); } - - }