diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 530085b3..bc3cefd0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,7 @@ jobs: - name: Meilisearch (latest) setup with Docker run: docker run -d -p 7700:7700 getmeili/meilisearch:latest meilisearch --master-key=masterKey --no-analytics - name: Build and run unit and integration tests - run: ./gradlew build integrationTest + run: ./gradlew build integrationTest --info - name: Archive test report uses: actions/upload-artifact@v4 if: failure() diff --git a/src/main/java/com/meilisearch/sdk/Index.java b/src/main/java/com/meilisearch/sdk/Index.java index ee0431a4..1eb01bec 100644 --- a/src/main/java/com/meilisearch/sdk/Index.java +++ b/src/main/java/com/meilisearch/sdk/Index.java @@ -1242,6 +1242,14 @@ public TaskInfo resetSearchCutoffMsSettings() throws MeilisearchException { return this.settingsHandler.resetSearchCutoffMsSettings(this.uid); } + /** + * Retrieves documents that are semantically similar to a given document + * + * @param query SimilarDocumentRequest containing parameters for the similar documents search + * @return SimilarDocumentsResults containing the search results + * @throws MeilisearchException if an error occurs + * @see API specification + */ public SimilarDocumentsResults searchSimilarDocuments(SimilarDocumentRequest query) throws MeilisearchException { return this.config.httpClient.post( @@ -1249,4 +1257,42 @@ public SimilarDocumentsResults searchSimilarDocuments(SimilarDocumentRequest que query, SimilarDocumentsResults.class); } + + /** + * Gets the embedders settings of the index + * + * @return a Map that contains all embedders settings + * @throws MeilisearchException if an error occurs + * @see API + * specification + */ + public Map getEmbeddersSettings() throws MeilisearchException { + return this.settingsHandler.getEmbedders(this.uid); + } + + /** + * Updates the embedders settings of the index + * + * @param embedders a Map that contains the new embedders settings + * @return TaskInfo instance + * @throws MeilisearchException if an error occurs + * @see API + * specification + */ + public TaskInfo updateEmbeddersSettings(Map embedders) + throws MeilisearchException { + return this.settingsHandler.updateEmbedders(this.uid, embedders); + } + + /** + * Resets the embedders settings of the index + * + * @return TaskInfo instance + * @throws MeilisearchException if an error occurs + * @see API + * specification + */ + public TaskInfo resetEmbeddersSettings() throws MeilisearchException { + return this.settingsHandler.resetEmbedders(this.uid); + } } diff --git a/src/main/java/com/meilisearch/sdk/IndexSearchRequest.java b/src/main/java/com/meilisearch/sdk/IndexSearchRequest.java index 3ba8eef4..8e35ab3e 100644 --- a/src/main/java/com/meilisearch/sdk/IndexSearchRequest.java +++ b/src/main/java/com/meilisearch/sdk/IndexSearchRequest.java @@ -38,13 +38,14 @@ public class IndexSearchRequest { private FederationOptions federationOptions; protected String[] locales; protected String distinct; + protected Boolean retrieveVectors; /** * Constructor for MultiSearchRequest for building search queries with the default values: * offset: 0, limit: 20, attributesToRetrieve: ["*"], attributesToCrop: null, cropLength: 200, * attributesToHighlight: null, filter: null, showMatchesPosition: false, facets: null, sort: * null, showRankingScore: false, showRankingScoreDetails: false, rankingScoreThreshold: null - * distinct: null + * distinct: null, retrieveVectors: false * * @param indexUid uid of the requested index String */ @@ -106,7 +107,8 @@ public String toString() { .putOpt("rankingScoreThreshold", this.rankingScoreThreshold) .putOpt("attributesToSearchOn", this.attributesToSearchOn) .putOpt("locales", this.locales) - .putOpt("distinct", this.distinct); + .putOpt("distinct", this.distinct) + .putOpt("retrieveVectors", this.retrieveVectors); return jsonObject.toString(); } diff --git a/src/main/java/com/meilisearch/sdk/SearchRequest.java b/src/main/java/com/meilisearch/sdk/SearchRequest.java index 77650ad5..43e043cc 100644 --- a/src/main/java/com/meilisearch/sdk/SearchRequest.java +++ b/src/main/java/com/meilisearch/sdk/SearchRequest.java @@ -1,5 +1,6 @@ package com.meilisearch.sdk; +import com.meilisearch.sdk.model.Hybrid; import com.meilisearch.sdk.model.MatchingStrategy; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -42,12 +43,15 @@ public class SearchRequest { protected Double rankingScoreThreshold; protected String[] locales; protected String distinct; - + protected Hybrid hybrid; + protected Double[] vector; + protected Boolean retrieveVectors; /** * Constructor for SearchRequest for building search queries with the default values: offset: 0, * limit: 20, attributesToRetrieve: ["*"], attributesToCrop: null, cropLength: 200, * attributesToHighlight: null, filter: null, showMatchesPosition: false, facets: null, sort: - * null, showRankingScore: false, showRankingScoreDetails: false, rankingScoreThreshold: null + * null, showRankingScore: false, showRankingScoreDetails: false, rankingScoreThreshold: null, + * retrieveVectors: false * * @param q Query String */ @@ -104,7 +108,13 @@ public String toString() { .putOpt("showRankingScoreDetails", this.showRankingScoreDetails) .putOpt("rankingScoreThreshold", this.rankingScoreThreshold) .putOpt("locales", this.locales) - .putOpt("distinct", this.distinct); + .putOpt("distinct", this.distinct) + .putOpt("vector", this.vector) + .putOpt("retrieveVectors", this.retrieveVectors); + + if (this.hybrid != null) { + jsonObject.put("hybrid", this.hybrid.toJSONObject()); + } return jsonObject.toString(); } diff --git a/src/main/java/com/meilisearch/sdk/SettingsHandler.java b/src/main/java/com/meilisearch/sdk/SettingsHandler.java index c70f9f80..9ee73a3a 100644 --- a/src/main/java/com/meilisearch/sdk/SettingsHandler.java +++ b/src/main/java/com/meilisearch/sdk/SettingsHandler.java @@ -2,6 +2,7 @@ import com.meilisearch.sdk.exceptions.MeilisearchException; import com.meilisearch.sdk.http.URLBuilder; +import com.meilisearch.sdk.model.Embedder; import com.meilisearch.sdk.model.Faceting; import com.meilisearch.sdk.model.LocalizedAttribute; import com.meilisearch.sdk.model.Pagination; @@ -769,4 +770,47 @@ public TaskInfo resetNonSeparatorTokensSettings(String uid) { return httpClient.delete( settingsPath(uid).addSubroute("non-separator-tokens").getURL(), TaskInfo.class); } + + /** + * Gets the embedders settings of the index + * + * @param uid Index identifier + * @return a Map that contains all embedders settings + * @throws MeilisearchException if an error occurs + */ + Map getEmbedders(String uid) throws MeilisearchException { + return httpClient.get( + settingsPath(uid).addSubroute("embedders").getURL(), + Map.class, + String.class, + Embedder.class); + } + + /** + * Updates the embedders settings of the index + * + * @param uid Index identifier + * @param embedders a Map that contains the new embedders settings + * @return TaskInfo instance + * @throws MeilisearchException if an error occurs + */ + TaskInfo updateEmbedders(String uid, Map embedders) + throws MeilisearchException { + return httpClient.patch( + settingsPath(uid).addSubroute("embedders").getURL(), + embedders == null ? null : httpClient.jsonHandler.encode(embedders), + TaskInfo.class); + } + + /** + * Resets the embedders settings of the index + * + * @param uid Index identifier + * @return TaskInfo instance + * @throws MeilisearchException if an error occurs + */ + TaskInfo resetEmbedders(String uid) throws MeilisearchException { + return httpClient.delete( + settingsPath(uid).addSubroute("embedders").getURL(), TaskInfo.class); + } } diff --git a/src/main/java/com/meilisearch/sdk/SimilarDocumentRequest.java b/src/main/java/com/meilisearch/sdk/SimilarDocumentRequest.java index 99ad5f46..0e9c869c 100644 --- a/src/main/java/com/meilisearch/sdk/SimilarDocumentRequest.java +++ b/src/main/java/com/meilisearch/sdk/SimilarDocumentRequest.java @@ -21,28 +21,22 @@ public class SimilarDocumentRequest { private Double rankingScoreThreshold; private Boolean retrieveVectors; - /** - * Constructor for SimilarDocumentsRequest for building search request for similar documents - * with the default values: id null, embedder "default", attributesToRetrieve ["*"], offset 0, - * limit 20, filter null, showRankingScore false, showRankingScoreDetails false, - * rankingScoreThreshold null, retrieveVectors false - */ + /** Constructor for SimilarDocumentsRequest for building search request for similar documents */ public SimilarDocumentRequest() {} @Override public String toString() { - JSONObject jsonObject = - new JSONObject() - .put("id", this.id) - .put("embedder", this.embedder) - .put("attributesToRetrieve", this.attributesToRetrieve) - .put("offset", this.offset) - .put("limit", this.limit) - .put("filter", this.filter) - .put("showRankingScore", this.showRankingScore) - .put("showRankingScoreDetails", this.showRankingScoreDetails) - .put("rankingScoreThreshold", this.rankingScoreThreshold) - .put("retrieveVectors", this.retrieveVectors); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", this.id); + jsonObject.put("embedder", this.embedder); + jsonObject.putOpt("attributesToRetrieve", this.attributesToRetrieve); + jsonObject.putOpt("offset", this.offset); + jsonObject.putOpt("limit", this.limit); + jsonObject.putOpt("filter", this.filter); + jsonObject.putOpt("showRankingScore", this.showRankingScore); + jsonObject.putOpt("showRankingScoreDetails", this.showRankingScoreDetails); + jsonObject.putOpt("rankingScoreThreshold", this.rankingScoreThreshold); + jsonObject.putOpt("retrieveVectors", this.retrieveVectors); return jsonObject.toString(); } diff --git a/src/main/java/com/meilisearch/sdk/model/Embedder.java b/src/main/java/com/meilisearch/sdk/model/Embedder.java new file mode 100644 index 00000000..7179428d --- /dev/null +++ b/src/main/java/com/meilisearch/sdk/model/Embedder.java @@ -0,0 +1,73 @@ +package com.meilisearch.sdk.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Map; +import lombok.*; +import lombok.experimental.Accessors; + +@Builder +@AllArgsConstructor(access = AccessLevel.PACKAGE) +@Getter +@Setter +@Accessors(chain = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Embedder { + /** Source of the embedder. Accepts: ollama, rest, openAI, huggingFace and userProvided */ + protected EmbedderSource source; + + /** + * API key for authentication with the embedder service. Optional: Only applicable for openAi, + * ollama, and rest sources. + */ + protected String apiKey; + + /** + * Model to use for generating embeddings. Optional: Only applicable for ollama, openAI, and + * huggingFace sources. + */ + protected String model; + + /** Template for document embedding. Optional. */ + protected String documentTemplate; + + /** + * Dimensions of the embedding vectors. Optional: Only applicable for openAi, huggingFace, + * ollama, and rest sources. + */ + protected Integer dimensions; + + /** Distribution configuration. Optional. */ + protected EmbedderDistribution distribution; + + /** Request configuration. Mandatory only when using rest embedder, optional otherwise. */ + protected Map request; + + /** Response configuration. Mandatory only when using rest embedder, optional otherwise. */ + protected Map response; + + /** Maximum bytes for document template. Optional. */ + protected Integer documentTemplateMaxBytes; + + /** Revision identifier. Optional: Only applicable for huggingFace. */ + protected String revision; + + /** HTTP headers. Optional: Only applicable for rest. */ + protected Map headers; + + /** Whether to use binary quantization. Optional. */ + protected Boolean binaryQuantized; + + /** URL for the embedder service. Optional. */ + protected String url; + + /** Input fields for the embedder. Optional. */ + protected String[] inputField; + + /** Type of input for the embedder. Optional. */ + protected EmbedderInputType inputType; + + /** Query for the embedder. Optional. */ + protected String query; + + public Embedder() {} +} diff --git a/src/main/java/com/meilisearch/sdk/model/EmbedderDistribution.java b/src/main/java/com/meilisearch/sdk/model/EmbedderDistribution.java new file mode 100644 index 00000000..e99480bb --- /dev/null +++ b/src/main/java/com/meilisearch/sdk/model/EmbedderDistribution.java @@ -0,0 +1,56 @@ +package com.meilisearch.sdk.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * Describes the natural distribution of search results for embedders. Contains mean and sigma + * values, each between 0 and 1. + */ +@Builder +@AllArgsConstructor(access = AccessLevel.PACKAGE) +@NoArgsConstructor(access = AccessLevel.PUBLIC) +@Getter +@Setter +@Accessors(chain = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class EmbedderDistribution { + /** Mean value of the distribution, between 0 and 1 */ + private Double mean; + + /** Sigma (standard deviation) value of the distribution, between 0 and 1 */ + private Double sigma; + + /** + * Creates a uniform distribution with default values + * + * @return An EmbedderDistribution instance with mean=0.5 and sigma=0.5 + */ + public static EmbedderDistribution uniform() { + return new EmbedderDistribution().setMean(0.5).setSigma(0.5); + } + + /** + * Creates a custom distribution with specified mean and sigma values + * + * @param mean Mean value between 0 and 1 + * @param sigma Sigma value between 0 and 1 + * @return An EmbedderDistribution instance with the specified values + * @throws IllegalArgumentException if mean or sigma are outside the valid range + */ + public static EmbedderDistribution custom(double mean, double sigma) { + if (mean < 0 || mean > 1) { + throw new IllegalArgumentException("Mean must be between 0 and 1"); + } + if (sigma < 0 || sigma > 1) { + throw new IllegalArgumentException("Sigma must be between 0 and 1"); + } + return new EmbedderDistribution().setMean(mean).setSigma(sigma); + } +} diff --git a/src/main/java/com/meilisearch/sdk/model/Embedders.java b/src/main/java/com/meilisearch/sdk/model/Embedders.java deleted file mode 100644 index 99fa7408..00000000 --- a/src/main/java/com/meilisearch/sdk/model/Embedders.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.meilisearch.sdk.model; - -import lombok.*; -import lombok.experimental.Accessors; - -@Builder -@AllArgsConstructor(access = AccessLevel.PACKAGE) -@Getter -@Setter -@Accessors(chain = true) -public class Embedders { - protected EmbedderSource source; - protected String url; - protected String apiKey; - protected String model; - protected String documentTemplate; - protected Integer dimensions; - protected String revision; - protected String[] inputField; - protected EmbedderInputType inputType; - protected String query; - - public Embedders() {} -} diff --git a/src/main/java/com/meilisearch/sdk/model/Hybrid.java b/src/main/java/com/meilisearch/sdk/model/Hybrid.java new file mode 100644 index 00000000..a95d0ec0 --- /dev/null +++ b/src/main/java/com/meilisearch/sdk/model/Hybrid.java @@ -0,0 +1,50 @@ +package com.meilisearch.sdk.model; + +// @brunoocasali: I don't think we should use the fasterxml.jackson annotations across the library, +// since this should be customizable 🤔 +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.json.JSONObject; + +/** Hybrid search configuration */ +@Builder +@AllArgsConstructor(access = AccessLevel.PACKAGE) +@NoArgsConstructor(access = AccessLevel.PACKAGE) +@Getter +@Setter +@Accessors(chain = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Hybrid { + /** Semantic ratio for hybrid search (between 0 and 1) */ + private Double semanticRatio; + + /** Embedder to use for hybrid search (mandatory if hybrid is set) */ + private String embedder; + + /** + * Method that returns the JSON representation of the Hybrid object + * + * @return JSONObject representation of the Hybrid object + */ + public JSONObject toJSONObject() { + return new JSONObject() + .putOpt("semanticRatio", this.semanticRatio) + .putOpt("embedder", this.embedder); + } + + /** + * Method that returns the JSON String of the Hybrid object + * + * @return JSON String of the Hybrid object + */ + @Override + public String toString() { + return toJSONObject().toString(); + } +} diff --git a/src/main/java/com/meilisearch/sdk/model/SearchResult.java b/src/main/java/com/meilisearch/sdk/model/SearchResult.java index a77f30a5..ef8c424a 100644 --- a/src/main/java/com/meilisearch/sdk/model/SearchResult.java +++ b/src/main/java/com/meilisearch/sdk/model/SearchResult.java @@ -22,6 +22,7 @@ public class SearchResult implements Searchable { int offset; int limit; int estimatedTotalHits; + HashMap _vectors; public SearchResult() {} } diff --git a/src/main/java/com/meilisearch/sdk/model/Settings.java b/src/main/java/com/meilisearch/sdk/model/Settings.java index 2a4233b6..419bd09f 100644 --- a/src/main/java/com/meilisearch/sdk/model/Settings.java +++ b/src/main/java/com/meilisearch/sdk/model/Settings.java @@ -31,7 +31,7 @@ public class Settings { protected Integer searchCutoffMs; protected String[] separatorTokens; protected String[] nonSeparatorTokens; - protected HashMap embedders; + protected HashMap embedders; protected LocalizedAttribute[] localizedAttributes; public Settings() {} diff --git a/src/test/java/com/meilisearch/integration/SearchTest.java b/src/test/java/com/meilisearch/integration/SearchTest.java index 86c17d92..c89a752d 100644 --- a/src/test/java/com/meilisearch/integration/SearchTest.java +++ b/src/test/java/com/meilisearch/integration/SearchTest.java @@ -1090,9 +1090,9 @@ public void testMultiSearchWithMergeFacets() { public void testSimilarDocuments() throws Exception { String indexUid = "SimilarDocuments"; Index index = client.index(indexUid); - HashMap embedders = new HashMap<>(); + HashMap embedders = new HashMap<>(); embedders.put( - "manual", new Embedders().setSource(EmbedderSource.USER_PROVIDED).setDimensions(3)); + "manual", new Embedder().setSource(EmbedderSource.USER_PROVIDED).setDimensions(3)); Settings settings = new Settings(); settings.setEmbedders(embedders); @@ -1116,6 +1116,81 @@ public void testSimilarDocuments() throws Exception { assertThat(hits.get(3).get("title"), is("Shazam!")); } + /** Test vector search */ + @Test + public void testVectorSearch() throws Exception { + String indexUid = "testVectorSearch"; + Index index = client.index(indexUid); + HashMap embedders = new HashMap<>(); + embedders.put( + "manual", new Embedder().setSource(EmbedderSource.USER_PROVIDED).setDimensions(3)); + + Settings settings = new Settings(); + settings.setEmbedders(embedders); + + index.updateSettings(settings); + + TestData testData = this.getTestData(VECTOR_MOVIES, Movie.class); + TaskInfo task = index.addDocuments(testData.getRaw()); + + index.waitForTask(task.getTaskUid()); + + SearchRequest searchRequest = + SearchRequest.builder() + .vector(new Double[] {0.1, 0.6, 0.8}) + .hybrid(Hybrid.builder().semanticRatio(0.5).embedder("manual").build()) + .build(); + + SearchResult searchResult = (SearchResult) index.search(searchRequest); + + assertThat(searchResult.getHits(), hasSize(5)); + // The most similar document should be "Escape Room" since its vector [0.1, 0.6, 0.8] + assertThat(searchResult.getHits().get(0).get("id"), is("522681")); + assertThat(searchResult.getHits().get(0).get("title"), is("Escape Room")); + } + + /** Test vector search with retrieveVectors option */ + @Test + public void testVectorSearchWithRetrieveVectors() throws Exception { + String indexUid = "testVectorSearchWithRetrieveVectors"; + Index index = client.index(indexUid); + HashMap embedders = new HashMap<>(); + embedders.put( + "manual", new Embedder().setSource(EmbedderSource.USER_PROVIDED).setDimensions(3)); + + Settings settings = new Settings(); + settings.setEmbedders(embedders); + + index.updateSettings(settings); + + TestData testData = this.getTestData(VECTOR_MOVIES, Movie.class); + TaskInfo task = index.addDocuments(testData.getRaw()); + + index.waitForTask(task.getTaskUid()); + + SearchRequest searchRequest = + SearchRequest.builder() + .vector(new Double[] {0.1, 0.6, 0.8}) + .hybrid(Hybrid.builder().semanticRatio(0.5).embedder("manual").build()) + .retrieveVectors(true) + .build(); + + SearchResult searchResult = (SearchResult) index.search(searchRequest); + + assertThat(searchResult.getHits(), hasSize(5)); + // The most similar document should be "Escape Room" since its vector [0.1, 0.6, 0.8] + assertThat(searchResult.getHits().get(0).get("id"), is("522681")); + assertThat(searchResult.getHits().get(0).get("title"), is("Escape Room")); + + // Verify that vectors are returned in the response + Map escapeRoomHit = searchResult.getHits().get(0); + assertThat(escapeRoomHit.containsKey("_vectors"), is(true)); + + @SuppressWarnings("unchecked") + Map vectors = (Map) escapeRoomHit.get("_vectors"); + assertThat(vectors.containsKey("manual"), is(true)); + } + /** Test Search with locales */ @Test public void testSearchWithLocales() throws Exception { @@ -1191,4 +1266,45 @@ public void testMultiSearchWithLocales() throws Exception { assertThat(results[0].getHits().size(), is(7)); assertThat(results[1].getHits().size(), is(2)); } + + /** Test search with retrieveVectors parameter */ + @Test + public void testSearchWithRetrieveVectors() throws Exception { + String indexUid = "testSearchWithRetrieveVectors"; + Index index = client.index(indexUid); + HashMap embedders = new HashMap<>(); + embedders.put( + "manual", new Embedder().setSource(EmbedderSource.USER_PROVIDED).setDimensions(3)); + + Settings settings = new Settings(); + settings.setEmbedders(embedders); + + index.updateSettings(settings); + + TestData testData = this.getTestData(VECTOR_MOVIES, Movie.class); + TaskInfo task = index.addDocuments(testData.getRaw()); + + index.waitForTask(task.getTaskUid()); + + // First search without retrieveVectors + SearchRequest searchRequestWithout = SearchRequest.builder().q("").build(); + SearchResult searchResultWithout = (SearchResult) index.search(searchRequestWithout); + + assertThat(searchResultWithout.getHits(), hasSize(5)); + Map hitWithout = searchResultWithout.getHits().get(0); + assertThat(hitWithout.containsKey("_vectors"), is(false)); + + // Then search with retrieveVectors + SearchRequest searchRequestWith = + SearchRequest.builder().q("").retrieveVectors(true).build(); + SearchResult searchResultWith = (SearchResult) index.search(searchRequestWith); + + assertThat(searchResultWith.getHits(), hasSize(5)); + Map hitWith = searchResultWith.getHits().get(0); + assertThat(hitWith.containsKey("_vectors"), is(true)); + + @SuppressWarnings("unchecked") + Map vectors = (Map) hitWith.get("_vectors"); + assertThat(vectors.containsKey("manual"), is(true)); + } } diff --git a/src/test/java/com/meilisearch/integration/SettingsTest.java b/src/test/java/com/meilisearch/integration/SettingsTest.java index b345bdac..a8dbbbe0 100644 --- a/src/test/java/com/meilisearch/integration/SettingsTest.java +++ b/src/test/java/com/meilisearch/integration/SettingsTest.java @@ -16,6 +16,9 @@ import com.meilisearch.integration.classes.AbstractIT; import com.meilisearch.integration.classes.TestData; import com.meilisearch.sdk.Index; +import com.meilisearch.sdk.model.Embedder; +import com.meilisearch.sdk.model.EmbedderDistribution; +import com.meilisearch.sdk.model.EmbedderSource; import com.meilisearch.sdk.model.FacetSortValue; import com.meilisearch.sdk.model.Faceting; import com.meilisearch.sdk.model.LocalizedAttribute; @@ -1465,4 +1468,69 @@ public void testResetNonSeparatorTokensSettings() throws Exception { nonSeparatorTokensAfterReset, is(arrayWithSize(initialNonSeparatorTokens.length))); assertThat(nonSeparatorTokensAfterReset, is(equalTo(initialNonSeparatorTokens))); } + + @Test + @DisplayName("Test get embedders settings by uid") + public void testGetEmbeddersSettings() throws Exception { + Index index = createIndex("testGetEmbeddersSettings"); + Settings initialSettings = index.getSettings(); + Map initialEmbedders = index.getEmbeddersSettings(); + + assertThat(initialEmbedders, is(equalTo(initialSettings.getEmbedders()))); + } + + public Embedder createUserProvidedEmbedder() { + return Embedder.builder() + .source(EmbedderSource.USER_PROVIDED) + .dimensions(1) + .distribution(EmbedderDistribution.builder().mean(0.7).sigma(0.3).build()) + .binaryQuantized(false) + .build(); + } + + @Test + @DisplayName("Test update embedders settings") + public void testUpdateEmbeddersSettings() throws Exception { + Index index = createEmptyIndex("testUpdateEmbeddersSettings"); + + HashMap newEmbedders = new HashMap<>(); + Embedder embedder = createUserProvidedEmbedder(); + newEmbedders.put("default", embedder); + TaskInfo task = index.updateEmbeddersSettings(newEmbedders); + index.waitForTask(task.getTaskUid()); + + Map updatedEmbedders = index.getEmbeddersSettings(); + assertThat(updatedEmbedders.size(), is(equalTo(1))); + Embedder retrievedEmbedder = updatedEmbedders.get("default"); + assertThat(retrievedEmbedder.getSource(), is(equalTo(embedder.getSource()))); + assertThat(retrievedEmbedder.getDimensions(), is(equalTo(embedder.getDimensions()))); + assertThat( + retrievedEmbedder.getDistribution().getMean(), + is(equalTo(embedder.getDistribution().getMean()))); + assertThat( + retrievedEmbedder.getDistribution().getSigma(), + is(equalTo(embedder.getDistribution().getSigma()))); + assertThat( + retrievedEmbedder.getBinaryQuantized(), is(equalTo(embedder.getBinaryQuantized()))); + } + + @Test + @DisplayName("Test reset embedders settings") + public void testResetEmbeddersSettings() throws Exception { + Index index = createEmptyIndex("testResetEmbeddersSettings"); + + HashMap embedders = new HashMap<>(); + embedders.put("custom", createUserProvidedEmbedder()); + TaskInfo updateTask = index.updateEmbeddersSettings(embedders); + index.waitForTask(updateTask.getTaskUid()); + + Map updatedEmbedders = index.getEmbeddersSettings(); + assertThat(updatedEmbedders.size(), is(equalTo(1))); + assertThat(updatedEmbedders.containsKey("custom"), is(true)); + + TaskInfo resetTask = index.resetEmbeddersSettings(); + index.waitForTask(resetTask.getTaskUid()); + Map resetEmbedders = index.getEmbeddersSettings(); + assertThat(resetEmbedders.size(), is(equalTo(0))); + } } diff --git a/src/test/java/com/meilisearch/integration/TasksTest.java b/src/test/java/com/meilisearch/integration/TasksTest.java index 0eec58d1..391b57ca 100644 --- a/src/test/java/com/meilisearch/integration/TasksTest.java +++ b/src/test/java/com/meilisearch/integration/TasksTest.java @@ -138,6 +138,13 @@ public void testClientGetTaskErrorWhenAddingDocuments() throws Exception { /** Test Get Tasks with limit and from */ @Test public void testClientGetTasksLimitAndFrom() throws Exception { + // Create several indexes to make sure we have enough tasks + int numIndexes = 4; + for (int i = 1; i <= numIndexes; i++) { + String indexUid = "GetClientTasksLimitFrom" + i; + TaskInfo response = client.createIndex(indexUid); + } + int limit = 2; int from = 2; TasksQuery query = new TasksQuery().setLimit(limit).setFrom(from); @@ -147,7 +154,7 @@ public void testClientGetTasksLimitAndFrom() throws Exception { assertThat(result.getFrom(), is(equalTo(from))); assertThat(result.getFrom(), is(notNullValue())); assertThat(result.getNext(), is(notNullValue())); - assertThat(result.getResults().length, is(notNullValue())); + assertThat(result.getResults().length, is(equalTo(limit))); } /** Test Get Tasks with uid as filter */ diff --git a/src/test/java/com/meilisearch/sdk/SearchRequestTest.java b/src/test/java/com/meilisearch/sdk/SearchRequestTest.java index 6eb9f850..e32ecd9f 100644 --- a/src/test/java/com/meilisearch/sdk/SearchRequestTest.java +++ b/src/test/java/com/meilisearch/sdk/SearchRequestTest.java @@ -5,7 +5,9 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import com.meilisearch.sdk.model.Hybrid; import com.meilisearch.sdk.model.MatchingStrategy; +import org.json.JSONObject; import org.junit.jupiter.api.Test; class SearchRequestTest { @@ -363,4 +365,58 @@ void toStringEveryParametersWithArrayMatchingStrategyNull() { assertThat(classToTest.getDistinct(), is(equalTo("distinct"))); assertThat(classToTest.toString(), is(equalTo(expectedToString))); } + + @Test + void toStringWithHybridUsingBuilder() { + SearchRequest classToTest = + SearchRequest.builder() + .q("This is a Test") + .hybrid(Hybrid.builder().semanticRatio(0.5).embedder("default").build()) + .build(); + + String expected = + "{\"q\":\"This is a Test\",\"hybrid\":{\"semanticRatio\":0.5,\"embedder\":\"default\"}}"; + assertThat(classToTest.toString(), is(equalTo(expected))); + + // Verify getters + assertThat(classToTest.getHybrid().getSemanticRatio(), is(equalTo(0.5))); + assertThat(classToTest.getHybrid().getEmbedder(), is(equalTo("default"))); + } + + @Test + void toStringWithHybridAndOtherParameters() { + SearchRequest classToTest = + SearchRequest.builder() + .q("This is a Test") + .offset(200) + .limit(900) + .hybrid( + Hybrid.builder() + .semanticRatio(0.7) + .embedder("custom-embedder") + .build()) + .build(); + + String expected = + "{\"q\":\"This is a Test\",\"hybrid\":{\"semanticRatio\":0.7,\"embedder\":\"custom-embedder\"},\"offset\":200,\"limit\":900}"; + assertThat(classToTest.toString(), is(equalTo(expected))); + } + + @Test + void toStringWithHybridOnlyEmbedder() { + SearchRequest classToTest = + new SearchRequest("This is a Test") + .setHybrid(Hybrid.builder().embedder("default").build()); + + String expected = "{\"q\":\"This is a Test\",\"hybrid\":{\"embedder\":\"default\"}}"; + assertThat(classToTest.toString(), is(equalTo(expected))); + } + + @Test + void toStringWithRetrieveVectors() { + SearchRequest searchRequest = new SearchRequest("test").setRetrieveVectors(true); + String result = searchRequest.toString(); + JSONObject json = new JSONObject(result); + assertThat(json.getBoolean("retrieveVectors"), is(true)); + } } diff --git a/src/test/java/com/meilisearch/sdk/SimilarDocumentRequestTest.java b/src/test/java/com/meilisearch/sdk/SimilarDocumentRequestTest.java new file mode 100644 index 00000000..7c649d69 --- /dev/null +++ b/src/test/java/com/meilisearch/sdk/SimilarDocumentRequestTest.java @@ -0,0 +1,89 @@ +package com.meilisearch.sdk; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +class SimilarDocumentRequestTest { + + @Test + void toStringDefaultValues() { + SimilarDocumentRequest request = new SimilarDocumentRequest(); + JSONObject json = new JSONObject(request.toString()); + + // only id and embedder should be absent (null) by default + assertThat(json.has("id"), is(false)); + assertThat(json.has("embedder"), is(false)); + assertThat(json.has("attributesToRetrieve"), is(false)); + assertThat(json.has("offset"), is(false)); + assertThat(json.has("limit"), is(false)); + assertThat(json.has("filter"), is(false)); + assertThat(json.has("showRankingScore"), is(false)); + assertThat(json.has("showRankingScoreDetails"), is(false)); + assertThat(json.has("rankingScoreThreshold"), is(false)); + assertThat(json.has("retrieveVectors"), is(false)); + } + + @Test + void toStringAllParameters() { + SimilarDocumentRequest request = + new SimilarDocumentRequest() + .setId("123") + .setEmbedder("custom") + .setAttributesToRetrieve(new String[] {"title", "description"}) + .setOffset(10) + .setLimit(20) + .setFilter("genre = 'action'") + .setShowRankingScore(true) + .setShowRankingScoreDetails(true) + .setRankingScoreThreshold(0.5) + .setRetrieveVectors(true); + + JSONObject json = new JSONObject(request.toString()); + assertThat(json.getString("id"), is(equalTo("123"))); + assertThat(json.getString("embedder"), is(equalTo("custom"))); + assertThat(json.getJSONArray("attributesToRetrieve").getString(0), is(equalTo("title"))); + assertThat( + json.getJSONArray("attributesToRetrieve").getString(1), is(equalTo("description"))); + assertThat(json.getInt("offset"), is(equalTo(10))); + assertThat(json.getInt("limit"), is(equalTo(20))); + assertThat(json.getString("filter"), is(equalTo("genre = 'action'"))); + assertThat(json.getBoolean("showRankingScore"), is(equalTo(true))); + assertThat(json.getBoolean("showRankingScoreDetails"), is(equalTo(true))); + assertThat(json.getDouble("rankingScoreThreshold"), is(equalTo(0.5))); + assertThat(json.getBoolean("retrieveVectors"), is(equalTo(true))); + } + + @Test + void gettersAndSetters() { + SimilarDocumentRequest request = + SimilarDocumentRequest.builder() + .id("123") + .embedder("custom") + .attributesToRetrieve(new String[] {"title", "description"}) + .offset(10) + .limit(20) + .filter("genre = 'action'") + .showRankingScore(true) + .showRankingScoreDetails(true) + .rankingScoreThreshold(0.5) + .retrieveVectors(true) + .build(); + + assertThat(request.getId(), is(equalTo("123"))); + assertThat(request.getEmbedder(), is(equalTo("custom"))); + assertThat( + request.getAttributesToRetrieve(), + is(equalTo(new String[] {"title", "description"}))); + assertThat(request.getOffset(), is(equalTo(10))); + assertThat(request.getLimit(), is(equalTo(20))); + assertThat(request.getFilter(), is(equalTo("genre = 'action'"))); + assertThat(request.getShowRankingScore(), is(equalTo(true))); + assertThat(request.getShowRankingScoreDetails(), is(equalTo(true))); + assertThat(request.getRankingScoreThreshold(), is(equalTo(0.5))); + assertThat(request.getRetrieveVectors(), is(equalTo(true))); + } +}