Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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:
data-dir:
39 changes: 22 additions & 17 deletions src/main/java/org/typesense/api/ApiCall.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,7 +19,6 @@
import java.util.Map;
import java.util.concurrent.TimeUnit;


public class ApiCall {

private final Configuration configuration;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -161,7 +159,7 @@ <Q, R> R delete(String endpoint, Q queryParameters, Class<R> responseClass) thro
}

<Q, T> T makeRequest(String endpoint, Q queryParameters, Request.Builder requestBuilder,
Class<T> responseClass) throws Exception {
Class<T> responseClass) throws Exception {
int num_tries = 0;
Exception lastException = new TypesenseError("Unknown client error", 400);

Expand All @@ -174,9 +172,9 @@ <Q, T> 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();

Expand All @@ -193,11 +191,11 @@ <Q, T> 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;
}
Expand Down Expand Up @@ -233,7 +231,7 @@ private <T> 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());
}
}
Expand Down Expand Up @@ -272,3 +270,10 @@ <T> T handleResponse(Response response, Class<T> responseClass) throws IOExcepti

}

class ErrorResponse {
private String message = null;

public String getMessage() {
return message;
}
}
81 changes: 79 additions & 2 deletions src/main/java/org/typesense/api/MultiSearch.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,8 +15,84 @@ public MultiSearch(ApiCall apiCall) {
this.apiCall = apiCall;
}

/**
* Performs a federated multi-search, returning individual result sets for each
* query.
* <p>
* 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
* <a href=
* "https://typesense.org/docs/latest/api/federated-multi-search.html#federated-search"
* target="_blank">Federated Search</a>.
* <p>
* 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<String, String> common_params) throws Exception {
return this.apiCall.post(MultiSearch.RESOURCEPATH, multiSearchParameters, common_params, MultiSearchResult.class);
Map<String, String> 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 <a href=
* "https://typesense.org/docs/latest/api/federated-multi-search.html#union-search"
* target="_blank">Union Search</a>
* and returns a single, combined search result.
* <p>
* 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.
* <p>
* 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<String, String> 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);
}
}
53 changes: 47 additions & 6 deletions src/test/java/org/typesense/api/MultiSearchTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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<String, Object> 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<String, Object> 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<String, Object> 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
Expand All @@ -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<String> 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");
}
}