Unverified Commit 9b8bdf36 authored by Tommy Hanks's avatar Tommy Hanks Committed by GitHub

Bring NRediSearch "even" with JRediSearch (#1267)

* The casing in the error message probably changed.

I checked the RediSearch source and found that it's been "Unsupported
language" since at least August 28, 2018.

* `FT.OPTIMIZE` has been deprecated.

Index optimizations are now handled by the internal garbage collector in
the background.

https://oss.redislabs.com/redisearch/Commands.html#ftoptimize

* Started work on porting the aggregation builder class and test from
JRediSearch.

* Ported in static support methods.

* Added Load, Limit, and SortBy

* Finished adding the sortby's

* Group by and apply have been ported.

* Added in the `Filter` method.

* Added in `Cursor` builder method.

Made the private `args` member naming more consistent the rest of the
project by prefixing with an underscore.

* Made this a privately settable property as this is basically what the
Java version had.

* Implemented `ArgsString` property here. This is from the Java
`getArgsString`

* Args list is now accessible just like the Java version.

* Added in serialize redis args method...

* Ported `TestAggregations` test from JRediSearch.

Kept everything as close to the original as possible.

* Marked these methods obsolete in favor of the ones that use the
AggregationBuilder.

* Introduced new overloads for the `Aggregate` method.

Marked the obsolete unit test as obsolete.

* For completeness I added in the commands that should be issued by the
setup portion of this test.

* Ported in the `TestApplyAndFilterAggregations` test.

* Porting over the support for dealing with aggregate cursors.

* Initial pass at implementing cursor delete.

* Initial pass at implementing `CursorRead` and `CursorReadAsync`

* Fixed issue with supplying the Redisearch command with sorting.

Fixed some assertions...

* Added support for return fields.

* Fixed apparently typo.

* Moved this test class to be more consistent with the JRedisearch
library.

* Cleaned up imports here.

* Initial pass at porting the tag for `AlterIndex`.

* Current progress porting the FT.ALTER command.

* Added in a new type for capturing the FT.INFO result...

* The test for AlterIndex (Add) is complete.

* Altered test here to meet assertions from JRediSearch.

* Ported support for the FT.MGET command.

* Ported the Suggestion with Builder from JRediSearch.

* Ported SuggestionOptions.

* Further fleshed out the suggestion options using JRediSearch as a guide.

* Ported over the expanded Suggestions functionality from JRediSearch.

* Ported this from JRediSearch.

* Ported more tests from JRediSearch.

Fixed some silly bugs...

* Ported the last three auto suggest tests from JRediSearch.

* More tests ported from JRediSearch.

* Implemented ability to add multiple documents at once. Started on
deleting multiple documents at once...

* In order to match the functionality found in JRediSearch, I'm catching
RedisServerExceptions that contain the message "Document already in
index".

* Added support for the `INKEYS` search query modifier.

* Ported in a covering test for the AggregationBuilder.

* Cleaned up builder access in the Suggestion and SuggestionOptions
classes.

* Refactored IndexOptions to make them behave more like JRediSearch.

Removed NOSCOREIDX as that has been deprecated.

* PR feedback.

Marked AggregationBuilder as sealed because (at least initially) we
don't expect any inheritance here.

Dropped the call to `ToString` when appending the value for "MAX" to a
SortBy clause because it isn't needed.

Changed `ArgsString` to a method to indicate that we're not simply
exposing internal state. Made the method internal as it's present for
test purposes.

Removed the `Args` property because it's not clear that we need it. It
was ported from JRediSearch because it was there.

* Simplified this with a default value.

* Removed calls to `ToString` in order to allow the library to worry about
proper formatting.

* Combined constructors here to keep things a bit simpler.

* Cleaned up the unused import there.

* Readded the `OptimizeIndex` and `OptimizeIndexAsync` methods here to
preserve backwards compatibility.

* Returning Array.Empty<Document> instead of a new empty array each time.

* Sealed the suggestion class as there should be no reason (currently) for
anything to inherit from it.

Cleaned up how we're ensuring that an object to compare is not null and
is of an appropriate type.

Fixed equality check so that it doesn't blow up on null values (payload
specifically).

* Converted the class `With` to be an enum.

* Now looking up these values on demand.

* Reintroduced the original GetInfo API methods and changed the new ones
to GetInfoParsed and GetInfoParsedAsync.

* Revertered changes that turned ConfiguredIndexOptions into IndexOptions.

Added in `SetNoStopwords` method to match the JRediSearch api as well as
provide a conveinient means for keeping the default index options AND
specifying that no stopwords be considered.

Fixed `TestStopwords` unit test by specifying `STOPWORDS 0` by calling
`SetNoStopwords` which adds the `DisableStopWords` option to the
configured index options.

* Since this optimization doesn't exist anymore it should be removed from
your index definitions.

* Added back the original get suggestions method, but this time it calls
the new version which leverages the suggestion builder.

Added a small covering test.

* Consolidated the constructors here as suggested by Marc.
Co-authored-by: 's avatarNick Craver <nrcraver@gmail.com>
Co-authored-by: 's avatarMarc Gravell <marc.gravell@gmail.com>
parent ef5af00d
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Collections.Generic;
using System.Linq;
using NRediSearch.Aggregation.Reducers;
namespace NRediSearch.Aggregation
{
public sealed class AggregationBuilder
{
private readonly List<object> _args = new List<object>();
public bool IsWithCursor { get; private set; }
internal string GetArgsString() => string.Join(" ", _args);
public AggregationBuilder(string query = "*") => _args.Add(query);
public AggregationBuilder Load(params string[] fields)
{
AddCommandArguments(_args, "LOAD", fields);
return this;
}
public AggregationBuilder Limit(int offset, int count)
{
var limit = new Limit(offset, count);
limit.SerializeRedisArgs(_args);
return this;
}
public AggregationBuilder Limit(int count) => Limit(0, count);
public AggregationBuilder SortBy(params SortedField[] fields)
{
_args.Add("SORTBY");
_args.Add(fields.Length * 2);
foreach (var field in fields)
{
_args.Add(field.Field);
_args.Add(field.OrderAsArg());
}
return this;
}
public AggregationBuilder SortBy(int max, params SortedField[] fields)
{
SortBy(fields);
if (max > 0)
{
_args.Add("MAX");
_args.Add(max);
}
return this;
}
public AggregationBuilder SortByAscending(string field) => SortBy(SortedField.Ascending(field));
public AggregationBuilder SortByDescending(string field) => SortBy(SortedField.Descending(field));
public AggregationBuilder Apply(string projection, string alias)
{
_args.Add("APPLY");
_args.Add(projection);
_args.Add("AS");
_args.Add(alias);
return this;
}
public AggregationBuilder GroupBy(IReadOnlyCollection<string> fields, IReadOnlyCollection<Reducer> reducers)
{
var group = new Group(fields.ToArray());
foreach (var r in reducers)
{
group.Reduce(r);
}
GroupBy(group);
return this;
}
public AggregationBuilder GroupBy(string field, params Reducer[] reducers) => GroupBy(new[] { field }, reducers);
public AggregationBuilder GroupBy(Group group)
{
_args.Add("GROUPBY");
group.SerializeRedisArgs(_args);
return this;
}
public AggregationBuilder Filter(string expression)
{
_args.Add("FILTER");
_args.Add(expression);
return this;
}
public AggregationBuilder Cursor(int count, long maxIdle)
{
IsWithCursor = true;
if (count > 0)
{
_args.Add("WITHCURSOR");
_args.Add("COUNT");
_args.Add(count);
if (maxIdle < long.MaxValue && maxIdle >= 0)
{
_args.Add("MAXIDLE");
_args.Add(maxIdle);
}
}
return this;
}
internal void SerializeRedisArgs(List<object> args)
{
foreach (var arg in _args)
{
args.Add(arg);
}
}
private static void AddCommandLength(List<object> list, string command, int length)
{
list.Add(command);
list.Add(length);
}
private static void AddCommandArguments(List<object> destination, string command, IReadOnlyCollection<object> source)
{
AddCommandLength(destination, command, source.Count);
destination.AddRange(source);
}
}
}
......@@ -9,7 +9,8 @@ namespace NRediSearch
public sealed class AggregationResult
{
private readonly Dictionary<string, RedisValue>[] _results;
internal AggregationResult(RedisResult result)
internal AggregationResult(RedisResult result, long cursorId = -1)
{
var arr = (RedisResult[])result;
......@@ -27,6 +28,8 @@ internal AggregationResult(RedisResult result)
}
_results[i - 1] = cur;
}
CursorId = cursorId;
}
public IReadOnlyList<Dictionary<string, RedisValue>> GetResults() => _results;
......@@ -38,5 +41,7 @@ internal AggregationResult(RedisResult result)
if (index >= _results.Length) return null;
return new Row(_results[index]);
}
public long CursorId { get; }
}
}
This diff is collapsed.
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System;
using System.Collections.Generic;
using StackExchange.Redis;
......@@ -18,7 +17,7 @@ public class Document
public Document(string id, double score, byte[] payload) : this(id, null, score, payload) { }
public Document(string id) : this(id, null, 1.0, null) { }
public Document(string id, Dictionary<string, RedisValue> fields, double score) : this(id, fields, score, null) { }
public Document(string id, Dictionary<string, RedisValue> fields, double score = 1.0) : this(id, fields, score, null) { }
public Document(string id, Dictionary<string, RedisValue> fields, double score, byte[] payload)
{
......
......@@ -3,20 +3,11 @@
using System;
using System.Globalization;
using StackExchange.Redis;
using static NRediSearch.Client;
namespace NRediSearch
{
public static class Extensions
{
/// <summary>
/// Set a custom stopword list
/// </summary>
/// <param name="options">The <see cref="IndexOptions"/> to set stopwords on.</param>
/// <param name="stopwords">The stopwords to set.</param>
public static ConfiguredIndexOptions SetStopwords(this IndexOptions options, params string[] stopwords)
=> new ConfiguredIndexOptions(options).SetStopwords(stopwords);
internal static string AsRedisString(this double value, bool forceDecimal = false)
{
if (double.IsNegativeInfinity(value))
......
using System.Collections.Generic;
using StackExchange.Redis;
namespace NRediSearch
{
public class InfoResult
{
private readonly Dictionary<string, RedisResult> _all = new Dictionary<string, RedisResult>();
public string IndexName => GetString("index_name");
public Dictionary<string, RedisResult[]> Fields => GetRedisResultsDictionary("fields");
public long NumDocs => GetLong("num_docs");
public long NumTerms => GetLong("num_terms");
public long NumRecords => GetLong("num_records");
public double InvertedSzMebibytes => GetDouble("inverted_sz_mb");
public double InvertedCapMebibytes => GetDouble("inverted_cap_mb");
public double InvertedCapOvh => GetDouble("inverted_cap_ovh");
public double OffsetVectorsSzMebibytes => GetDouble("offset_vectors_sz_mb");
public double SkipIndexSizeMebibytes => GetDouble("skip_index_size_mb");
public double ScoreIndexSizeMebibytes => GetDouble("score_index_size_mb");
public double RecordsPerDocAvg => GetDouble("records_per_doc_avg");
public double BytesPerRecordAvg => GetDouble("bytes_per_record_avg");
public double OffsetsPerTermAvg => GetDouble("offsets_per_term_avg");
public double OffsetBitsPerRecordAvg => GetDouble("offset_bits_per_record_avg");
public string MaxDocId => GetString("max_doc_id");
public double DocTableSizeMebibytes => GetDouble("doc_table_size_mb");
public double SortableValueSizeMebibytes => GetDouble("sortable_value_size_mb");
public double KeyTableSizeMebibytes => GetDouble("key_table_size_mb");
public Dictionary<string, RedisResult> GcStats => GetRedisResultDictionary("gc_stats");
public Dictionary<string, RedisResult> CursorStats => GetRedisResultDictionary("cursor_stats");
public InfoResult(RedisResult result)
{
var results = (RedisResult[])result;
for (var i = 0; i < results.Length; i += 2)
{
var key = (string)results[i];
var value = results[i + 1];
_all.Add(key, value);
}
}
private string GetString(string key) => _all.TryGetValue(key, out var value) ? (string)value : default;
private long GetLong(string key) => _all.TryGetValue(key, out var value) ? (long)value : default;
private double GetDouble(string key)
{
if (_all.TryGetValue(key, out var value))
{
if ((string)value == "-nan")
{
return default;
}
else
{
return (double)value;
}
}
else
{
return default;
}
}
private Dictionary<string, RedisResult> GetRedisResultDictionary(string key)
{
if (_all.TryGetValue(key, out var value))
{
var values = (RedisResult[])value;
var result = new Dictionary<string, RedisResult>();
for (var ii = 0; ii < values.Length; ii += 2)
{
result.Add((string)values[ii], values[ii + 1]);
}
return result;
}
else
{
return default;
}
}
private Dictionary<string, RedisResult[]> GetRedisResultsDictionary(string key)
{
if (_all.TryGetValue(key, out var value))
{
var result = new Dictionary<string, RedisResult[]>();
foreach (RedisResult[] fv in (RedisResult[])value)
{
result.Add((string)fv[0], fv);
}
return result;
}
else
{
return default;
}
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System;
using System.Collections.Generic;
using System.Globalization;
using StackExchange.Redis;
......@@ -301,6 +300,28 @@ internal void SerializeRedisArgs(List<object> args)
args.Add(_summarizeSeparator);
}
}
if (_keys != null && _keys.Length > 0)
{
args.Add("INKEYS".Literal());
args.Add(_keys.Length.Boxed());
foreach (var key in _keys)
{
args.Add(key);
}
}
if (_returnFields != null && _returnFields.Length > 0)
{
args.Add("RETURN".Literal());
args.Add(_returnFields.Length.Boxed());
foreach (var returnField in _returnFields)
{
args.Add(returnField);
}
}
}
/// <summary>
......@@ -340,7 +361,7 @@ public Query LimitFields(params string[] fields)
/// <summary>
/// Limit the query to results that are limited to a specific set of keys
/// </summary>
/// <param name="fields">fields a list of TEXT fields in the schemas</param>
/// <param name="keys">a list of the TEXT fields in the schemas</param>
/// <returns>the query object itself</returns>
public Query LimitKeys(params string[] keys)
{
......
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System;
using StackExchange.Redis;
namespace NRediSearch
{
public sealed class Suggestion
{
public string String { get; }
public double Score { get; }
public string Payload { get; }
private Suggestion(SuggestionBuilder builder)
{
String = builder._string;
Score = builder._score;
Payload = builder._payload;
}
public override bool Equals(object obj)
{
if (this == obj)
{
return true;
}
if(!(obj is Suggestion that))
{
return false;
}
return Score == that.Score && String == that.String && Payload == that.Payload;
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 31 + String.GetHashCode();
hash = hash * 31 + Score.GetHashCode();
hash = hash * 31 + Payload.GetHashCode();
return hash;
}
}
public override string ToString() =>
$"Suggestion{{string='{String}', score={Score}, payload='{Payload}'}}";
public SuggestionBuilder ToBuilder() => new SuggestionBuilder(this);
public static SuggestionBuilder Builder => new SuggestionBuilder();
public sealed class SuggestionBuilder
{
internal string _string;
internal double _score = 1.0;
internal string _payload;
public SuggestionBuilder() { }
public SuggestionBuilder(Suggestion suggestion)
{
_string = suggestion.String;
_score = suggestion.Score;
_payload = suggestion.Payload;
}
public SuggestionBuilder String(string @string)
{
_string = @string;
return this;
}
public SuggestionBuilder Score(double score)
{
_score = score;
return this;
}
public SuggestionBuilder Payload(string payload)
{
_payload = payload;
return this;
}
public Suggestion Build()
{
bool isStringMissing = _string == null;
bool isScoreOutOfRange = (_score < 0.0 || _score > 1.0);
if (isStringMissing || isScoreOutOfRange)
{
throw new RedisCommandException($"Missing required fields: {(isStringMissing ? "string" : string.Empty)} {(isScoreOutOfRange ? "score not within range" : string.Empty)}");
}
return new Suggestion(this);
}
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System;
namespace NRediSearch
{
public class SuggestionOptions
{
private readonly object WITHPAYLOADS_FLAG = "WITHPAYLOADS".Literal();
private readonly object WITHSCORES_FLAG = "WITHSCORES".Literal();
public SuggestionOptions(SuggestionOptionsBuilder builder)
{
With = builder._with;
Fuzzy = builder._fuzzy;
Max = builder._max;
}
public static SuggestionOptionsBuilder Builder => new SuggestionOptionsBuilder();
public WithOptions With { get; }
public bool Fuzzy { get; }
public int Max { get; } = 5;
public object[] GetFlags()
{
if (HasOption(WithOptions.PayloadsAndScores))
{
return new[] { WITHPAYLOADS_FLAG, WITHSCORES_FLAG };
}
if (HasOption(WithOptions.Payloads))
{
return new[] { WITHPAYLOADS_FLAG };
}
if (HasOption(WithOptions.Scores))
{
return new[] { WITHSCORES_FLAG };
}
return default;
}
public SuggestionOptionsBuilder ToBuilder() => new SuggestionOptionsBuilder(this);
internal bool GetIsPayloadAndScores() => HasOption(WithOptions.PayloadsAndScores);
internal bool GetIsPayload() => HasOption(WithOptions.Payloads);
internal bool GetIsScores() => HasOption(WithOptions.Scores);
[Flags]
public enum WithOptions
{
None = 0,
Payloads = 1,
Scores = 2,
PayloadsAndScores = Payloads | Scores
}
internal bool HasOption(WithOptions option) => (With & option) != 0;
public sealed class SuggestionOptionsBuilder
{
internal WithOptions _with;
internal bool _fuzzy;
internal int _max = 5;
public SuggestionOptionsBuilder() { }
public SuggestionOptionsBuilder(SuggestionOptions options)
{
_with = options.With;
_fuzzy = options.Fuzzy;
_max = options.Max;
}
public SuggestionOptionsBuilder Fuzzy()
{
_fuzzy = true;
return this;
}
public SuggestionOptionsBuilder Max(int max)
{
_max = max;
return this;
}
public SuggestionOptionsBuilder With(WithOptions with)
{
_with = with;
return this;
}
public SuggestionOptions Build()
{
return new SuggestionOptions(this);
}
}
}
}
using System.Threading;
using NRediSearch.Aggregation;
using NRediSearch.Aggregation.Reducers;
using StackExchange.Redis;
using Xunit;
using Xunit.Abstractions;
using static NRediSearch.Client;
namespace NRediSearch.Test.ClientTests
{
public class AggregationBuilderTests : RediSearchTestBase
{
public AggregationBuilderTests(ITestOutputHelper output) : base(output)
{
}
[Fact]
public void TestAggregations()
{
/**
127.0.0.1:6379> FT.CREATE test_index SCHEMA name TEXT SORTABLE count NUMERIC SORTABLE
OK
127.0.0.1:6379> FT.ADD test_index data1 1.0 FIELDS name abc count 10
OK
127.0.0.1:6379> FT.ADD test_index data2 1.0 FIELDS name def count 5
OK
127.0.0.1:6379> FT.ADD test_index data3 1.0 FIELDS name def count 25
*/
Client cl = GetClient();
Schema sc = new Schema();
sc.AddSortableTextField("name", 1.0);
sc.AddSortableNumericField("count");
cl.CreateIndex(sc, new ConfiguredIndexOptions());
cl.AddDocument(new Document("data1").Set("name", "abc").Set("count", 10));
cl.AddDocument(new Document("data2").Set("name", "def").Set("count", 5));
cl.AddDocument(new Document("data3").Set("name", "def").Set("count", 25));
AggregationBuilder r = new AggregationBuilder()
.GroupBy("@name", Reducers.Sum("@count").As("sum"))
.SortBy(10, SortedField.Descending("@sum"));
// actual search
AggregationResult res = cl.Aggregate(r);
Row? r1 = res.GetRow(0);
Assert.NotNull(r1);
Assert.Equal("def", r1.Value.GetString("name"));
Assert.Equal(30, r1.Value.GetInt64("sum"));
Assert.Equal(30.0, r1.Value.GetDouble("sum"));
Assert.Equal(0L, r1.Value.GetInt64("nosuchcol"));
Assert.Equal(0.0, r1.Value.GetDouble("nosuchcol"));
Assert.Null(r1.Value.GetString("nosuchcol"));
Row? r2 = res.GetRow(1);
Assert.NotNull(r2);
Assert.Equal("abc", r2.Value.GetString("name"));
Assert.Equal(10L, r2.Value.GetInt64("sum"));
}
[Fact]
public void TestApplyAndFilterAggregations()
{
/**
127.0.0.1:6379> FT.CREATE test_index SCHEMA name TEXT SORTABLE subj1 NUMERIC SORTABLE subj2 NUMERIC SORTABLE
OK
127.0.0.1:6379> FT.ADD test_index data1 1.0 FIELDS name abc subj1 20 subj2 70
OK
127.0.0.1:6379> FT.ADD test_index data2 1.0 FIELDS name def subj1 60 subj2 40
OK
127.0.0.1:6379> FT.ADD test_index data3 1.0 FIELDS name ghi subj1 50 subj2 80
OK
127.0.0.1:6379> FT.ADD test_index data1 1.0 FIELDS name abc subj1 30 subj2 20
OK
127.0.0.1:6379> FT.ADD test_index data2 1.0 FIELDS name def subj1 65 subj2 45
OK
127.0.0.1:6379> FT.ADD test_index data3 1.0 FIELDS name ghi subj1 70 subj2 70
OK
*/
Client cl = GetClient();
Schema sc = new Schema();
sc.AddSortableTextField("name", 1.0);
sc.AddSortableNumericField("subj1");
sc.AddSortableNumericField("subj2");
cl.CreateIndex(sc, new ConfiguredIndexOptions());
cl.AddDocument(new Document("data1").Set("name", "abc").Set("subj1", 20).Set("subj2", 70));
cl.AddDocument(new Document("data2").Set("name", "def").Set("subj1", 60).Set("subj2", 40));
cl.AddDocument(new Document("data3").Set("name", "ghi").Set("subj1", 50).Set("subj2", 80));
cl.AddDocument(new Document("data4").Set("name", "abc").Set("subj1", 30).Set("subj2", 20));
cl.AddDocument(new Document("data5").Set("name", "def").Set("subj1", 65).Set("subj2", 45));
cl.AddDocument(new Document("data6").Set("name", "ghi").Set("subj1", 70).Set("subj2", 70));
AggregationBuilder r = new AggregationBuilder().Apply("(@subj1+@subj2)/2", "attemptavg")
.GroupBy("@name", Reducers.Avg("@attemptavg").As("avgscore"))
.Filter("@avgscore>=50")
.SortBy(10, SortedField.Ascending("@name"));
// actual search
AggregationResult res = cl.Aggregate(r);
Row? r1 = res.GetRow(0);
Assert.NotNull(r1);
Assert.Equal("def", r1.Value.GetString("name"));
Assert.Equal(52.5, r1.Value.GetDouble("avgscore"));
Row? r2 = res.GetRow(1);
Assert.NotNull(r2);
Assert.Equal("ghi", r2.Value.GetString("name"));
Assert.Equal(67.5, r2.Value.GetDouble("avgscore"));
}
[Fact]
public void TestCursor()
{
/**
127.0.0.1:6379> FT.CREATE test_index SCHEMA name TEXT SORTABLE count NUMERIC SORTABLE
OK
127.0.0.1:6379> FT.ADD test_index data1 1.0 FIELDS name abc count 10
OK
127.0.0.1:6379> FT.ADD test_index data2 1.0 FIELDS name def count 5
OK
127.0.0.1:6379> FT.ADD test_index data3 1.0 FIELDS name def count 25
*/
Client cl = GetClient();
Schema sc = new Schema();
sc.AddSortableTextField("name", 1.0);
sc.AddSortableNumericField("count");
cl.CreateIndex(sc, new ConfiguredIndexOptions());
cl.AddDocument(new Document("data1").Set("name", "abc").Set("count", 10));
cl.AddDocument(new Document("data2").Set("name", "def").Set("count", 5));
cl.AddDocument(new Document("data3").Set("name", "def").Set("count", 25));
AggregationBuilder r = new AggregationBuilder()
.GroupBy("@name", Reducers.Sum("@count").As("sum"))
.SortBy(10, SortedField.Descending("@sum"))
.Cursor(1, 3000);
// actual search
AggregationResult res = cl.Aggregate(r);
Row? row = res.GetRow(0);
Assert.NotNull(row);
Assert.Equal("def", row.Value.GetString("name"));
Assert.Equal(30, row.Value.GetInt64("sum"));
Assert.Equal(30.0, row.Value.GetDouble("sum"));
Assert.Equal(0L, row.Value.GetInt64("nosuchcol"));
Assert.Equal(0.0, row.Value.GetDouble("nosuchcol"));
Assert.Null(row.Value.GetString("nosuchcol"));
res = cl.CursorRead(res.CursorId, 1);
Row? row2 = res.GetRow(0);
Assert.NotNull(row2);
Assert.Equal("abc", row2.Value.GetString("name"));
Assert.Equal(10, row2.Value.GetInt64("sum"));
Assert.True(cl.CursorDelete(res.CursorId));
try
{
cl.CursorRead(res.CursorId, 1);
Assert.True(false);
}
catch (RedisException) { }
AggregationBuilder r2 = new AggregationBuilder()
.GroupBy("@name", Reducers.Sum("@count").As("sum"))
.SortBy(10, SortedField.Descending("@sum"))
.Cursor(1, 1000);
Thread.Sleep(1000);
try
{
cl.CursorRead(res.CursorId, 1);
Assert.True(false);
}
catch (RedisException) { }
}
}
}
using NRediSearch.Aggregation;
using System;
using NRediSearch.Aggregation;
using NRediSearch.Aggregation.Reducers;
using Xunit;
using Xunit.Abstractions;
using static NRediSearch.Client;
namespace NRediSearch.Test.ClientTests
{
......@@ -10,6 +12,7 @@ public class AggregationTest : RediSearchTestBase
public AggregationTest(ITestOutputHelper output) : base(output) { }
[Fact]
[Obsolete]
public void TestAggregations()
{
/**
......@@ -26,7 +29,7 @@ public void TestAggregations()
Schema sc = new Schema();
sc.AddSortableTextField("name", 1.0);
sc.AddSortableNumericField("count");
cl.CreateIndex(sc, Client.IndexOptions.Default);
cl.CreateIndex(sc, new ConfiguredIndexOptions());
cl.AddDocument(new Document("data1").Set("name", "abc").Set("count", 10));
cl.AddDocument(new Document("data2").Set("name", "def").Set("count", 5));
cl.AddDocument(new Document("data3").Set("name", "def").Set("count", 25));
......
......@@ -4,6 +4,7 @@
using StackExchange.Redis;
using Xunit;
using Xunit.Abstractions;
using static NRediSearch.Client;
namespace NRediSearch.Test
{
......@@ -27,7 +28,7 @@ public void BasicUsage()
bool result = false;
try
{
result = client.CreateIndex(sc, Client.IndexOptions.Default);
result = client.CreateIndex(sc, new ConfiguredIndexOptions());
}
catch (RedisServerException ex)
{
......
using NRediSearch.Aggregation;
using static NRediSearch.QueryBuilder.QueryBuilder;
using static NRediSearch.QueryBuilder.Values;
using static NRediSearch.Aggregation.Reducers.Reducers;
using static NRediSearch.Aggregation.SortedField;
using Xunit;
using Xunit.Abstractions;
using System;
using System.Collections.Generic;
using NRediSearch.Aggregation;
using NRediSearch.QueryBuilder;
using System;
using StackExchange.Redis;
using System.Collections.Generic;
using Xunit;
using Xunit.Abstractions;
using static NRediSearch.Aggregation.Reducers.Reducers;
using static NRediSearch.Aggregation.SortedField;
using static NRediSearch.QueryBuilder.QueryBuilder;
using static NRediSearch.QueryBuilder.Values;
namespace NRediSearch.Test.QueryBuilder
{
......@@ -88,7 +88,7 @@ public void TestAggregation()
{
Assert.Equal("*", GetArgsString(new AggregationRequest()));
AggregationRequest r = new AggregationRequest().
GroupBy("@actor", Count().As ("cnt")).
GroupBy("@actor", Count().As("cnt")).
SortBy(Descending("@cnt"));
Assert.Equal("* GROUPBY 1 @actor REDUCE COUNT 0 AS cnt SORTBY 2 @cnt DESC", GetArgsString(r));
......@@ -103,5 +103,38 @@ public void TestAggregation()
Assert.Equal("* GROUPBY 1 @brand REDUCE QUANTILE 2 @price 0.5 AS q50 REDUCE QUANTILE 2 @price 0.9 AS q90 REDUCE QUANTILE 2 @price 0.95 AS q95 REDUCE AVG 1 @price REDUCE COUNT 0 AS count LIMIT 0 10 SORTBY 2 @count DESC",
GetArgsString(r));
}
[Fact]
public void TestAggregationBuilder()
{
Assert.Equal("*", new AggregationBuilder().GetArgsString());
AggregationBuilder r1 = new AggregationBuilder()
.GroupBy("@actor", Count().As("cnt"))
.SortBy(Descending("@cnt"));
Assert.Equal("* GROUPBY 1 @actor REDUCE COUNT 0 AS cnt SORTBY 2 @cnt DESC", r1.GetArgsString());
Group group = new Group("@brand")
.Reduce(Quantile("@price", 0.50).As("q50"))
.Reduce(Quantile("@price", 0.90).As("q90"))
.Reduce(Quantile("@price", 0.95).As("q95"))
.Reduce(Avg("@price"))
.Reduce(Count().As("count"));
AggregationBuilder r2 = new AggregationBuilder()
.GroupBy(group)
.Limit(10)
.SortByDescending("@count");
Assert.Equal("* GROUPBY 1 @brand REDUCE QUANTILE 2 @price 0.5 AS q50 REDUCE QUANTILE 2 @price 0.9 AS q90 REDUCE QUANTILE 2 @price 0.95 AS q95 REDUCE AVG 1 @price REDUCE COUNT 0 AS count LIMIT 0 10 SORTBY 2 @count DESC",
r2.GetArgsString());
AggregationBuilder r3 = new AggregationBuilder()
.Load("@count")
.Apply("@count%1000", "thousands")
.SortBy(Descending("@count"))
.Limit(0, 2);
Assert.Equal("* LOAD 1 @count APPLY @count%1000 AS thousands SORTBY 2 @count DESC LIMIT 0 2", r3.GetArgsString());
}
}
}
......@@ -112,6 +112,16 @@ public void LimitFields()
Assert.Equal(2, query._fields.Length);
}
[Fact]
public void ReturnFields()
{
var query = GetQuery();
Assert.Null(query._returnFields);
Assert.Same(query, query.ReturnFields("foo", "bar"));
Assert.Equal(2, query._returnFields.Length);
}
[Fact]
public void HighlightFields()
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment