From 8e8f58632c209a38027d3d0c9dca8f863259b966 Mon Sep 17 00:00:00 2001 From: Thomas Farr Date: Fri, 2 Aug 2024 19:00:39 +1200 Subject: [PATCH 1/2] Add support for search `ext` parameter Signed-off-by: Thomas Farr --- CHANGELOG.md | 1 + .../Search/Search/SearchRequest.cs | 15 +++++++++ tests/Tests/Search/Search/SearchApiTests.cs | 31 +++++++++++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 981c8171b5..3b71af66ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Added support for `MinScore` on `ScriptScoreQuery` ([#624](https://github.com/opensearch-project/opensearch-net/pull/624)) - Added support for the `Cat.PitSegments` and `Cat.SegmentReplication` APIs ([#527](https://github.com/opensearch-project/opensearch-net/pull/527)) - Added support for serializing the `DateOnly` and `TimeOnly` types ([#734](https://github.com/opensearch-project/opensearch-net/pull/734)) +- Added support for the `Ext` parameter on `SearchRequest` ([#738](https://github.com/opensearch-project/opensearch-net/pull/738)) ### Removed - Removed support for the `net461` target ([#256](https://github.com/opensearch-project/opensearch-net/pull/256)) diff --git a/src/OpenSearch.Client/Search/Search/SearchRequest.cs b/src/OpenSearch.Client/Search/Search/SearchRequest.cs index 47e688ed66..4ee0629a87 100644 --- a/src/OpenSearch.Client/Search/Search/SearchRequest.cs +++ b/src/OpenSearch.Client/Search/Search/SearchRequest.cs @@ -214,6 +214,10 @@ public partial interface ISearchRequest : ITypedSearchRequest /// [DataMember(Name = "runtime_mappings")] IRuntimeFields RuntimeFields { get; set; } + + [DataMember(Name = "ext")] + [JsonFormatter(typeof(VerbatimDictionaryInterfaceKeysFormatter))] + IDictionary Ext { get; set; } } [ReadAs(typeof(SearchRequest<>))] @@ -288,6 +292,8 @@ public partial class SearchRequest public bool? Version { get; set; } /// public IRuntimeFields RuntimeFields { get; set; } + /// + public IDictionary Ext { get; set; } protected override HttpMethod HttpMethod => RequestState.RequestParameters?.ContainsQueryString("source") == true @@ -347,6 +353,7 @@ public partial class SearchDescriptor where TInferDocument : cla TrackTotalHits ISearchRequest.TrackTotalHits { get; set; } bool? ISearchRequest.Version { get; set; } IRuntimeFields ISearchRequest.RuntimeFields { get; set; } + IDictionary ISearchRequest.Ext { get; set; } protected sealed override void RequestDefaults(SearchRequestParameters parameters) => TypedKeys(); @@ -538,6 +545,14 @@ public SearchDescriptor PointInTime(Func + public SearchDescriptor Ext(Func, FluentDictionary> selector) => + Assign(selector(new FluentDictionary()), (a, v) => a.Ext = v); + + /// + public SearchDescriptor Ext(IDictionary dictionary) => + Assign(dictionary, (a, v) => a.Ext = v); + protected override string ResolveUrl(RouteValues routeValues, IConnectionSettingsValues settings) => base.ResolveUrl(routeValues, settings); } } diff --git a/tests/Tests/Search/Search/SearchApiTests.cs b/tests/Tests/Search/Search/SearchApiTests.cs index 7b41f011b8..17a82f426e 100644 --- a/tests/Tests/Search/Search/SearchApiTests.cs +++ b/tests/Tests/Search/Search/SearchApiTests.cs @@ -75,7 +75,15 @@ public SearchApiTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(clust value = "Stable" } } - } + }, + ext = new + { + personalize_request_parameters = new + { + user_id = "", + context = new { DEVICE = "mobile phone"} + } + } }; protected override int ExpectStatusCode => 200; @@ -93,7 +101,13 @@ public SearchApiTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(clust ) .PostFilter(f => f .Term(p => p.State, StateOfBeing.Stable) - ); + ) + .Ext(e => e + .Add("personalize_request_parameters", new Dictionary + { + ["user_id"] = "", + ["context"] = new Dictionary{ ["DEVICE"] = "mobile phone" } + })); protected override HttpMethod HttpMethod => HttpMethod.POST; @@ -110,7 +124,18 @@ public SearchApiTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(clust { Field = "state", Value = "Stable" - }) + }), + Ext = new Dictionary + { + ["personalize_request_parameters"] = new Dictionary + { + ["user_id"] = "", + ["context"] = new Dictionary + { + ["DEVICE"] = "mobile phone" + } + } + } }; protected override string UrlPath => $"/project/_search"; From 658e9ae3b06207b5ca48649dd02f44a92cff9c8b Mon Sep 17 00:00:00 2001 From: Thomas Farr Date: Mon, 5 Aug 2024 16:23:34 +1200 Subject: [PATCH 2/2] Fix test Signed-off-by: Thomas Farr --- tests/Tests/Search/Search/SearchApiTests.cs | 1080 +++++++++---------- 1 file changed, 482 insertions(+), 598 deletions(-) diff --git a/tests/Tests/Search/Search/SearchApiTests.cs b/tests/Tests/Search/Search/SearchApiTests.cs index 17a82f426e..1f44d97f95 100644 --- a/tests/Tests/Search/Search/SearchApiTests.cs +++ b/tests/Tests/Search/Search/SearchApiTests.cs @@ -29,10 +29,10 @@ using System; using System.Collections.Generic; using System.Linq; -using OpenSearch.OpenSearch.Xunit.XunitPlumbing; using OpenSearch.Net; using FluentAssertions; using OpenSearch.Client; +using Tests.Core.Client; using Tests.Core.Extensions; using Tests.Core.ManagedOpenSearch.Clusters; using Tests.Domain; @@ -41,615 +41,499 @@ namespace Tests.Search.Search { - public class SearchApiTests - : ApiIntegrationTestBase, ISearchRequest, SearchDescriptor, SearchRequest> - { - public SearchApiTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } - - protected override bool ExpectIsValid => true; - - protected override object ExpectJson => new - { - from = 10, - size = 20, - query = new - { - match_all = new { } - }, - aggs = new - { - startDates = new - { - terms = new - { - field = "startedOn" - } - } - }, - post_filter = new - { - term = new - { - state = new - { - value = "Stable" - } - } - }, - ext = new + public class SearchApiTests + : ApiIntegrationTestBase, ISearchRequest, SearchDescriptor, SearchRequest> + { + public SearchApiTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override bool ExpectIsValid => true; + + protected override object ExpectJson => new + { + from = 10, + size = 20, + query = new { match_all = new { } }, + aggs = new { startDates = new { terms = new { field = "startedOn" } } }, + post_filter = new { term = new { state = new { value = "Stable" } } }, + ext = !TestClient.Configuration.RunIntegrationTests + ? new { personalize_request_parameters = new { user_id = "", context = new { DEVICE = "mobile phone" } } } + : null + }; + + protected override int ExpectStatusCode => 200; + + protected override Func, ISearchRequest> Fluent => s => + { + s = s + .From(10) + .Size(20) + .Query(q => q + .MatchAll() + ) + .Aggregations(a => a + .Terms("startDates", t => t + .Field(p => p.StartedOn) + ) + ) + .PostFilter(f => f + .Term(p => p.State, StateOfBeing.Stable) + ); + + if (!TestClient.Configuration.RunIntegrationTests) { - personalize_request_parameters = new + s = s.Ext(e => e + .Add("personalize_request_parameters", + new Dictionary + { + ["user_id"] = "", ["context"] = new Dictionary { ["DEVICE"] = "mobile phone" } + })); + } + + return s; + }; + + protected override HttpMethod HttpMethod => HttpMethod.POST; + + protected override SearchRequest Initializer => new SearchRequest() + { + From = 10, + Size = 20, + Query = new QueryContainer(new MatchAllQuery()), + Aggregations = new TermsAggregation("startDates") { Field = "startedOn" }, + PostFilter = new QueryContainer(new TermQuery { Field = "state", Value = "Stable" }), + Ext = !TestClient.Configuration.RunIntegrationTests + ? new Dictionary { - user_id = "", - context = new { DEVICE = "mobile phone"} + ["personalize_request_parameters"] = new Dictionary + { + ["user_id"] = "", ["context"] = new Dictionary { ["DEVICE"] = "mobile phone" } + } } + : null + }; + + protected override string UrlPath => $"/project/_search"; + + protected override LazyResponses ClientUsage() => Calls( + (c, f) => c.Search(f), + (c, f) => c.SearchAsync(f), + (c, r) => c.Search(r), + (c, r) => c.SearchAsync(r) + ); + + protected override void ExpectResponse(ISearchResponse response) + { + response.Total.Should().BeGreaterThan(0); + response.Hits.Count.Should().BeGreaterThan(0); + response.HitsMetadata.Total.Value.Should().Be(response.Total); + response.HitsMetadata.Total.Relation.Should().Be(TotalHitsRelation.EqualTo); + response.Hits.First().Should().NotBeNull(); + response.Hits.First().Source.Should().NotBeNull(); + response.Aggregations.Count.Should().BeGreaterThan(0); + response.Took.Should().BeGreaterThan(0); + var startDates = response.Aggregations.Terms("startDates"); + startDates.Should().NotBeNull(); + + foreach (var document in response.Documents) document.ShouldAdhereToSourceSerializerWhenSet(); + } + } + + public class SearchApiSequenceNumberPrimaryTermTests + : SearchApiTests + { + public SearchApiSequenceNumberPrimaryTermTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override object ExpectJson => new { query = new { match_all = new { } } }; + + protected override int ExpectStatusCode => 200; + + protected override Func, ISearchRequest> Fluent => s => s + .SequenceNumberPrimaryTerm() + .Query(q => q + .MatchAll() + ); + + protected override HttpMethod HttpMethod => HttpMethod.POST; + + protected override SearchRequest Initializer => new SearchRequest() + { + SequenceNumberPrimaryTerm = true, Query = new QueryContainer(new MatchAllQuery()), + }; + + protected override string UrlPath => $"/project/_search?seq_no_primary_term=true"; + + protected override void ExpectResponse(ISearchResponse response) + { + response.Total.Should().BeGreaterThan(0); + response.Hits.Count.Should().BeGreaterThan(0); + response.HitsMetadata.Total.Value.Should().Be(response.Total); + response.HitsMetadata.Total.Relation.Should().Be(TotalHitsRelation.EqualTo); + + foreach (var hit in response.Hits) + { + hit.Should().NotBeNull(); + hit.Source.Should().NotBeNull(); + hit.SequenceNumber.Should().HaveValue(); + hit.PrimaryTerm.Should().HaveValue(); } - }; - - protected override int ExpectStatusCode => 200; - - protected override Func, ISearchRequest> Fluent => s => s - .From(10) - .Size(20) - .Query(q => q - .MatchAll() - ) - .Aggregations(a => a - .Terms("startDates", t => t - .Field(p => p.StartedOn) - ) - ) - .PostFilter(f => f - .Term(p => p.State, StateOfBeing.Stable) - ) - .Ext(e => e - .Add("personalize_request_parameters", new Dictionary + } + } + + public class SearchApiStoredFieldsTests : SearchApiTests + { + public SearchApiStoredFieldsTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override object ExpectJson => new + { + from = 10, + size = 20, + query = new { match_all = new { } }, + aggs = new { startDates = new { terms = new { field = "startedOn" } } }, + post_filter = new { term = new { state = new { value = "Stable" } } }, + stored_fields = new[] { "name", "numberOfCommits" } + }; + + protected override Func, ISearchRequest> Fluent => s => s + .From(10) + .Size(20) + .Query(q => q + .MatchAll() + ) + .Aggregations(a => a + .Terms("startDates", t => t + .Field(p => p.StartedOn) + ) + ) + .PostFilter(f => f + .Term(p => p.State, StateOfBeing.Stable) + ) + .StoredFields(fs => fs + .Field(p => p.Name) + .Field(p => p.NumberOfCommits) + ); + + protected override SearchRequest Initializer => new SearchRequest() + { + From = 10, + Size = 20, + Query = new QueryContainer(new MatchAllQuery()), + Aggregations = new TermsAggregation("startDates") { Field = "startedOn" }, + PostFilter = new QueryContainer(new TermQuery { Field = "state", Value = "Stable" }), + StoredFields = Infer.Fields(p => p.Name, p => p.NumberOfCommits) + }; + + protected override void ExpectResponse(ISearchResponse response) + { + response.Hits.Count.Should().BeGreaterThan(0); + response.Hits.First().Should().NotBeNull(); + response.Hits.First().Fields.ValueOf(p => p.Name).Should().NotBeNullOrEmpty(); + response.Hits.First().Fields.ValueOf(p => p.NumberOfCommits).Should().BeGreaterThan(0); + response.Aggregations.Count.Should().BeGreaterThan(0); + var startDates = response.Aggregations.Terms("startDates"); + startDates.Should().NotBeNull(); + } + } + + public class SearchApiDocValueFieldsTests : SearchApiTests + { + public SearchApiDocValueFieldsTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override object ExpectJson => new + { + from = 10, + size = 20, + query = new { match_all = new { } }, + aggs = new { startDates = new { terms = new { field = "startedOn" } } }, + post_filter = new { term = new { state = new { value = "Stable" } } }, + docvalue_fields = new object[] { "name", new { field = "lastActivity", format = DateFormat.basic_date }, } + }; + + protected override Func, ISearchRequest> Fluent => s => s + .From(10) + .Size(20) + .Query(q => q + .MatchAll() + ) + .Aggregations(a => a + .Terms("startDates", t => t + .Field(p => p.StartedOn) + ) + ) + .PostFilter(f => f + .Term(p => p.State, StateOfBeing.Stable) + ) + .DocValueFields(fs => fs + .Field(p => p.Name) + .Field(p => p.LastActivity, format: DateFormat.basic_date) + ); + + protected override SearchRequest Initializer => new SearchRequest() + { + From = 10, + Size = 20, + Query = new QueryContainer(new MatchAllQuery()), + Aggregations = new TermsAggregation("startDates") { Field = "startedOn" }, + PostFilter = new QueryContainer(new TermQuery { Field = "state", Value = "Stable" }), + DocValueFields = Infer.Field(p => p.Name) + .And(p => p.LastActivity, format: DateFormat.basic_date) + }; + + protected override void ExpectResponse(ISearchResponse response) + { + response.HitsMetadata.Should().NotBeNull(); + response.Hits.Count().Should().BeGreaterThan(0); + response.Hits.First().Should().NotBeNull(); + if (Cluster.ClusterConfiguration.Version < "2.0.0") + response.Hits.First().Type.Should().NotBeNullOrWhiteSpace(); + response.Hits.First().Fields.ValueOf(p => p.Name).Should().NotBeNullOrEmpty(); + var lastActivityYear = Convert.ToInt32(response.Hits.First().Fields.Value("lastActivity")); + lastActivityYear.Should().BeGreaterThan(0); + response.Aggregations.Count.Should().BeGreaterThan(0); + var startDates = response.Aggregations.Terms("startDates"); + startDates.Should().NotBeNull(); + } + } + + public class SearchApiContainingConditionlessQueryContainerTests : SearchApiTests + { + public SearchApiContainingConditionlessQueryContainerTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override object ExpectJson => new + { + query = new + { + @bool = new { - ["user_id"] = "", - ["context"] = new Dictionary{ ["DEVICE"] = "mobile phone" } - })); - - protected override HttpMethod HttpMethod => HttpMethod.POST; - - protected override SearchRequest Initializer => new SearchRequest() - { - From = 10, - Size = 20, - Query = new QueryContainer(new MatchAllQuery()), - Aggregations = new TermsAggregation("startDates") - { - Field = "startedOn" - }, - PostFilter = new QueryContainer(new TermQuery - { - Field = "state", - Value = "Stable" - }), - Ext = new Dictionary + must = new object[] { new { query_string = new { query = "query" } } }, + should = new object[] { new { query_string = new { query = "query" } } }, + must_not = new object[] { new { query_string = new { query = "query" } } } + } + } + }; + + protected override Func, ISearchRequest> Fluent => s => s + .Query(q => q + .Bool(b => b + .Must( + m => m.QueryString(qs => qs.Query("query")), + m => m.QueryString(qs => qs.Query(string.Empty)), + m => m.QueryString(qs => qs.Query(null)), + m => new QueryContainer(), + null + ) + .Should( + m => m.QueryString(qs => qs.Query("query")), + m => m.QueryString(qs => qs.Query(string.Empty)), + m => m.QueryString(qs => qs.Query(null)), + m => new QueryContainer(), + null + ) + .MustNot( + m => m.QueryString(qs => qs.Query("query")), + m => m.QueryString(qs => qs.Query(string.Empty)), + m => m.QueryString(qs => qs.Query(null)), + m => new QueryContainer(), + null + ) + ) + ); + + protected override SearchRequest Initializer => new SearchRequest() + { + Query = new BoolQuery { - ["personalize_request_parameters"] = new Dictionary + Must = new List { - ["user_id"] = "", - ["context"] = new Dictionary - { - ["DEVICE"] = "mobile phone" - } + new QueryStringQuery { Query = "query" }, + new QueryStringQuery { Query = string.Empty }, + new QueryStringQuery { Query = null }, + new QueryContainer(), + null + }, + Should = new List + { + new QueryStringQuery { Query = "query" }, + new QueryStringQuery { Query = string.Empty }, + new QueryStringQuery { Query = null }, + new QueryContainer(), + null + }, + MustNot = new List + { + new QueryStringQuery { Query = "query" }, + new QueryStringQuery { Query = string.Empty }, + new QueryStringQuery { Query = null }, + new QueryContainer(), + null } } - }; - - protected override string UrlPath => $"/project/_search"; - - protected override LazyResponses ClientUsage() => Calls( - (c, f) => c.Search(f), - (c, f) => c.SearchAsync(f), - (c, r) => c.Search(r), - (c, r) => c.SearchAsync(r) - ); - - protected override void ExpectResponse(ISearchResponse response) - { - response.Total.Should().BeGreaterThan(0); - response.Hits.Count.Should().BeGreaterThan(0); - response.HitsMetadata.Total.Value.Should().Be(response.Total); - response.HitsMetadata.Total.Relation.Should().Be(TotalHitsRelation.EqualTo); - response.Hits.First().Should().NotBeNull(); - response.Hits.First().Source.Should().NotBeNull(); - response.Aggregations.Count.Should().BeGreaterThan(0); - response.Took.Should().BeGreaterThan(0); - var startDates = response.Aggregations.Terms("startDates"); - startDates.Should().NotBeNull(); - - foreach (var document in response.Documents) document.ShouldAdhereToSourceSerializerWhenSet(); - } - } - - public class SearchApiSequenceNumberPrimaryTermTests - : SearchApiTests - { - public SearchApiSequenceNumberPrimaryTermTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } - - protected override object ExpectJson => new - { - query = new - { - match_all = new { } - } - }; - - protected override int ExpectStatusCode => 200; - - protected override Func, ISearchRequest> Fluent => s => s - .SequenceNumberPrimaryTerm() - .Query(q => q - .MatchAll() - ); - - protected override HttpMethod HttpMethod => HttpMethod.POST; - - protected override SearchRequest Initializer => new SearchRequest() - { - SequenceNumberPrimaryTerm = true, - Query = new QueryContainer(new MatchAllQuery()), - }; - - protected override string UrlPath => $"/project/_search?seq_no_primary_term=true"; - - protected override void ExpectResponse(ISearchResponse response) - { - response.Total.Should().BeGreaterThan(0); - response.Hits.Count.Should().BeGreaterThan(0); - response.HitsMetadata.Total.Value.Should().Be(response.Total); - response.HitsMetadata.Total.Relation.Should().Be(TotalHitsRelation.EqualTo); - - foreach (var hit in response.Hits) - { - hit.Should().NotBeNull(); - hit.Source.Should().NotBeNull(); - hit.SequenceNumber.Should().HaveValue(); - hit.PrimaryTerm.Should().HaveValue(); - } - } - } - - public class SearchApiStoredFieldsTests : SearchApiTests - { - public SearchApiStoredFieldsTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } - - protected override object ExpectJson => new - { - from = 10, - size = 20, - query = new - { - match_all = new { } - }, - aggs = new - { - startDates = new - { - terms = new - { - field = "startedOn" - } - } - }, - post_filter = new - { - term = new - { - state = new - { - value = "Stable" - } - } - }, - stored_fields = new[] { "name", "numberOfCommits" } - }; - - protected override Func, ISearchRequest> Fluent => s => s - .From(10) - .Size(20) - .Query(q => q - .MatchAll() - ) - .Aggregations(a => a - .Terms("startDates", t => t - .Field(p => p.StartedOn) - ) - ) - .PostFilter(f => f - .Term(p => p.State, StateOfBeing.Stable) - ) - .StoredFields(fs => fs - .Field(p => p.Name) - .Field(p => p.NumberOfCommits) - ); - - protected override SearchRequest Initializer => new SearchRequest() - { - From = 10, - Size = 20, - Query = new QueryContainer(new MatchAllQuery()), - Aggregations = new TermsAggregation("startDates") - { - Field = "startedOn" - }, - PostFilter = new QueryContainer(new TermQuery - { - Field = "state", - Value = "Stable" - }), - StoredFields = Infer.Fields(p => p.Name, p => p.NumberOfCommits) - }; - - protected override void ExpectResponse(ISearchResponse response) - { - response.Hits.Count.Should().BeGreaterThan(0); - response.Hits.First().Should().NotBeNull(); - response.Hits.First().Fields.ValueOf(p => p.Name).Should().NotBeNullOrEmpty(); - response.Hits.First().Fields.ValueOf(p => p.NumberOfCommits).Should().BeGreaterThan(0); - response.Aggregations.Count.Should().BeGreaterThan(0); - var startDates = response.Aggregations.Terms("startDates"); - startDates.Should().NotBeNull(); - } - } - - public class SearchApiDocValueFieldsTests : SearchApiTests - { - public SearchApiDocValueFieldsTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } - - protected override object ExpectJson => new - { - from = 10, - size = 20, - query = new - { - match_all = new { } - }, - aggs = new - { - startDates = new - { - terms = new - { - field = "startedOn" - } - } - }, - post_filter = new - { - term = new - { - state = new - { - value = "Stable" - } - } - }, - docvalue_fields = new object[] - { - "name", - new - { - field = "lastActivity", - format = DateFormat.basic_date - }, - } - }; - - protected override Func, ISearchRequest> Fluent => s => s - .From(10) - .Size(20) - .Query(q => q - .MatchAll() - ) - .Aggregations(a => a - .Terms("startDates", t => t - .Field(p => p.StartedOn) - ) - ) - .PostFilter(f => f - .Term(p => p.State, StateOfBeing.Stable) - ) - .DocValueFields(fs => fs - .Field(p => p.Name) - .Field(p => p.LastActivity, format: DateFormat.basic_date) - ); - - protected override SearchRequest Initializer => new SearchRequest() - { - From = 10, - Size = 20, - Query = new QueryContainer(new MatchAllQuery()), - Aggregations = new TermsAggregation("startDates") - { - Field = "startedOn" - }, - PostFilter = new QueryContainer(new TermQuery - { - Field = "state", - Value = "Stable" - }), - DocValueFields = Infer.Field(p => p.Name) - .And(p => p.LastActivity, format: DateFormat.basic_date) - }; - - protected override void ExpectResponse(ISearchResponse response) - { - response.HitsMetadata.Should().NotBeNull(); - response.Hits.Count().Should().BeGreaterThan(0); - response.Hits.First().Should().NotBeNull(); - if (Cluster.ClusterConfiguration.Version < "2.0.0") - response.Hits.First().Type.Should().NotBeNullOrWhiteSpace(); - response.Hits.First().Fields.ValueOf(p => p.Name).Should().NotBeNullOrEmpty(); - var lastActivityYear = Convert.ToInt32(response.Hits.First().Fields.Value("lastActivity")); - lastActivityYear.Should().BeGreaterThan(0); - response.Aggregations.Count.Should().BeGreaterThan(0); - var startDates = response.Aggregations.Terms("startDates"); - startDates.Should().NotBeNull(); - } - } - - public class SearchApiContainingConditionlessQueryContainerTests : SearchApiTests - { - public SearchApiContainingConditionlessQueryContainerTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } - - protected override object ExpectJson => new - { - query = new - { - @bool = new - { - must = new object[] { new { query_string = new { query = "query" } } }, - should = new object[] { new { query_string = new { query = "query" } } }, - must_not = new object[] { new { query_string = new { query = "query" } } } - } - } - }; - - protected override Func, ISearchRequest> Fluent => s => s - .Query(q => q - .Bool(b => b - .Must( - m => m.QueryString(qs => qs.Query("query")), - m => m.QueryString(qs => qs.Query(string.Empty)), - m => m.QueryString(qs => qs.Query(null)), - m => new QueryContainer(), - null - ) - .Should( - m => m.QueryString(qs => qs.Query("query")), - m => m.QueryString(qs => qs.Query(string.Empty)), - m => m.QueryString(qs => qs.Query(null)), - m => new QueryContainer(), - null - ) - .MustNot( - m => m.QueryString(qs => qs.Query("query")), - m => m.QueryString(qs => qs.Query(string.Empty)), - m => m.QueryString(qs => qs.Query(null)), - m => new QueryContainer(), - null - ) - ) - ); - - protected override SearchRequest Initializer => new SearchRequest() - { - Query = new BoolQuery - { - Must = new List - { - new QueryStringQuery { Query = "query" }, - new QueryStringQuery { Query = string.Empty }, - new QueryStringQuery { Query = null }, - new QueryContainer(), - null - }, - Should = new List - { - new QueryStringQuery { Query = "query" }, - new QueryStringQuery { Query = string.Empty }, - new QueryStringQuery { Query = null }, - new QueryContainer(), - null - }, - MustNot = new List - { - new QueryStringQuery { Query = "query" }, - new QueryStringQuery { Query = string.Empty }, - new QueryStringQuery { Query = null }, - new QueryContainer(), - null - } - } - }; - - protected override void ExpectResponse(ISearchResponse response) => response.ShouldBeValid(); - } - - public class SearchApiNullQueryContainerTests : SearchApiTests - { - public SearchApiNullQueryContainerTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } - - protected override object ExpectJson => new { }; - - protected override Func, ISearchRequest> Fluent => s => s - .Query(q => q - .Bool(b => b - .Must((Func, QueryContainer>)null) - .Should((Func, QueryContainer>)null) - .MustNot((Func, QueryContainer>)null) - ) - ); - - protected override SearchRequest Initializer => new SearchRequest() - { - Query = new BoolQuery - { - Must = null, - Should = null, - MustNot = null - } - }; - - protected override void ExpectResponse(ISearchResponse response) => response.ShouldBeValid(); - } - - public class SearchApiNullQueriesInQueryContainerTests : SearchApiTests - { - public SearchApiNullQueriesInQueryContainerTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } - - protected override object ExpectJson => new - { - query = new - { - @bool = new { } - } - }; - - // There is no *direct equivalent* to a query container collection only with a null querycontainer - // since the fluent methods filter them out - protected override Func, ISearchRequest> Fluent => s => s - .Query(q => q - .Bool(b => - { - b.Verbatim(); - IBoolQuery bq = b; - bq.Must = new QueryContainer[] { null }; - bq.Should = new QueryContainer[] { null }; - bq.MustNot = new QueryContainer[] { null }; - return bq; - }) - ); - - protected override SearchRequest Initializer => new SearchRequest() - { - Query = new BoolQuery - { - IsVerbatim = true, - Must = new QueryContainer[] { null }, - Should = new QueryContainer[] { null }, - MustNot = new QueryContainer[] { null } - } - }; - - // when we serialize we write and empty bool, when we read the fact it was verbatim is lost so while - // we technically DO support deserialization here (and empty bool will get set) when we write it a second - // time it will NOT write that bool because the is verbatim did not carry over. - protected override bool SupportsDeserialization => false; - - protected override void ExpectResponse(ISearchResponse response) => response.ShouldBeValid(); - } - - - public class OpaqueIdApiTests - : ApiIntegrationTestBase - { - public OpaqueIdApiTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } - - protected override bool ExpectIsValid => true; - - protected override object ExpectJson => null; - protected override int ExpectStatusCode => 200; - - protected override Func Fluent => s => s - .RequestConfiguration(r => r.OpaqueId(CallIsolatedValue)); - - protected override HttpMethod HttpMethod => HttpMethod.GET; - - protected override ListTasksRequest Initializer => new ListTasksRequest() - { - RequestConfiguration = new RequestConfiguration { OpaqueId = CallIsolatedValue }, - }; - - protected override bool SupportsDeserialization => false; - protected override string UrlPath => $"/_tasks?pretty=true&error_trace=true"; - - protected override LazyResponses ClientUsage() => Calls( - (c, f) => c.Tasks.List(f), - (c, f) => c.Tasks.ListAsync(f), - (c, r) => c.Tasks.List(r), - (c, r) => c.Tasks.ListAsync(r) - ); - - protected override void OnBeforeCall(IOpenSearchClient client) - { - var searchResponse = client.Search(s => s - .RequestConfiguration(r => r.OpaqueId(CallIsolatedValue)) - .Scroll("10m") // Create a scroll in order to keep the task around. - ); - - searchResponse.ShouldBeValid(); - } - - protected override void ExpectResponse(ListTasksResponse response) - { - response.ShouldBeValid(); - foreach (var node in response.Nodes) - foreach (var task in node.Value.Tasks) - { - task.Value.Headers.Should().NotBeNull(); - if (task.Value.Headers.TryGetValue(RequestData.OpaqueIdHeader, out var opaqueIdValue)) - opaqueIdValue.Should() - .Be(CallIsolatedValue, - $"OpaqueId header {opaqueIdValue} did not match {CallIsolatedValue}"); - // TODO: Determine if this is a valid assertion i.e. should all tasks returned have an OpaqueId header? + }; + + protected override void ExpectResponse(ISearchResponse response) => response.ShouldBeValid(); + } + + public class SearchApiNullQueryContainerTests : SearchApiTests + { + public SearchApiNullQueryContainerTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override object ExpectJson => new { }; + + protected override Func, ISearchRequest> Fluent => s => s + .Query(q => q + .Bool(b => b + .Must((Func, QueryContainer>)null) + .Should((Func, QueryContainer>)null) + .MustNot((Func, QueryContainer>)null) + ) + ); + + protected override SearchRequest Initializer => new SearchRequest() + { + Query = new BoolQuery { Must = null, Should = null, MustNot = null } + }; + + protected override void ExpectResponse(ISearchResponse response) => response.ShouldBeValid(); + } + + public class SearchApiNullQueriesInQueryContainerTests : SearchApiTests + { + public SearchApiNullQueriesInQueryContainerTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override object ExpectJson => new { query = new { @bool = new { } } }; + + // There is no *direct equivalent* to a query container collection only with a null querycontainer + // since the fluent methods filter them out + protected override Func, ISearchRequest> Fluent => s => s + .Query(q => q + .Bool(b => + { + b.Verbatim(); + IBoolQuery bq = b; + bq.Must = new QueryContainer[] { null }; + bq.Should = new QueryContainer[] { null }; + bq.MustNot = new QueryContainer[] { null }; + return bq; + }) + ); + + protected override SearchRequest Initializer => new SearchRequest() + { + Query = new BoolQuery + { + IsVerbatim = true, + Must = new QueryContainer[] { null }, + Should = new QueryContainer[] { null }, + MustNot = new QueryContainer[] { null } + } + }; + + // when we serialize we write and empty bool, when we read the fact it was verbatim is lost so while + // we technically DO support deserialization here (and empty bool will get set) when we write it a second + // time it will NOT write that bool because the is verbatim did not carry over. + protected override bool SupportsDeserialization => false; + + protected override void ExpectResponse(ISearchResponse response) => response.ShouldBeValid(); + } + + + public class OpaqueIdApiTests + : ApiIntegrationTestBase + { + public OpaqueIdApiTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override bool ExpectIsValid => true; + + protected override object ExpectJson => null; + protected override int ExpectStatusCode => 200; + + protected override Func Fluent => s => s + .RequestConfiguration(r => r.OpaqueId(CallIsolatedValue)); + + protected override HttpMethod HttpMethod => HttpMethod.GET; + + protected override ListTasksRequest Initializer => new ListTasksRequest() + { + RequestConfiguration = new RequestConfiguration { OpaqueId = CallIsolatedValue }, + }; + + protected override bool SupportsDeserialization => false; + protected override string UrlPath => $"/_tasks?pretty=true&error_trace=true"; + + protected override LazyResponses ClientUsage() => Calls( + (c, f) => c.Tasks.List(f), + (c, f) => c.Tasks.ListAsync(f), + (c, r) => c.Tasks.List(r), + (c, r) => c.Tasks.ListAsync(r) + ); + + protected override void OnBeforeCall(IOpenSearchClient client) + { + var searchResponse = client.Search(s => s + .RequestConfiguration(r => r.OpaqueId(CallIsolatedValue)) + .Scroll("10m") // Create a scroll in order to keep the task around. + ); + + searchResponse.ShouldBeValid(); + } + + protected override void ExpectResponse(ListTasksResponse response) + { + response.ShouldBeValid(); + foreach (var node in response.Nodes) + foreach (var task in node.Value.Tasks) + { + task.Value.Headers.Should().NotBeNull(); + if (task.Value.Headers.TryGetValue(RequestData.OpaqueIdHeader, out var opaqueIdValue)) + opaqueIdValue.Should() + .Be(CallIsolatedValue, + $"OpaqueId header {opaqueIdValue} did not match {CallIsolatedValue}"); + // TODO: Determine if this is a valid assertion i.e. should all tasks returned have an OpaqueId header? // else // { // Assert.True(false, // $"No OpaqueId header for task {task.Key} and OpaqueId value {this.CallIsolatedValue}"); // } - } - } - } - - public class CrossClusterSearchApiTests - : ApiIntegrationTestBase, ISearchRequest, SearchDescriptor, SearchRequest> - { - public CrossClusterSearchApiTests(CrossCluster cluster, EndpointUsage usage) : base(cluster, usage) { } - - protected override bool ExpectIsValid => true; - - protected override object ExpectJson => new - { - query = new - { - match_all = new { } - } - }; - - protected override int ExpectStatusCode => 200; - - protected override Func, ISearchRequest> Fluent => s => s - .Index(OpenSearch.Client.Indices.Index().And("cluster_two:project")) - .Query(q => q - .MatchAll() - ); - - protected override HttpMethod HttpMethod => HttpMethod.POST; - - protected override SearchRequest Initializer => new SearchRequest(OpenSearch.Client.Indices.Index().And("cluster_two:project")) - { - Query = new MatchAllQuery() - }; - - protected override string UrlPath => $"/project%2Ccluster_two%3Aproject/_search"; - - protected override LazyResponses ClientUsage() => Calls( - (c, f) => c.Search(f), - (c, f) => c.SearchAsync(f), - (c, r) => c.Search(r), - (c, r) => c.SearchAsync(r) - ); - - protected override void ExpectResponse(ISearchResponse response) - { - response.Clusters.Should().NotBeNull(); - response.Clusters.Total.Should().Be(2); - response.Clusters.Skipped.Should().Be(1); - response.Clusters.Successful.Should().Be(1); - } - } + } + } + } + + public class CrossClusterSearchApiTests + : ApiIntegrationTestBase, ISearchRequest, SearchDescriptor, SearchRequest> + { + public CrossClusterSearchApiTests(CrossCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override bool ExpectIsValid => true; + + protected override object ExpectJson => new { query = new { match_all = new { } } }; + + protected override int ExpectStatusCode => 200; + + protected override Func, ISearchRequest> Fluent => s => s + .Index(OpenSearch.Client.Indices.Index().And("cluster_two:project")) + .Query(q => q + .MatchAll() + ); + + protected override HttpMethod HttpMethod => HttpMethod.POST; + + protected override SearchRequest Initializer => + new SearchRequest(OpenSearch.Client.Indices.Index().And("cluster_two:project")) { Query = new MatchAllQuery() }; + + protected override string UrlPath => $"/project%2Ccluster_two%3Aproject/_search"; + + protected override LazyResponses ClientUsage() => Calls( + (c, f) => c.Search(f), + (c, f) => c.SearchAsync(f), + (c, r) => c.Search(r), + (c, r) => c.SearchAsync(r) + ); + + protected override void ExpectResponse(ISearchResponse response) + { + response.Clusters.Should().NotBeNull(); + response.Clusters.Total.Should().Be(2); + response.Clusters.Skipped.Should().Be(1); + response.Clusters.Successful.Should().Be(1); + } + } }