From dcc4978a1058e2db1176f5a06609eaab395f9657 Mon Sep 17 00:00:00 2001 From: Dan Fehrenbach Date: Wed, 8 Jan 2025 10:12:38 -0600 Subject: [PATCH 1/5] init to pull latest. --- src/Meilisearch/Index.Documents.cs | 30 ++++++++ src/Meilisearch/SimilarDocumentsResult.cs | 48 +++++++++++++ src/Meilisearch/SimilarDocumentsSearch.cs | 71 +++++++++++++++++++ .../SearchSimilarDocumentsTests.cs | 35 +++++++++ .../ServerConfigs/BaseUriServer.cs | 8 +++ 5 files changed, 192 insertions(+) create mode 100644 src/Meilisearch/SimilarDocumentsResult.cs create mode 100644 src/Meilisearch/SimilarDocumentsSearch.cs create mode 100644 tests/Meilisearch.Tests/SearchSimilarDocumentsTests.cs diff --git a/src/Meilisearch/Index.Documents.cs b/src/Meilisearch/Index.Documents.cs index e21c0097..42253880 100644 --- a/src/Meilisearch/Index.Documents.cs +++ b/src/Meilisearch/Index.Documents.cs @@ -572,5 +572,35 @@ public async Task FacetSearchAsync(string facetName, .ReadFromJsonAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); } + + /// + /// Retrieve documents similar to a specific search result. + /// + /// Type parameter to return. + /// + /// + /// + /// + public async Task> SearchSimilarDocuments(string id, + SimilarDocumentsSearch searchAttributes = default, CancellationToken cancellationToken = default) + { + SimilarDocumentsSearch similarSearch; + if(searchAttributes == null) + { + similarSearch = new SimilarDocumentsSearch() { Id = id }; + } + else + { + similarSearch = searchAttributes; + similarSearch.Id = id; + } + + var responseMessage = await _http.PostAsJsonAsync($"indexes/{Uid}/similar", similarSearch, cancellationToken) + .ConfigureAwait(false); + + return await responseMessage.Content + .ReadFromJsonAsync>() + .ConfigureAwait(false); + } } } diff --git a/src/Meilisearch/SimilarDocumentsResult.cs b/src/Meilisearch/SimilarDocumentsResult.cs new file mode 100644 index 00000000..96dc2233 --- /dev/null +++ b/src/Meilisearch/SimilarDocumentsResult.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Meilisearch +{ + /// + /// Represents the result of a similar documents search. + /// + /// Hit type. + public class SimilarDocumentsResult + { + /// + /// Documents similar to Id. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Results of the query. + /// + [JsonPropertyName("hits")] + public IReadOnlyCollection Hits { get; } + + /// + /// Number of documents skipped. + /// + [JsonPropertyName("offset")] + public int Offset { get; } + + /// + /// Number of documents to take. + /// + [JsonPropertyName("limit")] + public int Limit { get; } + + /// + /// Gets the estimated total number of hits returned by the search. + /// + [JsonPropertyName("estimatedTotalHits")] + public int EstimatedTotalHits { get; } + + /// + /// Processing time of the query. + /// + [JsonPropertyName("processingTimeMs")] + public int ProcessingTimeMs { get; } + } +} diff --git a/src/Meilisearch/SimilarDocumentsSearch.cs b/src/Meilisearch/SimilarDocumentsSearch.cs new file mode 100644 index 00000000..15c66941 --- /dev/null +++ b/src/Meilisearch/SimilarDocumentsSearch.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Meilisearch +{ + /// + /// Retrieve documents similar to a specific search result. + /// + public class SimilarDocumentsSearch + { + /// + /// Identifier of the target document. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// Embedder to use when computing recommendations. + /// + [JsonPropertyName("embedder")] + public string Embedder { get; set; } = "default"; + + /// + /// Attributes to display in the returned documents. + /// + [JsonPropertyName("attributesToRetrieve")] + public IEnumerable AttributesToRetrieve { get; set; } = new[] { "*" }; + + /// + /// Number of documents to skip. + /// + [JsonPropertyName("offset")] + public int Offset { get; set; } = 0; + + /// + /// Maximum number of documents returned. + /// + [JsonPropertyName("limit")] + public int Limit { get; set; } = 20; + + /// + /// Filter queries by an attribute's value. + /// + [JsonPropertyName("filter")] + public string Filter { get; set; } = null; + + /// + /// Display the global ranking score of a document. + /// + [JsonPropertyName("showRankingScore")] + public bool ShowRankingScore { get; set; } + + /// + /// Display detailed ranking score information. + /// + [JsonPropertyName("showRankingScoreDetails")] + public bool ShowRankingScoreDetails { get; set; } + + /// + /// Exclude results with low ranking scores. + /// + [JsonPropertyName("rankingScoreThreshold")] + public decimal? RankingScoreThreshold { get; set; } + + /// + /// Return document vector data. + /// + [JsonPropertyName("retrieveVectors")] + public bool RetrieveVectors { get; set; } + } +} diff --git a/tests/Meilisearch.Tests/SearchSimilarDocumentsTests.cs b/tests/Meilisearch.Tests/SearchSimilarDocumentsTests.cs new file mode 100644 index 00000000..f83374da --- /dev/null +++ b/tests/Meilisearch.Tests/SearchSimilarDocumentsTests.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; + +using Xunit; + +namespace Meilisearch.Tests +{ + public abstract class SearchSimilarDocumentsTests : IAsyncLifetime where TFixture : IndexFixture + { + private Index _basicIndex; + + private readonly TFixture _fixture; + + public SearchSimilarDocumentsTests(TFixture fixture) + { + _fixture = fixture; + } + + public async Task InitializeAsync() + { + await _fixture.DeleteAllIndexes(); + _basicIndex = await _fixture.SetUpBasicIndex("BasicIndex-SearchTests"); + } + + public Task DisposeAsync() => Task.CompletedTask; + + [Fact] + public async Task SearchSimilarDocuments() + { + _basicIndex.UpdateSettingsAsync() + + var response = await _basicIndex.SearchSimilarDocuments("13"); + Assert.Single(response.Hits); + } + } +} diff --git a/tests/Meilisearch.Tests/ServerConfigs/BaseUriServer.cs b/tests/Meilisearch.Tests/ServerConfigs/BaseUriServer.cs index 38d17dbf..63bf4def 100644 --- a/tests/Meilisearch.Tests/ServerConfigs/BaseUriServer.cs +++ b/tests/Meilisearch.Tests/ServerConfigs/BaseUriServer.cs @@ -56,6 +56,14 @@ public MeilisearchClientTests(ConfigFixture fixture) : base(fixture) } } + [Collection(CollectionFixtureName)] + public class SearchSimilarDocumentsTests : SearchSimilarDocumentsTests + { + public SearchSimilarDocumentsTests(ConfigFixture fixture) : base(fixture) + { + } + } + [Collection(CollectionFixtureName)] public class SearchTests : SearchTests { From ad0ad01129205ba0f9d68514999f4f10ef885735 Mon Sep 17 00:00:00 2001 From: Dan Fehrenbach Date: Wed, 8 Jan 2025 11:14:31 -0600 Subject: [PATCH 2/5] adding experimental features configuration endpoints --- src/Meilisearch/MeilisearchClient.cs | 29 +++++++++++++++ .../MeilisearchClientTests.cs | 35 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/Meilisearch/MeilisearchClient.cs b/src/Meilisearch/MeilisearchClient.cs index c926da86..b9c7688b 100644 --- a/src/Meilisearch/MeilisearchClient.cs +++ b/src/Meilisearch/MeilisearchClient.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Json; +using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -422,6 +423,33 @@ public string GenerateTenantToken(string apiKeyUid, TenantTokenRules searchRules return TenantToken.GenerateToken(apiKeyUid, searchRules, apiKey ?? ApiKey, expiresAt); } + /// + /// Get a list of all experimental features that can be activated via the /experimental-features route and whether or not they are currently activated. + /// + /// The cancellation token for this call. + /// A dictionary of experimental features and their current state. + public async Task> GetExperimentalFeaturesAsync(CancellationToken cancellationToken = default) + { + var response = await _http.GetAsync("experimental-features", cancellationToken).ConfigureAwait(false); + return await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Activate or deactivate experimental features. + /// + /// true to activate, false to deactivate. + /// Experimental feature name to change. + /// The cancellation token for this call. + /// The experimental feature's updated state. + public async Task> UpdateExperimentalFeatureAsync(string featureName, bool activeState, CancellationToken cancellationToken = default) + { + var feature = new Dictionary() { { featureName, activeState } }; + var response = await _http.PatchAsJsonAsync($"experimental-features", feature, Constants.JsonSerializerOptionsRemoveNulls, cancellationToken).ConfigureAwait(false); + + var responseData = await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken).ConfigureAwait(false); + return responseData.FirstOrDefault(); + } + /// /// Create a local reference to a task, without doing an HTTP call. /// @@ -437,5 +465,6 @@ private TaskEndpoint TaskEndpoint() return _taskEndpoint; } + } } diff --git a/tests/Meilisearch.Tests/MeilisearchClientTests.cs b/tests/Meilisearch.Tests/MeilisearchClientTests.cs index 21c4757b..971506cf 100644 --- a/tests/Meilisearch.Tests/MeilisearchClientTests.cs +++ b/tests/Meilisearch.Tests/MeilisearchClientTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; @@ -232,5 +233,39 @@ public void Deserialize_KnownTaskType_ReturnsEnumValue() Assert.Equal(TaskInfoType.IndexCreation, result); } + [Fact] + public async Task GetExperimentalFeatures() + { + await ResetExperimentalFeatures(); + + var features = await _defaultClient.GetExperimentalFeaturesAsync(); + + Assert.NotNull(features); + Assert.NotEmpty(features); + Assert.All(features, x => Assert.False(x.Value)); + } + + [Fact] + public async Task UpdateExperimentalFeatures() + { + await ResetExperimentalFeatures(); + + var currentFeatures = await _defaultClient.GetExperimentalFeaturesAsync(); + Assert.Contains(currentFeatures, x => x.Key == "vectorStore" && x.Value == false); + + var result = await _defaultClient.UpdateExperimentalFeatureAsync("vectorStore", true); + + Assert.Equal("vectorStore", result.Key); + Assert.True(result.Value); + + var updatedFeatures = await _defaultClient.GetExperimentalFeaturesAsync(); + Assert.Contains(updatedFeatures, x => x.Key == "vectorStore" && x.Value == true); + } + + private async Task ResetExperimentalFeatures() + { + foreach (var feature in await _defaultClient.GetExperimentalFeaturesAsync()) + await _defaultClient.UpdateExperimentalFeatureAsync(feature.Key, false); + } } } From 0ec6bd1ec671e10a7b93a5162ad995ff6d683e60 Mon Sep 17 00:00:00 2001 From: Dan Fehrenbach Date: Wed, 8 Jan 2025 11:19:58 -0600 Subject: [PATCH 3/5] rm extra usings --- src/Meilisearch/MeilisearchClient.cs | 1 - tests/Meilisearch.Tests/MeilisearchClientTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Meilisearch/MeilisearchClient.cs b/src/Meilisearch/MeilisearchClient.cs index b9c7688b..d1b6f67a 100644 --- a/src/Meilisearch/MeilisearchClient.cs +++ b/src/Meilisearch/MeilisearchClient.cs @@ -4,7 +4,6 @@ using System.Net; using System.Net.Http; using System.Net.Http.Json; -using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Threading; using System.Threading.Tasks; diff --git a/tests/Meilisearch.Tests/MeilisearchClientTests.cs b/tests/Meilisearch.Tests/MeilisearchClientTests.cs index 971506cf..f6dc6613 100644 --- a/tests/Meilisearch.Tests/MeilisearchClientTests.cs +++ b/tests/Meilisearch.Tests/MeilisearchClientTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; From c5911984db9d50c1fc7886865151602ffc67bb41 Mon Sep 17 00:00:00 2001 From: Dan Fehrenbach Date: Wed, 8 Jan 2025 12:02:35 -0600 Subject: [PATCH 4/5] add dataset with vectors --- tests/Meilisearch.Tests/Datasets.cs | 1 + .../Datasets/movies_with_vector_store.json | 36 +++++++++++++++++++ tests/Meilisearch.Tests/IndexFixture.cs | 15 ++++++++ .../SearchSimilarDocumentsTests.cs | 8 +++-- 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/Meilisearch.Tests/Datasets/movies_with_vector_store.json diff --git a/tests/Meilisearch.Tests/Datasets.cs b/tests/Meilisearch.Tests/Datasets.cs index 610454a2..9efb6080 100644 --- a/tests/Meilisearch.Tests/Datasets.cs +++ b/tests/Meilisearch.Tests/Datasets.cs @@ -17,6 +17,7 @@ internal static class Datasets public static readonly string MoviesForFacetingJsonPath = Path.Combine(BasePath, "movies_for_faceting.json"); public static readonly string MoviesWithIntIdJsonPath = Path.Combine(BasePath, "movies_with_int_id.json"); public static readonly string MoviesWithInfoJsonPath = Path.Combine(BasePath, "movies_with_info.json"); + public static readonly string MoviesWithVectorStoreJsonPath = Path.Combine(BasePath, "movies_with_vector_store.json"); public static readonly string ProductsForDistinctJsonPath = Path.Combine(BasePath, "products_for_distinct_search.json"); } diff --git a/tests/Meilisearch.Tests/Datasets/movies_with_vector_store.json b/tests/Meilisearch.Tests/Datasets/movies_with_vector_store.json new file mode 100644 index 00000000..26db300e --- /dev/null +++ b/tests/Meilisearch.Tests/Datasets/movies_with_vector_store.json @@ -0,0 +1,36 @@ +[ + { + "title": "Shazam!", + "release_year": 2019, + "id": "287947", + // Three semantic properties: + // 1. magic, anything that reminds you of magic + // 2. authority, anything that inspires command + // 3. horror, anything that inspires fear or dread + "_vectors": { "manual": [ 0.8, 0.4, -0.5 ] } + }, + { + "title": "Captain Marvel", + "release_year": 2019, + "id": "299537", + "_vectors": { "manual": [ 0.6, 0.8, -0.2 ] } + }, + { + "title": "Escape Room", + "release_year": 2019, + "id": "522681", + "_vectors": { "manual": [ 0.1, 0.6, 0.8 ] } + }, + { + "title": "How to Train Your Dragon: The Hidden World", + "release_year": 2019, + "id": "166428", + "_vectors": { "manual": [ 0.7, 0.7, -0.4 ] } + }, + { + "title": "All Quiet on the Western Front", + "release_year": 1930, + "id": "143", + "_vectors": { "manual": [ -0.5, 0.3, 0.85 ] } + } +] diff --git a/tests/Meilisearch.Tests/IndexFixture.cs b/tests/Meilisearch.Tests/IndexFixture.cs index 485a3d79..4ef0cbc4 100644 --- a/tests/Meilisearch.Tests/IndexFixture.cs +++ b/tests/Meilisearch.Tests/IndexFixture.cs @@ -173,6 +173,21 @@ public async Task SetUpIndexForRankingScoreThreshold(string indexUid) return index; } + public async Task SetUpIndexForSimilarDocumentsSearch(string indexUid) + { + var index = DefaultClient.Index(indexUid); + var movies = await JsonFileReader.ReadAsync>(Datasets.MoviesWithVectorStoreJsonPath); + var task = await index.AddDocumentsAsync(movies); + + var finishedTask = await index.WaitForTaskAsync(task.TaskUid); + if (finishedTask.Status != TaskInfoStatus.Succeeded) + { + throw new Exception("The documents were not added during SetUpIndexForSimilarDocumentsSearch. Impossible to run the tests."); + } + + return index; + } + public async Task DeleteAllIndexes() { var indexes = await DefaultClient.GetAllIndexesAsync(); diff --git a/tests/Meilisearch.Tests/SearchSimilarDocumentsTests.cs b/tests/Meilisearch.Tests/SearchSimilarDocumentsTests.cs index f83374da..2ef7dc05 100644 --- a/tests/Meilisearch.Tests/SearchSimilarDocumentsTests.cs +++ b/tests/Meilisearch.Tests/SearchSimilarDocumentsTests.cs @@ -6,6 +6,7 @@ namespace Meilisearch.Tests { public abstract class SearchSimilarDocumentsTests : IAsyncLifetime where TFixture : IndexFixture { + private readonly MeilisearchClient _client; private Index _basicIndex; private readonly TFixture _fixture; @@ -13,12 +14,13 @@ public abstract class SearchSimilarDocumentsTests : IAsyncLifetime whe public SearchSimilarDocumentsTests(TFixture fixture) { _fixture = fixture; + _client = fixture.DefaultClient; } public async Task InitializeAsync() { await _fixture.DeleteAllIndexes(); - _basicIndex = await _fixture.SetUpBasicIndex("BasicIndex-SearchTests"); + _basicIndex = await _fixture.SetUpIndexForSimilarDocumentsSearch("BasicIndexWithVectorStore-SearchTests"); } public Task DisposeAsync() => Task.CompletedTask; @@ -26,7 +28,9 @@ public async Task InitializeAsync() [Fact] public async Task SearchSimilarDocuments() { - _basicIndex.UpdateSettingsAsync() + await _client.UpdateExperimentalFeatureAsync("vectorStore", true); + + //TODO: add embedder var response = await _basicIndex.SearchSimilarDocuments("13"); Assert.Single(response.Hits); From 4f6848106c520367b693a3be68cc488625fa506c Mon Sep 17 00:00:00 2001 From: Dan Fehrenbach Date: Wed, 22 Jan 2025 15:32:10 -0600 Subject: [PATCH 5/5] whitespace --- src/Meilisearch/Index.Documents.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Meilisearch/Index.Documents.cs b/src/Meilisearch/Index.Documents.cs index 42253880..21fd7952 100644 --- a/src/Meilisearch/Index.Documents.cs +++ b/src/Meilisearch/Index.Documents.cs @@ -585,7 +585,7 @@ public async Task> SearchSimilarDocuments(string id SimilarDocumentsSearch searchAttributes = default, CancellationToken cancellationToken = default) { SimilarDocumentsSearch similarSearch; - if(searchAttributes == null) + if (searchAttributes == null) { similarSearch = new SimilarDocumentsSearch() { Id = id }; }