diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index b5b82ec5..a4fb31fd 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -613,6 +613,22 @@ update_localized_attribute_settings_1: |- ); reset_localized_attribute_settings_1: |- client.index("INDEX_NAME").resetLocalizedAttributesSettings(); +get_foreign_keys_setting_1: |- + client.index("INDEX_NAME").getForeignKeysSettings(); +update_foreign_keys_setting_1: |- + ForeignKey firstForeignKey = new ForeignKey(); + firstForeignKey.setForeignIndexUid("authors"); + firstForeignKey.setFieldName("author"); + + ForeignKey secondForeignKey = new ForeignKey(); + secondForeignKey.setForeignIndexUid("authors"); + secondForeignKey.setFieldName("related_authors"); + + client.index("books").updateForeignKeysSettings( + new ForeignKey[]{firstForeignKey, secondForeignKey} + ); +reset_foreign_keys_setting_1: |- + client.index("INDEX_NAME").resetForeignKeysSettings(); export_post_1: |- Map indexes = new HashMap<>(); indexes.put("*", ExportIndexFilter.builder().overrideSettings(true).build()); @@ -639,4 +655,4 @@ webhooks_patch_1: |- Webhook updated_webhook = this.client.updateWebhook(webhook.getUuid(), webhookReq2); webhooks_delete_1: |- - this.client.deleteWebhook("WEBHOOK_UUID"); + this.client.deleteWebhook("WEBHOOK_UUID"); \ No newline at end of file diff --git a/build.gradle b/build.gradle index be1c8c05..70e66eb8 100644 --- a/build.gradle +++ b/build.gradle @@ -109,13 +109,15 @@ jacocoTestReport { } tasks.register('integrationTest', Test) { - useJUnitPlatform { - includeTags 'integration' - } - testLogging { - events 'passed', 'skipped', 'failed' - showStandardStreams = System.getenv('DEBUG') != null - } + testClassesDirs = sourceSets.test.output.classesDirs // tells Gradle WHERE test classes are + classpath = sourceSets.test.runtimeClasspath // gives JUnit all dependencies it needs + useJUnitPlatform { + includeTags 'integration' + } + testLogging { + events 'passed', 'skipped', 'failed' + showStandardStreams = System.getenv('DEBUG') != null + } } java { diff --git a/gradlew.bat b/gradlew.bat index e509b2dd..c4bdd3ab 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,93 +1,93 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/com/meilisearch/sdk/Index.java b/src/main/java/com/meilisearch/sdk/Index.java index 97d397b7..2aeb0a4d 100644 --- a/src/main/java/com/meilisearch/sdk/Index.java +++ b/src/main/java/com/meilisearch/sdk/Index.java @@ -1435,6 +1435,49 @@ public TaskInfo resetEmbeddersSettings() throws MeilisearchException { return this.settingsHandler.resetEmbedders(this.uid); } + /** + * Gets the foreign keys settings of the index. + * + *

Foreign keys are an experimental feature (Meilisearch v1.39+) that allow cross-index + * document hydration. The user must enable this experimental feature on their Meilisearch + * instance before using these settings. + * + * @return an array of ForeignKey that describes cross-index relationships + * @throws MeilisearchException if an error occurs + * @see API + * specification + */ + public ForeignKey[] getForeignKeysSettings() throws MeilisearchException { + // Delegates to SettingsHandler, passing the index uid. + return this.settingsHandler.getForeignKeysSettings(this.uid); + } + + /** + * Updates the foreign keys settings of the index. + * + * @param foreignKeys an array of ForeignKey objects describing cross-index relationships + * @return TaskInfo instance + * @throws MeilisearchException if an error occurs + * @see API + * specification + */ + public TaskInfo updateForeignKeysSettings(ForeignKey[] foreignKeys) + throws MeilisearchException { + return this.settingsHandler.updateForeignKeysSettings(this.uid, foreignKeys); + } + + /** + * Resets the foreign keys settings of the index to the default (empty) value. + * + * @return TaskInfo instance + * @throws MeilisearchException if an error occurs + * @see API + * specification + */ + public TaskInfo resetForeignKeysSettings() throws MeilisearchException { + return this.settingsHandler.resetForeignKeysSettings(this.uid); + } + /** * Compacts the database for this index to reclaim unused space * diff --git a/src/main/java/com/meilisearch/sdk/SettingsHandler.java b/src/main/java/com/meilisearch/sdk/SettingsHandler.java index 1a20056b..4ff06222 100644 --- a/src/main/java/com/meilisearch/sdk/SettingsHandler.java +++ b/src/main/java/com/meilisearch/sdk/SettingsHandler.java @@ -6,6 +6,7 @@ import com.meilisearch.sdk.model.Faceting; import com.meilisearch.sdk.model.FilterableAttributesConfig; import com.meilisearch.sdk.model.FilterableAttributesLegacyAdapter; +import com.meilisearch.sdk.model.ForeignKey; import com.meilisearch.sdk.model.LocalizedAttribute; import com.meilisearch.sdk.model.Pagination; import com.meilisearch.sdk.model.Settings; @@ -853,4 +854,61 @@ TaskInfo resetEmbedders(String uid) throws MeilisearchException { return httpClient.delete( settingsPath(uid).addSubroute("embedders").getURL(), TaskInfo.class); } + + /** + * Gets the foreign keys settings of the index. + * + *

Foreign keys are an experimental feature (Meilisearch v1.39+) that allow cross-index + * document hydration. The user must enable this experimental feature on their Meilisearch + * instance before using these settings. + * + * @param uid Index identifier + * @return an array of ForeignKey that contains the foreign keys settings + * @throws MeilisearchException if an error occurs + * @see API + * specification + */ + ForeignKey[] getForeignKeysSettings(String uid) throws MeilisearchException { + // GET /indexes/{uid}/settings/foreign-keys + // Returns an array of ForeignKey objects, each with a foreignIndexUid and fieldName. + return httpClient.get( + settingsPath(uid).addSubroute("foreign-keys").getURL(), ForeignKey[].class); + } + + /** + * Updates the foreign keys settings of the index. + * + * @param uid Index identifier + * @param foreignKeys an array of ForeignKey objects describing cross-index relationships + * @return TaskInfo instance + * @throws MeilisearchException if an error occurs + * @see API + * specification + */ + TaskInfo updateForeignKeysSettings(String uid, ForeignKey[] foreignKeys) + throws MeilisearchException { + // PUT /indexes/{uid}/settings/foreign-keys + // If foreignKeys is null, we encode it as a JSON null to reset the setting. + // Otherwise we send the array directly and let the HTTP client serialize it to JSON. + return httpClient.put( + settingsPath(uid).addSubroute("foreign-keys").getURL(), + foreignKeys == null ? httpClient.jsonHandler.encode(foreignKeys) : foreignKeys, + TaskInfo.class); + } + + /** + * Resets the foreign keys settings of the index to the default (empty) value. + * + * @param uid Index identifier + * @return TaskInfo instance + * @throws MeilisearchException if an error occurs + * @see API + * specification + */ + TaskInfo resetForeignKeysSettings(String uid) throws MeilisearchException { + // DELETE /indexes/{uid}/settings/foreign-keys resets the setting to its default empty + // value. + return httpClient.delete( + settingsPath(uid).addSubroute("foreign-keys").getURL(), TaskInfo.class); + } } diff --git a/src/main/java/com/meilisearch/sdk/model/ForeignKey.java b/src/main/java/com/meilisearch/sdk/model/ForeignKey.java new file mode 100644 index 00000000..a2e20a08 --- /dev/null +++ b/src/main/java/com/meilisearch/sdk/model/ForeignKey.java @@ -0,0 +1,28 @@ +package com.meilisearch.sdk.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * ForeignKey setting data structure for cross-index document hydration. + * + *

Each ForeignKey entry links a field in the current index to a field in another index, allowing + * search results to be automatically enriched with related data. + * + * @see API + * specification + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ForeignKey { + + // The uid of the foreign (related) index this entry points to. + protected String foreignIndexUid; + + // The name of the field in the current index that holds the foreign key value. + protected String fieldName; +} diff --git a/src/main/java/com/meilisearch/sdk/model/Settings.java b/src/main/java/com/meilisearch/sdk/model/Settings.java index 8b74c4d0..8998d333 100644 --- a/src/main/java/com/meilisearch/sdk/model/Settings.java +++ b/src/main/java/com/meilisearch/sdk/model/Settings.java @@ -39,6 +39,10 @@ public class Settings { protected HashMap embedders; protected LocalizedAttribute[] localizedAttributes; + // Holds the list of foreign key relationships for cross-index document hydration. + // This is an experimental Meilisearch feature (v1.39+). + protected ForeignKey[] foreignKeys; + public Settings() {} /** Granular filterable attributes accessor. */ diff --git a/src/test/java/com/meilisearch/integration/SettingsTest.java b/src/test/java/com/meilisearch/integration/SettingsTest.java index bbb8c84e..567745dc 100644 --- a/src/test/java/com/meilisearch/integration/SettingsTest.java +++ b/src/test/java/com/meilisearch/integration/SettingsTest.java @@ -26,6 +26,7 @@ import com.meilisearch.sdk.model.FilterableAttributesConfig; import com.meilisearch.sdk.model.FilterableAttributesFeatures; import com.meilisearch.sdk.model.FilterableAttributesFilter; +import com.meilisearch.sdk.model.ForeignKey; import com.meilisearch.sdk.model.LocalizedAttribute; import com.meilisearch.sdk.model.Pagination; import com.meilisearch.sdk.model.Settings; @@ -1639,4 +1640,102 @@ public void testResetEmbeddersSettings() throws Exception { Map resetEmbedders = index.getEmbeddersSettings(); assertThat(resetEmbedders.size(), is(equalTo(0))); } + + /** Tests of the foreignKeys setting methods (experimental feature, Meilisearch v1.39+) */ + @Test + @DisplayName("Test get foreign keys settings by uid") + public void testGetForeignKeysSettings() throws Exception { + Index index = createIndex("testGetForeignKeysSettings"); + + // Fetch global settings and the dedicated endpoint — they must agree. + Settings initialSettings = index.getSettings(); + ForeignKey[] initialForeignKeys = index.getForeignKeysSettings(); + + int dedicatedSize = initialForeignKeys == null ? 0 : initialForeignKeys.length; + int globalSize = + initialSettings.getForeignKeys() == null + ? 0 + : initialSettings.getForeignKeys().length; + assertThat(dedicatedSize, is(equalTo(globalSize))); + } + + @Test + @DisplayName("Test update foreign keys settings") + public void testUpdateForeignKeysSettings() throws Exception { + Index index = createIndex("testUpdateForeignKeysSettings"); + + // Record what is there before we make any changes. + ForeignKey[] initialForeignKeys = index.getForeignKeysSettings(); + + // Build two ForeignKey entries using setters (mirrors the LocalizedAttribute pattern). + ForeignKey firstKey = new ForeignKey(); + firstKey.setForeignIndexUid("authors"); // the related index uid + firstKey.setFieldName("author_id"); // the field in THIS index that holds the link + + ForeignKey secondKey = new ForeignKey(); + secondKey.setForeignIndexUid("genres"); + secondKey.setFieldName("genre_id"); + + ForeignKey[] newForeignKeys = new ForeignKey[] {firstKey, secondKey}; + + // Push the update and wait for the async task to finish. + index.waitForTask(index.updateForeignKeysSettings(newForeignKeys).getTaskUid()); + + // Fetch the setting back and verify every field was stored correctly. + ForeignKey[] updatedForeignKeys = index.getForeignKeysSettings(); + + assertThat(updatedForeignKeys, is(arrayWithSize(newForeignKeys.length))); + assertThat( + updatedForeignKeys[0].getForeignIndexUid(), + is(equalTo(newForeignKeys[0].getForeignIndexUid()))); + assertThat( + updatedForeignKeys[0].getFieldName(), + is(equalTo(newForeignKeys[0].getFieldName()))); + assertThat( + updatedForeignKeys[1].getForeignIndexUid(), + is(equalTo(newForeignKeys[1].getForeignIndexUid()))); + assertThat( + updatedForeignKeys[1].getFieldName(), + is(equalTo(newForeignKeys[1].getFieldName()))); + + // The updated setting must differ from what was there initially. + assertThat(updatedForeignKeys, is(not(equalTo(initialForeignKeys)))); + } + + @Test + @DisplayName("Test reset foreign keys settings") + public void testResetForeignKeysSettings() throws Exception { + Index index = createIndex("testResetForeignKeysSettings"); + + ForeignKey[] initialForeignKeys = index.getForeignKeysSettings(); + + // Apply some foreign keys first. + ForeignKey firstKey = new ForeignKey(); + firstKey.setForeignIndexUid("authors"); + firstKey.setFieldName("author_id"); + + ForeignKey secondKey = new ForeignKey(); + secondKey.setForeignIndexUid("genres"); + secondKey.setFieldName("genre_id"); + + ForeignKey[] newForeignKeys = new ForeignKey[] {firstKey, secondKey}; + + index.waitForTask(index.updateForeignKeysSettings(newForeignKeys).getTaskUid()); + ForeignKey[] updatedForeignKeys = index.getForeignKeysSettings(); + + // Now reset and verify the setting returned to its initial state. + index.waitForTask(index.resetForeignKeysSettings().getTaskUid()); + ForeignKey[] foreignKeysAfterReset = index.getForeignKeysSettings(); + + assertThat(updatedForeignKeys, is(arrayWithSize(newForeignKeys.length))); + assertThat( + updatedForeignKeys[0].getForeignIndexUid(), + is(equalTo(newForeignKeys[0].getForeignIndexUid()))); + assertThat( + updatedForeignKeys[0].getFieldName(), + is(equalTo(newForeignKeys[0].getFieldName()))); + + // After reset the setting must differ from the updated state. + assertThat(foreignKeysAfterReset, is(not(equalTo(updatedForeignKeys)))); + } }