From 3703f296494b7bff190262354d036b1b6e667da5 Mon Sep 17 00:00:00 2001 From: Hayden Hung Hoang Date: Fri, 1 Aug 2025 21:18:59 +0700 Subject: [PATCH 1/2] fix build error ErrorResponse is unused in the api spec and is removed in https://github.com/typesense/typesense-api-spec/pull/93. --- src/main/java/org/typesense/api/ApiCall.java | 39 +++++++++++--------- 1 file changed, 22 insertions(+), 17 deletions(-) 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 From 80f2a5b4106a3d072ceec9839dc1dcd2b3977cee Mon Sep 17 00:00:00 2001 From: Hayden Hung Hoang Date: Sat, 2 Aug 2025 16:56:48 +0700 Subject: [PATCH 2/2] feat(multi-search): add union search functionality and update docker-compose version --- docker-compose.yml | 12 +-- .../java/org/typesense/api/MultiSearch.java | 81 ++++++++++++++++++- .../org/typesense/api/MultiSearchTest.java | 53 ++++++++++-- 3 files changed, 132 insertions(+), 14 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/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"); + } }