Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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 @@ -843,3 +843,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