diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index b6ff2632..46421344 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -694,6 +694,15 @@ public static Tuple> ToProfileSear return new Tuple>(searchResult, profile); } + public static Tuple ParseProfileSearchResult(this RedisResult result, Query q) + { + var results = (RedisResult[])result!; + + var searchResult = results[0].ToSearchResult(q); + var profile = new ProfilingInformation(results[1]); + return new Tuple(searchResult, profile); + } + public static SearchResult ToSearchResult(this RedisResult result, Query q) { return new SearchResult((RedisResult[])result!, !q.NoContent, q.WithScores, q.WithPayloads/*, q.ExplainScore*/); @@ -707,6 +716,14 @@ public static Tuple> ToProfil return new Tuple>(aggregateResult, profile); } + public static Tuple ParseProfileAggregateResult(this RedisResult result, AggregationRequest q) + { + var results = (RedisResult[])result!; + var aggregateResult = results[0].ToAggregationResult(q); + var profile = new ProfilingInformation(results[1]); + return new Tuple(aggregateResult, profile); + } + public static AggregationResult ToAggregationResult(this RedisResult result, AggregationRequest query) { if (query.IsWithCursor()) diff --git a/src/NRedisStack/Search/ISearchCommands.cs b/src/NRedisStack/Search/ISearchCommands.cs index 45843dbe..2d9e090b 100644 --- a/src/NRedisStack/Search/ISearchCommands.cs +++ b/src/NRedisStack/Search/ISearchCommands.cs @@ -177,8 +177,18 @@ public interface ISearchCommands /// The query string. /// Removes details of reader iterator. /// + [Obsolete("Consider using ProfileOnSearch with Redis CE 8.0 and later")] Tuple> ProfileSearch(string indexName, Query q, bool limited = false); + /// + /// Apply FT.SEARCH command to collect performance details. + /// + /// The index name, created using FT.CREATE. + /// The query string. + /// Removes details of reader iterator. + /// + Tuple ProfileOnSearch(string indexName, Query q, bool limited = false); + /// /// Apply FT.AGGREGATE command to collect performance details. /// @@ -186,8 +196,18 @@ public interface ISearchCommands /// The query string. /// Removes details of reader iterator. /// + [Obsolete("Consider using ProfileOnAggregate with Redis CE 8.0 and later")] Tuple> ProfileAggregate(string indexName, AggregationRequest query, bool limited = false); + /// + /// Apply FT.AGGREGATE command to collect performance details. + /// + /// The index name, created using FT.CREATE. + /// The query string. + /// Removes details of reader iterator. + /// + Tuple ProfileOnAggregate(string indexName, AggregationRequest query, bool limited = false); + /// /// Search the index /// diff --git a/src/NRedisStack/Search/ISearchCommandsAsync.cs b/src/NRedisStack/Search/ISearchCommandsAsync.cs index b42013f8..0e1e7333 100644 --- a/src/NRedisStack/Search/ISearchCommandsAsync.cs +++ b/src/NRedisStack/Search/ISearchCommandsAsync.cs @@ -169,7 +169,6 @@ public interface ISearchCommandsAsync /// Task InfoAsync(RedisValue index); - /// /// Apply FT.SEARCH command to collect performance details. /// @@ -177,8 +176,17 @@ public interface ISearchCommandsAsync /// The query string. /// Removes details of reader iterator. /// + [Obsolete("Consider using ProfileOnSearchAsync with Redis CE 8.0 and later")] Task>> ProfileSearchAsync(string indexName, Query q, bool limited = false); + /// + /// Apply FT.SEARCH command to collect performance details. + /// + /// The index name, created using FT.CREATE. + /// The query string. + /// Removes details of reader iterator. + /// + Task> ProfileOnSearchAsync(string indexName, Query q, bool limited = false); /// /// Apply FT.AGGREGATE command to collect performance details. @@ -187,8 +195,18 @@ public interface ISearchCommandsAsync /// The query string. /// Removes details of reader iterator. /// + [Obsolete("Consider using ProfileOnAggregateAsync with Redis CE 8.0 and later")] Task>> ProfileAggregateAsync(string indexName, AggregationRequest query, bool limited = false); + /// + /// Apply FT.AGGREGATE command to collect performance details. + /// + /// The index name, created using FT.CREATE. + /// The query string. + /// Removes details of reader iterator. + /// + Task> ProfileOnAggregateAsync(string indexName, AggregationRequest query, bool limited = false); + /// /// Search the index /// diff --git a/src/NRedisStack/Search/ProfilingInformation.cs b/src/NRedisStack/Search/ProfilingInformation.cs new file mode 100644 index 00000000..4ccc1590 --- /dev/null +++ b/src/NRedisStack/Search/ProfilingInformation.cs @@ -0,0 +1,15 @@ +using StackExchange.Redis; + +namespace NRedisStack.Search +{ + + public class ProfilingInformation + { + public RedisResult Info { get; private set; } + public ProfilingInformation(RedisResult info) + { + this.Info = info; + } + + } +} \ No newline at end of file diff --git a/src/NRedisStack/Search/SearchCommands.cs b/src/NRedisStack/Search/SearchCommands.cs index 6d9a64e6..7a362ede 100644 --- a/src/NRedisStack/Search/SearchCommands.cs +++ b/src/NRedisStack/Search/SearchCommands.cs @@ -131,18 +131,37 @@ public InfoResult Info(RedisValue index) => new InfoResult(_db.Execute(SearchCommandBuilder.Info(index))); /// + [Obsolete("Consider using ProfileOnSearch with Redis CE 8.0 and later")] public Tuple> ProfileSearch(string indexName, Query q, bool limited = false) { return _db.Execute(SearchCommandBuilder.ProfileSearch(indexName, q, limited)) - .ToProfileSearchResult(q); + .ToProfileSearchResult(q); } + /// + public Tuple ProfileOnSearch(string indexName, Query q, bool limited = false) + { + return _db.Execute(SearchCommandBuilder.ProfileSearch(indexName, q, limited)) + .ParseProfileSearchResult(q); + } + + /// + [Obsolete("Consider using ProfileOnAggregate with Redis CE 8.0 and later")] public Tuple> ProfileAggregate(string indexName, AggregationRequest query, bool limited = false) { setDefaultDialectIfUnset(query); return _db.Execute(SearchCommandBuilder.ProfileAggregate(indexName, query, limited)) .ToProfileAggregateResult(query); } + + /// + public Tuple ProfileOnAggregate(string indexName, AggregationRequest query, bool limited = false) + { + setDefaultDialectIfUnset(query); + return _db.Execute(SearchCommandBuilder.ProfileAggregate(indexName, query, limited)) + .ParseProfileAggregateResult(query); + } + /// public SearchResult Search(string indexName, Query q) { diff --git a/src/NRedisStack/Search/SearchCommandsAsync.cs b/src/NRedisStack/Search/SearchCommandsAsync.cs index 0da1c1f0..23a24027 100644 --- a/src/NRedisStack/Search/SearchCommandsAsync.cs +++ b/src/NRedisStack/Search/SearchCommandsAsync.cs @@ -162,17 +162,32 @@ public async Task InfoAsync(RedisValue index) => new InfoResult(await _db.ExecuteAsync(SearchCommandBuilder.Info(index))); /// + [Obsolete("Consider using ProfileOnSearchAsync with Redis CE 8.0 and later")] public async Task>> ProfileSearchAsync(string indexName, Query q, bool limited = false) { return (await _db.ExecuteAsync(SearchCommandBuilder.ProfileSearch(indexName, q, limited))) .ToProfileSearchResult(q); } + + /// + public async Task> ProfileOnSearchAsync(string indexName, Query q, bool limited = false) + { + return (await _db.ExecuteAsync(SearchCommandBuilder.ProfileSearch(indexName, q, limited))) + .ParseProfileSearchResult(q); + } /// + [Obsolete("Consider using ProfileOnSearchAsync with Redis CE 8.0 and later")] public async Task>> ProfileAggregateAsync(string indexName, AggregationRequest query, bool limited = false) { return (await _db.ExecuteAsync(SearchCommandBuilder.ProfileAggregate(indexName, query, limited))) .ToProfileAggregateResult(query); } + /// + public async Task> ProfileOnAggregateAsync(string indexName, AggregationRequest query, bool limited = false) + { + return (await _db.ExecuteAsync(SearchCommandBuilder.ProfileAggregate(indexName, query, limited))) + .ParseProfileAggregateResult(query); + } /// public async Task SearchAsync(string indexName, Query q) diff --git a/tests/NRedisStack.Tests/CustomAssertions.cs b/tests/NRedisStack.Tests/CustomAssertions.cs new file mode 100644 index 00000000..c1a6f596 --- /dev/null +++ b/tests/NRedisStack.Tests/CustomAssertions.cs @@ -0,0 +1,20 @@ +using Xunit; + +namespace NRedisStack.Tests; + +public static class CustomAssertions +{ + // Generic method to assert that 'actual' is greater than 'expected' + public static void GreaterThan(T actual, T expected) where T : IComparable + { + Assert.True(actual.CompareTo(expected) > 0, + $"Failure: Expected value to be greater than {expected}, but found {actual}."); + } + + // Generic method to assert that 'actual' is less than 'expected' + public static void LessThan(T actual, T expected) where T : IComparable + { + Assert.True(actual.CompareTo(expected) < 0, + $"Failure: Expected value to be less than {expected}, but found {actual}."); + } +} diff --git a/tests/NRedisStack.Tests/Search/SearchTests.cs b/tests/NRedisStack.Tests/Search/SearchTests.cs index e0c2fade..2bbf43b8 100644 --- a/tests/NRedisStack.Tests/Search/SearchTests.cs +++ b/tests/NRedisStack.Tests/Search/SearchTests.cs @@ -2776,7 +2776,7 @@ public async Task getSuggestionLengthAndDeleteSuggestionAsync() Assert.Equal(2L, await ft.SugLenAsync(key)); } - [SkipIfRedis(Is.Enterprise, Comparison.GreaterThanOrEqual, "7.3.240")] + [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.9")] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public void TestProfileSearch(string endpointId) { @@ -2790,17 +2790,16 @@ public void TestProfileSearch(string endpointId) new HashEntry("t1", "foo"), new HashEntry("t2", "bar")}); - var profile = ft.ProfileSearch(index, new Query("foo")); + var profile = ft.ProfileOnSearch(index, new Query("foo")); // Iterators profile={Type=TEXT, Time=0.0, Term=foo, Counter=1, Size=1} - profile.Item2["Iterators profile"].ToDictionary(); - var iteratorsProfile = profile.Item2["Iterators profile"].ToDictionary(); - Assert.Equal("TEXT", iteratorsProfile["Type"].ToString()); - Assert.Equal("foo", iteratorsProfile["Term"].ToString()); - Assert.Equal("1", iteratorsProfile["Counter"].ToString()); - Assert.Equal("1", iteratorsProfile["Size"].ToString()); + var info = (RedisResult[])profile.Item2.Info; + int shardsIndex = Array.FindIndex(info, item => item.ToString() == "Shards"); + int coordinatorIndex = Array.FindIndex(info, item => item.ToString() == "Coordinator"); + CustomAssertions.GreaterThan(shardsIndex, -1); + CustomAssertions.GreaterThan(coordinatorIndex, -1); } - [SkipIfRedis(Is.Enterprise, Comparison.GreaterThanOrEqual, "7.3.240")] + [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.9")] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public async Task TestProfileSearchAsync(string endpointId) { @@ -2814,18 +2813,53 @@ public async Task TestProfileSearchAsync(string endpointId) new HashEntry("t1", "foo"), new HashEntry("t2", "bar")}); - var profile = await ft.ProfileSearchAsync(index, new Query("foo")); - // Iterators profile={Type=TEXT, Time=0.0, Term=foo, Counter=1, Size=1} - profile.Item2["Iterators profile"].ToDictionary(); - var iteratorsProfile = profile.Item2["Iterators profile"].ToDictionary(); - Assert.Equal("TEXT", iteratorsProfile["Type"].ToString()); - Assert.Equal("foo", iteratorsProfile["Term"].ToString()); - Assert.Equal("1", iteratorsProfile["Counter"].ToString()); - Assert.Equal("1", iteratorsProfile["Size"].ToString()); + var profile = await ft.ProfileOnSearchAsync(index, new Query("foo")); + var info = (RedisResult[])profile.Item2.Info; + int shardsIndex = Array.FindIndex(info, item => item.ToString() == "Shards"); + int coordinatorIndex = Array.FindIndex(info, item => item.ToString() == "Coordinator"); + CustomAssertions.GreaterThan(shardsIndex, -1); + CustomAssertions.GreaterThan(coordinatorIndex, -1); } + [SkipIfRedis(Is.Enterprise, Comparison.GreaterThanOrEqual, "7.9")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestProfileSearch_WithoutCoordinator(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ft = db.FT(); - [SkipIfRedis(Is.Enterprise, Comparison.GreaterThanOrEqual, "7.3.240")] + Schema sc = new Schema().AddTextField("t1", 1.0).AddTextField("t2", 1.0); + Assert.True(ft.Create(index, new FTCreateParams(), sc)); + + db.HashSet("doc1", new HashEntry[] { + new HashEntry("t1", "foo"), + new HashEntry("t2", "bar")}); + + var profile = ft.ProfileSearch(index, new Query("foo")); + var info = profile.Item2; + CustomAssertions.GreaterThan(info.Count, 4); + } + + [SkipIfRedis(Is.Enterprise, Comparison.GreaterThanOrEqual, "7.9")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestProfileSearchAsync_WithoutCoordinator(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ft = db.FT(); + + Schema sc = new Schema().AddTextField("t1", 1.0).AddTextField("t2", 1.0); + Assert.True(ft.Create(index, new FTCreateParams(), sc)); + + db.HashSet("doc1", new HashEntry[] { + new HashEntry("t1", "foo"), + new HashEntry("t2", "bar")}); + + var profile = await ft.ProfileSearchAsync(index, new Query("foo")); + var info = profile.Item2; + CustomAssertions.GreaterThan(info.Count, 4); + } + + [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.9")] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public void TestProfile(string endpointId) { @@ -2836,28 +2870,100 @@ public void TestProfile(string endpointId) db.HashSet("1", "t", "hello"); db.HashSet("2", "t", "world"); + // check using Query + var q = new Query("hello|world").SetNoContent(); + var profileSearch = ft.ProfileOnSearch(index, q); + var searchRes = profileSearch.Item1; + var searchDet = (RedisResult[])profileSearch.Item2.Info; + + Assert.Equal(2, searchRes.Documents.Count); + int shardsIndex = Array.FindIndex(searchDet, item => item.ToString() == "Shards"); + int coordinatorIndex = Array.FindIndex(searchDet, item => item.ToString() == "Coordinator"); + CustomAssertions.GreaterThan(shardsIndex, -1); + CustomAssertions.GreaterThan(coordinatorIndex, -1); + + // check using AggregationRequest + var aggReq = new AggregationRequest("*").Load(FieldName.Of("t")).Apply("startswith(@t, 'hel')", "prefix"); + var profileAggregate = ft.ProfileOnAggregate(index, aggReq); + var aggregateRes = profileAggregate.Item1; + var aggregateDet = (RedisResult[])profileAggregate.Item2.Info; + + Assert.Equal(2, aggregateRes.TotalResults); + shardsIndex = Array.FindIndex(aggregateDet, item => item.ToString() == "Shards"); + coordinatorIndex = Array.FindIndex(aggregateDet, item => item.ToString() == "Coordinator"); + CustomAssertions.GreaterThan(shardsIndex, -1); + CustomAssertions.GreaterThan(coordinatorIndex, -1); + } + + [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.9")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public async Task TestProfileAsync(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ft = db.FT(); + + await ft.CreateAsync(index, new Schema().AddTextField("t")); // Calling FT.CREATR without FTCreateParams + db.HashSet("1", "t", "hello"); + db.HashSet("2", "t", "world"); + + // check using Query + var q = new Query("hello|world").SetNoContent(); + var profileSearch = await ft.ProfileOnSearchAsync(index, q); + var searchRes = profileSearch.Item1; + var searchDet = (RedisResult[])profileSearch.Item2.Info; + + Assert.Equal(2, searchRes.Documents.Count); + int shardsIndex = Array.FindIndex(searchDet, item => item.ToString() == "Shards"); + int coordinatorIndex = Array.FindIndex(searchDet, item => item.ToString() == "Coordinator"); + CustomAssertions.GreaterThan(shardsIndex, -1); + CustomAssertions.GreaterThan(coordinatorIndex, -1); + + // check using AggregationRequest + var aggReq = new AggregationRequest("*").Load(FieldName.Of("t")).Apply("startswith(@t, 'hel')", "prefix"); + var profileAggregate = await ft.ProfileOnAggregateAsync(index, aggReq); + var aggregateRes = profileAggregate.Item1; + var aggregateDet = (RedisResult[])profileAggregate.Item2.Info; + + Assert.Equal(2, aggregateRes.TotalResults); + shardsIndex = Array.FindIndex(aggregateDet, item => item.ToString() == "Shards"); + coordinatorIndex = Array.FindIndex(aggregateDet, item => item.ToString() == "Coordinator"); + CustomAssertions.GreaterThan(shardsIndex, -1); + CustomAssertions.GreaterThan(coordinatorIndex, -1); + } + + [SkipIfRedis(Is.Enterprise, Comparison.GreaterThanOrEqual, "7.9")] + [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] + public void TestProfile_WithoutCoordinator(string endpointId) + { + IDatabase db = GetCleanDatabase(endpointId); + var ft = db.FT(); + + ft.Create(index, new Schema().AddTextField("t")); // Calling FT.CREATR without FTCreateParams + db.HashSet("1", "t", "hello"); + db.HashSet("2", "t", "world"); + // check using Query var q = new Query("hello|world").SetNoContent(); var profileSearch = ft.ProfileSearch(index, q); var searchRes = profileSearch.Item1; var searchDet = profileSearch.Item2; - Assert.Equal(5, searchDet.Count); Assert.Equal(2, searchRes.Documents.Count); - + CustomAssertions.GreaterThan(searchDet.Count, 4); // check using AggregationRequest var aggReq = new AggregationRequest("*").Load(FieldName.Of("t")).Apply("startswith(@t, 'hel')", "prefix"); var profileAggregate = ft.ProfileAggregate(index, aggReq); var aggregateRes = profileAggregate.Item1; var aggregateDet = profileAggregate.Item2; - Assert.Equal(5, aggregateDet.Count); + Assert.Equal(2, aggregateRes.TotalResults); + CustomAssertions.GreaterThan(aggregateDet.Count, 4); } - [SkipIfRedis(Is.Enterprise, Comparison.GreaterThanOrEqual, "7.3.240")] + [SkipIfRedis(Is.Enterprise, Comparison.GreaterThanOrEqual, "7.9")] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] - public async Task TestProfileAsync(string endpointId) + public async Task TestProfileAsync_WithoutCoordinator(string endpointId) { IDatabase db = GetCleanDatabase(endpointId); var ft = db.FT(); @@ -2872,19 +2978,20 @@ public async Task TestProfileAsync(string endpointId) var searchRes = profileSearch.Item1; var searchDet = profileSearch.Item2; - Assert.Equal(5, searchDet.Count); Assert.Equal(2, searchRes.Documents.Count); + CustomAssertions.GreaterThan(searchDet.Count, 4); // check using AggregationRequest var aggReq = new AggregationRequest("*").Load(FieldName.Of("t")).Apply("startswith(@t, 'hel')", "prefix"); var profileAggregate = await ft.ProfileAggregateAsync(index, aggReq); var aggregateRes = profileAggregate.Item1; var aggregateDet = profileAggregate.Item2; - Assert.Equal(5, aggregateDet.Count); + Assert.Equal(2, aggregateRes.TotalResults); + CustomAssertions.GreaterThan(searchDet.Count, 4); } - [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.3.242")] + [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.3.240")] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public void TestProfileIssue306(string endpointId) { @@ -2897,24 +3004,24 @@ public void TestProfileIssue306(string endpointId) // check using Query var q = new Query("hello|world").SetNoContent(); - var profileSearch = ft.ProfileSearch(index, q); + var profileSearch = ft.ProfileOnSearch(index, q); var searchRes = profileSearch.Item1; - var searchDet = profileSearch.Item2; + var searchDet = (RedisResult[])profileSearch.Item2.Info; - Assert.Equal(6, searchDet.Count); + CustomAssertions.GreaterThan(searchDet.Length, 3); Assert.Equal(2, searchRes.Documents.Count); // check using AggregationRequest var aggReq = new AggregationRequest("*").Load(FieldName.Of("t")).Apply("startswith(@t, 'hel')", "prefix"); - var profileAggregate = ft.ProfileAggregate(index, aggReq); + var profileAggregate = ft.ProfileOnAggregate(index, aggReq); var aggregateRes = profileAggregate.Item1; - var aggregateDet = profileAggregate.Item2; - Assert.True(aggregateDet.Count >= 6); + var aggregateDet = (RedisResult[])profileAggregate.Item2.Info; + CustomAssertions.GreaterThan(aggregateDet.Length, 3); Assert.Equal(2, aggregateRes.TotalResults); } - [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.3.242")] + [SkipIfRedis(Is.Enterprise, Comparison.LessThan, "7.3.240")] [MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))] public async Task TestProfileAsyncIssue306(string endpointId) { @@ -2927,19 +3034,19 @@ public async Task TestProfileAsyncIssue306(string endpointId) // check using Query var q = new Query("hello|world").SetNoContent(); - var profileSearch = await ft.ProfileSearchAsync(index, q); + var profileSearch = await ft.ProfileOnSearchAsync(index, q); var searchRes = profileSearch.Item1; - var searchDet = profileSearch.Item2; + var searchDet = (RedisResult[])profileSearch.Item2.Info; - Assert.Equal(6, searchDet.Count); + CustomAssertions.GreaterThan(searchDet.Length, 3); Assert.Equal(2, searchRes.Documents.Count); // check using AggregationRequest var aggReq = new AggregationRequest("*").Load(FieldName.Of("t")).Apply("startswith(@t, 'hel')", "prefix"); - var profileAggregate = await ft.ProfileAggregateAsync(index, aggReq); + var profileAggregate = await ft.ProfileOnAggregateAsync(index, aggReq); var aggregateRes = profileAggregate.Item1; - var aggregateDet = profileAggregate.Item2; - Assert.True(aggregateDet.Count >= 6); + var aggregateDet = (RedisResult[])profileAggregate.Item2.Info; + CustomAssertions.GreaterThan(aggregateDet.Length, 3); Assert.Equal(2, aggregateRes.TotalResults); }