From 5897b7171ab6d8a28ab59437610dad21f66a7ff9 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Tue, 28 Mar 2023 12:27:37 +0200 Subject: [PATCH 1/3] make _formatted value user friendly, also remove the FormattedMovie class from tests since it's useless now --- src/Meilisearch/DefaultFormattable.cs | 34 ++++++++++ src/Meilisearch/IFormatContainer.cs | 23 +++++++ .../IFormatContainerJsonConverter.cs | 62 +++++++++++++++++ src/Meilisearch/Index.Documents.cs | 66 +++++++++++++------ tests/Meilisearch.Tests/Movie.cs | 12 ---- tests/Meilisearch.Tests/SearchTests.cs | 56 ++++++++-------- 6 files changed, 194 insertions(+), 59 deletions(-) create mode 100644 src/Meilisearch/DefaultFormattable.cs create mode 100644 src/Meilisearch/IFormatContainer.cs create mode 100644 src/Meilisearch/IFormatContainerJsonConverter.cs diff --git a/src/Meilisearch/DefaultFormattable.cs b/src/Meilisearch/DefaultFormattable.cs new file mode 100644 index 00000000..d7d2eff0 --- /dev/null +++ b/src/Meilisearch/DefaultFormattable.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace Meilisearch +{ + /// + /// The default implmentation of + /// + /// + /// + public class DefaultFormattable : IFormatContainer + { + /// + /// Creates a formatted document + /// + /// + /// + public DefaultFormattable(TOriginal original, TFormatted formatted) + { + Original = original; + Formatted = formatted; + } + + /// + /// The original document + /// + public TOriginal Original { get; } + + /// + /// The formatted document + /// + [JsonPropertyName("_formatted")] + public TFormatted Formatted { get; } + } +} diff --git a/src/Meilisearch/IFormatContainer.cs b/src/Meilisearch/IFormatContainer.cs new file mode 100644 index 00000000..52c1b503 --- /dev/null +++ b/src/Meilisearch/IFormatContainer.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace Meilisearch +{ + /// + /// Used to receive document formatting information + /// + /// Original data type + /// Formatted data type + [JsonConverter(typeof(IFormatContainerJsonConverterFactory))] + public interface IFormatContainer + { + /// + /// The original result + /// + TOriginal Original { get; } + + /// + /// The formatted result + /// + TFormatted Formatted { get; } + } +} diff --git a/src/Meilisearch/IFormatContainerJsonConverter.cs b/src/Meilisearch/IFormatContainerJsonConverter.cs new file mode 100644 index 00000000..80068e9e --- /dev/null +++ b/src/Meilisearch/IFormatContainerJsonConverter.cs @@ -0,0 +1,62 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Meilisearch +{ + public class IFormatContainerJsonConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsInterface + && typeToConvert.IsGenericType + && (typeToConvert.GetGenericTypeDefinition() == typeof(IFormatContainer<,>)); + } + + public override JsonConverter CreateConverter( + Type typeToConvert, + JsonSerializerOptions options + ) + { + var genericArgs = typeToConvert.GetGenericArguments(); + var converterType = typeof(IFormatContainerJsonConverter<,>).MakeGenericType( + genericArgs[0], + genericArgs[1] + ); + var converter = (JsonConverter)Activator.CreateInstance(converterType); + return converter; + } + } + + public class IFormatContainerJsonConverter + : JsonConverter> + where TFormatted : class + where TOriginal : class + { + public override IFormatContainer Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var document = JsonSerializer.Deserialize(ref reader, options); + var original = document.Deserialize(options); + + if (document.TryGetProperty("_formatted", out var formattedElement)) + { + var formatted = formattedElement.Deserialize(options); + return new DefaultFormattable(original, formatted); + } + return new DefaultFormattable(original, null); + } + + public override void Write( + Utf8JsonWriter writer, + IFormatContainer value, + JsonSerializerOptions options + ) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Meilisearch/Index.Documents.cs b/src/Meilisearch/Index.Documents.cs index 17984a07..10dcf891 100644 --- a/src/Meilisearch/Index.Documents.cs +++ b/src/Meilisearch/Index.Documents.cs @@ -447,16 +447,8 @@ public async Task DeleteAllDocumentsAsync(CancellationToken cancellati .ConfigureAwait(false); } - /// - /// Search documents according to search parameters. - /// - /// Query Parameter with Search. - /// Attributes to search. - /// The cancellation token for this call. - /// Type parameter to return. - /// Returns Enumerable of items. - public async Task> SearchAsync(string query, - SearchQuery searchAttributes = default(SearchQuery), CancellationToken cancellationToken = default) + private async Task<(SearchQuery, HttpResponseMessage)> SendSearchRequest(string query, + SearchQuery searchAttributes = default, CancellationToken cancellationToken = default) { SearchQuery body; if (searchAttributes == null) @@ -473,18 +465,54 @@ public async Task DeleteAllDocumentsAsync(CancellationToken cancellati Constants.JsonSerializerOptionsRemoveNulls, cancellationToken: cancellationToken) .ConfigureAwait(false); - if (body.Page != null || body.HitsPerPage != null) - { - return await responseMessage.Content + return (body, responseMessage); + } + + /// + /// Search documents according to search parameters. + /// + /// Query Parameter with Search. + /// Attributes to search. + /// The cancellation token for this call. + /// Type parameter to return. + /// Returns Enumerable of items. + public async Task> SearchAsync(string query, + SearchQuery searchAttributes = default, CancellationToken cancellationToken = default) + { + + var (body, responseMessage) = await SendSearchRequest(query, searchAttributes, cancellationToken); + + return body.Page != null || body.HitsPerPage != null + ? await responseMessage.Content .ReadFromJsonAsync>(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - else - { - return await responseMessage.Content + .ConfigureAwait(false) + : (ISearchable)await responseMessage.Content .ReadFromJsonAsync>(cancellationToken: cancellationToken) .ConfigureAwait(false); - } } + + /// + /// Search documents according to search parameters, Including format. + /// + /// Query Parameter with Search. + /// Attributes to search. + /// The cancellation token for this call. + /// Type parameter to return. + /// formatted document type. + /// Returns Enumerable of items. + public async Task>> SearchAsync(string query, + SearchQuery searchAttributes = default, CancellationToken cancellationToken = default) + { + var (body, responseMessage) = await SendSearchRequest(query, searchAttributes, cancellationToken); + + return body.Page != null || body.HitsPerPage != null + ? await responseMessage.Content + .ReadFromJsonAsync>>(cancellationToken: cancellationToken) + .ConfigureAwait(false) + : (ISearchable>)await responseMessage.Content + .ReadFromJsonAsync>>(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + } } diff --git a/tests/Meilisearch.Tests/Movie.cs b/tests/Meilisearch.Tests/Movie.cs index fe279502..5a92c7e7 100644 --- a/tests/Meilisearch.Tests/Movie.cs +++ b/tests/Meilisearch.Tests/Movie.cs @@ -34,16 +34,4 @@ public class MovieWithIntId public string Genre { get; set; } } - - public class FormattedMovie - { - public string Id { get; set; } - - public string Name { get; set; } - - public string Genre { get; set; } - -#pragma warning disable SA1300 - public Movie _Formatted { get; set; } - } } diff --git a/tests/Meilisearch.Tests/SearchTests.cs b/tests/Meilisearch.Tests/SearchTests.cs index 04e4f354..af71bb10 100644 --- a/tests/Meilisearch.Tests/SearchTests.cs +++ b/tests/Meilisearch.Tests/SearchTests.cs @@ -107,46 +107,46 @@ public async Task CustomSearchWithAttributesToHighlight() task.TaskUid.Should().BeGreaterOrEqualTo(0); await _basicIndex.WaitForTaskAsync(task.TaskUid); - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { AttributesToHighlight = new string[] { "name" } }); movies.Hits.Should().NotBeEmpty(); - movies.Hits.First().Id.Should().NotBeEmpty(); - movies.Hits.First().Name.Should().NotBeEmpty(); - movies.Hits.First().Genre.Should().NotBeEmpty(); - movies.Hits.First()._Formatted.Name.Should().NotBeEmpty(); + movies.Hits.First().Original.Id.Should().NotBeEmpty(); + movies.Hits.First().Original.Name.Should().NotBeEmpty(); + movies.Hits.First().Original.Genre.Should().NotBeEmpty(); + movies.Hits.First().Formatted.Name.Should().NotBeEmpty(); } [Fact] public async Task CustomSearchWithNoQuery() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( null, new SearchQuery { AttributesToHighlight = new string[] { "name" } }); movies.Hits.Should().NotBeEmpty(); - movies.Hits.First().Id.Should().NotBeNull(); - movies.Hits.First().Name.Should().NotBeNull(); - movies.Hits.First()._Formatted.Id.Should().NotBeNull(); - movies.Hits.First()._Formatted.Name.Should().NotBeNull(); + movies.Hits.First().Original.Id.Should().NotBeNull(); + movies.Hits.First().Original.Name.Should().NotBeNull(); + movies.Hits.First().Formatted.Id.Should().NotBeNull(); + movies.Hits.First().Formatted.Name.Should().NotBeNull(); } [Fact] public async Task CustomSearchWithEmptyQuery() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( string.Empty, new SearchQuery { AttributesToHighlight = new string[] { "name" } }); movies.Hits.Should().NotBeEmpty(); - movies.Hits.First().Id.Should().NotBeNull(); - movies.Hits.First().Name.Should().NotBeNull(); - movies.Hits.First()._Formatted.Id.Should().NotBeNull(); - movies.Hits.First()._Formatted.Name.Should().NotBeNull(); + movies.Hits.First().Original.Id.Should().NotBeNull(); + movies.Hits.First().Original.Name.Should().NotBeNull(); + movies.Hits.First().Formatted.Id.Should().NotBeNull(); + movies.Hits.First().Formatted.Name.Should().NotBeNull(); } [Fact] public async Task CustomSearchWithMultipleOptions() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { @@ -158,12 +158,12 @@ public async Task CustomSearchWithMultipleOptions() Assert.NotEmpty(movies.Hits); Assert.Single(movies.Hits); - Assert.NotEmpty(firstHit.Name); - Assert.NotEmpty(firstHit.Id); - Assert.Null(firstHit.Genre); - Assert.NotEmpty(firstHit._Formatted.Name); - Assert.Equal("15", firstHit._Formatted.Id); - Assert.Null(firstHit._Formatted.Genre); + Assert.NotEmpty(firstHit.Original.Name); + Assert.NotEmpty(firstHit.Original.Id); + Assert.Null(firstHit.Original.Genre); + Assert.NotEmpty(firstHit.Formatted.Name); + Assert.Equal("15", firstHit.Formatted.Id); + Assert.Null(firstHit.Formatted.Genre); } [Fact] @@ -341,31 +341,31 @@ public async Task CustomSearchWithSort() [Fact] public async Task CustomSearchWithCroppingParameters() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { CropLength = 1, AttributesToCrop = new string[] { "*" } } ); Assert.NotEmpty(movies.Hits); - Assert.Equal("…Man", movies.Hits.First()._Formatted.Name); + Assert.Equal("…Man", movies.Hits.First().Formatted.Name); } [Fact] public async Task CustomSearchWithCropMarker() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { CropLength = 1, AttributesToCrop = new string[] { "*" }, CropMarker = "[…] " } ); Assert.NotEmpty(movies.Hits); - Assert.Equal("[…] Man", movies.Hits.First()._Formatted.Name); + Assert.Equal("[…] Man", movies.Hits.First().Formatted.Name); } [Fact] public async Task CustomSearchWithCustomHighlightTags() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { @@ -376,7 +376,7 @@ public async Task CustomSearchWithCustomHighlightTags() ); Assert.NotEmpty(movies.Hits); - Assert.Equal("Iron Man", movies.Hits.First()._Formatted.Name); + Assert.Equal("Iron Man", movies.Hits.First().Formatted.Name); } [Fact] From 72a5f3db0d0e5043dc630726aca7ae963ac7df57 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela Date: Tue, 28 Mar 2023 18:04:11 +0200 Subject: [PATCH 2/3] Implemented write to json --- .../IFormatContainerJsonConverter.cs | 4 ++- tests/Meilisearch.Tests/SearchTests.cs | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Meilisearch/IFormatContainerJsonConverter.cs b/src/Meilisearch/IFormatContainerJsonConverter.cs index 80068e9e..55196706 100644 --- a/src/Meilisearch/IFormatContainerJsonConverter.cs +++ b/src/Meilisearch/IFormatContainerJsonConverter.cs @@ -56,7 +56,9 @@ JsonSerializerOptions options JsonSerializerOptions options ) { - throw new NotImplementedException(); + var serialized = JsonSerializer.SerializeToNode(value.Original, options).AsObject(); + serialized["_formatted"] = JsonSerializer.SerializeToNode(value.Formatted, options); + serialized.WriteTo(writer, options); } } } diff --git a/tests/Meilisearch.Tests/SearchTests.cs b/tests/Meilisearch.Tests/SearchTests.cs index af71bb10..cef2dbb2 100644 --- a/tests/Meilisearch.Tests/SearchTests.cs +++ b/tests/Meilisearch.Tests/SearchTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; @@ -32,6 +33,33 @@ public async Task InitializeAsync() public Task DisposeAsync() => Task.CompletedTask; + [Fact] + public async Task TestJsonConverter() + { + MovieWithIntId[] movies = + { + new MovieWithIntId { Id = 1, Name = "Batman" }, + new MovieWithIntId { Id = 2, Name = "Reservoir Dogs" }, + new MovieWithIntId { Id = 3, Name = "Taxi Driver" }, + new MovieWithIntId { Id = 4, Name = "Interstellar" }, + new MovieWithIntId { Id = 5, Name = "Titanic" }, + }; ; + var formattable = movies + .Select(x => new DefaultFormattable(x, new Movie() + { + Id = x.Id.ToString(), + Genre = x.Genre, + Name = x.Name + })) + .Cast>(); + + var elements = formattable.Select(x => JsonSerializer.SerializeToElement(x)).ToList(); + elements.Should().AllSatisfy(x => x.GetProperty("_formatted").Should().NotBeNull()); + + var deserialized = elements.Select(x => JsonSerializer.Deserialize>(x)).ToList(); + deserialized.Should().AllSatisfy(x => x.Formatted.Should().NotBeNull()); + } + [Fact] public async Task BasicSearch() { From b16c4f2c89d4f1e8b37a4694cc3e9db8732147dc Mon Sep 17 00:00:00 2001 From: Ahmed Fwela <63286031+ahmednfwela@users.noreply.github.com> Date: Wed, 6 Sep 2023 17:50:18 +0300 Subject: [PATCH 3/3] Update src/Meilisearch/DefaultFormattable.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Amélie --- src/Meilisearch/DefaultFormattable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Meilisearch/DefaultFormattable.cs b/src/Meilisearch/DefaultFormattable.cs index d7d2eff0..b31fbd6f 100644 --- a/src/Meilisearch/DefaultFormattable.cs +++ b/src/Meilisearch/DefaultFormattable.cs @@ -3,7 +3,7 @@ namespace Meilisearch { /// - /// The default implmentation of + /// The default implementation of /// /// ///