diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 9e145690..fbeb3509 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -3,6 +3,35 @@ # the documentation on build # You can read more on https://github.com/meilisearch/documentation/tree/master/.vuepress/code-samples --- +faceted_search_2: |- + await client.MultiSearchAsync(new MultiSearchQuery() + { + Queries = new System.Collections.Generic.List() + { + new SearchQuery() { + IndexUid = "books", + Filter = new[] { + new[] {"language = English", "language = French"}, + new[] {"genres = Fiction"} + }, + Facets = new[] { "language", "genres", "author", "format" } + }, + new SearchQuery() { + IndexUid = "books", + Filter = new[] { + new[] {"genres = Fiction"} + }, + Facets = new[] { "language" } + }, + new SearchQuery() { + IndexUid = "books", + Filter = new[] { + new[] {"language = English", "language = French"} + }, + Facets = new[] { "genres" } + } + } + }); getting_started_faceting: |- var faceting = new Faceting { MaxValuesPerFacet = 2 @@ -80,7 +109,13 @@ delete_an_index_1: |- get_one_document_1: |- await client.Index("movies").GetDocumentAsync(25684, new List { "id", "title", "poster", "release_date" }); get_documents_1: |- - await client.Index("movies").GetDocumentsAsync(new DocumentsQuery() { Limit = 2 }); + await client.Index("movies").GetDocumentsAsync(new DocumentsQuery() { Limit = 2, Filter = "genres = action" }); +get_documents_post_1: |- + await client.Index("movies").GetDocumentsAsync(new DocumentsQuery() { + Limit = 3, + Fields = new List { "title", "genres", "rating", "language"}, + Filter = "(rating > 3 AND (genres=Adventure OR genres=Fiction)) AND language=English" + }); add_or_replace_documents_1: |- var movie = new[] { @@ -104,8 +139,10 @@ delete_all_documents_1: |- await client.Index("movies").DeleteAllDocumentsAsync(); delete_one_document_1: |- await client.Index("movies").DeleteOneDocumentAsync("25684"); -delete_documents_1: |- +delete_documents_by_batch_1: |- await client.Index("movies").DeleteDocumentsAsync(new[] { "23488", "153738", "437035", "363869" }); +delete_documents_by_filter_1: |- + await client.Index("movies").DeleteDocumentsAsync(new DeleteDocumentsQuery() { Filter = "genres = action OR genres = adventure" }); search_post_1: |- await client.Index("movies").SearchAsync("American ninja"); get_task_1: | @@ -325,7 +362,7 @@ getting_started_add_documents_md: |- { static async Task Main(string[] args) { - MeilisearchClient client = new MeilisearchClient("http://localhost:7700", "masterKey"); + MeilisearchClient client = new MeilisearchClient("http://localhost:7700", "aSampleMasterKey"); var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true @@ -425,7 +462,7 @@ getting_started_communicating_with_a_protected_instance: |- MeilisearchClient client = new MeilisearchClient("http://localhost:7700", "apiKey"); await client.Index("Movies").SearchAsync(""); -faceted_search_update_settings_1: |- +filtering_update_settings_1: |- await client.Index("movies").UpdateFilterableAttributesAsync(new [] { "director", "genres" }); faceted_search_filter_1: |- SearchQuery filters = new SearchQuery() @@ -450,6 +487,15 @@ faceted_search_walkthrough_filter_1: |- Filter = "(genre = 'Horror' AND genre = 'Mystery') OR director = 'Jordan Peele'" }; await client.Index("movies").SearchAsync("thriller", sq); +faceted_search_update_settings_1: |- + List attributes = new() { "genres", "rating", "language" }; + TaskInfo result = await client.Index("books").UpdateFilterableAttributesAsync(attributes); +faceted_search_1: |- + var sq = new SearchQuery + { + Facets = new string[] { "genres", "rating", "language" } + }; + await client.Index("books").SearchAsync("classic", sq); add_movies_json_1: |- // Make sure to add this using to your code using System.IO; @@ -541,6 +587,15 @@ geosearch_guide_sort_usage_2: |- } }; + SearchResult restaurants = await client.Index("restaurants").SearchAsync("restaurants", filters); +geosearch_guide_filter_usage_3: |- + SearchQuery filters = new SearchQuery() + { + Sort = new string[] { + "_geoBoundingBox([45.494181, 9.179175], [45.449484, 9.214024])", + "rating:desc" + } + }; SearchResult restaurants = await client.Index("restaurants").SearchAsync("restaurants", filters); primary_field_guide_create_index_primary_key: |- TaskInfo task = await client.CreateIndexAsync("books", "reference_number"); @@ -688,3 +743,24 @@ update_faceting_settings_1: |- await client.Index("movies").UpdateFacetingAsync(faceting); reset_faceting_settings_1: |- await client.Index("movies").ResetFacetingAsync(); +multi_search_1: |- + await client.MultiSearchAsync(new MultiSearchQuery() + { + Queries = new System.Collections.Generic.List() + { + new SearchQuery() { + IndexUid = "movies", + Q = "booh", + Limit = 5 + }, + new SearchQuery() { + IndexUid = "movies", + Q = "nemo", + Limit = 5 + }, + new SearchQuery() { + IndexUid = "movie_ratings", + Q = "us", + }, + } + }); diff --git a/.env b/.env index 27411543..7fa0c6a5 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -MEILISEARCH_VERSION=v1.1.0 +MEILISEARCH_VERSION=v1.2.0 PROXIED_MEILISEARCH=http://nginx/api/ MEILISEARCH_URL=http://meilisearch:7700 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e077e813..2e77d5c9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3 with: - dotnet-version: "3.1.x" + dotnet-version: "6.0.x" - name: Install dependencies run: dotnet restore - name: Build @@ -39,9 +39,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3 with: - dotnet-version: "3.1.x" - - name: Install dotnet-format - run: dotnet tool install -g dotnet-format + dotnet-version: "6.0.x" - name: Check with dotnet-format run: dotnet format --version - name: Check with dotnet-format diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3200437c..37ddad93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,7 @@ You can set up your local environment natively or using `docker`, check out the Example of running all the checks with docker: ```bash -docker-compose run --rm package bash -c "dotnet test && dotnet format --check Meilisearch.sln" +docker-compose run --rm package bash -c "dotnet test && dotnet format --verbosity normal --verify-no-changes" ``` To install dependencies: diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 356fbb4b..00000000 --- a/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:3.1-bullseye - -RUN dotnet tool install -g dotnet-format -ENV PATH="$PATH:/root/.dotnet/tools" diff --git a/README.md b/README.md index d128d2cf..b9d358ee 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

Meilisearch | -Meilisearch Cloud | +Meilisearch Cloud | Documentation | Discord | Roadmap | @@ -51,7 +51,7 @@ For general information on how to use Meilisearch—such as our API reference, t ## ⚡ Supercharge your Meilisearch experience -Say goodbye to server deployment and manual updates with [Meilisearch Cloud](https://www.meilisearch.com/pricing?utm_campaign=oss&utm_source=integration&utm_medium=meilisearch-dotnet). No credit card required. +Say goodbye to server deployment and manual updates with [Meilisearch Cloud](https://www.meilisearch.com/cloud?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-dotnet). Get started with a 14-day free trial! No credit card required. ## 🔧 Installation diff --git a/docker-compose.yml b/docker-compose.yml index e0b3a019..9b513b75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ volumes: services: package: - build: . + image: mcr.microsoft.com/dotnet/sdk:6.0 tty: true stdin_open: true working_dir: /home/package diff --git a/src/Meilisearch/Constants.cs b/src/Meilisearch/Constants.cs index d147bbb0..5aa0a1b3 100644 --- a/src/Meilisearch/Constants.cs +++ b/src/Meilisearch/Constants.cs @@ -25,5 +25,11 @@ internal static class Constants DefaultIgnoreCondition = JsonIgnoreCondition.Never, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; + + internal static string VersionErrorHintMessage(string message, string method) + { + return + $"{message}\nHint: It might not be working because maybe you're not up to date with the Meilisearch version that ${method} call requires."; + } } } diff --git a/src/Meilisearch/Index.Documents.cs b/src/Meilisearch/Index.Documents.cs index 88f35761..307f0e28 100644 --- a/src/Meilisearch/Index.Documents.cs +++ b/src/Meilisearch/Index.Documents.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -69,7 +70,8 @@ public async Task AddDocumentsJsonAsync(string documents, string prima /// One ASCII character used to customize the delimiter for CSV. Comma used by default. /// The cancellation token for this call. /// Returns the task info. - public async Task AddDocumentsCsvAsync(string documents, string primaryKey = default, char csvDelimiter = default, + public async Task AddDocumentsCsvAsync(string documents, string primaryKey = default, + char csvDelimiter = default, CancellationToken cancellationToken = default) { var uri = $"indexes/{Uid}/documents"; @@ -79,6 +81,7 @@ public async Task AddDocumentsCsvAsync(string documents, string primar { queryString.Add("primaryKey", primaryKey); } + if (csvDelimiter != default) { queryString.Add("csvDelimiter", csvDelimiter.ToString()); @@ -146,12 +149,14 @@ public async Task> AddDocumentsInBatchesAsync(IEnumerab /// The cancellation token for this call. /// Returns the task list. public async Task> AddDocumentsCsvInBatchesAsync(string documents, - int batchSize = 1000, string primaryKey = default, char csvDelimiter = default, CancellationToken cancellationToken = default) + int batchSize = 1000, string primaryKey = default, char csvDelimiter = default, + CancellationToken cancellationToken = default) { var tasks = new List(); foreach (var chunk in documents.GetCsvChunks(batchSize)) { - tasks.Add(await AddDocumentsCsvAsync(chunk, primaryKey, csvDelimiter, cancellationToken).ConfigureAwait(false)); + tasks.Add(await AddDocumentsCsvAsync(chunk, primaryKey, csvDelimiter, cancellationToken) + .ConfigureAwait(false)); } return tasks; @@ -196,7 +201,9 @@ public async Task UpdateDocumentsAsync(IEnumerable documents, st uri = $"{uri}?{new { primaryKey = primaryKey }.ToQueryString()}"; } - responseMessage = await _http.PutJsonCustomAsync(uri, documents, Constants.JsonSerializerOptionsRemoveNulls, cancellationToken).ConfigureAwait(false); + responseMessage = await _http + .PutJsonCustomAsync(uri, documents, Constants.JsonSerializerOptionsRemoveNulls, cancellationToken) + .ConfigureAwait(false); return await responseMessage.Content.ReadFromJsonAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -340,7 +347,8 @@ public async Task> UpdateDocumentsNdjsonInBatchesAsync(str /// The cancellation token for this call. /// Type of the document. /// Returns the document, with the according type if the object is available. - public async Task GetDocumentAsync(string documentId, List fields = default, CancellationToken cancellationToken = default) + public async Task GetDocumentAsync(string documentId, List fields = default, + CancellationToken cancellationToken = default) { var uri = $"indexes/{Uid}/documents/{documentId}"; if (fields != null) @@ -361,7 +369,8 @@ public async Task GetDocumentAsync(string documentId, List fields /// The cancellation token for this call. /// Type to return for document. /// Type if the object is availble. - public async Task GetDocumentAsync(int documentId, List fields = default, CancellationToken cancellationToken = default) + public async Task GetDocumentAsync(int documentId, List fields = default, + CancellationToken cancellationToken = default) { return await GetDocumentAsync(documentId.ToString(), fields, cancellationToken); } @@ -376,14 +385,37 @@ public async Task GetDocumentAsync(int documentId, List fields = d public async Task>> GetDocumentsAsync(DocumentsQuery query = default, CancellationToken cancellationToken = default) { - var uri = $"indexes/{Uid}/documents"; - if (query != null) + if (query != null && query.Filter != null) { - uri = $"{uri}?{query.ToQueryString()}"; + try + { + //Use the fetch route + var uri = $"indexes/{Uid}/documents/fetch"; + var result = await _http.PostAsJsonAsync(uri, query, Constants.JsonSerializerOptionsRemoveNulls, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + return await result.Content + .ReadFromJsonAsync>>(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + catch (MeilisearchCommunicationError e) + { + throw new MeilisearchCommunicationError( + Constants.VersionErrorHintMessage(e.Message, nameof(GetDocumentsAsync)), e); + } + } + else + { + var uri = $"indexes/{Uid}/documents"; + if (query != null) + { + uri = $"{uri}?{query.ToQueryString()}"; + } + + return await _http + .GetFromJsonAsync>>(uri, cancellationToken: cancellationToken) + .ConfigureAwait(false); } - - return await _http.GetFromJsonAsync>>(uri, cancellationToken: cancellationToken) - .ConfigureAwait(false); } /// @@ -430,6 +462,33 @@ await _http.PostAsJsonAsync($"indexes/{Uid}/documents/delete-batch", documentIds .ConfigureAwait(false); } + /// + /// Delete documents from an index based on a filter. + /// + /// Available ONLY with Meilisearch v1.2 and newer. + /// A hash containing a filter that should match documents. + /// The cancellation token for this call. + /// Return the task info. + public async Task DeleteDocumentsAsync(DeleteDocumentsQuery query, + CancellationToken cancellationToken = default) + { + try + { + var httpresponse = + await _http.PostAsJsonAsync($"indexes/{Uid}/documents/delete", query, + Constants.JsonSerializerOptionsRemoveNulls, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + return await httpresponse.Content.ReadFromJsonAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + catch (MeilisearchCommunicationError e) + { + throw new MeilisearchCommunicationError( + Constants.VersionErrorHintMessage(e.Message, nameof(DeleteDocumentsAsync)), e); + } + } + /// /// Delete documents in batch. /// @@ -477,6 +536,7 @@ public async Task> SearchAsync(string query, body = searchAttributes; body.Q = query; } + body.IndexUid = default; var responseMessage = await _http.PostAsJsonAsync($"indexes/{Uid}/search", body, @@ -484,8 +544,8 @@ public async Task> SearchAsync(string query, .ConfigureAwait(false); return await responseMessage.Content - .ReadFromJsonAsync>(cancellationToken: cancellationToken) - .ConfigureAwait(false); + .ReadFromJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(false); } } } diff --git a/src/Meilisearch/Meilisearch.csproj b/src/Meilisearch/Meilisearch.csproj index d6086172..c3405f4b 100644 --- a/src/Meilisearch/Meilisearch.csproj +++ b/src/Meilisearch/Meilisearch.csproj @@ -4,7 +4,7 @@ netstandard2.0 Library MeiliSearch - 0.14.2 + 0.14.3 .NET wrapper for Meilisearch, an open-source search engine https://github.com/meilisearch/meilisearch-dotnet meilisearch;dotnet;sdk;search-engine;search;instant-search @@ -21,13 +21,13 @@ - + - - + + diff --git a/src/Meilisearch/QueryParameters/DeleteDocumentsQuery.cs b/src/Meilisearch/QueryParameters/DeleteDocumentsQuery.cs new file mode 100644 index 00000000..008e15fa --- /dev/null +++ b/src/Meilisearch/QueryParameters/DeleteDocumentsQuery.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Meilisearch.QueryParameters +{ + public class DeleteDocumentsQuery + { + [JsonPropertyName("filter")] + public object Filter { get; set; } + } +} diff --git a/src/Meilisearch/QueryParameters/DocumentsQuery.cs b/src/Meilisearch/QueryParameters/DocumentsQuery.cs index c6ae88c8..d5e2a191 100644 --- a/src/Meilisearch/QueryParameters/DocumentsQuery.cs +++ b/src/Meilisearch/QueryParameters/DocumentsQuery.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Meilisearch.QueryParameters { @@ -10,16 +11,25 @@ public class DocumentsQuery /// /// Gets or sets the limit. /// + [JsonPropertyName("limit")] public int? Limit { get; set; } /// /// Gets or sets the offset. /// + [JsonPropertyName("offset")] public int? Offset { get; set; } /// /// Gets or sets the attributes to retrieve. /// + [JsonPropertyName("fields")] public List Fields { get; set; } + + /// + /// An optional filter to apply + /// + [JsonPropertyName("filter")] + public object Filter { get; set; } } } diff --git a/tests/Meilisearch.Tests/DocumentTests.cs b/tests/Meilisearch.Tests/DocumentTests.cs index c87d5ff4..ebdaa3fc 100644 --- a/tests/Meilisearch.Tests/DocumentTests.cs +++ b/tests/Meilisearch.Tests/DocumentTests.cs @@ -23,7 +23,8 @@ public DocumentTests(TFixture fixture) _client = fixture.DefaultClient; } - public async Task InitializeAsync() => await _fixture.DeleteAllIndexes(); // Test context cleaned for each [Fact] + public async Task InitializeAsync() => + await _fixture.DeleteAllIndexes(); // Test context cleaned for each [Fact] public Task DisposeAsync() => Task.CompletedTask; @@ -134,10 +135,8 @@ public async Task BasicDocumentsAdditionInBatches() // Add the documents Movie[] movies = { - new Movie { Id = "1", Name = "Batman" }, - new Movie { Id = "2", Name = "Reservoir Dogs" }, - new Movie { Id = "3", Name = "Taxi Driver" }, - new Movie { Id = "4", Name = "Interstellar" }, + new Movie { Id = "1", Name = "Batman" }, new Movie { Id = "2", Name = "Reservoir Dogs" }, + new Movie { Id = "3", Name = "Taxi Driver" }, new Movie { Id = "4", Name = "Interstellar" }, new Movie { Id = "5", Name = "Titanic" }, }; var tasks = await index.AddDocumentsInBatchesAsync(movies, 2); @@ -419,10 +418,8 @@ public async Task BasicDocumentsUpdateInBatches() // Add the documents Movie[] movies = { - new Movie { Id = "1", Name = "Batman" }, - new Movie { Id = "2", Name = "Reservoir Dogs" }, - new Movie { Id = "3", Name = "Taxi Driver" }, - new Movie { Id = "4", Name = "Interstellar" }, + new Movie { Id = "1", Name = "Batman" }, new Movie { Id = "2", Name = "Reservoir Dogs" }, + new Movie { Id = "3", Name = "Taxi Driver" }, new Movie { Id = "4", Name = "Interstellar" }, new Movie { Id = "5", Name = "Titanic" }, }; var tasks = await index.AddDocumentsInBatchesAsync(movies, 2); @@ -590,6 +587,20 @@ public async Task GetMultipleExistingDocuments() documents.Results.Last().Id.Should().Be("16"); } + [Fact] + public async Task GetMultipleExistingDocumentsWithQuery() + { + var index = await _fixture.SetUpBasicIndex("GetMultipleExistingDocumentWithQueryTest"); + var taskUpdate = await index.UpdateFilterableAttributesAsync(new[] { "genre" }); + taskUpdate.TaskUid.Should().BeGreaterOrEqualTo(0); + await index.WaitForTaskAsync(taskUpdate.TaskUid); + + var documents = await index.GetDocumentsAsync(new DocumentsQuery() { Filter = "genre = 'SF'" }); + Assert.Equal(2, documents.Results.Count()); + documents.Results.Should().ContainSingle(x => x.Id == "12"); + documents.Results.Should().ContainSingle(x => x.Id == "13"); + } + [Fact] public async Task GetMultipleExistingDocumentsWithLimit() { @@ -604,7 +615,12 @@ public async Task GetMultipleExistingDocumentsWithLimit() public async Task GetMultipleExistingDocumentsWithField() { var index = await _fixture.SetUpBasicIndex("GetMultipleExistingDocumentWithLimitTest"); - var documents = await index.GetDocumentsAsync(new DocumentsQuery() { Limit = 2, Fields = new List { "id" } }); + var documents = + await index.GetDocumentsAsync(new DocumentsQuery() + { + Limit = 2, + Fields = new List { "id" } + }); Assert.Equal(2, documents.Results.Count()); documents.Results.First().Id.Should().Be("10"); documents.Results.First().Name.Should().BeNull(); @@ -615,7 +631,12 @@ public async Task GetMultipleExistingDocumentsWithField() public async Task GetMultipleExistingDocumentsWithMultipleFields() { var index = await _fixture.SetUpBasicIndex("GetMultipleExistingDocumentWithLimitTest"); - var documents = await index.GetDocumentsAsync(new DocumentsQuery() { Limit = 2, Fields = new List { "id", "name" } }); + var documents = + await index.GetDocumentsAsync(new DocumentsQuery() + { + Limit = 2, + Fields = new List { "id", "name" } + }); Assert.Equal(2, documents.Results.Count()); documents.Results.First().Id.Should().Be("10"); documents.Results.First().Name.Should().Be("Gladiator"); @@ -700,6 +721,29 @@ public async Task DeleteMultipleDocumentsWithIntegerId() Assert.Equal("document_not_found", ex.Code); } + [Fact] + public async Task DeleteMultipleDocumentsByFilter() + { + var index = await _fixture.SetUpBasicIndex("DeleteMultipleDocumentsByFilterTest"); + var taskUpdate = await index.UpdateFilterableAttributesAsync(new[] { "genre" }); + taskUpdate.TaskUid.Should().BeGreaterOrEqualTo(0); + await index.WaitForTaskAsync(taskUpdate.TaskUid); + + // Delete the documents + var task = await index.DeleteDocumentsAsync(new DeleteDocumentsQuery() { Filter = "genre = SF" }); + task.TaskUid.Should().BeGreaterOrEqualTo(0); + await index.WaitForTaskAsync(task.TaskUid); + + // Check the documents have been deleted + var docs = await index.GetDocumentsAsync(); + Assert.Equal(5, docs.Results.Count()); + MeilisearchApiError ex; + ex = await Assert.ThrowsAsync(() => index.GetDocumentAsync("12")); + Assert.Equal("document_not_found", ex.Code); + ex = await Assert.ThrowsAsync(() => index.GetDocumentAsync("13")); + Assert.Equal("document_not_found", ex.Code); + } + [Fact] public async Task DeleteAllExistingDocuments() { diff --git a/tests/Meilisearch.Tests/Meilisearch.Tests.csproj b/tests/Meilisearch.Tests/Meilisearch.Tests.csproj index 56d654c6..c4f5a7df 100644 --- a/tests/Meilisearch.Tests/Meilisearch.Tests.csproj +++ b/tests/Meilisearch.Tests/Meilisearch.Tests.csproj @@ -1,17 +1,16 @@ - + - netcoreapp3.1 + net6.0 false - - - - + + +