Skip to content
Open
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
5 changes: 5 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -837,3 +837,8 @@ facet_search_3: |-
FacetQuery = "c"
};
await client.Index("books").FacetSearchAsync("genres", query);
get_similar_documents_post_1: |-
await client.Index("movies").GetSimilarDocumentsAsync<Movie>(new SimilarDocumentsQuery() {
Id = 143,
Embedder = "manual"
});
72 changes: 72 additions & 0 deletions src/Meilisearch/Converters/EmbedderSourceConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System.Text.Json;
using System;
using System.Text.Json.Serialization;

namespace Meilisearch.Converters
{
/// <summary>
///
/// </summary>
public class EmbedderSourceConverter: JsonConverter<EmbedderSource>
{
/// <summary>
///
/// </summary>
/// <param name="reader"></param>
/// <param name="typeToConvert"></param>
/// <param name="options"></param>
/// <returns></returns>
public override EmbedderSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
switch (value)
{
case "openAi":
return EmbedderSource.OpenAi;
case "huggingFace":
return EmbedderSource.HuggingFace;
case "ollama":
return EmbedderSource.Ollama;
case "rest":
return EmbedderSource.Rest;
case "userProvided":
return EmbedderSource.UserProvided;
default:
return EmbedderSource.Empty;
}
}

/// <summary>
///
/// </summary>
/// <param name="writer"></param>
/// <param name="value"></param>
/// <param name="options"></param>
public override void Write(Utf8JsonWriter writer, EmbedderSource value, JsonSerializerOptions options)
{
string stringValue;
switch (value)
{
case EmbedderSource.OpenAi:
stringValue = "openAi";
break;
case EmbedderSource.HuggingFace:
stringValue = "huggingFace";
break;
case EmbedderSource.Ollama:
stringValue = "ollama";
break;
case EmbedderSource.Rest:
stringValue = "rest";
break;
case EmbedderSource.UserProvided:
stringValue = "userProvided";
break;
default:
stringValue = string.Empty;
break;
}
writer.WriteStringValue(stringValue);
}
}
}
145 changes: 145 additions & 0 deletions src/Meilisearch/Embedder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

using Meilisearch.Converters;

namespace Meilisearch
{
/// <summary>
/// Configure at least one embedder to use AI-powered search.
/// </summary>
public class Embedder
{
/// <summary>
/// Use source to configure an embedder's source.
/// This field is mandatory.
/// </summary>
[JsonPropertyName("source")]
public EmbedderSource Source { get; set; }

/// <summary>
/// Meilisearch queries url to generate vector embeddings for queries and documents.
/// </summary>
[JsonPropertyName("url")]
public string Url { get; set; }

/// <summary>
/// Authentication token Meilisearch should send with each request to the embedder.
/// </summary>
[JsonPropertyName("apiKey")]
public string ApiKey { get; set; }

/// <summary>
/// The model your embedder uses when generating vectors.
/// </summary>
[JsonPropertyName("model")]
public string Model { get; set; }

/// <summary>
/// documentTemplate is a string containing a Liquid template.
/// </summary>
[JsonPropertyName("documentTemplate")]
public string DocumentTemplate { get; set; }

/// <summary>
/// The maximum size of a rendered document template. Longer texts are truncated to fit the configured limit.
/// </summary>
[JsonPropertyName("documentTemplateMaxBytes")]
public int? DocumentTemplateMaxBytes { get; set; }

/// <summary>
/// Number of dimensions in the chosen model. If not supplied, Meilisearch tries to infer this value.
/// </summary>
[JsonPropertyName("dimensions")]
public int? Dimensions { get; set; }

/// <summary>
/// Use this field to use a specific revision of a model.
/// </summary>
[JsonPropertyName("revision")]
public string Revision { get; set; }

/// <summary>
/// Use distribution when configuring an embedder to correct the returned
/// _rankingScores of the semantic hits with an affine transformation
/// </summary>
[JsonPropertyName("distribution")]
public Distribution Distribution { get; set; }

///// <summary>
///// request must be a JSON object with the same structure
///// and data of the request you must send to your rest embedder.
///// </summary>
//[JsonPropertyName("request")]
//public object Request { get; set; }

///// <summary>
///// response must be a JSON object with the same structure
///// and data of the response you expect to receive from your rest embedder.
///// </summary>
//[JsonPropertyName("response")]
//public object Response { get; set; }

/// <summary>
/// When set to true, compresses vectors by representing each dimension with 1-bit values.
/// </summary>
[JsonPropertyName("binaryQuantized")]
public bool? BinaryQuantized { get; set; }
}

/// <summary>
/// Configuring distribution requires a certain amount of trial and error,
/// in which you must perform semantic searches and monitor the results.
/// Based on their rankingScores and relevancy, add the observed mean and sigma values for that index.
/// </summary>
public class Distribution
{
/// <summary>
/// a number between 0 and 1 indicating the semantic score of "somewhat relevant"
/// hits before using the distribution setting.
/// </summary>
[JsonPropertyName("mean")]
public float? Mean { get; set; }

/// <summary>
/// a number between 0 and 1 indicating the average absolute difference in
/// _rankingScores between "very relevant" hits and "somewhat relevant" hits,
/// and "somewhat relevant" hits and "irrelevant hits".
/// </summary>
[JsonPropertyName("sigma")]
public float? Sigma { get; set; }
}

/// <summary>
///
/// </summary>
[JsonConverter(typeof(EmbedderSourceConverter))]
public enum EmbedderSource
{
/// <summary>
/// empty source
/// </summary>
Empty,
/// <summary>
/// openAi source
/// </summary>
OpenAi,
/// <summary>
/// guggingFace source
/// </summary>
HuggingFace,
/// <summary>
/// ollama source
/// </summary>
Ollama,
/// <summary>
/// use rest to auto-generate embeddings with any embedder offering a REST API.
/// </summary>
Rest,
/// <summary>
/// You may also configure a userProvided embedder.
/// In this case, you must manually include vector data in your documents' _vectors field.
/// </summary>
UserProvided
}
}
4 changes: 4 additions & 0 deletions src/Meilisearch/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ internal static string ToQueryString(this object source, BindingFlags bindingAtt
{
values.Add(key + "=" + Uri.EscapeDataString(datetimeValue.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz")));
}
else if(value is Boolean boolValue)
{
values.Add(key + "=" + (boolValue ? "true" : "false"));
}
else
{
values.Add(key + "=" + Uri.EscapeDataString(value.ToString()));
Expand Down
27 changes: 27 additions & 0 deletions src/Meilisearch/Index.Documents.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
Expand Down Expand Up @@ -413,6 +414,32 @@ public async Task<ResourceResults<IEnumerable<T>>> GetDocumentsAsync<T>(Document
}
}

/// <summary>
/// Get similar documents with the allowed Query Parameters.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<SimilarDocumentsResult<T>> GetSimilarDocumentsAsync<T>(SimilarDocumentsQuery query = default,
CancellationToken cancellationToken = default)
{
try{
var uri = $"indexes/{Uid}/similar";
var result = await _http.PostAsJsonAsync(uri, query, Constants.JsonSerializerOptionsRemoveNulls,
cancellationToken: cancellationToken)
.ConfigureAwait(false);
return await result.Content
.ReadFromJsonAsync<SimilarDocumentsResult<T>>(cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
catch (MeilisearchCommunicationError e)
{
throw new MeilisearchCommunicationError(
Constants.VersionErrorHintMessage(e.Message, nameof(GetDocumentsAsync)), e);
}
Comment on lines +438 to +440
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Method reference in error message doesn't match the current method.

The error message references GetDocumentsAsync instead of GetSimilarDocumentsAsync, which could be confusing during troubleshooting.

throw new MeilisearchCommunicationError(
-    Constants.VersionErrorHintMessage(e.Message, nameof(GetDocumentsAsync)), e);
+    Constants.VersionErrorHintMessage(e.Message, nameof(GetSimilarDocumentsAsync)), e);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw new MeilisearchCommunicationError(
Constants.VersionErrorHintMessage(e.Message, nameof(GetDocumentsAsync)), e);
}
throw new MeilisearchCommunicationError(
Constants.VersionErrorHintMessage(e.Message, nameof(GetSimilarDocumentsAsync)), e);
}

}

/// <summary>
/// Delete one document.
/// </summary>
Expand Down
50 changes: 50 additions & 0 deletions src/Meilisearch/Index.Embedders.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;

using Meilisearch.Extensions;
namespace Meilisearch
{
public partial class Index
{
/// <summary>
/// Gets the embedders setting.
/// </summary>
/// <param name="cancellationToken">The cancellation token for this call.</param>
/// <returns>Returns the embedders setting.</returns>
public async Task<Dictionary<string, Embedder>> GetEmbeddersAsync(CancellationToken cancellationToken = default)
{
return await _http.GetFromJsonAsync<Dictionary<string, Embedder>>($"indexes/{Uid}/settings/embedders", cancellationToken: cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Updates the embedders setting.
/// </summary>
/// <param name="embedders">Collection of embedders</param>
/// <param name="cancellationToken">The cancellation token for this call.</param>
/// <returns>Returns the task info of the asynchronous task.</returns>
public async Task<TaskInfo> UpdateEmbeddersAsync(Dictionary<string, Embedder> embedders, CancellationToken cancellationToken = default)
{
var responseMessage =
await _http.PatchAsJsonAsync($"indexes/{Uid}/settings/embedders", embedders, Constants.JsonSerializerOptionsRemoveNulls, cancellationToken: cancellationToken)
.ConfigureAwait(false);
return await responseMessage.Content.ReadFromJsonAsync<TaskInfo>(cancellationToken: cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Resets the embedders setting.
/// </summary>
/// <param name="cancellationToken">The cancellation token for this call.</param>
/// <returns>Returns the task info of the asynchronous task.</returns>
public async Task<TaskInfo> ResetEmbeddersAsync(CancellationToken cancellationToken = default)
{
var response = await _http.DeleteAsync($"indexes/{Uid}/settings/embedders", cancellationToken)
.ConfigureAwait(false);

return await response.Content.ReadFromJsonAsync<TaskInfo>(cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}
6 changes: 6 additions & 0 deletions src/Meilisearch/QueryParameters/DocumentsQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,11 @@ public class DocumentsQuery
/// </summary>
[JsonPropertyName("filter")]
public object Filter { get; set; }

/// <summary>
/// Return document vector data with search result
/// </summary>
[JsonPropertyName("retrieveVectors")]
public bool? RetrieveVectors { get; set; }
}
}
Loading
Loading