From fe13de1ffc7cf3573329882a0b72136ca7deffb2 Mon Sep 17 00:00:00 2001 From: Fanis <93339170+tharropoulos@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:09:25 +0200 Subject: [PATCH 01/15] feat(analytics): add analytics rules and events support (#72) * feat(analytics): add Analytics rules API support Initial implementation of Analytics rules API for the Typesense Java client. Includes CRUD operations for analytics rules and corresponding test coverage. The changes include: - New Analytics, AnalyticsRule, and AnalyticsRules classes - Analytics rules integration in Client class - Comprehensive test coverage for analytics operations - Documentation updates showcasing how to use the new feature feat(analytics): implement Analytics class feat(analytics): add AnalyticsRule class for individual rule operations feat(analytics): add AnalyticsRules class for bulk operations test(analytics): add comprehensive test coverage for analytics rules refactor(client): integrate analytics support docs(analytics): add documenation on proper analytics rules usage * feat(analytics): add Analytics events API support Implements analytics events tracking capabilities to complement existing rules functionality. Adds ability to create custom analytics events with type, name and data fields. Changes include: - New AnalyticsEvents class for event operations - Integration with Analytics class - Test coverage with sample search event creation - Helper updates for events in analytics rules - Documentation updates on how to use new feature feat(analytics): add AnalyticsEvents class test(analytics): add AnalyticsEventsTest feat(analytics): integrate events into Analytics class refactor(helper): update createTestAnalyticsRule for events docs(analytics): add documenation on proper analytics rules usage * fix: use regular hashmap on analytics test for older java versions --- README.md | 56 ++++++++ .../java/org/typesense/api/Analytics.java | 37 +++++ .../org/typesense/api/AnalyticsEvents.java | 18 +++ .../java/org/typesense/api/AnalyticsRule.java | 29 ++++ .../org/typesense/api/AnalyticsRules.java | 30 ++++ src/main/java/org/typesense/api/Client.java | 9 ++ .../typesense/api/AnalyticsEventsTest.java | 48 +++++++ .../org/typesense/api/AnalyticsRulesTest.java | 131 ++++++++++++++++++ src/test/java/org/typesense/api/Helper.java | 75 +++++++--- 9 files changed, 415 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/typesense/api/Analytics.java create mode 100644 src/main/java/org/typesense/api/AnalyticsEvents.java create mode 100644 src/main/java/org/typesense/api/AnalyticsRule.java create mode 100644 src/main/java/org/typesense/api/AnalyticsRules.java create mode 100644 src/test/java/org/typesense/api/AnalyticsEventsTest.java create mode 100644 src/test/java/org/typesense/api/AnalyticsRulesTest.java diff --git a/README.md b/README.md index 5524147..c917dd4 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,62 @@ client.collections("Countries").delete(); client.collections("Countries").documents().export(); ``` +### Create an analytics rule +```java +AnalyticsRuleSchema analyticsRule = new AnalyticsRuleSchema(); +analyticsRule.setName("popular-queries"); +analyticsRule.setType(AnalyticsRuleSchema.TypeEnum.POPULAR_QUERIES); +analyticsRule.setParams(new AnalyticsRuleParameters() + .source(new AnalyticsRuleParametersSource() + .collections(Arrays.asList("Countries"))) + .destination(new AnalyticsRuleParametersDestination() + .collection("top_searches"))); + +client.analytics().rules().create(analyticsRule); +``` + +### Upsert an analytics rule +```java +AnalyticsRuleUpsertSchema analyticsRule = new AnalyticsRuleUpsertSchema() + .type(AnalyticsRuleUpsertSchema.TypeEnum.NOHITS_QUERIES) + .params(new AnalyticsRuleParameters() + .source(new AnalyticsRuleParametersSource() + .collections(Arrays.asList("Countries"))) + .destination(new AnalyticsRuleParametersDestination() + .collection("failed_searches"))); + +client.analytics().rules().upsert("failed-searches", analyticsRule); +``` + +### Retrieve all analytics rules +```java +AnalyticsRulesRetrieveSchema rules = client.analytics().rules().retrieve(); +``` + +### Retrieve a single analytics rule +```java +AnalyticsRuleSchema rule = client.analytics().rules("failed-searches").retrieve(); +``` + +### Delete an analytics rule +```java +client.analytics().rules("failed-searches").delete(); +``` + +#### Create an analytics event +```java +AnalyticsEventCreateSchema analyticsEvent = new AnalyticsEventCreateSchema() + .type("conversion") + .name("purchase_made") + .data(Map.of( + "product_id", "123", + "user_id", "user_456", + "amount", "99.99" + )); + +client.analytics().events().create(analyticsEvent); +``` + ### Create an API key ```java ApiKeySchema apiKeySchema = new ApiKeySchema(); diff --git a/src/main/java/org/typesense/api/Analytics.java b/src/main/java/org/typesense/api/Analytics.java new file mode 100644 index 0000000..4503160 --- /dev/null +++ b/src/main/java/org/typesense/api/Analytics.java @@ -0,0 +1,37 @@ +package org.typesense.api; + +import java.util.HashMap; +import java.util.Map; + +public class Analytics { + private final ApiCall apiCall; + private final AnalyticsRules rules; + private final Map individualRules; + private final AnalyticsEvents events; + + public Analytics(ApiCall apiCall) { + this.apiCall = apiCall; + this.rules = new AnalyticsRules(this.apiCall); + this.individualRules = new HashMap<>(); + this.events = new AnalyticsEvents(this.apiCall); + } + + public AnalyticsRules rules() { + return this.rules; + } + + public AnalyticsRule rules(String ruleId) { + AnalyticsRule retVal; + + if (!this.individualRules.containsKey(ruleId)) { + this.individualRules.put(ruleId, new AnalyticsRule(ruleId, apiCall)); + } + + retVal = this.individualRules.get(ruleId); + return retVal; + } + + public AnalyticsEvents events() { + return this.events; + } +} diff --git a/src/main/java/org/typesense/api/AnalyticsEvents.java b/src/main/java/org/typesense/api/AnalyticsEvents.java new file mode 100644 index 0000000..4889aa3 --- /dev/null +++ b/src/main/java/org/typesense/api/AnalyticsEvents.java @@ -0,0 +1,18 @@ +package org.typesense.api; + +import org.typesense.model.AnalyticsEventCreateResponse; +import org.typesense.model.AnalyticsEventCreateSchema; + + +public class AnalyticsEvents { + private final ApiCall apiCall; + public final static String RESOURCE_PATH = "/analytics/events"; + + public AnalyticsEvents(ApiCall apiCall) { + this.apiCall = apiCall; + } + + public AnalyticsEventCreateResponse create(AnalyticsEventCreateSchema event) throws Exception { + return this.apiCall.post(RESOURCE_PATH, event, null, AnalyticsEventCreateResponse.class); + } +} diff --git a/src/main/java/org/typesense/api/AnalyticsRule.java b/src/main/java/org/typesense/api/AnalyticsRule.java new file mode 100644 index 0000000..2ed2f28 --- /dev/null +++ b/src/main/java/org/typesense/api/AnalyticsRule.java @@ -0,0 +1,29 @@ +package org.typesense.api; + +import org.typesense.api.utils.URLEncoding; +import org.typesense.model.AnalyticsRuleDeleteResponse; +import org.typesense.model.AnalyticsRuleSchema; + +public class AnalyticsRule { + private final ApiCall apiCall; + private final String ruleId; + + public AnalyticsRule(String ruleId, ApiCall apiCall) { + this.apiCall = apiCall; + this.ruleId = ruleId; + } + + + public AnalyticsRuleSchema retrieve() throws Exception { + return this.apiCall.get(this.getEndpoint(), null, AnalyticsRuleSchema.class); + } + + public AnalyticsRuleDeleteResponse delete() throws Exception { + return this.apiCall.delete(this.getEndpoint(), null, AnalyticsRuleDeleteResponse.class); + } + + private String getEndpoint() { + return AnalyticsRules.RESOURCE_PATH + "/" + URLEncoding.encodeURIComponent(ruleId); + } + +} diff --git a/src/main/java/org/typesense/api/AnalyticsRules.java b/src/main/java/org/typesense/api/AnalyticsRules.java new file mode 100644 index 0000000..f910b05 --- /dev/null +++ b/src/main/java/org/typesense/api/AnalyticsRules.java @@ -0,0 +1,30 @@ +package org.typesense.api; + +import org.typesense.api.utils.URLEncoding; +import org.typesense.model.AnalyticsRuleSchema; +import org.typesense.model.AnalyticsRuleUpsertSchema; +import org.typesense.model.AnalyticsRulesRetrieveSchema; + +public class AnalyticsRules { + + private final ApiCall apiCall; + public final static String RESOURCE_PATH = "/analytics/rules"; + + public AnalyticsRules(ApiCall apiCall) { + this.apiCall = apiCall; + } + + public AnalyticsRuleSchema create(AnalyticsRuleSchema rule) throws Exception { + return this.apiCall.post(RESOURCE_PATH, rule, null, AnalyticsRuleSchema.class); + } + + public AnalyticsRuleSchema upsert(String name, AnalyticsRuleUpsertSchema rule) throws Exception { + return this.apiCall.put(RESOURCE_PATH + "/" + URLEncoding.encodeURIComponent(name), rule, null, + AnalyticsRuleSchema.class); + } + + public AnalyticsRulesRetrieveSchema retrieve() throws Exception { + return this.apiCall.get(RESOURCE_PATH, null, AnalyticsRulesRetrieveSchema.class); + } + +} diff --git a/src/main/java/org/typesense/api/Client.java b/src/main/java/org/typesense/api/Client.java index 6f69a60..6177674 100644 --- a/src/main/java/org/typesense/api/Client.java +++ b/src/main/java/org/typesense/api/Client.java @@ -1,5 +1,6 @@ package org.typesense.api; + import java.util.HashMap; import java.util.Map; @@ -17,6 +18,9 @@ public class Client { private Keys keys; private Map individualKeys; + + private Analytics analytics; + public Health health; public Operations operations; public Metrics metrics; @@ -37,6 +41,7 @@ public Client(Configuration configuration){ this.metrics = new Metrics(this.apiCall); this.debug = new Debug(this.apiCall); this.multiSearch = new MultiSearch(this.apiCall); + this.analytics = new Analytics(this.apiCall); } public Collection collections(String name){ @@ -85,4 +90,8 @@ public Key keys(Long id){ retVal = this.individualKeys.get(id); return retVal; } + + public Analytics analytics(){ + return this.analytics; + } } diff --git a/src/test/java/org/typesense/api/AnalyticsEventsTest.java b/src/test/java/org/typesense/api/AnalyticsEventsTest.java new file mode 100644 index 0000000..2f88002 --- /dev/null +++ b/src/test/java/org/typesense/api/AnalyticsEventsTest.java @@ -0,0 +1,48 @@ +package org.typesense.api; + +import java.util.HashMap; + +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.typesense.model.AnalyticsEventCreateResponse; +import org.typesense.model.AnalyticsEventCreateSchema; + +public class AnalyticsEventsTest { + + private Client client; + private Helper helper; + + @BeforeEach + void setUp() throws Exception { + helper = new Helper(); + client = helper.getClient(); + helper.teardown(); + helper.createTestCollection(); + helper.createTestQueryCollection(); + helper.createTestAnalyticsRule(); + } + + @AfterEach + void tearDown() throws Exception { + helper.teardown(); + } + + @Test + void testCreate() throws Exception { + HashMap eventData = new HashMap<>(); + eventData.put("q", "running shoes"); + eventData.put("user_id", "1234"); + AnalyticsEventCreateSchema analyticsEvent = new AnalyticsEventCreateSchema() + .type("search") + .name("products_search_event") + .data(eventData); + + AnalyticsEventCreateResponse result = this.client.analytics().events().create(analyticsEvent); + assertNotNull(result); + assertEquals(true, result.isOk()); + + } +} diff --git a/src/test/java/org/typesense/api/AnalyticsRulesTest.java b/src/test/java/org/typesense/api/AnalyticsRulesTest.java new file mode 100644 index 0000000..34f5dd2 --- /dev/null +++ b/src/test/java/org/typesense/api/AnalyticsRulesTest.java @@ -0,0 +1,131 @@ +package org.typesense.api; + +import java.util.Arrays; + +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.typesense.model.AnalyticsRuleDeleteResponse; +import org.typesense.model.AnalyticsRuleParameters; +import org.typesense.model.AnalyticsRuleParametersDestination; +import org.typesense.model.AnalyticsRuleParametersSource; +import org.typesense.model.AnalyticsRuleSchema; +import org.typesense.model.AnalyticsRuleUpsertSchema; +import org.typesense.model.AnalyticsRulesRetrieveSchema; + +public class AnalyticsRulesTest { + + private Client client; + private Helper helper; + + @BeforeEach + void setUp() throws Exception { + helper = new Helper(); + client = helper.getClient(); + helper.teardown(); + helper.createTestCollection(); + helper.createTestQueryCollection(); + } + + @AfterEach + void tearDown() throws Exception { + helper.teardown(); + } + + @Test + void testCreate() throws Exception { + AnalyticsRuleSchema analyticsRuleSchema = new AnalyticsRuleSchema(); + analyticsRuleSchema.setName("nohits-queries"); + analyticsRuleSchema.setType(AnalyticsRuleSchema.TypeEnum.NOHITS_QUERIES); + analyticsRuleSchema.setParams(new AnalyticsRuleParameters() + .source(new AnalyticsRuleParametersSource() + .collections(Arrays.asList("books"))) + .destination(new AnalyticsRuleParametersDestination() + .collection("queries"))); + + AnalyticsRuleSchema result = this.client.analytics().rules().create(analyticsRuleSchema); + assertNotNull(result); + assertEquals("nohits-queries", result.getName()); + + AnalyticsRuleParameters params = result.getParams(); + assertNotNull(params); + + assertNotNull(params.getSource()); + assertEquals(Arrays.asList("books"), params.getSource().getCollections()); + + assertNotNull(params.getDestination()); + assertEquals("queries", params.getDestination().getCollection()); + } + + @Test + void testUpsert() throws Exception { + AnalyticsRuleUpsertSchema analyticsRuleSchema = new AnalyticsRuleUpsertSchema() + .type(AnalyticsRuleUpsertSchema.TypeEnum.NOHITS_QUERIES) + .params(new AnalyticsRuleParameters() + .source(new AnalyticsRuleParametersSource() + .collections(Arrays.asList("books"))) + .destination(new AnalyticsRuleParametersDestination() + .collection("queries"))); + + AnalyticsRuleSchema result = this.client.analytics().rules().upsert("nohits-queries", analyticsRuleSchema); + assertNotNull(result); + assertEquals("nohits-queries", result.getName()); + + AnalyticsRuleParameters params = result.getParams(); + assertNotNull(params); + + assertNotNull(params.getSource()); + assertEquals(Arrays.asList("books"), params.getSource().getCollections()); + + assertNotNull(params.getDestination()); + assertEquals("queries", params.getDestination().getCollection()); + } + + @Test + void testRetrieve() throws Exception { + helper.createTestAnalyticsRule(); + AnalyticsRuleSchema result = this.client.analytics().rules("analytics-rule").retrieve(); + + assertNotNull(result); + assertEquals("analytics-rule", result.getName()); + + AnalyticsRuleParameters params = result.getParams(); + assertNotNull(params); + + assertNotNull(params.getSource()); + assertEquals(Arrays.asList("books"), params.getSource().getCollections()); + + assertNotNull(params.getDestination()); + assertEquals("queries", params.getDestination().getCollection()); + } + + @Test + void testRetrieveAll() throws Exception { + helper.createTestAnalyticsRule(); + AnalyticsRulesRetrieveSchema result = this.client.analytics().rules().retrieve(); + + assertNotNull(result); + assertEquals("analytics-rule", result.getRules().get(0).getName()); + assertEquals(1, result.getRules().size()); + + AnalyticsRuleParameters params = result.getRules().get(0).getParams(); + assertNotNull(params); + + assertNotNull(params.getSource()); + assertEquals(Arrays.asList("books"), params.getSource().getCollections()); + + assertNotNull(params.getDestination()); + assertEquals("queries", params.getDestination().getCollection()); + } + + @Test + void testDelete() throws Exception { + helper.createTestAnalyticsRule(); + AnalyticsRuleDeleteResponse result = this.client.analytics().rules("analytics-rule").delete(); + + assertNotNull(result); + assertEquals("analytics-rule", result.getName()); + } +} diff --git a/src/test/java/org/typesense/api/Helper.java b/src/test/java/org/typesense/api/Helper.java index b9b943f..33781ea 100644 --- a/src/test/java/org/typesense/api/Helper.java +++ b/src/test/java/org/typesense/api/Helper.java @@ -1,5 +1,17 @@ package org.typesense.api; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import org.typesense.model.AnalyticsRuleParameters; +import org.typesense.model.AnalyticsRuleParametersDestination; +import org.typesense.model.AnalyticsRuleParametersSource; +import org.typesense.model.AnalyticsRuleParametersSourceEvents; +import org.typesense.model.AnalyticsRuleSchema; +import org.typesense.model.AnalyticsRuleUpsertSchema; import org.typesense.model.ApiKey; import org.typesense.model.ApiKeySchema; import org.typesense.model.ApiKeysResponse; @@ -15,19 +27,14 @@ import org.typesense.model.SearchSynonymSchema; import org.typesense.resources.Node; -import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - public class Helper { private final Client client; Helper() { List nodes = new ArrayList<>(); - nodes.add(new Node("http","localhost","8108")); + nodes.add(new Node("http", "localhost", "8108")); - Configuration configuration = new Configuration(nodes, Duration.ofSeconds(3),"xyz"); + Configuration configuration = new Configuration(nodes, Duration.ofSeconds(3), "xyz"); this.client = new Client(configuration); } @@ -42,15 +49,26 @@ public void createTestCollection() throws Exception { client.collections().create(collectionSchema); } + public void createTestQueryCollection() throws Exception { + List fields = new ArrayList<>(); + fields.add(new Field().name("q").type((FieldTypes.STRING))); + fields.add(new Field().name("count").type((FieldTypes.INT32))); + + CollectionSchema collectionSchema = new CollectionSchema(); + collectionSchema.name("queries").fields(fields); + + client.collections().create(collectionSchema); + } + public void createTestDocument() throws Exception { - String[] authors = {"shakspeare","william"}; + String[] authors = { "shakspeare", "william" }; HashMap hmap = new HashMap<>(); - hmap.put("title","Romeo and juliet"); - hmap.put("authors",authors); - hmap.put("publication_year",1666); - hmap.put("ratings_count",124); - hmap.put("average_rating",3.2); - hmap.put("id","1"); + hmap.put("title", "Romeo and juliet"); + hmap.put("authors", authors); + hmap.put("publication_year", 1666); + hmap.put("ratings_count", 124); + hmap.put("average_rating", 3.2); + hmap.put("id", "1"); client.collections("books").documents().create(hmap); } @@ -88,22 +106,43 @@ public void createTestSynonym() throws Exception { SearchSynonymSchema synonym = new SearchSynonymSchema(); synonym.addSynonymsItem("blazer").addSynonymsItem("coat").addSynonymsItem("jacket"); - client.collections("books").synonyms().upsert("coat-synonyms",synonym); + client.collections("books").synonyms().upsert("coat-synonyms", synonym); + } + + public void createTestAnalyticsRule() throws Exception { + AnalyticsRuleUpsertSchema analyticsRuleSchema = new AnalyticsRuleUpsertSchema() + .type(AnalyticsRuleUpsertSchema.TypeEnum.NOHITS_QUERIES) + .params(new AnalyticsRuleParameters() + .source(new AnalyticsRuleParametersSource() + .collections(Arrays.asList("books")) + .events(Arrays.asList( + new AnalyticsRuleParametersSourceEvents() + .type("search") + .name("products_search_event")))) + .destination(new AnalyticsRuleParametersDestination() + .collection("queries"))); + + client.analytics().rules().upsert("analytics-rule", analyticsRuleSchema); } public void teardown() throws Exception { CollectionResponse[] collectionResponses = client.collections().retrieve(); - for(CollectionResponse c:collectionResponses) { + for (CollectionResponse c : collectionResponses) { client.collections(c.getName()).delete(); } CollectionAliasesResponse collectionAliasesResponse = client.aliases().retrieve(); - for(CollectionAlias a:collectionAliasesResponse.getAliases()) { + for (CollectionAlias a : collectionAliasesResponse.getAliases()) { client.aliases(a.getName()).delete(); } + AnalyticsRules analyticsRules = client.analytics().rules(); + for (AnalyticsRuleSchema r : analyticsRules.retrieve().getRules()) { + client.analytics().rules(r.getName()).delete(); + } + ApiKeysResponse apiKeysResponse = client.keys().retrieve(); - for (ApiKey k: apiKeysResponse.getKeys()) { + for (ApiKey k : apiKeysResponse.getKeys()) { client.keys(k.getId()).delete(); } } From 4af0a7a26f97351c6ef6039200899d026de71f1c Mon Sep 17 00:00:00 2001 From: Kishore Nallan Date: Mon, 4 Nov 2024 20:42:46 +0530 Subject: [PATCH 02/15] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 76d1591..7e9ecef 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } group = 'org.typesense' -version = '1.0.0' +version = '1.1.0' sourceCompatibility = 1.8 targetCompatibility = 1.8 From 2faea8a26fe17d179abeadff3b8b8b0f1d19d2ad Mon Sep 17 00:00:00 2001 From: Kishore Nallan Date: Mon, 4 Nov 2024 20:43:24 +0530 Subject: [PATCH 03/15] Update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c917dd4..6cc5fe7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The client is available on Maven central: ```groovy dependencies { - implementation 'org.typesense:typesense-java:0.9.0' + implementation 'org.typesense:typesense-java:1.1.0' } ``` @@ -16,7 +16,7 @@ dependencies { org.typesense typesense-java - 0.9.0 + 1.1.0 ``` From 1eee81de4f399c73a136fcff83295186e10ba2da Mon Sep 17 00:00:00 2001 From: Fanis <93339170+tharropoulos@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:38:13 +0200 Subject: [PATCH 04/15] feat(stopwords): add stopwords support (#73) Implements stopwords management capabilities allowing users to create and manage stopwords sets for search optimization. Adds complete CRUD operations for stopwords with proper error handling and test coverage. Changes include: - New Stopwords and StopwordsSet classes for managing stopwords - Client integration for stopwords management - Full test coverage with realistic scenarios - Helper class updates for testing stopwords functionality - Documentation updates with usage examples feat(stopwords): add Stopwords class for bulk operations feat(stopwords): add StopwordsSet class for individual set management test(stopwords): add StopwordsTest test coverage refactor(client): integrate stopwords support refactor(helper): add stopwords support in test helper docs(stopwords): add documentation and examples --- README.md | 28 ++++++ src/main/java/org/typesense/api/Client.java | 20 ++++ .../java/org/typesense/api/Stopwords.java | 29 ++++++ .../java/org/typesense/api/StopwordsSet.java | 28 ++++++ src/test/java/org/typesense/api/Helper.java | 20 ++++ .../java/org/typesense/api/StopwordsTest.java | 91 +++++++++++++++++++ 6 files changed, 216 insertions(+) create mode 100644 src/main/java/org/typesense/api/Stopwords.java create mode 100644 src/main/java/org/typesense/api/StopwordsSet.java create mode 100644 src/test/java/org/typesense/api/StopwordsTest.java diff --git a/README.md b/README.md index 6cc5fe7..0fc93bb 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,34 @@ AnalyticsEventCreateSchema analyticsEvent = new AnalyticsEventCreateSchema() client.analytics().events().create(analyticsEvent); ``` +### Upsert a stopwords set +```java +List stopwords = new ArrayList<>(); +stopwords.add("the"); +stopwords.add("of"); +stopwords.add("and"); + +StopwordsSetUpsertSchema stopwordsSet = new StopwordsSetUpsertSchema(); +stopwordsSet.stopwords(stopwords); + +client.stopwords().upsert("common-words", stopwordsSet); +``` + +### Retrieve a stopwords set +```java +StopwordsSetRetrieveSchema set = client.stopwords("common-words").retrieve(); +``` + +### Retrieve all stopwords sets +```java +StopwordsSetsRetrieveAllSchema sets = client.stopwords().retrieve(); +``` + +### Delete a stopwords set +```java +client.stopwords("common-words").delete(); +``` + ### Create an API key ```java ApiKeySchema apiKeySchema = new ApiKeySchema(); diff --git a/src/main/java/org/typesense/api/Client.java b/src/main/java/org/typesense/api/Client.java index 6177674..84e94f0 100644 --- a/src/main/java/org/typesense/api/Client.java +++ b/src/main/java/org/typesense/api/Client.java @@ -21,6 +21,9 @@ public class Client { private Analytics analytics; + private Stopwords stopwords; + private Map individualStopwordsSets; + public Health health; public Operations operations; public Metrics metrics; @@ -42,6 +45,8 @@ public Client(Configuration configuration){ this.debug = new Debug(this.apiCall); this.multiSearch = new MultiSearch(this.apiCall); this.analytics = new Analytics(this.apiCall); + this.stopwords = new Stopwords(this.apiCall); + this.individualStopwordsSets = new HashMap<>(); } public Collection collections(String name){ @@ -94,4 +99,19 @@ public Key keys(Long id){ public Analytics analytics(){ return this.analytics; } + + public Stopwords stopwords() { + return this.stopwords; + } + + public StopwordsSet stopwords(String stopwordsSetId) { + StopwordsSet retVal; + + if (!this.individualStopwordsSets.containsKey(stopwordsSetId)) { + this.individualStopwordsSets.put(stopwordsSetId, new StopwordsSet(stopwordsSetId, this.apiCall)); + } + + retVal = this.individualStopwordsSets.get(stopwordsSetId); + return retVal; + } } diff --git a/src/main/java/org/typesense/api/Stopwords.java b/src/main/java/org/typesense/api/Stopwords.java new file mode 100644 index 0000000..921e43b --- /dev/null +++ b/src/main/java/org/typesense/api/Stopwords.java @@ -0,0 +1,29 @@ +package org.typesense.api; + +import org.typesense.api.utils.URLEncoding; +import org.typesense.model.StopwordsSetSchema; +import org.typesense.model.StopwordsSetUpsertSchema; +import org.typesense.model.StopwordsSetsRetrieveAllSchema; + +public class Stopwords { + public final static String RESOURCEPATH = "/stopwords"; + + private final ApiCall apiCall; + + public Stopwords(ApiCall apiCall) { + this.apiCall = apiCall; + } + + public StopwordsSetSchema upsert(String stopwordSetId, StopwordsSetUpsertSchema stopwordSet) throws Exception { + return this.apiCall.put(getEndpoint(stopwordSetId), stopwordSet, null, StopwordsSetSchema.class); + } + + public StopwordsSetsRetrieveAllSchema retrieve() throws Exception { + return this.apiCall.get(Stopwords.RESOURCEPATH, null, StopwordsSetsRetrieveAllSchema.class); + } + + private String getEndpoint(String stopwordSetId) { + return RESOURCEPATH + "/" + URLEncoding.encodeURIComponent(stopwordSetId); + } + +} diff --git a/src/main/java/org/typesense/api/StopwordsSet.java b/src/main/java/org/typesense/api/StopwordsSet.java new file mode 100644 index 0000000..9200960 --- /dev/null +++ b/src/main/java/org/typesense/api/StopwordsSet.java @@ -0,0 +1,28 @@ +package org.typesense.api; + +import org.typesense.api.utils.URLEncoding; +import org.typesense.model.StopwordsSetRetrieveSchema; +import org.typesense.model.StopwordsSetSchema; + +public class StopwordsSet { + private final ApiCall apiCall; + private final String stopwordsSetId; + + public StopwordsSet(String stopwordsSetId, ApiCall apiCall) { + this.stopwordsSetId = stopwordsSetId; + this.apiCall = apiCall; + } + + public StopwordsSetRetrieveSchema retrieve() throws Exception { + return this.apiCall.get(this.getEndpoint(), null, StopwordsSetRetrieveSchema.class); + } + + public StopwordsSetSchema delete() throws Exception { + return this.apiCall.delete(this.getEndpoint(), null, StopwordsSetSchema.class); + } + + private String getEndpoint() { + return Stopwords.RESOURCEPATH + "/" + URLEncoding.encodeURIComponent(this.stopwordsSetId); + } + +} diff --git a/src/test/java/org/typesense/api/Helper.java b/src/test/java/org/typesense/api/Helper.java index 33781ea..de910e3 100644 --- a/src/test/java/org/typesense/api/Helper.java +++ b/src/test/java/org/typesense/api/Helper.java @@ -25,6 +25,9 @@ import org.typesense.model.SearchOverrideRule; import org.typesense.model.SearchOverrideSchema; import org.typesense.model.SearchSynonymSchema; +import org.typesense.model.StopwordsSetSchema; +import org.typesense.model.StopwordsSetUpsertSchema; +import org.typesense.model.StopwordsSetsRetrieveAllSchema; import org.typesense.resources.Node; public class Helper { @@ -125,6 +128,18 @@ public void createTestAnalyticsRule() throws Exception { client.analytics().rules().upsert("analytics-rule", analyticsRuleSchema); } + public void createTestStopwordsSet() throws Exception { + List stopwords = new ArrayList<>(); + stopwords.add("the"); + stopwords.add("of"); + stopwords.add("and"); + + StopwordsSetUpsertSchema stopwordsSetSchema = new StopwordsSetUpsertSchema(); + stopwordsSetSchema.stopwords(stopwords); + + client.stopwords().upsert("common-words", stopwordsSetSchema); + } + public void teardown() throws Exception { CollectionResponse[] collectionResponses = client.collections().retrieve(); for (CollectionResponse c : collectionResponses) { @@ -145,5 +160,10 @@ public void teardown() throws Exception { for (ApiKey k : apiKeysResponse.getKeys()) { client.keys(k.getId()).delete(); } + + StopwordsSetsRetrieveAllSchema stopwords = client.stopwords().retrieve(); + for (StopwordsSetSchema s : stopwords.getStopwords()) { + client.stopwords(s.getId()).delete(); + } } } diff --git a/src/test/java/org/typesense/api/StopwordsTest.java b/src/test/java/org/typesense/api/StopwordsTest.java new file mode 100644 index 0000000..91aeb27 --- /dev/null +++ b/src/test/java/org/typesense/api/StopwordsTest.java @@ -0,0 +1,91 @@ +package org.typesense.api; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.typesense.model.StopwordsSetRetrieveSchema; +import org.typesense.model.StopwordsSetSchema; +import org.typesense.model.StopwordsSetUpsertSchema; +import org.typesense.model.StopwordsSetsRetrieveAllSchema; + +public class StopwordsTest { + + private Client client; + private Helper helper; + + @BeforeEach + void setUp() throws Exception { + helper = new Helper(); + client = helper.getClient(); + helper.teardown(); + helper.createTestCollection(); + } + + @AfterEach + void tearDown() throws Exception { + helper.teardown(); + } + + @Test + void testUpsert() throws Exception { + List stopwords = new ArrayList<>(); + stopwords.add("the"); + stopwords.add("of"); + stopwords.add("and"); + + StopwordsSetUpsertSchema stopwordsSetSchema = new StopwordsSetUpsertSchema(); + stopwordsSetSchema.stopwords(stopwords); + + client.stopwords().upsert("common-words", stopwordsSetSchema); + } + + @Test + void testRetrieve() throws Exception { + helper.createTestStopwordsSet(); + + StopwordsSetRetrieveSchema result = this.client.stopwords("common-words").retrieve(); + + assertNotNull(result); + + StopwordsSetSchema set = result.getStopwords(); + + assertEquals("common-words", set.getId()); + assertEquals(3, set.getStopwords().size()); + assertEquals("and", set.getStopwords().get(0)); + assertEquals("the", set.getStopwords().get(1)); + assertEquals("of", set.getStopwords().get(2)); + } + + @Test + void testRetrieveAll() throws Exception { + helper.createTestStopwordsSet(); + + StopwordsSetsRetrieveAllSchema result = this.client.stopwords().retrieve(); + + assertNotNull(result); + assertEquals(1, result.getStopwords().size()); + + StopwordsSetSchema set = result.getStopwords().get(0); + + assertEquals("common-words", set.getId()); + assertEquals(3, set.getStopwords().size()); + assertEquals("and", set.getStopwords().get(0)); + assertEquals("the", set.getStopwords().get(1)); + assertEquals("of", set.getStopwords().get(2)); + } + + @Test + void testDelete() throws Exception { + helper.createTestStopwordsSet(); + + StopwordsSetSchema result = this.client.stopwords("common-words").delete(); + + assertNotNull(result); + assertEquals("common-words", result.getId()); + } +} From 0571a01cf4582c291d4bd2812cdb490b8dc7748a Mon Sep 17 00:00:00 2001 From: Kishore Nallan Date: Mon, 3 Feb 2025 16:19:26 +0530 Subject: [PATCH 05/15] Bump version. --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0fc93bb..fce2df9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The client is available on Maven central: ```groovy dependencies { - implementation 'org.typesense:typesense-java:1.1.0' + implementation 'org.typesense:typesense-java:1.2.0' } ``` @@ -16,7 +16,7 @@ dependencies { org.typesense typesense-java - 1.1.0 + 1.2.0 ``` diff --git a/build.gradle b/build.gradle index 7e9ecef..4c55dc0 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } group = 'org.typesense' -version = '1.1.0' +version = '1.2.0' sourceCompatibility = 1.8 targetCompatibility = 1.8 From 3b3ffea2efca04cfe35a54cc8ab40f261cca75ca Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos <93339170+tharropoulos@users.noreply.github.com> Date: Sun, 16 Feb 2025 07:32:10 +0200 Subject: [PATCH 06/15] v28: Add Stemming API (#83) * feat(api): add stemming dictionary management support - Add `Stemming` class to manage dictionary operations - Add `StemmingDictionaries` to handle bulk dictionary operations - Add `StemmingDictionary` to manage individual dictionaries - Add `StemmingDictionariesRetrieveSchema` for API responses - Add tests for dictionary creation and retrieval * ci: update typesense server version to 28.0.rc36 --- .github/workflows/test.yml | 2 +- src/main/java/org/typesense/api/Client.java | 7 +++ src/main/java/org/typesense/api/Stemming.java | 32 ++++++++++ .../typesense/api/StemmingDictionaries.java | 59 +++++++++++++++++++ .../StemmingDictionariesRetrieveSchema.java | 15 +++++ .../org/typesense/api/StemmingDictionary.java | 23 ++++++++ src/test/java/org/typesense/api/Helper.java | 11 ++++ .../java/org/typesense/api/StemmingTest.java | 54 +++++++++++++++++ 8 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/typesense/api/Stemming.java create mode 100644 src/main/java/org/typesense/api/StemmingDictionaries.java create mode 100644 src/main/java/org/typesense/api/StemmingDictionariesRetrieveSchema.java create mode 100644 src/main/java/org/typesense/api/StemmingDictionary.java create mode 100644 src/test/java/org/typesense/api/StemmingTest.java diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 01b97d8..4a68e42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: java: ['8', '11', '17'] services: typesense: - image: typesense/typesense:27.0 + image: typesense/typesense:28.0.rc36 ports: - 8108:8108/tcp volumes: diff --git a/src/main/java/org/typesense/api/Client.java b/src/main/java/org/typesense/api/Client.java index 84e94f0..7215485 100644 --- a/src/main/java/org/typesense/api/Client.java +++ b/src/main/java/org/typesense/api/Client.java @@ -21,6 +21,8 @@ public class Client { private Analytics analytics; + private Stemming stemming; + private Stopwords stopwords; private Map individualStopwordsSets; @@ -45,6 +47,7 @@ public Client(Configuration configuration){ this.debug = new Debug(this.apiCall); this.multiSearch = new MultiSearch(this.apiCall); this.analytics = new Analytics(this.apiCall); + this.stemming = new Stemming(this.apiCall); this.stopwords = new Stopwords(this.apiCall); this.individualStopwordsSets = new HashMap<>(); } @@ -100,6 +103,10 @@ public Analytics analytics(){ return this.analytics; } + public Stemming stemming(){ + return this.stemming; + } + public Stopwords stopwords() { return this.stopwords; } diff --git a/src/main/java/org/typesense/api/Stemming.java b/src/main/java/org/typesense/api/Stemming.java new file mode 100644 index 0000000..0942c5d --- /dev/null +++ b/src/main/java/org/typesense/api/Stemming.java @@ -0,0 +1,32 @@ +package org.typesense.api; + +import java.util.HashMap; +import java.util.Map; + +public class Stemming { + private final ApiCall apiCall; + private final StemmingDictionaries dictionaries; + private final Map individualDictionaries; + + + public Stemming(ApiCall apiCall) { + this.apiCall = apiCall; + this.dictionaries = new StemmingDictionaries(this.apiCall); + this.individualDictionaries = new HashMap<>(); + } + + public StemmingDictionaries dictionaries() { + return this.dictionaries; + } + + public StemmingDictionary dictionaries(String dictionaryId) { + StemmingDictionary retVal; + + if (!this.individualDictionaries.containsKey(dictionaryId)) { + this.individualDictionaries.put(dictionaryId, new StemmingDictionary(dictionaryId, apiCall)); + } + + retVal = this.individualDictionaries.get(dictionaryId); + return retVal; + } +} diff --git a/src/main/java/org/typesense/api/StemmingDictionaries.java b/src/main/java/org/typesense/api/StemmingDictionaries.java new file mode 100644 index 0000000..f97842b --- /dev/null +++ b/src/main/java/org/typesense/api/StemmingDictionaries.java @@ -0,0 +1,59 @@ +package org.typesense.api; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.typesense.model.StemmingDictionaryWords; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class StemmingDictionaries { + private final ApiCall apiCall; + public final static String RESOURCE_PATH = "/stemming/dictionaries"; + + public StemmingDictionaries(ApiCall apiCall) { + this.apiCall = apiCall; + } + + public String upsert(String dictionaryId, String wordRootCombinations) throws Exception { + Map params = Collections.singletonMap("id", dictionaryId); + + return this.apiCall.post(this.getEndPoint("import"), wordRootCombinations, params, String.class); + } + + public List upsert(String dictionaryId, List wordRootCombinations) + throws Exception { + ObjectMapper mapper = new ObjectMapper(); + List jsonLines = new ArrayList<>(); + List objectList = new ArrayList<>(); + + for (StemmingDictionaryWords word : wordRootCombinations) { + jsonLines.add(mapper.writeValueAsString(word)); + } + + String reqBody = String.join("\n", jsonLines); + + Map params = Collections.singletonMap("id", dictionaryId); + + String resInJsonLineFormat = this.apiCall.post(this.getEndPoint("import"), reqBody, params, String.class); + + for (String line : resInJsonLineFormat.split("\n")) { + objectList.add(mapper.readValue(line, StemmingDictionaryWords.class)); + } + + return objectList; + } + + public StemmingDictionariesRetrieveSchema retrieve() throws Exception { + StemmingDictionariesRetrieveSchema response = this.apiCall.get(RESOURCE_PATH, null, + StemmingDictionariesRetrieveSchema.class); + return response != null ? response : new StemmingDictionariesRetrieveSchema(); + } + + public String getEndPoint(String target) { + return RESOURCE_PATH + "/" + target; + } + +} \ No newline at end of file diff --git a/src/main/java/org/typesense/api/StemmingDictionariesRetrieveSchema.java b/src/main/java/org/typesense/api/StemmingDictionariesRetrieveSchema.java new file mode 100644 index 0000000..aa956ac --- /dev/null +++ b/src/main/java/org/typesense/api/StemmingDictionariesRetrieveSchema.java @@ -0,0 +1,15 @@ +package org.typesense.api; + +import java.util.List; + +public class StemmingDictionariesRetrieveSchema { + private List dictionaries; + + public List getDictionaries() { + return dictionaries; + } + + public void setDictionaries(List dictionaries) { + this.dictionaries = dictionaries; + } +} \ No newline at end of file diff --git a/src/main/java/org/typesense/api/StemmingDictionary.java b/src/main/java/org/typesense/api/StemmingDictionary.java new file mode 100644 index 0000000..b13e69f --- /dev/null +++ b/src/main/java/org/typesense/api/StemmingDictionary.java @@ -0,0 +1,23 @@ +package org.typesense.api; + +import org.typesense.api.utils.URLEncoding; + +public class StemmingDictionary { + private final ApiCall apiCall; + private final String dictionaryId; + + public StemmingDictionary(String dictionaryId, ApiCall apiCall) { + this.apiCall = apiCall; + this.dictionaryId = dictionaryId; + } + + + public org.typesense.model.StemmingDictionary retrieve() throws Exception { + return this.apiCall.get(this.getEndpoint(), null, org.typesense.model.StemmingDictionary.class); + } + + private String getEndpoint() { + return StemmingDictionaries.RESOURCE_PATH + "/" + URLEncoding.encodeURIComponent(this.dictionaryId); + } + +} \ No newline at end of file diff --git a/src/test/java/org/typesense/api/Helper.java b/src/test/java/org/typesense/api/Helper.java index de910e3..8943d39 100644 --- a/src/test/java/org/typesense/api/Helper.java +++ b/src/test/java/org/typesense/api/Helper.java @@ -25,6 +25,7 @@ import org.typesense.model.SearchOverrideRule; import org.typesense.model.SearchOverrideSchema; import org.typesense.model.SearchSynonymSchema; +import org.typesense.model.StemmingDictionaryWords; import org.typesense.model.StopwordsSetSchema; import org.typesense.model.StopwordsSetUpsertSchema; import org.typesense.model.StopwordsSetsRetrieveAllSchema; @@ -140,6 +141,16 @@ public void createTestStopwordsSet() throws Exception { client.stopwords().upsert("common-words", stopwordsSetSchema); } + + public void createStemmingDictionary() throws Exception{ + List stemmingDictionaryWords = new ArrayList<>(); + + stemmingDictionaryWords.add(new StemmingDictionaryWords().word("ran").root("run")); + stemmingDictionaryWords.add(new StemmingDictionaryWords().word("running").root("run")); + + client.stemming().dictionaries().upsert("irregular-plurals", stemmingDictionaryWords); + } + public void teardown() throws Exception { CollectionResponse[] collectionResponses = client.collections().retrieve(); for (CollectionResponse c : collectionResponses) { diff --git a/src/test/java/org/typesense/api/StemmingTest.java b/src/test/java/org/typesense/api/StemmingTest.java new file mode 100644 index 0000000..5122f68 --- /dev/null +++ b/src/test/java/org/typesense/api/StemmingTest.java @@ -0,0 +1,54 @@ +package org.typesense.api; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.typesense.model.StemmingDictionary; +import org.typesense.model.StemmingDictionaryWords; + +public class StemmingTest { + private Client client; + private Helper helper; + + @BeforeEach + void setUp() throws Exception { + helper = new Helper(); + helper.teardown(); + client = helper.getClient(); + helper.createStemmingDictionary(); + } + + @Test + void testUpsert() throws Exception { + List stemmingDictionaryWords = new ArrayList<>(); + + stemmingDictionaryWords.add(new StemmingDictionaryWords().word("ran").root("run")); + stemmingDictionaryWords.add(new StemmingDictionaryWords().word("running").root("run")); + + List res = client.stemming().dictionaries().upsert("irregular-plurals", + stemmingDictionaryWords); + + assertEquals(2, res.size()); + assertEquals("ran", res.get(0).getWord()); + assertEquals("run", res.get(0).getRoot()); + assertEquals("running", res.get(1).getWord()); + assertEquals("run", res.get(1).getRoot()); + } + + @Test + void testRetrieveOne() throws Exception { + StemmingDictionary res = client.stemming().dictionaries("irregular-plurals").retrieve(); + assertEquals("irregular-plurals", res.getId()); + assertEquals(2, res.getWords().size()); + } + + @Test + void testRetrieveAll() throws Exception { + StemmingDictionariesRetrieveSchema res = client.stemming().dictionaries().retrieve(); + assertEquals(1, res.getDictionaries().size()); + assertEquals("irregular-plurals", res.getDictionaries().get(0)); + } +} From 88a19f6efad0e865d8b42a857388aadf5394af26 Mon Sep 17 00:00:00 2001 From: Kishore Nallan Date: Tue, 18 Feb 2025 21:25:25 +0530 Subject: [PATCH 07/15] Bump version. --- README.md | 4 ++-- build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fce2df9..d492dc0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The client is available on Maven central: ```groovy dependencies { - implementation 'org.typesense:typesense-java:1.2.0' + implementation 'org.typesense:typesense-java:1.3.0' } ``` @@ -16,7 +16,7 @@ dependencies { org.typesense typesense-java - 1.2.0 + 1.3.0 ``` diff --git a/build.gradle b/build.gradle index 4c55dc0..9a00b2d 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } group = 'org.typesense' -version = '1.2.0' +version = '1.3.0' sourceCompatibility = 1.8 targetCompatibility = 1.8 From 2a9be827f7acbf238330d149e801136418ce6486 Mon Sep 17 00:00:00 2001 From: Hayden Hung Hoang <94696449+haydenhoang@users.noreply.github.com> Date: Mon, 11 Aug 2025 19:14:15 +0700 Subject: [PATCH 08/15] feat: add support for union search (#92) * fix build error ErrorResponse is unused in the api spec and is removed in https://github.com/typesense/typesense-api-spec/pull/93. * feat(multi-search): add union search functionality and update docker-compose version --- docker-compose.yml | 12 +-- src/main/java/org/typesense/api/ApiCall.java | 39 +++++---- .../java/org/typesense/api/MultiSearch.java | 81 ++++++++++++++++++- .../org/typesense/api/MultiSearchTest.java | 53 ++++++++++-- 4 files changed, 154 insertions(+), 31 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 32d3daf..5c228c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,17 @@ -version: "3" +version: '3' services: typesense: - image: typesense/typesense:0.24.1 - container_name: "typesense" + image: typesense/typesense:29.0 + container_name: 'typesense' ports: - - "8108:8108" + - '8108:8108' volumes: - data-dir:/data environment: TYPESENSE_DATA_DIR: /data TYPESENSE_API_KEY: xyz - restart: "no" + restart: 'no' volumes: - data-dir: \ No newline at end of file + data-dir: diff --git a/src/main/java/org/typesense/api/ApiCall.java b/src/main/java/org/typesense/api/ApiCall.java index 57b2f91..23ceed3 100644 --- a/src/main/java/org/typesense/api/ApiCall.java +++ b/src/main/java/org/typesense/api/ApiCall.java @@ -7,12 +7,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.typesense.api.exceptions.*; -import org.typesense.model.ErrorResponse; import org.typesense.resources.Node; import javax.net.ssl.SSLException; import java.io.IOException; -import java.net.SocketTimeoutException; import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; @@ -21,7 +19,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; - public class ApiCall { private final Configuration configuration; @@ -61,14 +58,15 @@ public ApiCall(Configuration configuration) { mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); client = new OkHttpClient() - .newBuilder() - .connectTimeout(configuration.connectionTimeout.getSeconds(), TimeUnit.SECONDS) - .readTimeout(configuration.readTimeout.getSeconds(), TimeUnit.SECONDS) - .build(); + .newBuilder() + .connectTimeout(configuration.connectionTimeout.getSeconds(), TimeUnit.SECONDS) + .readTimeout(configuration.readTimeout.getSeconds(), TimeUnit.SECONDS) + .build(); } boolean isDueForHealthCheck(Node node) { - return Duration.between(node.lastAccessTimestamp, LocalDateTime.now()).getSeconds() > configuration.healthCheckInterval.getSeconds(); + return Duration.between(node.lastAccessTimestamp, LocalDateTime.now()) + .getSeconds() > configuration.healthCheckInterval.getSeconds(); } // Loops in a round-robin fashion to check for a healthy node and returns it @@ -161,7 +159,7 @@ R delete(String endpoint, Q queryParameters, Class responseClass) thro } T makeRequest(String endpoint, Q queryParameters, Request.Builder requestBuilder, - Class responseClass) throws Exception { + Class responseClass) throws Exception { int num_tries = 0; Exception lastException = new TypesenseError("Unknown client error", 400); @@ -174,9 +172,9 @@ T makeRequest(String endpoint, Q queryParameters, Request.Builder request String url = URI + endpoint; String fullUrl = populateQueryParameters(url, queryParameters).toString(); Request request = requestBuilder - .url(fullUrl) - .header(API_KEY_HEADER, apiKey) - .build(); + .url(fullUrl) + .header(API_KEY_HEADER, apiKey) + .build(); Response response = client.newCall(request).execute(); @@ -193,11 +191,11 @@ T makeRequest(String endpoint, Q queryParameters, Request.Builder request } catch (Exception e) { boolean handleError = (e instanceof ServerError) || - (e instanceof ServiceUnavailable) || - (e.getClass().getPackage().getName().startsWith("java.net")) || - (e instanceof SSLException); + (e instanceof ServiceUnavailable) || + (e.getClass().getPackage().getName().startsWith("java.net")) || + (e instanceof SSLException); - if(!handleError) { + if (!handleError) { // we just throw and move on throw e; } @@ -233,7 +231,7 @@ private HttpUrl.Builder populateQueryParameters(String url, T queryParameter value.append(","); } httpBuilder.addQueryParameter(entry.getKey(), value.toString()); - } else if (entry.getValue() != null){ + } else if (entry.getValue() != null) { httpBuilder.addQueryParameter(entry.getKey(), entry.getValue().toString()); } } @@ -272,3 +270,10 @@ T handleResponse(Response response, Class responseClass) throws IOExcepti } +class ErrorResponse { + private String message = null; + + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/java/org/typesense/api/MultiSearch.java b/src/main/java/org/typesense/api/MultiSearch.java index 6878876..12ac458 100644 --- a/src/main/java/org/typesense/api/MultiSearch.java +++ b/src/main/java/org/typesense/api/MultiSearch.java @@ -1,6 +1,7 @@ package org.typesense.api; import org.typesense.model.MultiSearchResult; +import org.typesense.model.SearchResult; import org.typesense.model.MultiSearchSearchesParameter; import java.util.Map; @@ -14,8 +15,84 @@ public MultiSearch(ApiCall apiCall) { this.apiCall = apiCall; } + /** + * Performs a federated multi-search, returning individual result sets for each + * query. + *

+ * This method is used for running multiple search queries in a single API call, + * where + * the results for each query are returned as separate and distinct sets. This + * is + * also known as a + * Federated Search. + *

+ * This method strictly handles non-union searches. It will validate that the + * {@code union} flag is set to {@code false}. If the flag is {@code true}, it + * will + * throw an {@link IllegalArgumentException} to prevent unexpected behavior. For + * union + * searches, use the {@link #performUnion(MultiSearchSearchesParameter, Map)} + * method instead. + * + * @param multiSearchParameters The object containing the list of search queries + * to perform. + * The {@code union} flag must be {@code false} or + * unset. + * @param common_params A map of common parameters that will be applied + * to every + * search query in the request. Can be null or + * empty. + * @return A {@link MultiSearchResult} object containing a list of individual + * search + * results. The order of results in this list is guaranteed to match the + * order of the queries sent in the request. + * @throws IllegalArgumentException if the {@code union} flag in + * {@code multiSearchParameters} + * is set to {@code true}, as this method is + * strictly for + * non-union federated searches. + * @throws Exception if there is an issue with the API call, such + * as a network + * problem or an error response from the + * server. + */ public MultiSearchResult perform(MultiSearchSearchesParameter multiSearchParameters, - Map common_params) throws Exception { - return this.apiCall.post(MultiSearch.RESOURCEPATH, multiSearchParameters, common_params, MultiSearchResult.class); + Map common_params) throws Exception { + if (multiSearchParameters.isUnion()) { + throw new IllegalArgumentException( + "The 'perform()' method is for non-union searches. For a union search, please use the 'performUnion()' method."); + } + return this.apiCall.post(MultiSearch.RESOURCEPATH, multiSearchParameters, common_params, + MultiSearchResult.class); + } + + /** + * Performs a Union Search + * and returns a single, combined search result. + *

+ * This method offers a convenient way to perform a union search. It + * automatically + * enforces {@code union=true}, merging results from all queries into a single + * ordered set of hits without requiring you to set the flag manually. + *

+ * This method is guaranteed to not modify the provided + * {@code multiSearchParameters} + * object, making it safe to reuse the same parameter object across multiple API + * calls. + * + */ + public SearchResult performUnion(MultiSearchSearchesParameter multiSearchParameters, + Map common_params) throws Exception { + // Create a shallow copy to safely enforce union=true without modifying the + // caller's original parameters. + MultiSearchSearchesParameter copiedParams = new MultiSearchSearchesParameter(); + copiedParams.setSearches(multiSearchParameters.getSearches()); + copiedParams.setUnion(true); + + return this.apiCall.post(MultiSearch.RESOURCEPATH, copiedParams, common_params, SearchResult.class); } } diff --git a/src/test/java/org/typesense/api/MultiSearchTest.java b/src/test/java/org/typesense/api/MultiSearchTest.java index 9259cbe..6b07e9c 100644 --- a/src/test/java/org/typesense/api/MultiSearchTest.java +++ b/src/test/java/org/typesense/api/MultiSearchTest.java @@ -7,14 +7,18 @@ import org.typesense.model.Field; import org.typesense.model.MultiSearchCollectionParameters; import org.typesense.model.MultiSearchResult; +import org.typesense.model.SearchResult; import org.typesense.model.MultiSearchSearchesParameter; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class MultiSearchTest { @@ -35,12 +39,23 @@ void setUp() throws Exception { CollectionSchema collectionSchema = new CollectionSchema(); collectionSchema.name("embeddings").fields(fields); client.collections().create(collectionSchema); + // create another collection for union search + collectionSchema.name("embeddings-2").fields(fields); + client.collections().create(collectionSchema); + + float[] vecVals = { 0.12f, 0.45f, 0.87f, 0.18f }; + Map doc1 = new HashMap<>(); + doc1.put("title", "Romeo and Juliet"); + doc1.put("vec", vecVals); + doc1.put("source", "embeddings_1"); + client.collections("embeddings").documents().create(doc1); - float[] vecVals = {0.12f, 0.45f, 0.87f, 0.18f}; - Map doc = new HashMap<>(); - doc.put("title", "Romeo and Juliet"); - doc.put("vec", vecVals); - client.collections("embeddings").documents().create(doc); + // Document for the second collection + Map doc2 = new HashMap<>(); + doc2.put("title", "Romeo and Juliet from collection 2"); + doc2.put("vec", new float[] { 0.12f, 0.45f, 0.87f, 0.18f }); + doc2.put("source", "embeddings_2"); + client.collections("embeddings-2").documents().create(doc2); } @AfterEach @@ -55,10 +70,36 @@ void testSearch() throws Exception { search1.setQ("*"); search1.setVectorQuery("vec:([0.96826,0.94,0.39557,0.306488], k:10)"); - MultiSearchSearchesParameter multiSearchParameters = new MultiSearchSearchesParameter().addSearchesItem(search1); + MultiSearchSearchesParameter multiSearchParameters = new MultiSearchSearchesParameter() + .addSearchesItem(search1); MultiSearchResult response = this.client.multiSearch.perform(multiSearchParameters, null); assertEquals(1, response.getResults().size()); assertEquals(1, response.getResults().get(0).getHits().size()); assertEquals("0", response.getResults().get(0).getHits().get(0).getDocument().get("id")); } + + @Test + void testUnionSearch() throws Exception { + MultiSearchCollectionParameters search1 = new MultiSearchCollectionParameters(); + search1.setCollection("embeddings"); + search1.setQ("*"); + + MultiSearchCollectionParameters search2 = new MultiSearchCollectionParameters(); + search2.setCollection("embeddings-2"); + search2.setQ("*"); + + MultiSearchSearchesParameter multiSearchParameters = new MultiSearchSearchesParameter() + .addSearchesItem(search1).addSearchesItem(search2); + SearchResult response = this.client.multiSearch.performUnion(multiSearchParameters, null); + + assertEquals(2, response.getHits().size()); + assertEquals(2, response.getUnionRequestParams().size()); + + Set sources = new HashSet<>(); + sources.add((String) response.getHits().get(0).getDocument().get("source")); + sources.add((String) response.getHits().get(1).getDocument().get("source")); + + assertTrue(sources.contains("embeddings_1"), "Results should contain a document from embeddings_1"); + assertTrue(sources.contains("embeddings_2"), "Results should contain a document from embeddings_2"); + } } From 9221a981e7bf80527f10bcfed7d986a3ca30a773 Mon Sep 17 00:00:00 2001 From: Younes Charfaoui Date: Sun, 14 Sep 2025 09:41:08 +0100 Subject: [PATCH 09/15] Added a constructor with custom http client (#86) --- src/main/java/org/typesense/api/Client.java | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/org/typesense/api/Client.java b/src/main/java/org/typesense/api/Client.java index 7215485..ae391d5 100644 --- a/src/main/java/org/typesense/api/Client.java +++ b/src/main/java/org/typesense/api/Client.java @@ -1,6 +1,8 @@ package org.typesense.api; +import okhttp3.OkHttpClient; + import java.util.HashMap; import java.util.Map; @@ -32,6 +34,26 @@ public class Client { public Debug debug; public MultiSearch multiSearch; + public Client(Configuration configuration, OkHttpClient okHttpClient){ + this.configuration = configuration; + this.apiCall = new ApiCall(configuration, okHttpClient); + collections = new Collections(apiCall); + this.individualCollections = new HashMap<>(); + this.aliases = new Aliases(this.apiCall); + this.individualAliases = new HashMap<>(); + this.keys = new Keys(this.apiCall); + this.individualKeys = new HashMap<>(); + this.health = new Health(this.apiCall); + this.operations = new Operations(this.apiCall); + this.metrics = new Metrics(this.apiCall); + this.debug = new Debug(this.apiCall); + this.multiSearch = new MultiSearch(this.apiCall); + this.analytics = new Analytics(this.apiCall); + this.stemming = new Stemming(this.apiCall); + this.stopwords = new Stopwords(this.apiCall); + this.individualStopwordsSets = new HashMap<>(); + } + public Client(Configuration configuration){ this.configuration = configuration; this.apiCall = new ApiCall(configuration); From 0c51f1a68cff9e01451c61f1bee17fa56ffb511e Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos <93339170+tharropoulos@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:22:25 +0200 Subject: [PATCH 10/15] feat!: add new v30 changes (#93) * feat(synonyms): add new synonym set classes * feat(client): register synonym set classes to client object * feat(test): add helper method to skip deprecated tests * feat(test): add util for creating test synonym sets * feat(test): cleanup synonym sets after tests * test(synonyms): add tests for synonym sets * fix(test): skip old synonym tests on v30 and above * feat(synonyms): add deprecation logging and lsp info on old synonyms * feat(serializer): add custom serializer for analytics rules * feat(analytics): update analytic event class to new api * feat(analytics): update analytic rule classes to new api * feat(test): add test helper methods for new analytics * test(analytics): test new analytics api implementation * refactor(test): use `Arrays.asList` instead of `List.of` for java 8 compat * chore(test): only remove existing resources on teardown * chore(test): remove old curation tests * chore(test): remove old curation test helpers * chore(curation): remove old override classes * feat(curation): add curation set classes * test(curation): test curation sets * ci: update typesense to v30 * fix(test): handle both prefixed and non-prefixed versions on debug check --- .github/workflows/test.yml | 2 +- .../org/typesense/api/AnalyticsEvents.java | 11 +- .../java/org/typesense/api/AnalyticsRule.java | 29 +- .../api/AnalyticsRuleSerializer.java | 138 ++++++++++ .../org/typesense/api/AnalyticsRules.java | 75 +++++- src/main/java/org/typesense/api/Client.java | 40 +++ .../java/org/typesense/api/Collection.java | 40 +-- .../java/org/typesense/api/CurationSet.java | 33 +++ .../java/org/typesense/api/CurationSets.java | 26 ++ src/main/java/org/typesense/api/Override.java | 30 --- .../java/org/typesense/api/Overrides.java | 33 --- src/main/java/org/typesense/api/Synonym.java | 9 + .../java/org/typesense/api/SynonymSet.java | 34 +++ .../java/org/typesense/api/SynonymSets.java | 27 ++ src/main/java/org/typesense/api/Synonyms.java | 9 + .../typesense/api/AnalyticsEventsTest.java | 32 ++- .../org/typesense/api/AnalyticsRulesTest.java | 253 +++++++++++------- .../org/typesense/api/CurationSetTest.java | 131 +++++++++ .../org/typesense/api/CurationSetsTest.java | 92 +++++++ src/test/java/org/typesense/api/Helper.java | 188 ++++++++++--- .../java/org/typesense/api/OverridesTest.java | 65 ----- .../org/typesense/api/SynonymSetTest.java | 93 +++++++ .../org/typesense/api/SynonymSetsTest.java | 80 ++++++ .../java/org/typesense/api/SynonymsTest.java | 7 +- 24 files changed, 1166 insertions(+), 311 deletions(-) create mode 100644 src/main/java/org/typesense/api/AnalyticsRuleSerializer.java create mode 100644 src/main/java/org/typesense/api/CurationSet.java create mode 100644 src/main/java/org/typesense/api/CurationSets.java delete mode 100644 src/main/java/org/typesense/api/Override.java delete mode 100644 src/main/java/org/typesense/api/Overrides.java create mode 100644 src/main/java/org/typesense/api/SynonymSet.java create mode 100644 src/main/java/org/typesense/api/SynonymSets.java create mode 100644 src/test/java/org/typesense/api/CurationSetTest.java create mode 100644 src/test/java/org/typesense/api/CurationSetsTest.java delete mode 100644 src/test/java/org/typesense/api/OverridesTest.java create mode 100644 src/test/java/org/typesense/api/SynonymSetTest.java create mode 100644 src/test/java/org/typesense/api/SynonymSetsTest.java diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a68e42..ff5425d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: java: ['8', '11', '17'] services: typesense: - image: typesense/typesense:28.0.rc36 + image: typesense/typesense:30.0.alpha1 ports: - 8108:8108/tcp volumes: diff --git a/src/main/java/org/typesense/api/AnalyticsEvents.java b/src/main/java/org/typesense/api/AnalyticsEvents.java index 4889aa3..de2e295 100644 --- a/src/main/java/org/typesense/api/AnalyticsEvents.java +++ b/src/main/java/org/typesense/api/AnalyticsEvents.java @@ -1,8 +1,9 @@ package org.typesense.api; +import org.typesense.model.AnalyticsEvent; import org.typesense.model.AnalyticsEventCreateResponse; -import org.typesense.model.AnalyticsEventCreateSchema; - +import org.typesense.model.AnalyticsEventsResponse; +import java.util.Map; public class AnalyticsEvents { private final ApiCall apiCall; @@ -12,7 +13,11 @@ public AnalyticsEvents(ApiCall apiCall) { this.apiCall = apiCall; } - public AnalyticsEventCreateResponse create(AnalyticsEventCreateSchema event) throws Exception { + public AnalyticsEventCreateResponse create(AnalyticsEvent event) throws Exception { return this.apiCall.post(RESOURCE_PATH, event, null, AnalyticsEventCreateResponse.class); } + + public AnalyticsEventsResponse retrieve(Map params) throws Exception { + return this.apiCall.get(RESOURCE_PATH, params, AnalyticsEventsResponse.class); + } } diff --git a/src/main/java/org/typesense/api/AnalyticsRule.java b/src/main/java/org/typesense/api/AnalyticsRule.java index 2ed2f28..cc9ef79 100644 --- a/src/main/java/org/typesense/api/AnalyticsRule.java +++ b/src/main/java/org/typesense/api/AnalyticsRule.java @@ -1,29 +1,42 @@ package org.typesense.api; import org.typesense.api.utils.URLEncoding; -import org.typesense.model.AnalyticsRuleDeleteResponse; -import org.typesense.model.AnalyticsRuleSchema; +import org.typesense.model.AnalyticsRuleUpdate; public class AnalyticsRule { private final ApiCall apiCall; private final String ruleId; + private final AnalyticsRuleSerializer serializer; public AnalyticsRule(String ruleId, ApiCall apiCall) { - this.apiCall = apiCall; + this.apiCall = apiCall; this.ruleId = ruleId; + this.serializer = new AnalyticsRuleSerializer(); + } + + public AnalyticsRule(String ruleId, ApiCall apiCall, AnalyticsRuleSerializer serializer) { + this.apiCall = apiCall; + this.ruleId = ruleId; + this.serializer = serializer; } + public org.typesense.model.AnalyticsRule retrieve() throws Exception { + String response = this.apiCall.get(this.getEndpoint(), null, String.class); + return serializer.parseFromJson(response); + } - public AnalyticsRuleSchema retrieve() throws Exception { - return this.apiCall.get(this.getEndpoint(), null, AnalyticsRuleSchema.class); + public org.typesense.model.AnalyticsRule delete() throws Exception { + String response = this.apiCall.delete(this.getEndpoint(), null, String.class); + org.typesense.model.AnalyticsRule result = new org.typesense.model.AnalyticsRule(); + result.name(this.ruleId); + return result; } - public AnalyticsRuleDeleteResponse delete() throws Exception { - return this.apiCall.delete(this.getEndpoint(), null, AnalyticsRuleDeleteResponse.class); + public org.typesense.model.AnalyticsRule update(AnalyticsRuleUpdate rule) throws Exception { + return this.apiCall.put(this.getEndpoint(), rule, null, org.typesense.model.AnalyticsRule.class); } private String getEndpoint() { return AnalyticsRules.RESOURCE_PATH + "/" + URLEncoding.encodeURIComponent(ruleId); } - } diff --git a/src/main/java/org/typesense/api/AnalyticsRuleSerializer.java b/src/main/java/org/typesense/api/AnalyticsRuleSerializer.java new file mode 100644 index 0000000..c3719e8 --- /dev/null +++ b/src/main/java/org/typesense/api/AnalyticsRuleSerializer.java @@ -0,0 +1,138 @@ +package org.typesense.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.DeserializationFeature; +import org.typesense.model.AnalyticsRule; +import org.typesense.model.AnalyticsRuleCreate; +import org.typesense.model.AnalyticsRuleCreateParams; +import java.util.List; +import java.util.ArrayList; + +/** + * Serializer for AnalyticsRule objects to avoid Jackson type discriminator issues + */ +public class AnalyticsRuleSerializer { + + private final ObjectMapper objectMapper; + + public AnalyticsRuleSerializer() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + this.objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); + } + + /** + * Parse a single AnalyticsRule from JsonNode + */ + public AnalyticsRule parseFromJsonNode(JsonNode node) { + AnalyticsRule rule = new AnalyticsRule(); + + if (node.has("name")) { + rule.name(node.get("name").asText()); + } + + if (node.has("type")) { + String typeStr = node.get("type").asText(); + if ("counter".equals(typeStr)) { + rule.type(AnalyticsRuleCreate.TypeEnum.COUNTER); + } else if ("popular_queries".equals(typeStr)) { + rule.type(AnalyticsRuleCreate.TypeEnum.POPULAR_QUERIES); + } else if ("nohits_queries".equals(typeStr)) { + rule.type(AnalyticsRuleCreate.TypeEnum.NOHITS_QUERIES); + } else if ("log".equals(typeStr)) { + rule.type(AnalyticsRuleCreate.TypeEnum.LOG); + } + } + + if (node.has("collection")) { + rule.collection(node.get("collection").asText()); + } + + if (node.has("event_type")) { + rule.eventType(node.get("event_type").asText()); + } + + if (node.has("rule_tag")) { + rule.ruleTag(node.get("rule_tag").asText()); + } + + if (node.has("params")) { + JsonNode paramsNode = node.get("params"); + AnalyticsRuleCreateParams params = new AnalyticsRuleCreateParams(); + + if (paramsNode.has("counter_field")) { + params.counterField(paramsNode.get("counter_field").asText()); + } + + if (paramsNode.has("weight")) { + params.weight(paramsNode.get("weight").asInt()); + } + + if (paramsNode.has("destination_collection")) { + params.destinationCollection(paramsNode.get("destination_collection").asText()); + } + + if (paramsNode.has("limit")) { + params.limit(paramsNode.get("limit").asInt()); + } + + if (paramsNode.has("capture_search_requests")) { + params.captureSearchRequests(paramsNode.get("capture_search_requests").asBoolean()); + } + + if (paramsNode.has("meta_fields")) { + List metaFields = new ArrayList<>(); + JsonNode metaFieldsNode = paramsNode.get("meta_fields"); + if (metaFieldsNode.isArray()) { + for (JsonNode metaField : metaFieldsNode) { + metaFields.add(metaField.asText()); + } + } + params.metaFields(metaFields); + } + + if (paramsNode.has("expand_query")) { + params.expandQuery(paramsNode.get("expand_query").asBoolean()); + } + + rule.params(params); + } + + return rule; + } + + /** + * Parse AnalyticsRule from JSON string + */ + public AnalyticsRule parseFromJson(String jsonResponse) throws Exception { + JsonNode rootNode = objectMapper.readTree(jsonResponse); + return parseFromJsonNode(rootNode); + } + + /** + * Parse a list of AnalyticsRule objects from JSON string + */ + public List parseListFromJson(String jsonResponse) throws Exception { + JsonNode rootNode = objectMapper.readTree(jsonResponse); + List rules = new ArrayList<>(); + + if (rootNode.isArray()) { + for (JsonNode item : rootNode) { + if (item.has("error")) { + String error = item.get("error").asText(); + throw new RuntimeException("Analytics rule parsing failed: " + error); + } else { + AnalyticsRule rule = parseFromJsonNode(item); + rules.add(rule); + } + } + } else { + // Single object response + AnalyticsRule rule = parseFromJsonNode(rootNode); + rules.add(rule); + } + + return rules; + } +} \ No newline at end of file diff --git a/src/main/java/org/typesense/api/AnalyticsRules.java b/src/main/java/org/typesense/api/AnalyticsRules.java index f910b05..7c113c2 100644 --- a/src/main/java/org/typesense/api/AnalyticsRules.java +++ b/src/main/java/org/typesense/api/AnalyticsRules.java @@ -1,30 +1,83 @@ package org.typesense.api; -import org.typesense.api.utils.URLEncoding; -import org.typesense.model.AnalyticsRuleSchema; -import org.typesense.model.AnalyticsRuleUpsertSchema; -import org.typesense.model.AnalyticsRulesRetrieveSchema; +import org.typesense.model.AnalyticsRule; +import org.typesense.model.AnalyticsRuleCreate; +import java.util.List; public class AnalyticsRules { private final ApiCall apiCall; + private final AnalyticsRuleSerializer serializer; public final static String RESOURCE_PATH = "/analytics/rules"; public AnalyticsRules(ApiCall apiCall) { this.apiCall = apiCall; + this.serializer = new AnalyticsRuleSerializer(); + } + + public AnalyticsRules(ApiCall apiCall, AnalyticsRuleSerializer serializer) { + this.apiCall = apiCall; + this.serializer = serializer; } - public AnalyticsRuleSchema create(AnalyticsRuleSchema rule) throws Exception { - return this.apiCall.post(RESOURCE_PATH, rule, null, AnalyticsRuleSchema.class); + public AnalyticsRulesResponse create(List rules) throws Exception { + String response = this.apiCall.post(RESOURCE_PATH, rules, null, String.class); + return parseCreateResponse(response); } - public AnalyticsRuleSchema upsert(String name, AnalyticsRuleUpsertSchema rule) throws Exception { - return this.apiCall.put(RESOURCE_PATH + "/" + URLEncoding.encodeURIComponent(name), rule, null, - AnalyticsRuleSchema.class); + public List retrieve() throws Exception { + String response = this.apiCall.get(RESOURCE_PATH, null, String.class); + return parseRetrieveResponse(response); } - public AnalyticsRulesRetrieveSchema retrieve() throws Exception { - return this.apiCall.get(RESOURCE_PATH, null, AnalyticsRulesRetrieveSchema.class); + /** + * Parse the create response which can be either a single AnalyticsRule or an array + */ + private AnalyticsRulesResponse parseCreateResponse(String jsonResponse) throws Exception { + List rules = serializer.parseListFromJson(jsonResponse); + + for (AnalyticsRule rule : rules) { + if (rule.getName() == null) { + throw new RuntimeException("Analytics rule creation failed: rule name is null"); + } + } + + return new AnalyticsRulesResponse(rules); + } + + /** + * Parse the retrieve response which is always an array of AnalyticsRule objects + */ + private List parseRetrieveResponse(String jsonResponse) throws Exception { + List rules = serializer.parseListFromJson(jsonResponse); + + return rules; } + /** + * Response wrapper for analytics rules operations + */ + public static class AnalyticsRulesResponse { + private final List rules; + + public AnalyticsRulesResponse(List rules) { + this.rules = rules; + } + + public List getRules() { + return rules; + } + + public AnalyticsRule getFirstRule() { + return rules.isEmpty() ? null : rules.get(0); + } + + public int getCount() { + return rules.size(); + } + + public boolean isEmpty() { + return rules.isEmpty(); + } + } } diff --git a/src/main/java/org/typesense/api/Client.java b/src/main/java/org/typesense/api/Client.java index ae391d5..b892628 100644 --- a/src/main/java/org/typesense/api/Client.java +++ b/src/main/java/org/typesense/api/Client.java @@ -28,6 +28,12 @@ public class Client { private Stopwords stopwords; private Map individualStopwordsSets; + private SynonymSets synonymSets; + private Map individualSynonymSets; + + private CurationSets curationSets; + private Map individualCurationSets; + public Health health; public Operations operations; public Metrics metrics; @@ -72,6 +78,10 @@ public Client(Configuration configuration){ this.stemming = new Stemming(this.apiCall); this.stopwords = new Stopwords(this.apiCall); this.individualStopwordsSets = new HashMap<>(); + this.synonymSets = new SynonymSets(this.apiCall); + this.individualSynonymSets = new HashMap<>(); + this.curationSets = new CurationSets(this.apiCall); + this.individualCurationSets = new HashMap<>(); } public Collection collections(String name){ @@ -143,4 +153,34 @@ public StopwordsSet stopwords(String stopwordsSetId) { retVal = this.individualStopwordsSets.get(stopwordsSetId); return retVal; } + + public SynonymSets synonymSets() { + return this.synonymSets; + } + + public SynonymSet synonymSet(String synonymSetName) { + SynonymSet retVal; + + if (!this.individualSynonymSets.containsKey(synonymSetName)) { + this.individualSynonymSets.put(synonymSetName, new SynonymSet(synonymSetName, this.apiCall)); + } + + retVal = this.individualSynonymSets.get(synonymSetName); + return retVal; + } + + public CurationSets curationSets() { + return this.curationSets; + } + + public CurationSet curationSet(String curationSetName) { + CurationSet retVal; + + if (!this.individualCurationSets.containsKey(curationSetName)) { + this.individualCurationSets.put(curationSetName, new CurationSet(curationSetName, this.apiCall)); + } + + retVal = this.individualCurationSets.get(curationSetName); + return retVal; + } } diff --git a/src/main/java/org/typesense/api/Collection.java b/src/main/java/org/typesense/api/Collection.java index 6f2acab..cc5524f 100644 --- a/src/main/java/org/typesense/api/Collection.java +++ b/src/main/java/org/typesense/api/Collection.java @@ -21,9 +21,6 @@ public class Collection { private Synonyms synonyms; private Map individualSynonyms; - private Overrides overrides; - private Map individualOverrides; - private final String endpoint; Collection(String name, ApiCall apiCall, Configuration configuration) { @@ -35,8 +32,6 @@ public class Collection { this.individualDocuments = new HashMap<>(); this.synonyms = new Synonyms(this.name, this.apiCall); this.individualSynonyms = new HashMap<>(); - this.overrides = new Overrides(this.name, this.apiCall); - this.individualOverrides = new HashMap<>(); } public CollectionResponse retrieve() throws Exception { @@ -66,11 +61,31 @@ public Document documents(String documentId) { return retVal; } + /** + * @deprecated This method is deprecated and will be removed in a future version. + * Use {@link Client#synonymSets()} instead for the new synonym sets API. + * + * Note: The old synonyms API is only available on Typesense v29.0 and below. + * For Typesense v30.0 and above, use the new synonym sets API. + */ + @Deprecated public Synonyms synonyms() { + System.err.println("DEPRECATED: Using deprecated synonyms API. This API is only available on Typesense v29.0 and below. " + + "For Typesense v30.0 and above, use the new synonym sets API via Client.synonymSets()."); return this.synonyms; } + /** + * @deprecated This method is deprecated and will be removed in a future version. + * Use {@link Client#synonymSet(String)} instead for the new synonym sets API. + * + * Note: The old synonyms API is only available on Typesense v29.0 and below. + * For Typesense v30.0 and above, use the new synonym sets API. + */ + @Deprecated public Synonym synonyms(String synonymId) { + System.err.println("DEPRECATED: Using deprecated synonyms API. This API is only available on Typesense v29.0 and below. " + + "For Typesense v30.0 and above, use the new synonym sets API via Client.synonymSet(String)."); Synonym retVal; if (!this.individualSynonyms.containsKey(synonymId)) { @@ -80,19 +95,4 @@ public Synonym synonyms(String synonymId) { retVal = this.individualSynonyms.get(synonymId); return retVal; } - - public Overrides overrides() { - return this.overrides; - } - - public Override overrides(String overrideId) { - Override retVal; - - if (!this.individualOverrides.containsKey(overrideId)) { - this.individualOverrides.put(overrideId, new Override(this.name, overrideId, this.apiCall)); - } - - retVal = this.individualOverrides.get(overrideId); - return retVal; - } } diff --git a/src/main/java/org/typesense/api/CurationSet.java b/src/main/java/org/typesense/api/CurationSet.java new file mode 100644 index 0000000..8d9f8a1 --- /dev/null +++ b/src/main/java/org/typesense/api/CurationSet.java @@ -0,0 +1,33 @@ +package org.typesense.api; + +import org.typesense.api.utils.URLEncoding; +import org.typesense.model.CurationSetCreateSchema; +import org.typesense.model.CurationSetSchema; +import org.typesense.model.CurationSetDeleteSchema; + +public class CurationSet { + + private String curationSetName; + private ApiCall apiCall; + + public CurationSet(String curationSetName, ApiCall apiCall) { + this.curationSetName = curationSetName; + this.apiCall = apiCall; + } + + public CurationSetCreateSchema retrieve() throws Exception { + return this.apiCall.get(this.getEndpoint(), null, CurationSetCreateSchema.class); + } + + public CurationSetSchema upsert(CurationSetCreateSchema curationSetCreateSchema) throws Exception { + return this.apiCall.put(this.getEndpoint(), curationSetCreateSchema, null, CurationSetSchema.class); + } + + public CurationSetDeleteSchema delete() throws Exception { + return this.apiCall.delete(this.getEndpoint(), null, CurationSetDeleteSchema.class); + } + + public String getEndpoint() { + return "/curation_sets/" + URLEncoding.encodeURIComponent(this.curationSetName); + } +} diff --git a/src/main/java/org/typesense/api/CurationSets.java b/src/main/java/org/typesense/api/CurationSets.java new file mode 100644 index 0000000..5bcb197 --- /dev/null +++ b/src/main/java/org/typesense/api/CurationSets.java @@ -0,0 +1,26 @@ +package org.typesense.api; + +import org.typesense.model.CurationSetCreateSchema; +import org.typesense.model.CurationSetSchema; + +public class CurationSets { + + private ApiCall apiCall; + public final static String RESOURCEPATH = "/curation_sets"; + + public CurationSets(ApiCall apiCall) { + this.apiCall = apiCall; + } + + public CurationSetSchema upsert(String curationSetName, CurationSetCreateSchema curationSetCreateSchema) throws Exception { + return this.apiCall.put(getEndpoint(curationSetName), curationSetCreateSchema, null, CurationSetSchema.class); + } + + public CurationSetSchema[] retrieve() throws Exception { + return this.apiCall.get(this.getEndpoint(null), null, CurationSetSchema[].class); + } + + public String getEndpoint(String operation) { + return RESOURCEPATH + "/" + (operation == null ? "" : operation); + } +} diff --git a/src/main/java/org/typesense/api/Override.java b/src/main/java/org/typesense/api/Override.java deleted file mode 100644 index e81fb96..0000000 --- a/src/main/java/org/typesense/api/Override.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.typesense.api; - -import org.typesense.api.utils.URLEncoding; -import org.typesense.model.SearchOverride; - -public class Override { - - private String collectionName; - private String overrideId; - private ApiCall apiCall; - - public Override(String collectionName, String overrideId, ApiCall apiCall) { - this.collectionName = collectionName; - this.overrideId = overrideId; - this.apiCall = apiCall; - } - - public SearchOverride retrieve() throws Exception { - return this.apiCall.get(this.getEndpoint(), null, SearchOverride.class); - } - - public SearchOverride delete() throws Exception { - return this.apiCall.delete(this.getEndpoint(), null, SearchOverride.class); - } - - public String getEndpoint() { - return Collections.RESOURCE_PATH + "/" + URLEncoding.encodeURIComponent(this.collectionName) + "/" - + Overrides.RESOURCEPATH + "/" + URLEncoding.encodeURIComponent(this.overrideId); - } -} diff --git a/src/main/java/org/typesense/api/Overrides.java b/src/main/java/org/typesense/api/Overrides.java deleted file mode 100644 index 5bb9acd..0000000 --- a/src/main/java/org/typesense/api/Overrides.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.typesense.api; - -import org.typesense.api.utils.URLEncoding; -import org.typesense.model.SearchOverride; -import org.typesense.model.SearchOverrideSchema; -import org.typesense.model.SearchOverridesResponse; - -public class Overrides { - - public static String RESOURCEPATH = "/overrides"; - - private String collectionName; - private ApiCall apiCall; - - public Overrides(String collectionName, ApiCall apiCall) { - this.collectionName = collectionName; - this.apiCall = apiCall; - } - - public SearchOverride upsert(String overrideId, SearchOverrideSchema searchOverrideSchema) throws Exception { - return this.apiCall.put(getEndpoint(overrideId), searchOverrideSchema, null, SearchOverride.class); - } - - public SearchOverridesResponse retrieve() throws Exception { - return this.apiCall.get(this.getEndpoint(null), null, SearchOverridesResponse.class); - } - - public String getEndpoint(String operation) { - return Collections.RESOURCE_PATH + "/" + URLEncoding.encodeURIComponent(this.collectionName) - + Overrides.RESOURCEPATH + "/" + (operation == null ? "" - : URLEncoding.encodeURIComponent(operation)); - } -} diff --git a/src/main/java/org/typesense/api/Synonym.java b/src/main/java/org/typesense/api/Synonym.java index d7e0340..1a8affe 100644 --- a/src/main/java/org/typesense/api/Synonym.java +++ b/src/main/java/org/typesense/api/Synonym.java @@ -3,6 +3,15 @@ import org.typesense.api.utils.URLEncoding; import org.typesense.model.SearchSynonym; +/** + * @deprecated This class is deprecated and will be removed in a future version. + * Use {@link SynonymSet} instead for the new synonym sets API. + * + * Note: The old synonyms API is only available on Typesense v29.0 and below. + * For Typesense v30.0 and above, use the new synonym sets API. + */ +@Deprecated + public class Synonym { private String collectionName; diff --git a/src/main/java/org/typesense/api/SynonymSet.java b/src/main/java/org/typesense/api/SynonymSet.java new file mode 100644 index 0000000..4672409 --- /dev/null +++ b/src/main/java/org/typesense/api/SynonymSet.java @@ -0,0 +1,34 @@ +package org.typesense.api; + +import org.typesense.api.utils.URLEncoding; + +import org.typesense.model.SynonymSetCreateSchema; +import org.typesense.model.SynonymSetSchema; +import org.typesense.model.SynonymSetDeleteSchema; + +public class SynonymSet { + + private String synonymSetName; + private ApiCall apiCall; + + public SynonymSet(String synonymSetName, ApiCall apiCall) { + this.synonymSetName = synonymSetName; + this.apiCall = apiCall; + } + + public SynonymSetCreateSchema retrieve() throws Exception { + return this.apiCall.get(this.getEndpoint(), null, SynonymSetCreateSchema.class); + } + + public SynonymSetSchema upsert(SynonymSetCreateSchema synonymSetCreateSchema) throws Exception { + return this.apiCall.put(this.getEndpoint(), synonymSetCreateSchema, null, SynonymSetSchema.class); + } + + public SynonymSetDeleteSchema delete() throws Exception { + return this.apiCall.delete(this.getEndpoint(), null, SynonymSetDeleteSchema.class); + } + + public String getEndpoint() { + return "/synonym_sets/" + URLEncoding.encodeURIComponent(this.synonymSetName); + } +} \ No newline at end of file diff --git a/src/main/java/org/typesense/api/SynonymSets.java b/src/main/java/org/typesense/api/SynonymSets.java new file mode 100644 index 0000000..764cb3c --- /dev/null +++ b/src/main/java/org/typesense/api/SynonymSets.java @@ -0,0 +1,27 @@ +package org.typesense.api; + +import java.util.List; +import org.typesense.model.SynonymSetCreateSchema; +import org.typesense.model.SynonymSetSchema; + +public class SynonymSets { + + private ApiCall apiCall; + public final static String RESOURCEPATH = "/synonym_sets"; + + public SynonymSets(ApiCall apiCall) { + this.apiCall = apiCall; + } + + public SynonymSetSchema upsert(String synonymSetName, SynonymSetCreateSchema synonymSetCreateSchema) throws Exception { + return this.apiCall.put(getEndpoint(synonymSetName), synonymSetCreateSchema, null, SynonymSetSchema.class); + } + + public SynonymSetSchema[] retrieve() throws Exception { + return this.apiCall.get(this.getEndpoint(null), null, SynonymSetSchema[].class); + } + + public String getEndpoint(String operation) { + return RESOURCEPATH + "/" + (operation == null ? "" : operation); + } +} \ No newline at end of file diff --git a/src/main/java/org/typesense/api/Synonyms.java b/src/main/java/org/typesense/api/Synonyms.java index 7f5a9ac..e46fc8b 100644 --- a/src/main/java/org/typesense/api/Synonyms.java +++ b/src/main/java/org/typesense/api/Synonyms.java @@ -5,6 +5,15 @@ import org.typesense.model.SearchSynonymSchema; import org.typesense.model.SearchSynonymsResponse; +/** + * @deprecated This class is deprecated and will be removed in a future version. + * Use {@link SynonymSets} instead for the new synonym sets API. + * + * Note: The old synonyms API is only available on Typesense v29.0 and below. + * For Typesense v30.0 and above, use the new synonym sets API. + */ +@Deprecated + public class Synonyms { private String collectionName; diff --git a/src/test/java/org/typesense/api/AnalyticsEventsTest.java b/src/test/java/org/typesense/api/AnalyticsEventsTest.java index 2f88002..036a2ee 100644 --- a/src/test/java/org/typesense/api/AnalyticsEventsTest.java +++ b/src/test/java/org/typesense/api/AnalyticsEventsTest.java @@ -7,22 +7,34 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.typesense.model.AnalyticsEvent; import org.typesense.model.AnalyticsEventCreateResponse; -import org.typesense.model.AnalyticsEventCreateSchema; +import org.typesense.model.AnalyticsEventData; public class AnalyticsEventsTest { private Client client; private Helper helper; + + private String ruleName; @BeforeEach void setUp() throws Exception { helper = new Helper(); client = helper.getClient(); + + if (!Helper.isV30OrAbove(client)) { + org.junit.jupiter.api.Assumptions.assumeTrue(false, "Skipping test - requires Typesense v30 or above"); + } + helper.teardown(); helper.createTestCollection(); helper.createTestQueryCollection(); - helper.createTestAnalyticsRule(); + helper.createTestQueryDocument(); + helper.createTestAnalyticsCollection(); + String ruleName = helper.createTestAnalyticsRule(); + + this.ruleName = ruleName; } @AfterEach @@ -32,17 +44,19 @@ void tearDown() throws Exception { @Test void testCreate() throws Exception { - HashMap eventData = new HashMap<>(); - eventData.put("q", "running shoes"); - eventData.put("user_id", "1234"); - AnalyticsEventCreateSchema analyticsEvent = new AnalyticsEventCreateSchema() - .type("search") - .name("products_search_event") + + AnalyticsEventData eventData = new AnalyticsEventData() + .q("running shoes") + .userId("1234") + .docId("query1"); + + AnalyticsEvent analyticsEvent = new AnalyticsEvent() + .eventType("search") + .name(ruleName) .data(eventData); AnalyticsEventCreateResponse result = this.client.analytics().events().create(analyticsEvent); assertNotNull(result); assertEquals(true, result.isOk()); - } } diff --git a/src/test/java/org/typesense/api/AnalyticsRulesTest.java b/src/test/java/org/typesense/api/AnalyticsRulesTest.java index 34f5dd2..199ea0a 100644 --- a/src/test/java/org/typesense/api/AnalyticsRulesTest.java +++ b/src/test/java/org/typesense/api/AnalyticsRulesTest.java @@ -1,21 +1,31 @@ package org.typesense.api; -import java.util.Arrays; - import org.junit.jupiter.api.AfterEach; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.typesense.model.AnalyticsRuleDeleteResponse; -import org.typesense.model.AnalyticsRuleParameters; -import org.typesense.model.AnalyticsRuleParametersDestination; -import org.typesense.model.AnalyticsRuleParametersSource; -import org.typesense.model.AnalyticsRuleSchema; -import org.typesense.model.AnalyticsRuleUpsertSchema; -import org.typesense.model.AnalyticsRulesRetrieveSchema; -public class AnalyticsRulesTest { +import org.typesense.model.AnalyticsRule; +import org.typesense.model.AnalyticsRuleCreate; +import org.typesense.model.AnalyticsRuleCreateParams; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import org.typesense.model.InlineResponse2003; +import org.typesense.model.CollectionSchema; +import org.typesense.model.Field; +import java.util.Arrays; +import java.util.Map; +import java.util.HashMap; + + +class AnalyticsRulesTest { private Client client; private Helper helper; @@ -24,9 +34,35 @@ public class AnalyticsRulesTest { void setUp() throws Exception { helper = new Helper(); client = helper.getClient(); - helper.teardown(); - helper.createTestCollection(); - helper.createTestQueryCollection(); + + if (!Helper.isV30OrAbove(client)) { + org.junit.jupiter.api.Assumptions.assumeTrue(false, "Skipping test - requires Typesense v30 or above"); + } + + try { + CollectionSchema collectionSchema = new CollectionSchema() + .name("companies") + .fields(Arrays.asList( + new Field().name("company_name").type("string"), + new Field().name("num_employees").type("int32"), + new Field().name("country").type("string") + )); + client.collections().create(collectionSchema); + } catch (Exception e) { + } + + try { + CollectionSchema analyticsCollectionSchema = new CollectionSchema() + .name("analytics_data") + .fields(Arrays.asList( + new Field().name("rule_name").type("string"), + new Field().name("event_type").type("string"), + new Field().name("counter_value").type("int32"), + new Field().name("timestamp").type("int64") + )); + client.collections().create(analyticsCollectionSchema); + } catch (Exception e) { + } } @AfterEach @@ -36,96 +72,127 @@ void tearDown() throws Exception { @Test void testCreate() throws Exception { - AnalyticsRuleSchema analyticsRuleSchema = new AnalyticsRuleSchema(); - analyticsRuleSchema.setName("nohits-queries"); - analyticsRuleSchema.setType(AnalyticsRuleSchema.TypeEnum.NOHITS_QUERIES); - analyticsRuleSchema.setParams(new AnalyticsRuleParameters() - .source(new AnalyticsRuleParametersSource() - .collections(Arrays.asList("books"))) - .destination(new AnalyticsRuleParametersDestination() - .collection("queries"))); - - AnalyticsRuleSchema result = this.client.analytics().rules().create(analyticsRuleSchema); + + String ruleName = "test-rule-" + System.currentTimeMillis(); + + AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() + .name(ruleName) + .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .collection("analytics_data") + .eventType("click") + .params(new AnalyticsRuleCreateParams() + .counterField("num_employees") + .weight(1)); + + List rules = new ArrayList<>(); + rules.add(analyticsRule); + + AnalyticsRules.AnalyticsRulesResponse result = this.client.analytics().rules().create(rules); assertNotNull(result); - assertEquals("nohits-queries", result.getName()); - - AnalyticsRuleParameters params = result.getParams(); - assertNotNull(params); - - assertNotNull(params.getSource()); - assertEquals(Arrays.asList("books"), params.getSource().getCollections()); - - assertNotNull(params.getDestination()); - assertEquals("queries", params.getDestination().getCollection()); - } - - @Test - void testUpsert() throws Exception { - AnalyticsRuleUpsertSchema analyticsRuleSchema = new AnalyticsRuleUpsertSchema() - .type(AnalyticsRuleUpsertSchema.TypeEnum.NOHITS_QUERIES) - .params(new AnalyticsRuleParameters() - .source(new AnalyticsRuleParametersSource() - .collections(Arrays.asList("books"))) - .destination(new AnalyticsRuleParametersDestination() - .collection("queries"))); - - AnalyticsRuleSchema result = this.client.analytics().rules().upsert("nohits-queries", analyticsRuleSchema); - assertNotNull(result); - assertEquals("nohits-queries", result.getName()); - - AnalyticsRuleParameters params = result.getParams(); - assertNotNull(params); - - assertNotNull(params.getSource()); - assertEquals(Arrays.asList("books"), params.getSource().getCollections()); - - assertNotNull(params.getDestination()); - assertEquals("queries", params.getDestination().getCollection()); + assertFalse(result.isEmpty()); + assertEquals(1, result.getCount()); + + AnalyticsRule createdRule = result.getFirstRule(); + assertNotNull(createdRule); + assertEquals(ruleName, createdRule.getName()); + assertEquals(AnalyticsRuleCreate.TypeEnum.COUNTER, createdRule.getType()); + assertEquals("analytics_data", createdRule.getCollection()); + assertEquals("click", createdRule.getEventType()); } @Test void testRetrieve() throws Exception { - helper.createTestAnalyticsRule(); - AnalyticsRuleSchema result = this.client.analytics().rules("analytics-rule").retrieve(); - + + String ruleName = "test-rule-" + System.currentTimeMillis(); + + AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() + .name(ruleName) + .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .collection("analytics_data") + .eventType("click") + .params(new AnalyticsRuleCreateParams() + .counterField("num_employees") + .weight(1)); + + List rules = new ArrayList<>(); + rules.add(analyticsRule); + + this.client.analytics().rules().create(rules); + + List result = this.client.analytics().rules().retrieve(); assertNotNull(result); - assertEquals("analytics-rule", result.getName()); - - AnalyticsRuleParameters params = result.getParams(); - assertNotNull(params); - - assertNotNull(params.getSource()); - assertEquals(Arrays.asList("books"), params.getSource().getCollections()); - - assertNotNull(params.getDestination()); - assertEquals("queries", params.getDestination().getCollection()); + assertTrue(result.size() >= 1, "number of rules is invalid"); + + AnalyticsRule foundRule = null; + for (AnalyticsRule rule : result) { + if (ruleName.equals(rule.getName())) { + foundRule = rule; + break; + } + } + + assertNotNull(foundRule, "rule not found"); + assertEquals(ruleName, foundRule.getName()); + assertEquals(AnalyticsRuleCreate.TypeEnum.COUNTER, foundRule.getType()); + assertEquals("analytics_data", foundRule.getCollection()); + assertEquals("click", foundRule.getEventType()); } - + @Test - void testRetrieveAll() throws Exception { - helper.createTestAnalyticsRule(); - AnalyticsRulesRetrieveSchema result = this.client.analytics().rules().retrieve(); - + void testRetrieveSingle() throws Exception { + + String ruleName = "test-rule-" + System.currentTimeMillis(); + + AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() + .name(ruleName) + .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .collection("analytics_data") + .eventType("click") + .params(new AnalyticsRuleCreateParams() + .counterField("num_employees") + .weight(1)); + + List rules = new ArrayList<>(); + rules.add(analyticsRule); + + this.client.analytics().rules().create(rules); + + AnalyticsRule result = this.client.analytics().rules(ruleName).retrieve(); assertNotNull(result); - assertEquals("analytics-rule", result.getRules().get(0).getName()); - assertEquals(1, result.getRules().size()); - - AnalyticsRuleParameters params = result.getRules().get(0).getParams(); - assertNotNull(params); - - assertNotNull(params.getSource()); - assertEquals(Arrays.asList("books"), params.getSource().getCollections()); - - assertNotNull(params.getDestination()); - assertEquals("queries", params.getDestination().getCollection()); + assertEquals(ruleName, result.getName()); + assertEquals(AnalyticsRuleCreate.TypeEnum.COUNTER, result.getType()); + assertEquals("analytics_data", result.getCollection()); + assertEquals("click", result.getEventType()); } - + @Test void testDelete() throws Exception { - helper.createTestAnalyticsRule(); - AnalyticsRuleDeleteResponse result = this.client.analytics().rules("analytics-rule").delete(); - + + String ruleName = "test-rule-" + System.currentTimeMillis(); + + AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() + .name(ruleName) + .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .collection("analytics_data") + .eventType("click") + .params(new AnalyticsRuleCreateParams() + .counterField("num_employees") + .weight(1)); + + List rules = new ArrayList<>(); + rules.add(analyticsRule); + + this.client.analytics().rules().create(rules); + + AnalyticsRule result = this.client.analytics().rules(ruleName).delete(); assertNotNull(result); - assertEquals("analytics-rule", result.getName()); + assertEquals(ruleName, result.getName()); + + try { + this.client.analytics().rules(ruleName).retrieve(); + fail("Rule should have been deleted"); + } catch (Exception e) { + assertTrue(e.getMessage().contains("not found") || e.getMessage().contains("404")); + } } } diff --git a/src/test/java/org/typesense/api/CurationSetTest.java b/src/test/java/org/typesense/api/CurationSetTest.java new file mode 100644 index 0000000..fb5a0b3 --- /dev/null +++ b/src/test/java/org/typesense/api/CurationSetTest.java @@ -0,0 +1,131 @@ +package org.typesense.api; + +import java.util.ArrayList; +import java.util.Arrays; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.typesense.model.CurationItemCreateSchema; +import org.typesense.model.CurationSetCreateSchema; +import org.typesense.model.CurationSetSchema; +import org.typesense.model.CurationSetDeleteSchema; +import org.typesense.model.CurationRule; +import org.typesense.model.CurationInclude; +import org.typesense.model.CurationExclude; + +import static org.junit.jupiter.api.Assertions.*; + +class CurationSetTest { + + private Client client; + private Helper helper; + private String curationSetName; + + @BeforeEach + void setUp() throws Exception { + helper = new Helper(); + client = helper.getClient(); + + if (!Helper.isV30OrAbove(client)) { + org.junit.jupiter.api.Assumptions.assumeTrue(false, "Skipping test - requires Typesense v30 or above"); + } + + helper.teardown(); + helper.createTestCollection(); + curationSetName = "test-curation-set-" + System.currentTimeMillis(); + } + + @AfterEach + void tearDown() throws Exception { + try { + client.curationSet(curationSetName).delete(); + } catch (Exception e) { + } + helper.teardown(); + } + + @Test + void testRetrieve() throws Exception { + CurationSetCreateSchema curationSetData = helper.createTestCurationSetData(); + client.curationSets().upsert(curationSetName, curationSetData); + + CurationSetCreateSchema result = client.curationSet(curationSetName).retrieve(); + + assertNotNull(result); + assertEquals(1, result.getItems().size()); + assertEquals("dummy", result.getItems().get(0).getId()); + assertEquals("apple", result.getItems().get(0).getRule().getQuery()); + assertNotNull(result.getItems().get(0).getIncludes()); + assertEquals(2, result.getItems().get(0).getIncludes().size()); + assertEquals("422", result.getItems().get(0).getIncludes().get(0).getId()); + assertEquals("54", result.getItems().get(0).getIncludes().get(1).getId()); + assertNotNull(result.getItems().get(0).getExcludes()); + assertEquals(1, result.getItems().get(0).getExcludes().size()); + assertEquals("287", result.getItems().get(0).getExcludes().get(0).getId()); + } + + @Test + void testUpdate() throws Exception { + CurationSetCreateSchema originalData = helper.createTestCurationSetData(); + client.curationSets().upsert(curationSetName, originalData); + + CurationRule updatedRule = new CurationRule(); + updatedRule.setQuery("updated query"); + updatedRule.setMatch(CurationRule.MatchEnum.EXACT); + + CurationInclude include1 = new CurationInclude(); + include1.setId("422"); + include1.setPosition(1); + CurationInclude include2 = new CurationInclude(); + include2.setId("54"); + include2.setPosition(2); + CurationInclude include3 = new CurationInclude(); + include3.setId("999"); + include3.setPosition(3); + + CurationExclude exclude1 = new CurationExclude(); + exclude1.setId("287"); + + CurationItemCreateSchema updatedItem = new CurationItemCreateSchema(); + updatedItem.setId("dummy"); + updatedItem.setRule(updatedRule); + updatedItem.setIncludes(Arrays.asList(include1, include2, include3)); + updatedItem.setExcludes(Arrays.asList(exclude1)); + updatedItem.setRemoveMatchedTokens(true); + updatedItem.setFilterBy("category:=Electronics"); + updatedItem.setStopProcessing(true); + + CurationSetCreateSchema updatedData = new CurationSetCreateSchema(); + updatedData.setItems(Arrays.asList(updatedItem)); + updatedData.setDescription("Updated test curation set"); + + CurationSetSchema result = client.curationSet(curationSetName).upsert(updatedData); + + assertEquals(curationSetName, result.getName()); + assertEquals(1, result.getItems().size()); + assertEquals("dummy", result.getItems().get(0).getId()); + assertEquals("updated query", result.getItems().get(0).getRule().getQuery()); + assertNotNull(result.getItems().get(0).getIncludes()); + assertEquals(3, result.getItems().get(0).getIncludes().size()); + assertEquals("422", result.getItems().get(0).getIncludes().get(0).getId()); + assertEquals("54", result.getItems().get(0).getIncludes().get(1).getId()); + assertEquals("999", result.getItems().get(0).getIncludes().get(2).getId()); + assertNotNull(result.getItems().get(0).getExcludes()); + assertEquals(1, result.getItems().get(0).getExcludes().size()); + assertEquals("287", result.getItems().get(0).getExcludes().get(0).getId()); + } + + @Test + void testDelete() throws Exception { + CurationSetCreateSchema curationSetData = helper.createTestCurationSetData(); + client.curationSets().upsert(curationSetName, curationSetData); + + CurationSetDeleteSchema result = client.curationSet(curationSetName).delete(); + + assertEquals(curationSetName, result.getName()); + + assertThrows(Exception.class, () -> { + client.curationSet(curationSetName).retrieve(); + }); + } +} diff --git a/src/test/java/org/typesense/api/CurationSetsTest.java b/src/test/java/org/typesense/api/CurationSetsTest.java new file mode 100644 index 0000000..1488952 --- /dev/null +++ b/src/test/java/org/typesense/api/CurationSetsTest.java @@ -0,0 +1,92 @@ +package org.typesense.api; + +import java.util.Arrays; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.typesense.model.CurationItemCreateSchema; +import org.typesense.model.CurationSetCreateSchema; +import org.typesense.model.CurationSetSchema; + +import static org.junit.jupiter.api.Assertions.*; + +class CurationSetsTest { + + private Client client; + private Helper helper; + private String curationSetName; + + @BeforeEach + void setUp() throws Exception { + helper = new Helper(); + client = helper.getClient(); + + if (!Helper.isV30OrAbove(client)) { + org.junit.jupiter.api.Assumptions.assumeTrue(false, "Skipping test - requires Typesense v30 or above"); + } + + helper.teardown(); + helper.createTestCollection(); + curationSetName = "test-curation-set-" + System.currentTimeMillis(); + } + + @AfterEach + void tearDown() throws Exception { + try { + client.curationSet(curationSetName).delete(); + } catch (Exception e) { + } + helper.teardown(); + } + + @Test + void testUpsert() throws Exception { + CurationSetCreateSchema curationSetData = helper.createTestCurationSetData(); + + CurationSetSchema result = client.curationSets().upsert(curationSetName, curationSetData); + + assertEquals(curationSetName, result.getName()); + assertEquals(1, result.getItems().size()); + assertEquals("dummy", result.getItems().get(0).getId()); + assertEquals("apple", result.getItems().get(0).getRule().getQuery()); + assertNotNull(result.getItems().get(0).getIncludes()); + assertEquals(2, result.getItems().get(0).getIncludes().size()); + assertEquals("422", result.getItems().get(0).getIncludes().get(0).getId()); + assertEquals("54", result.getItems().get(0).getIncludes().get(1).getId()); + assertNotNull(result.getItems().get(0).getExcludes()); + assertEquals(1, result.getItems().get(0).getExcludes().size()); + assertEquals("287", result.getItems().get(0).getExcludes().get(0).getId()); + } + + @Test + void testRetrieve() throws Exception { + CurationSetCreateSchema curationSetData = helper.createTestCurationSetData(); + client.curationSets().upsert(curationSetName, curationSetData); + + CurationSetSchema[] result = client.curationSets().retrieve(); + + assertNotNull(result); + assertTrue(result.length >= 1); + + CurationSetSchema foundCurationSet = null; + for (CurationSetSchema cs : result) { + if (curationSetName.equals(cs.getName())) { + foundCurationSet = cs; + break; + } + } + + assertNotNull(foundCurationSet); + assertEquals(curationSetName, foundCurationSet.getName()); + assertEquals(1, foundCurationSet.getItems().size()); + assertEquals("dummy", foundCurationSet.getItems().get(0).getId()); + assertEquals("apple", foundCurationSet.getItems().get(0).getRule().getQuery()); + assertNotNull(foundCurationSet.getItems().get(0).getIncludes()); + assertEquals(2, foundCurationSet.getItems().get(0).getIncludes().size()); + assertEquals("422", foundCurationSet.getItems().get(0).getIncludes().get(0).getId()); + assertEquals("54", foundCurationSet.getItems().get(0).getIncludes().get(1).getId()); + assertNotNull(foundCurationSet.getItems().get(0).getExcludes()); + assertEquals(1, foundCurationSet.getItems().get(0).getExcludes().size()); + assertEquals("287", foundCurationSet.getItems().get(0).getExcludes().get(0).getId()); + } +} diff --git a/src/test/java/org/typesense/api/Helper.java b/src/test/java/org/typesense/api/Helper.java index 8943d39..9d9c8c6 100644 --- a/src/test/java/org/typesense/api/Helper.java +++ b/src/test/java/org/typesense/api/Helper.java @@ -2,16 +2,10 @@ import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; - -import org.typesense.model.AnalyticsRuleParameters; -import org.typesense.model.AnalyticsRuleParametersDestination; -import org.typesense.model.AnalyticsRuleParametersSource; -import org.typesense.model.AnalyticsRuleParametersSourceEvents; -import org.typesense.model.AnalyticsRuleSchema; -import org.typesense.model.AnalyticsRuleUpsertSchema; +import java.util.Map; +import java.util.Arrays; import org.typesense.model.ApiKey; import org.typesense.model.ApiKeySchema; import org.typesense.model.ApiKeysResponse; @@ -21,19 +15,64 @@ import org.typesense.model.CollectionResponse; import org.typesense.model.CollectionSchema; import org.typesense.model.Field; -import org.typesense.model.SearchOverrideInclude; -import org.typesense.model.SearchOverrideRule; -import org.typesense.model.SearchOverrideSchema; import org.typesense.model.SearchSynonymSchema; import org.typesense.model.StemmingDictionaryWords; import org.typesense.model.StopwordsSetSchema; import org.typesense.model.StopwordsSetUpsertSchema; import org.typesense.model.StopwordsSetsRetrieveAllSchema; +import org.typesense.model.SynonymItemSchema; +import org.typesense.model.SynonymSetCreateSchema; +import org.typesense.model.SynonymSetSchema; +import org.typesense.model.SynonymSetsRetrieveSchema; +import org.typesense.model.CurationItemCreateSchema; +import org.typesense.model.CurationSetCreateSchema; +import org.typesense.model.CurationRule; +import org.typesense.model.CurationInclude; +import org.typesense.model.CurationExclude; import org.typesense.resources.Node; +import org.typesense.model.AnalyticsRuleCreate; +import org.typesense.model.AnalyticsRuleCreateParams; +import org.typesense.model.AnalyticsRule; +import org.typesense.api.AnalyticsRules; public class Helper { private final Client client; + public static boolean isV30OrAbove(Client client) { + try { + Map debugResponse = client.debug.retrieve(); + if (debugResponse == null) { + return false; + } + + Object version = debugResponse.get("version"); + if (version == null) { + return false; + } + + String versionStr = version.toString(); + if ("nightly".equals(versionStr)) { + return true; + } + + String numberedVersion = versionStr; + if (versionStr.startsWith("v")) { + numberedVersion = versionStr.substring(1); + } + + String[] parts = numberedVersion.split("\\."); + if (parts.length == 0) { + return false; + } + + int majorVersion = Integer.parseInt(parts[0]); + return majorVersion >= 30; + + } catch (Exception e) { + return false; + } + } + Helper() { List nodes = new ArrayList<>(); nodes.add(new Node("http", "localhost", "8108")); @@ -64,6 +103,15 @@ public void createTestQueryCollection() throws Exception { client.collections().create(collectionSchema); } + public void createTestQueryDocument() throws Exception { + HashMap hmap = new HashMap<>(); + hmap.put("q", "running shoes"); + hmap.put("count", 42); + hmap.put("id", "query1"); + + client.collections("queries").documents().create(hmap); + } + public void createTestDocument() throws Exception { String[] authors = { "shakspeare", "william" }; HashMap hmap = new HashMap<>(); @@ -97,15 +145,6 @@ public ApiKey createTestKey() throws Exception { return client.keys().create(apiKeySchema); } - public void createTestOverrirde() throws Exception { - SearchOverrideSchema searchOverrideSchema = new SearchOverrideSchema(); - List searchOverrideIncludes = new ArrayList<>(); - searchOverrideIncludes.add(new SearchOverrideInclude().id("422").position(1)); - searchOverrideSchema.rule(new SearchOverrideRule().query("apple").match(SearchOverrideRule.MatchEnum.EXACT)) - .includes(searchOverrideIncludes); - client.collections("books").overrides().upsert("customize-apple", searchOverrideSchema); - } - public void createTestSynonym() throws Exception { SearchSynonymSchema synonym = new SearchSynonymSchema(); synonym.addSynonymsItem("blazer").addSynonymsItem("coat").addSynonymsItem("jacket"); @@ -113,20 +152,36 @@ public void createTestSynonym() throws Exception { client.collections("books").synonyms().upsert("coat-synonyms", synonym); } - public void createTestAnalyticsRule() throws Exception { - AnalyticsRuleUpsertSchema analyticsRuleSchema = new AnalyticsRuleUpsertSchema() - .type(AnalyticsRuleUpsertSchema.TypeEnum.NOHITS_QUERIES) - .params(new AnalyticsRuleParameters() - .source(new AnalyticsRuleParametersSource() - .collections(Arrays.asList("books")) - .events(Arrays.asList( - new AnalyticsRuleParametersSourceEvents() - .type("search") - .name("products_search_event")))) - .destination(new AnalyticsRuleParametersDestination() - .collection("queries"))); + public void createTestAnalyticsCollection() throws Exception { + List fields = new ArrayList<>(); + fields.add(new Field().name("rule_name").type(FieldTypes.STRING)); + fields.add(new Field().name("event_type").type(FieldTypes.STRING)); + fields.add(new Field().name("counter_value").type(FieldTypes.INT32)); + fields.add(new Field().name("timestamp").type(FieldTypes.INT64)); - client.analytics().rules().upsert("analytics-rule", analyticsRuleSchema); + CollectionSchema collectionSchema = new CollectionSchema(); + collectionSchema.name("analytics_data").fields(fields); + + client.collections().create(collectionSchema); + } + + public String createTestAnalyticsRule() throws Exception { + String ruleName = "analytics-rule-" + System.currentTimeMillis(); + + AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() + .name(ruleName) + .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .collection("analytics_data") + .eventType("click") + .params(new AnalyticsRuleCreateParams() + .counterField("num_employees") + .weight(1)); + + List rules = new ArrayList<>(); + rules.add(analyticsRule); + + client.analytics().rules().create(rules); + return ruleName; } public void createTestStopwordsSet() throws Exception { @@ -151,20 +206,69 @@ public void createStemmingDictionary() throws Exception{ client.stemming().dictionaries().upsert("irregular-plurals", stemmingDictionaryWords); } + public SynonymSetCreateSchema createTestSynonymSetData() { + SynonymItemSchema synonymItem = new SynonymItemSchema(); + synonymItem.setId("dummy"); + synonymItem.setSynonyms(Arrays.asList("foo", "bar", "baz")); + + SynonymSetCreateSchema synonymSetData = new SynonymSetCreateSchema(); + synonymSetData.setItems(Arrays.asList(synonymItem)); + return synonymSetData; + } + + public CurationSetCreateSchema createTestCurationSetData() { + CurationRule rule = new CurationRule(); + rule.setQuery("apple"); + rule.setMatch(CurationRule.MatchEnum.EXACT); + + CurationInclude include1 = new CurationInclude(); + include1.setId("422"); + include1.setPosition(1); + CurationInclude include2 = new CurationInclude(); + include2.setId("54"); + include2.setPosition(2); + + CurationExclude exclude1 = new CurationExclude(); + exclude1.setId("287"); + + CurationItemCreateSchema curationItem = new CurationItemCreateSchema(); + curationItem.setId("dummy"); + curationItem.setRule(rule); + curationItem.setIncludes(Arrays.asList(include1, include2)); + curationItem.setExcludes(Arrays.asList(exclude1)); + curationItem.setRemoveMatchedTokens(true); + curationItem.setFilterBy("category:=Electronics"); + curationItem.setStopProcessing(true); + + CurationSetCreateSchema curationSetData = new CurationSetCreateSchema(); + curationSetData.setItems(Arrays.asList(curationItem)); + curationSetData.setDescription("Test curation set"); + return curationSetData; + } + public void teardown() throws Exception { CollectionResponse[] collectionResponses = client.collections().retrieve(); for (CollectionResponse c : collectionResponses) { - client.collections(c.getName()).delete(); + if (c.getName() != null) { + client.collections(c.getName()).delete(); + } } CollectionAliasesResponse collectionAliasesResponse = client.aliases().retrieve(); for (CollectionAlias a : collectionAliasesResponse.getAliases()) { - client.aliases(a.getName()).delete(); + if (a.getName() != null) { + client.aliases(a.getName()).delete(); + } } AnalyticsRules analyticsRules = client.analytics().rules(); - for (AnalyticsRuleSchema r : analyticsRules.retrieve().getRules()) { - client.analytics().rules(r.getName()).delete(); + List rules = analyticsRules.retrieve(); + if (rules != null) { + for (AnalyticsRule r : rules) { + if (r.getName() != null) { + client.analytics().rules(r.getName()).delete(); + } + } } ApiKeysResponse apiKeysResponse = client.keys().retrieve(); @@ -176,5 +280,15 @@ public void teardown() throws Exception { for (StopwordsSetSchema s : stopwords.getStopwords()) { client.stopwords(s.getId()).delete(); } + + try { + SynonymSetSchema[] synonymSets = client.synonymSets().retrieve(); + for (SynonymSetSchema s : synonymSets) { + if (s.getName() != null) { + client.synonymSet(s.getName()).delete(); + } + } + } catch (Exception e) { + } } } diff --git a/src/test/java/org/typesense/api/OverridesTest.java b/src/test/java/org/typesense/api/OverridesTest.java deleted file mode 100644 index b034679..0000000 --- a/src/test/java/org/typesense/api/OverridesTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.typesense.api; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.typesense.model.SearchOverrideExclude; -import org.typesense.model.SearchOverrideInclude; -import org.typesense.model.SearchOverrideRule; -import org.typesense.model.SearchOverrideSchema; - -import java.util.ArrayList; -import java.util.List; - -class OverridesTest { - - private Client client; - private Helper helper; - - @BeforeEach - void setUp() throws Exception { - helper = new Helper(); - helper.teardown(); - client = helper.getClient(); - helper.createTestCollection(); - helper.createTestOverrirde(); - } - - @AfterEach - void tearDown() throws Exception { - helper.teardown(); - } - - @Test - void testUpsert() throws Exception { - SearchOverrideSchema searchOverrideSchema = new SearchOverrideSchema(); - - List searchOverrideIncludes = new ArrayList<>(); - searchOverrideIncludes.add(new SearchOverrideInclude().id("422").position(1)); - searchOverrideIncludes.add(new SearchOverrideInclude().id("54").position(2)); - - List searchOverrideExcludes = new ArrayList<>(); - searchOverrideExcludes.add(new SearchOverrideExclude().id("287")); - - searchOverrideSchema.rule(new SearchOverrideRule().query("apple").match(SearchOverrideRule.MatchEnum.EXACT)) - .includes(searchOverrideIncludes) - .excludes(searchOverrideExcludes); - - System.out.println(client.collections("books").overrides().upsert("apple", searchOverrideSchema)); - } - - @Test - void testRetrieveAll() throws Exception { - System.out.println(this.client.collections("books").overrides().retrieve()); - } - - @Test - void testRetrieve() throws Exception { - System.out.println(this.client.collections("books").overrides("customize-apple").retrieve()); - } - - @Test - void testDelete() throws Exception { - System.out.println(this.client.collections("books").overrides("customize-apple").delete()); - } -} \ No newline at end of file diff --git a/src/test/java/org/typesense/api/SynonymSetTest.java b/src/test/java/org/typesense/api/SynonymSetTest.java new file mode 100644 index 0000000..ea17727 --- /dev/null +++ b/src/test/java/org/typesense/api/SynonymSetTest.java @@ -0,0 +1,93 @@ +package org.typesense.api; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.typesense.model.SynonymItemSchema; +import org.typesense.model.SynonymSetCreateSchema; +import org.typesense.model.SynonymSetSchema; +import org.typesense.model.SynonymSetDeleteSchema; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class SynonymSetTest { + + private Client client; + private Helper helper; + private String synonymSetName; + + @BeforeEach + void setUp() throws Exception { + helper = new Helper(); + client = helper.getClient(); + + if (!Helper.isV30OrAbove(client)) { + org.junit.jupiter.api.Assumptions.assumeTrue(false, "Skipping test - requires Typesense v30 or above"); + } + + helper.teardown(); + helper.createTestCollection(); + synonymSetName = "test-synonym-set-" + System.currentTimeMillis(); + } + + @AfterEach + void tearDown() throws Exception { + try { + client.synonymSet(synonymSetName).delete(); + } catch (Exception e) { + } + helper.teardown(); + } + + @Test + void testRetrieve() throws Exception { + SynonymSetCreateSchema synonymSetData = helper.createTestSynonymSetData(); + client.synonymSets().upsert(synonymSetName, synonymSetData); + + SynonymSetCreateSchema result = client.synonymSet(synonymSetName).retrieve(); + + assertNotNull(result); + assertEquals(1, result.getItems().size()); + assertEquals("dummy", result.getItems().get(0).getId()); + assertEquals(Arrays.asList("foo", "bar", "baz"), result.getItems().get(0).getSynonyms()); + } + + @Test + void testUpdate() throws Exception { + SynonymSetCreateSchema originalData = helper.createTestSynonymSetData(); + client.synonymSets().upsert(synonymSetName, originalData); + + SynonymSetCreateSchema updatedData = new SynonymSetCreateSchema(); + List items = new ArrayList<>(); + SynonymItemSchema item = new SynonymItemSchema(); + item.setId("dummy"); + item.setSynonyms(Arrays.asList("foo", "bar", "baz", "qux")); + items.add(item); + updatedData.setItems(items); + + SynonymSetSchema result = client.synonymSet(synonymSetName).upsert(updatedData); + + assertEquals(synonymSetName, result.getName()); + assertEquals(1, result.getItems().size()); + assertEquals("dummy", result.getItems().get(0).getId()); + assertEquals(Arrays.asList("foo", "bar", "baz", "qux"), result.getItems().get(0).getSynonyms()); + } + + @Test + void testDelete() throws Exception { + SynonymSetCreateSchema synonymSetData = helper.createTestSynonymSetData(); + client.synonymSets().upsert(synonymSetName, synonymSetData); + + SynonymSetDeleteSchema result = client.synonymSet(synonymSetName).delete(); + + assertEquals(synonymSetName, result.getName()); + + assertThrows(Exception.class, () -> { + client.synonymSet(synonymSetName).retrieve(); + }); + } +} \ No newline at end of file diff --git a/src/test/java/org/typesense/api/SynonymSetsTest.java b/src/test/java/org/typesense/api/SynonymSetsTest.java new file mode 100644 index 0000000..430cfad --- /dev/null +++ b/src/test/java/org/typesense/api/SynonymSetsTest.java @@ -0,0 +1,80 @@ +package org.typesense.api; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.typesense.model.SynonymItemSchema; +import org.typesense.model.SynonymSetCreateSchema; +import org.typesense.model.SynonymSetSchema; + +import static org.junit.jupiter.api.Assertions.*; + +class SynonymSetsTest { + + private Client client; + private Helper helper; + private String synonymSetName; + + @BeforeEach + void setUp() throws Exception { + helper = new Helper(); + client = helper.getClient(); + + if (!Helper.isV30OrAbove(client)) { + org.junit.jupiter.api.Assumptions.assumeTrue(false, "Skipping test - requires Typesense v30 or above"); + } + + helper.teardown(); + helper.createTestCollection(); + synonymSetName = "test-synonym-set-" + System.currentTimeMillis(); + } + + @AfterEach + void tearDown() throws Exception { + try { + client.synonymSet(synonymSetName).delete(); + } catch (Exception e) { + } + helper.teardown(); + } + + @Test + void testUpsert() throws Exception { + SynonymSetCreateSchema synonymSetData = helper.createTestSynonymSetData(); + + SynonymSetSchema result = client.synonymSets().upsert(synonymSetName, synonymSetData); + + assertEquals(synonymSetName, result.getName()); + assertEquals(1, result.getItems().size()); + assertEquals("dummy", result.getItems().get(0).getId()); + assertEquals(Arrays.asList("foo", "bar", "baz"), result.getItems().get(0).getSynonyms()); + } + + @Test + void testRetrieve() throws Exception { + SynonymSetCreateSchema synonymSetData = helper.createTestSynonymSetData(); + client.synonymSets().upsert(synonymSetName, synonymSetData); + + SynonymSetSchema[] result = client.synonymSets().retrieve(); + + assertNotNull(result); + assertTrue(result.length >= 1); + + SynonymSetSchema foundSynonymSet = null; + for (SynonymSetSchema ss : result) { + if (synonymSetName.equals(ss.getName())) { + foundSynonymSet = ss; + break; + } + } + + assertNotNull(foundSynonymSet); + assertEquals(synonymSetName, foundSynonymSet.getName()); + assertEquals(1, foundSynonymSet.getItems().size()); + assertEquals("dummy", foundSynonymSet.getItems().get(0).getId()); + assertEquals(Arrays.asList("foo", "bar", "baz"), foundSynonymSet.getItems().get(0).getSynonyms()); + } +} diff --git a/src/test/java/org/typesense/api/SynonymsTest.java b/src/test/java/org/typesense/api/SynonymsTest.java index 57da150..13947d2 100644 --- a/src/test/java/org/typesense/api/SynonymsTest.java +++ b/src/test/java/org/typesense/api/SynonymsTest.java @@ -14,6 +14,11 @@ class SynonymsTest { void setUp() throws Exception { helper = new Helper(); client = helper.getClient(); + + if (Helper.isV30OrAbove(client)) { + org.junit.jupiter.api.Assumptions.assumeTrue(false, "Skipping test - requires Typesense v30 or above"); + } + helper.teardown(); helper.createTestCollection(); helper.createTestSynonym(); @@ -49,4 +54,4 @@ void testRetrieveAll() throws Exception { void testDelete() throws Exception { System.out.println(this.client.collections("books").synonyms("coat-synonyms").delete()); } -} \ No newline at end of file +} From 34d75911b5273cb31f0d503db978c62bbe62e59f Mon Sep 17 00:00:00 2001 From: Kishore Nallan Date: Wed, 19 Nov 2025 18:15:56 +0400 Subject: [PATCH 11/15] Bump version 2.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9a00b2d..23baf13 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } group = 'org.typesense' -version = '1.3.0' +version = '2.0.0' sourceCompatibility = 1.8 targetCompatibility = 1.8 From 52e1cb6f264aae5aaf3d0b31a575de3cf780a9da Mon Sep 17 00:00:00 2001 From: Kishore Nallan Date: Wed, 19 Nov 2025 19:13:40 +0400 Subject: [PATCH 12/15] Fix publishing. --- build.gradle | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 23baf13..226ce3d 100644 --- a/build.gradle +++ b/build.gradle @@ -143,10 +143,8 @@ sourceSets.main.resources.srcDir "${swaggerSources.typesense.code.outputDir}/src nexusPublishing { repositories { sonatype { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - username = findProperty("ossrhToken") as String - password = findProperty("ossrhTokenPassword") as String + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) } } } From 9e7514dcdd7a86afb3c9af2091480162aa53a9fd Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos <93339170+tharropoulos@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:13:30 +0200 Subject: [PATCH 13/15] docs: add compatibility section in the README (#98) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index d492dc0..2511b9d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ import org.typesense.model.*; import org.typesense.resources.*; ``` +## Compatibility +`typesense-java` is compatible with Java versions 8+ and Android SDK versions 26+ + ## Usage ### Create a new client From e473a926d8ca96a3f5ca264eb66cc55696b09573 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos <93339170+tharropoulos@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:28:45 +0200 Subject: [PATCH 14/15] V30.0 spec updates (#99) * fix(analytics): fix analytics types based on api spec changes * chore: patch spec generation for synonymset items --------- Co-authored-by: Fanis --- .../main/groovy/DownloadSpecsPlugin.groovy | 70 ++++++++++++++++--- .../api/AnalyticsRuleSerializer.java | 12 ++-- .../org/typesense/api/AnalyticsRulesTest.java | 15 ++-- src/test/java/org/typesense/api/Helper.java | 3 +- 4 files changed, 76 insertions(+), 24 deletions(-) diff --git a/buildSrc/src/main/groovy/DownloadSpecsPlugin.groovy b/buildSrc/src/main/groovy/DownloadSpecsPlugin.groovy index b208b5a..d4e71f0 100644 --- a/buildSrc/src/main/groovy/DownloadSpecsPlugin.groovy +++ b/buildSrc/src/main/groovy/DownloadSpecsPlugin.groovy @@ -11,14 +11,68 @@ class DownloadSpecsPlugin implements Plugin { @Override void apply(Project project) { project.tasks.register("downloadApiSpec") { - println('Downloading spec') - File file = new File("${project.buildDir}/openapi.yml") - if (!file.getParentFile().exists()) - file.getParentFile().mkdirs(); - if (!file.exists()) - file.createNewFile(); - file.withOutputStream { out -> - new URL(specUrl).withInputStream { from -> out << from } + doLast { + println('Downloading spec') + File file = new File("${project.buildDir}/openapi.yml") + if (!file.getParentFile().exists()) + file.getParentFile().mkdirs(); + if (!file.exists()) + file.createNewFile(); + file.withOutputStream { out -> + new URL(specUrl).withInputStream { from -> out << from } + } + + // Patch the OpenAPI spec to fix SynonymItemSchema allOf issue + // The swagger code generator doesn't handle allOf correctly, so we convert it + // to a direct definition that includes all fields + println('Patching OpenAPI spec for SynonymItemSchema') + String content = file.text + + // Find and replace the allOf structure with a direct definition + String allOfBlock = ''' SynonymItemSchema: + allOf: + - type: object + required: + - id + properties: + id: + type: string + description: Unique identifier for the synonym item + - $ref: "#/components/schemas/SynonymItemUpsertSchema"''' + + String replacement = ''' SynonymItemSchema: + type: object + required: + - id + - synonyms + properties: + id: + type: string + description: Unique identifier for the synonym item + synonyms: + type: array + description: Array of words that should be considered as synonyms + items: + type: string + root: + type: string + description: For 1-way synonyms, indicates the root word that words in the synonyms parameter map to + locale: + type: string + description: Locale for the synonym, leave blank to use the standard tokenizer + symbols_to_index: + type: array + description: By default, special characters are dropped from synonyms. Use this attribute to specify which special characters should be indexed as is + items: + type: string''' + + if (content.contains(allOfBlock)) { + content = content.replace(allOfBlock, replacement) + file.text = content + println('Successfully patched SynonymItemSchema') + } else { + println('Warning: SynonymItemSchema allOf block not found, skipping patch') + } } } } diff --git a/src/main/java/org/typesense/api/AnalyticsRuleSerializer.java b/src/main/java/org/typesense/api/AnalyticsRuleSerializer.java index c3719e8..2e943ea 100644 --- a/src/main/java/org/typesense/api/AnalyticsRuleSerializer.java +++ b/src/main/java/org/typesense/api/AnalyticsRuleSerializer.java @@ -6,6 +6,7 @@ import org.typesense.model.AnalyticsRule; import org.typesense.model.AnalyticsRuleCreate; import org.typesense.model.AnalyticsRuleCreateParams; +import org.typesense.model.AnalyticsRuleType; import java.util.List; import java.util.ArrayList; @@ -34,14 +35,9 @@ public AnalyticsRule parseFromJsonNode(JsonNode node) { if (node.has("type")) { String typeStr = node.get("type").asText(); - if ("counter".equals(typeStr)) { - rule.type(AnalyticsRuleCreate.TypeEnum.COUNTER); - } else if ("popular_queries".equals(typeStr)) { - rule.type(AnalyticsRuleCreate.TypeEnum.POPULAR_QUERIES); - } else if ("nohits_queries".equals(typeStr)) { - rule.type(AnalyticsRuleCreate.TypeEnum.NOHITS_QUERIES); - } else if ("log".equals(typeStr)) { - rule.type(AnalyticsRuleCreate.TypeEnum.LOG); + AnalyticsRuleType type = AnalyticsRuleType.fromValue(typeStr); + if (type != null) { + rule.type(type); } } diff --git a/src/test/java/org/typesense/api/AnalyticsRulesTest.java b/src/test/java/org/typesense/api/AnalyticsRulesTest.java index 199ea0a..5415492 100644 --- a/src/test/java/org/typesense/api/AnalyticsRulesTest.java +++ b/src/test/java/org/typesense/api/AnalyticsRulesTest.java @@ -7,6 +7,7 @@ import org.typesense.model.AnalyticsRule; import org.typesense.model.AnalyticsRuleCreate; import org.typesense.model.AnalyticsRuleCreateParams; +import org.typesense.model.AnalyticsRuleType; import java.util.ArrayList; import java.util.List; @@ -77,7 +78,7 @@ void testCreate() throws Exception { AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() .name(ruleName) - .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .type(AnalyticsRuleType.COUNTER) .collection("analytics_data") .eventType("click") .params(new AnalyticsRuleCreateParams() @@ -95,7 +96,7 @@ void testCreate() throws Exception { AnalyticsRule createdRule = result.getFirstRule(); assertNotNull(createdRule); assertEquals(ruleName, createdRule.getName()); - assertEquals(AnalyticsRuleCreate.TypeEnum.COUNTER, createdRule.getType()); + assertEquals(AnalyticsRuleType.COUNTER, createdRule.getType()); assertEquals("analytics_data", createdRule.getCollection()); assertEquals("click", createdRule.getEventType()); } @@ -107,7 +108,7 @@ void testRetrieve() throws Exception { AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() .name(ruleName) - .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .type(AnalyticsRuleType.COUNTER) .collection("analytics_data") .eventType("click") .params(new AnalyticsRuleCreateParams() @@ -133,7 +134,7 @@ void testRetrieve() throws Exception { assertNotNull(foundRule, "rule not found"); assertEquals(ruleName, foundRule.getName()); - assertEquals(AnalyticsRuleCreate.TypeEnum.COUNTER, foundRule.getType()); + assertEquals(AnalyticsRuleType.COUNTER, foundRule.getType()); assertEquals("analytics_data", foundRule.getCollection()); assertEquals("click", foundRule.getEventType()); } @@ -145,7 +146,7 @@ void testRetrieveSingle() throws Exception { AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() .name(ruleName) - .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .type(AnalyticsRuleType.COUNTER) .collection("analytics_data") .eventType("click") .params(new AnalyticsRuleCreateParams() @@ -160,7 +161,7 @@ void testRetrieveSingle() throws Exception { AnalyticsRule result = this.client.analytics().rules(ruleName).retrieve(); assertNotNull(result); assertEquals(ruleName, result.getName()); - assertEquals(AnalyticsRuleCreate.TypeEnum.COUNTER, result.getType()); + assertEquals(AnalyticsRuleType.COUNTER, result.getType()); assertEquals("analytics_data", result.getCollection()); assertEquals("click", result.getEventType()); } @@ -172,7 +173,7 @@ void testDelete() throws Exception { AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() .name(ruleName) - .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .type(AnalyticsRuleType.COUNTER) .collection("analytics_data") .eventType("click") .params(new AnalyticsRuleCreateParams() diff --git a/src/test/java/org/typesense/api/Helper.java b/src/test/java/org/typesense/api/Helper.java index 9d9c8c6..767aa30 100644 --- a/src/test/java/org/typesense/api/Helper.java +++ b/src/test/java/org/typesense/api/Helper.java @@ -33,6 +33,7 @@ import org.typesense.model.AnalyticsRuleCreate; import org.typesense.model.AnalyticsRuleCreateParams; import org.typesense.model.AnalyticsRule; +import org.typesense.model.AnalyticsRuleType; import org.typesense.api.AnalyticsRules; public class Helper { @@ -170,7 +171,7 @@ public String createTestAnalyticsRule() throws Exception { AnalyticsRuleCreate analyticsRule = new AnalyticsRuleCreate() .name(ruleName) - .type(AnalyticsRuleCreate.TypeEnum.COUNTER) + .type(AnalyticsRuleType.COUNTER) .collection("analytics_data") .eventType("click") .params(new AnalyticsRuleCreateParams() From d0330911927ea27215525e558ccbb40ce5cd8e27 Mon Sep 17 00:00:00 2001 From: Kishore Nallan Date: Fri, 19 Dec 2025 16:21:52 +0400 Subject: [PATCH 15/15] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 226ce3d..c7b6b60 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { } group = 'org.typesense' -version = '2.0.0' +version = '2.1.0' sourceCompatibility = 1.8 targetCompatibility = 1.8