Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get similar documents #607

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
30 changes: 30 additions & 0 deletions src/Meilisearch/Index.Documents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -572,5 +572,35 @@ public async Task<FacetSearchResult> FacetSearchAsync(string facetName,
.ReadFromJsonAsync<FacetSearchResult>(cancellationToken: cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Retrieve documents similar to a specific search result.
/// </summary>
/// <typeparam name="T">Type parameter to return.</typeparam>
/// <param name="id"></param>
/// <param name="searchAttributes"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<SimilarDocumentsResult<T>> SearchSimilarDocuments<T>(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<SimilarDocumentsResult<T>>()
.ConfigureAwait(false);
}
}
}
28 changes: 28 additions & 0 deletions src/Meilisearch/MeilisearchClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,33 @@ public string GenerateTenantToken(string apiKeyUid, TenantTokenRules searchRules
return TenantToken.GenerateToken(apiKeyUid, searchRules, apiKey ?? ApiKey, expiresAt);
}

/// <summary>
/// Get a list of all experimental features that can be activated via the /experimental-features route and whether or not they are currently activated.
/// </summary>
/// <param name="cancellationToken">The cancellation token for this call.</param>
/// <returns>A dictionary of experimental features and their current state.</returns>
public async Task<Dictionary<string, bool>> GetExperimentalFeaturesAsync(CancellationToken cancellationToken = default)
{
var response = await _http.GetAsync("experimental-features", cancellationToken).ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<Dictionary<string, bool>>(cancellationToken: cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Activate or deactivate experimental features.
/// </summary>
/// <param name="activeState">true to activate, false to deactivate.</param>
/// <param name="featureName">Experimental feature name to change.</param>
/// <param name="cancellationToken">The cancellation token for this call.</param>
/// <returns>The experimental feature's updated state.</returns>
public async Task<KeyValuePair<string, bool>> UpdateExperimentalFeatureAsync(string featureName, bool activeState, CancellationToken cancellationToken = default)
{
var feature = new Dictionary<string, bool>() { { featureName, activeState } };
var response = await _http.PatchAsJsonAsync($"experimental-features", feature, Constants.JsonSerializerOptionsRemoveNulls, cancellationToken).ConfigureAwait(false);

var responseData = await response.Content.ReadFromJsonAsync<Dictionary<string, bool>>(cancellationToken: cancellationToken).ConfigureAwait(false);
return responseData.FirstOrDefault();
}

/// <summary>
/// Create a local reference to a task, without doing an HTTP call.
/// </summary>
Expand All @@ -437,5 +464,6 @@ private TaskEndpoint TaskEndpoint()
return _taskEndpoint;
}


}
}
48 changes: 48 additions & 0 deletions src/Meilisearch/SimilarDocumentsResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Meilisearch
{
/// <summary>
/// Represents the result of a similar documents search.
/// </summary>
/// <typeparam name="T">Hit type.</typeparam>
public class SimilarDocumentsResult<T>
{
/// <summary>
/// Documents similar to Id.
/// </summary>
[JsonPropertyName("id")]
public string Id { get; set; }

/// <summary>
/// Results of the query.
/// </summary>
[JsonPropertyName("hits")]
public IReadOnlyCollection<T> Hits { get; }

/// <summary>
/// Number of documents skipped.
/// </summary>
[JsonPropertyName("offset")]
public int Offset { get; }

/// <summary>
/// Number of documents to take.
/// </summary>
[JsonPropertyName("limit")]
public int Limit { get; }

/// <summary>
/// Gets the estimated total number of hits returned by the search.
/// </summary>
[JsonPropertyName("estimatedTotalHits")]
public int EstimatedTotalHits { get; }

/// <summary>
/// Processing time of the query.
/// </summary>
[JsonPropertyName("processingTimeMs")]
public int ProcessingTimeMs { get; }
}
}
71 changes: 71 additions & 0 deletions src/Meilisearch/SimilarDocumentsSearch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Meilisearch
{
/// <summary>
/// Retrieve documents similar to a specific search result.
/// </summary>
public class SimilarDocumentsSearch
{
/// <summary>
/// Identifier of the target document.
/// </summary>
[JsonPropertyName("id")]
public string Id { get; set; }

/// <summary>
/// Embedder to use when computing recommendations.
/// </summary>
[JsonPropertyName("embedder")]
public string Embedder { get; set; } = "default";

/// <summary>
/// Attributes to display in the returned documents.
/// </summary>
[JsonPropertyName("attributesToRetrieve")]
public IEnumerable<string> AttributesToRetrieve { get; set; } = new[] { "*" };

/// <summary>
/// Number of documents to skip.
/// </summary>
[JsonPropertyName("offset")]
public int Offset { get; set; } = 0;

/// <summary>
/// Maximum number of documents returned.
/// </summary>
[JsonPropertyName("limit")]
public int Limit { get; set; } = 20;

/// <summary>
/// Filter queries by an attribute's value.
/// </summary>
[JsonPropertyName("filter")]
public string Filter { get; set; } = null;

/// <summary>
/// Display the global ranking score of a document.
/// </summary>
[JsonPropertyName("showRankingScore")]
public bool ShowRankingScore { get; set; }

/// <summary>
/// Display detailed ranking score information.
/// </summary>
[JsonPropertyName("showRankingScoreDetails")]
public bool ShowRankingScoreDetails { get; set; }

/// <summary>
/// Exclude results with low ranking scores.
/// </summary>
[JsonPropertyName("rankingScoreThreshold")]
public decimal? RankingScoreThreshold { get; set; }

/// <summary>
/// Return document vector data.
/// </summary>
[JsonPropertyName("retrieveVectors")]
public bool RetrieveVectors { get; set; }
}
}
1 change: 1 addition & 0 deletions tests/Meilisearch.Tests/Datasets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
36 changes: 36 additions & 0 deletions tests/Meilisearch.Tests/Datasets/movies_with_vector_store.json
Original file line number Diff line number Diff line change
@@ -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 ] }
}
]
15 changes: 15 additions & 0 deletions tests/Meilisearch.Tests/IndexFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@ public async Task<Index> SetUpIndexForRankingScoreThreshold(string indexUid)
return index;
}

public async Task<Index> SetUpIndexForSimilarDocumentsSearch(string indexUid)
{
var index = DefaultClient.Index(indexUid);
var movies = await JsonFileReader.ReadAsync<List<MovieWithInfo>>(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();
Expand Down
34 changes: 34 additions & 0 deletions tests/Meilisearch.Tests/MeilisearchClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,5 +232,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);
}
}
}
39 changes: 39 additions & 0 deletions tests/Meilisearch.Tests/SearchSimilarDocumentsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Threading.Tasks;

using Xunit;

namespace Meilisearch.Tests
{
public abstract class SearchSimilarDocumentsTests<TFixture> : IAsyncLifetime where TFixture : IndexFixture
{
private readonly MeilisearchClient _client;
private Index _basicIndex;

private readonly TFixture _fixture;

public SearchSimilarDocumentsTests(TFixture fixture)
{
_fixture = fixture;
_client = fixture.DefaultClient;
}

public async Task InitializeAsync()
{
await _fixture.DeleteAllIndexes();
_basicIndex = await _fixture.SetUpIndexForSimilarDocumentsSearch("BasicIndexWithVectorStore-SearchTests");
}

public Task DisposeAsync() => Task.CompletedTask;

[Fact]
public async Task SearchSimilarDocuments()
{
await _client.UpdateExperimentalFeatureAsync("vectorStore", true);

//TODO: add embedder

var response = await _basicIndex.SearchSimilarDocuments<Movie>("13");
Assert.Single(response.Hits);
}
}
}
8 changes: 8 additions & 0 deletions tests/Meilisearch.Tests/ServerConfigs/BaseUriServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ public MeilisearchClientTests(ConfigFixture fixture) : base(fixture)
}
}

[Collection(CollectionFixtureName)]
public class SearchSimilarDocumentsTests : SearchSimilarDocumentsTests<ConfigFixture>
{
public SearchSimilarDocumentsTests(ConfigFixture fixture) : base(fixture)
{
}
}

[Collection(CollectionFixtureName)]
public class SearchTests : SearchTests<ConfigFixture>
{
Expand Down
Loading