Commit 3cade0cc authored by Marc Gravell's avatar Marc Gravell

Redisearch aggregate (#888)

* start work on nreisearch aggregates

* port chunks of JRediSearch

* port the rest of aggregates and the highlight/summarize API

* Port QueryTest

* Import all of ClientTest from JRediSearch

* Add aggregate tests; normlaize tests

* NRediSearch : all tests passing

* add source prefix to a bunch of files

# Conflicts:
#	NRediSearch.Test/ExampleUsage.cs
#	NRediSearch/NRediSearch.csproj
parent efbc2dc8
using NRediSearch.Aggregation;
using NRediSearch.Aggregation.Reducers;
using Xunit;
using Xunit.Abstractions;
namespace NRediSearch.Test.ClientTests
{
public class AggregationTest : RediSearchTestBase
{
public AggregationTest(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, Client.IndexOptions.Default);
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));
AggregationRequest r = new AggregationRequest()
.GroupBy("@name", Reducers.Sum("@count").As("sum"))
.SortBy(SortedField.Descending("@sum"), 10);
// actual search
AggregationResult res = cl.Aggregate(r);
var r1 = res.GetRow(0);
Assert.NotNull(r1);
Assert.Equal("def", r1.Value.GetString("name"));
Assert.Equal(30, r1.Value.GetInt64("sum"));
var r2 = res.GetRow(1);
Assert.NotNull(r2);
Assert.Equal("abc", r2.Value.GetString("name"));
Assert.Equal(10, r2.Value.GetInt64("sum"));
}
}
}
This diff is collapsed.
using System;
using Xunit;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using StackExchange.Redis;
using Xunit;
using Xunit.Abstractions;
namespace NRediSearch.Test
{
public class ExampleUsage : IDisposable
public class ExampleUsage : RediSearchTestBase
{
private ConnectionMultiplexer conn;
private IDatabase db;
public ExampleUsage()
{
conn = ConnectionMultiplexer.Connect("127.0.0.1:6379");
db = conn.GetDatabase();
}
public void Dispose()
{
conn?.Dispose();
conn = null;
db = null;
}
public ExampleUsage(ITestOutputHelper output) : base(output) { }
[Fact]
public void BasicUsage()
{
var client = new Client("testung", db);
var client = GetClient();
try { client.DropIndex(); } catch { } // reset DB
......
......@@ -3,6 +3,7 @@
<OutputType>Library</OutputType>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StackExchange.Redis\StackExchange.Redis.csproj" />
......
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 NRediSearch.QueryBuilder;
using System;
using StackExchange.Redis;
using System.Collections.Generic;
namespace NRediSearch.Test.QueryBuilder
{
public class BuilderTest : RediSearchTestBase
{
public BuilderTest(ITestOutputHelper output) : base(output) { }
[Fact]
public void testTag()
{
Value v = Tags("foo");
Assert.Equal("{foo}", v.ToString());
v = Tags("foo", "bar");
Assert.Equal("{foo | bar}", v.ToString());
}
[Fact]
public void testEmptyTag()
{
Assert.Throws<ArgumentException>(() =>
{
Tags();
});
}
[Fact]
public void testRange()
{
Value v = Between(1, 10);
Assert.Equal("[1.0 10.0]", v.ToString());
v = Between(1, 10).InclusiveMax(false);
Assert.Equal("[1.0 (10.0]", v.ToString());
v = Between(1, 10).InclusiveMin(false);
Assert.Equal("[(1.0 10.0]", v.ToString());
// le, gt, etc.
Assert.Equal("[42.0 42.0]", Equal(42).ToString());
Assert.Equal("[-inf (42.0]", LessThan(42).ToString());
Assert.Equal("[-inf 42.0]", LessThanOrEqual(42).ToString());
Assert.Equal("[(42.0 inf]", GreaterThan(42).ToString());
Assert.Equal("[42.0 inf]", GreaterThanOrEqual(42).ToString());
// string value
Assert.Equal("s", Value("s").ToString());
// Geo value
Assert.Equal("[1.0 2.0 3.0 km]",
new GeoValue(1.0, 2.0, 3.0, GeoUnit.Kilometers).ToString());
}
[Fact]
public void testIntersectionBasic()
{
INode n = Intersect().Add("name", "mark");
Assert.Equal("@name:mark", n.ToString());
n = Intersect().Add("name", "mark", "dvir");
Assert.Equal("@name:(mark dvir)", n.ToString());
}
[Fact]
public void testIntersectionNested()
{
INode n = Intersect().
Add(Union("name", Value("mark"), Value("dvir"))).
Add("time", Between(100, 200)).
Add(Disjunct("created", LessThan(1000)));
Assert.Equal("(@name:(mark|dvir) @time:[100.0 200.0] -@created:[-inf (1000.0])", n.ToString());
}
static string GetArgsString(AggregationRequest request)
{
var args = new List<object>();
request.SerializeRedisArgs(args);
return string.Join(" ", args);
}
[Fact]
public void testAggregation()
{
Assert.Equal("*", GetArgsString(new AggregationRequest()));
AggregationRequest r = new AggregationRequest().
GroupBy("@actor", Count().As ("cnt")).
SortBy(Descending("@cnt"));
Assert.Equal("* GROUPBY 1 @actor REDUCE COUNT 0 AS cnt SORTBY 2 @cnt DESC", GetArgsString(r));
r = new AggregationRequest().GroupBy("@brand",
Quantile("@price", 0.50).As("q50"),
Quantile("@price", 0.90).As("q90"),
Quantile("@price", 0.95).As("q95"),
Avg("@price"),
Count().As("count")).
SortByDescending("@count").
Limit(10);
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));
}
}
}
using System.Collections.Generic;
using Xunit;
namespace NRediSearch.Test
{
public class QueryTest
{
public static Query GetQuery() => new Query("hello world");
[Fact]
public void getNoContent()
{
var query = GetQuery();
Assert.False(query.NoContent);
Assert.Same(query, query.SetNoContent());
Assert.True(query.NoContent);
}
[Fact]
public void getWithScores()
{
var query = GetQuery();
Assert.False(query.WithScores);
Assert.Same(query, query.SetWithScores());
Assert.True(query.WithScores);
}
[Fact]
public void serializeRedisArgs()
{
var query = new Query("hello world")
{
NoContent = true,
Language = "",
NoStopwords = true,
Verbatim = true,
WithPayloads = true,
WithScores = true
};
var args = new List<object>();
query.SerializeRedisArgs(args);
Assert.Equal(8, args.Count);
Assert.Equal(query.QueryString, (string)args[0]);
Assert.Contains("NOCONTENT".Literal(), args);
Assert.Contains("NOSTOPWORDS".Literal(), args);
Assert.Contains("VERBATIM".Literal(), args);
Assert.Contains("WITHPAYLOADS".Literal(), args);
Assert.Contains("WITHSCORES".Literal(), args);
Assert.Contains("LANGUAGE".Literal(), args);
Assert.Contains("", args);
var languageIndex = args.IndexOf("LANGUAGE".Literal());
Assert.Equal("", args[languageIndex + 1]);
}
[Fact]
public void limit()
{
var query = GetQuery();
Assert.Equal(0, query._paging.Offset);
Assert.Equal(10, query._paging.Count);
Assert.Same(query, query.Limit(1, 30));
Assert.Equal(1, query._paging.Offset);
Assert.Equal(30, query._paging.Count);
}
[Fact]
public void addFilter()
{
var query = GetQuery();
Assert.Empty(query._filters);
Query.NumericFilter f = new Query.NumericFilter("foo", 0, 100);
Assert.Same(query, query.AddFilter(f));
Assert.Same(f, query._filters[0]);
}
[Fact]
public void setVerbatim()
{
var query = GetQuery();
Assert.False(query.Verbatim);
Assert.Same(query, query.SetVerbatim());
Assert.True(query.Verbatim);
}
[Fact]
public void setNoStopwords()
{
var query = GetQuery();
Assert.False(query.NoStopwords);
Assert.Same(query, query.SetNoStopwords());
Assert.True(query.NoStopwords);
}
[Fact]
public void setLanguage()
{
var query = GetQuery();
Assert.Null(query.Language);
Assert.Same(query, query.SetLanguage("chinese"));
Assert.Equal("chinese", query.Language);
}
[Fact]
public void limitFields()
{
var query = GetQuery();
Assert.Null(query._fields);
Assert.Same(query, query.LimitFields("foo", "bar"));
Assert.Equal(2, query._fields.Length);
}
[Fact]
public void highlightFields()
{
var query = GetQuery();
Assert.False(query._wantsHighlight);
Assert.Null(query._highlightFields);
query = new Query("Hello");
Assert.Same(query, query.HighlightFields("foo", "bar"));
Assert.Equal(2, query._highlightFields.Length);
Assert.Null(query._highlightTags);
Assert.True(query._wantsHighlight);
query = new Query("Hello").HighlightFields();
Assert.Null(query._highlightFields);
Assert.Null(query._highlightTags);
Assert.True(query._wantsHighlight);
Assert.Same(query, query.HighlightFields(new Query.HighlightTags("<b>", "</b>")));
Assert.Null(query._highlightFields);
Assert.NotNull(query._highlightTags);
Assert.Equal("<b>", query._highlightTags.Value.Open);
Assert.Equal("</b>", query._highlightTags.Value.Close);
}
[Fact]
public void summarizeFields()
{
var query = GetQuery();
Assert.False(query._wantsSummarize);
Assert.Null(query._summarizeFields);
query = new Query("Hello");
Assert.Equal(query, query.SummarizeFields());
Assert.True(query._wantsSummarize);
Assert.Null(query._summarizeFields);
Assert.Equal(-1, query._summarizeFragmentLen);
Assert.Equal(-1, query._summarizeNumFragments);
query = new Query("Hello");
Assert.Equal(query, query.SummarizeFields("someField"));
Assert.True(query._wantsSummarize);
Assert.Single(query._summarizeFields);
Assert.Equal(-1, query._summarizeFragmentLen);
Assert.Equal(-1, query._summarizeNumFragments);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using StackExchange.Redis;
using Xunit.Abstractions;
namespace NRediSearch.Test
{
public abstract class RediSearchTestBase : IDisposable
{
protected readonly ITestOutputHelper Output;
public RediSearchTestBase(ITestOutputHelper output)
{
muxer = GetWithFT(output);
Output = output;
Db = muxer.GetDatabase();
}
private ConnectionMultiplexer muxer;
protected IDatabase Db { get; private set; }
public void Dispose()
{
muxer?.Dispose();
muxer = null;
Db = null;
}
protected Client GetClient([CallerMemberName] string caller = null)
=> Reset(new Client(GetType().Name + ":" + caller, Db));
protected static Client Reset(Client client)
{
try
{
client.DropIndex(); // tests create them
}
catch (RedisServerException ex)
{
if (ex.Message != "Unknown Index name") throw;
}
return client;
}
internal static ConnectionMultiplexer GetWithFT(ITestOutputHelper output)
{
const string ep = "127.0.0.1:6379";
var options = new ConfigurationOptions
{
EndPoints = { ep },
AllowAdmin = true
};
var conn = ConnectionMultiplexer.Connect(options);
var server = conn.GetServer(ep);
var arr = (RedisResult[])server.Execute("module", "list");
bool found = false;
foreach (var module in arr)
{
var parsed = Parse(module);
if (parsed.TryGetValue("name", out var val) && val == "ft")
{
found = true;
if (parsed.TryGetValue("ver", out val))
output?.WriteLine($"Version: {val}");
break;
}
}
if (!found)
{
output?.WriteLine("Module not found; attempting to load...");
var config = server.Info("server").SelectMany(_ => _).FirstOrDefault(x => x.Key == "config_file").Value;
if (!string.IsNullOrEmpty(config))
{
var i = config.LastIndexOf('/');
var modulePath = config.Substring(0, i + 1) + "redisearch.so";
var result = server.Execute("module", "load", modulePath);
output?.WriteLine((string)result);
}
}
return conn;
}
static Dictionary<string, RedisValue> Parse(RedisResult module)
{
var data = new Dictionary<string, RedisValue>();
var lines = (RedisResult[])module;
for (int i = 0; i < lines.Length;)
{
var key = (string)lines[i++];
var value = (RedisValue)lines[i++];
data[key] = value;
}
return data;
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
namespace NRediSearch
{
public sealed class AddOptions
{
public enum ReplacementPolicy
{
/// <summary>
/// The default mode. This will cause the add operation to fail if the document already exists
/// </summary>
None,
/// <summary>
/// Replace/reindex the entire document. This has the effect of atomically deleting the previous
/// document and replacing it with the context of the new document. Fields in the old document which
/// are not present in the new document are lost
/// </summary>
Full,
/// <summary>
/// Only reindex/replace fields that are updated in the command. Fields in the old document which are
/// not present in the new document are preserved.Fields that are present in both are overwritten by
/// the new document
/// </summary>
Partial,
}
public string Language { get; set; }
public bool NoSave { get; set; }
public ReplacementPolicy ReplacePolicy { get; set; }
/// <summary>
/// Create a new DocumentOptions object. Methods can later be chained via a builder-like pattern
/// </summary>
public AddOptions() { }
/// <summary>
/// Set the indexing language
/// </summary>
/// <param name="language">Set the indexing language</param>
public AddOptions SetLanguage(string language)
{
Language = language;
return this;
}
/// <summary>
/// Whether document's contents should not be stored in the database.
/// </summary>
/// <param name="enabled">if enabled, the document is <b>not</b> stored on the server. This saves disk/memory space on the
/// server but prevents retrieving the document itself.</param>
public AddOptions SetNoSave(bool enabled)
{
NoSave = enabled;
return this;
}
/// <summary>
/// Indicate the behavior for the existing document.
/// </summary>
/// <param name="mode">One of the replacement modes.</param>
public AddOptions SetReplacementPolicy(ReplacementPolicy mode)
{
ReplacePolicy = mode;
return this;
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System;
using System.Collections.Generic;
using NRediSearch.Aggregation.Reducers;
using StackExchange.Redis;
namespace NRediSearch.Aggregation
{
public class AggregationRequest
{
private readonly string _query;
private readonly List<string> _load = new List<string>();
private readonly List<Group> _groups = new List<Group>();
private readonly List<SortedField> _sortby = new List<SortedField>();
private readonly Dictionary<string, string> _projections = new Dictionary<string, string>();
private Limit _limit = new Limit(0, 0);
private int _sortByMax = 0;
public AggregationRequest(string query)
{
_query = query;
}
public AggregationRequest() : this("*") { }
public AggregationRequest Load(string field)
{
_load.Add(field);
return this;
}
public AggregationRequest Load(params string[] fields)
{
_load.AddRange(fields);
return this;
}
public AggregationRequest Limit(int offset, int count)
{
var limit = new Limit(offset, count);
if (_groups.Count == 0)
{
_limit = limit;
}
else
{
_groups[_groups.Count - 1].Limit(limit);
}
return this;
}
public AggregationRequest Limit(int count) => Limit(0, count);
public AggregationRequest SortBy(SortedField field)
{
_sortby.Add(field);
return this;
}
public AggregationRequest SortBy(params SortedField[] fields)
{
_sortby.AddRange(fields);
return this;
}
public AggregationRequest SortBy(IList<SortedField> fields, int max)
{
_sortby.AddRange(fields);
_sortByMax = max;
return this;
}
public AggregationRequest SortBy(SortedField field, int max)
{
_sortby.Add(field);
_sortByMax = max;
return this;
}
public AggregationRequest SortBy(string field, Order order) => SortBy(new SortedField(field, order));
public AggregationRequest SortByAscending(string field) => SortBy(field, Order.Ascending);
public AggregationRequest SortByDescending(string field) => SortBy(field, Order.Descending);
public AggregationRequest Apply(string projection, string alias)
{
_projections.Add(alias, projection);
return this;
}
public AggregationRequest GroupBy(IList<string> fields, IList<Reducer> reducers)
{
Group g = new Group(fields);
foreach (var r in reducers)
{
g.Reduce(r);
}
_groups.Add(g);
return this;
}
public AggregationRequest GroupBy(String field, params Reducer[] reducers)
{
return GroupBy(new string[] { field }, reducers);
}
public AggregationRequest GroupBy(Group group)
{
_groups.Add(group);
return this;
}
private static void AddCmdLen(List<object> list, string cmd, int len)
{
list.Add(cmd.Literal());
list.Add(len);
}
private static void AddCmdArgs<T>(List<object> dst, string cmd, IList<T> src)
{
AddCmdLen(dst, cmd, src.Count);
foreach (var obj in src)
dst.Add(obj);
}
internal void SerializeRedisArgs(List<object> args)
{
args.Add(_query);
if (_load.Count != 0)
{
AddCmdArgs(args, "LOAD", _load);
}
if (_groups.Count != 0)
{
foreach (var group in _groups)
{
args.Add("GROUPBY".Literal());
group.SerializeRedisArgs(args);
}
}
if (_projections.Count != 0)
{
args.Add("APPLY".Literal());
foreach (var e in _projections)
{
args.Add(e.Value);
args.Add("AS".Literal());
args.Add(e.Key);
}
}
if (_sortby.Count != 0)
{
args.Add("SORTBY".Literal());
args.Add((_sortby.Count * 2).Boxed());
foreach (var field in _sortby)
{
args.Add(field.Field);
args.Add(field.OrderAsArg());
}
if (_sortByMax > 0)
{
args.Add("MAX".Literal());
args.Add(_sortByMax.Boxed());
}
}
_limit.SerializeRedisArgs(args);
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Collections.Generic;
using NRediSearch.Aggregation.Reducers;
namespace NRediSearch.Aggregation
{
public sealed class Group
{
private readonly IList<Reducer> _reducers = new List<Reducer>();
private readonly IList<string> _fields;
private Limit _limit = new Limit(0, 0);
public Group(params string[] fields) => _fields = fields;
public Group(IList<string> fields) => _fields = fields;
internal Group Limit(Limit limit)
{
_limit = limit;
return this;
}
internal Group Reduce(Reducer r)
{
_reducers.Add(r);
return this;
}
internal void SerializeRedisArgs(List<object> args)
{
args.Add(_fields.Count.Boxed());
foreach (var field in _fields)
args.Add(field);
foreach (var r in _reducers)
{
args.Add("REDUCE".Literal());
args.Add(r.Name.Literal());
r.SerializeRedisArgs(args);
var alias = r.Alias;
if (!string.IsNullOrEmpty(alias))
{
args.Add("AS".Literal());
args.Add(alias);
}
}
_limit.SerializeRedisArgs(args);
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Collections.Generic;
namespace NRediSearch.Aggregation
{
internal readonly struct Limit
{
private readonly int _offset, _count;
public Limit(int offset, int count)
{
_offset = offset;
_count = count;
}
internal void SerializeRedisArgs(List<object> args)
{
if (_count == 0) return;
args.Add("LIMIT".Literal());
args.Add(_offset.Boxed());
args.Add(_count.Boxed());
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System;
using System.Collections.Generic;
namespace NRediSearch.Aggregation.Reducers
{
// This class is normally received via one of the subclasses or via Reducers
public abstract class Reducer
{
public override string ToString() => Name;
private string _field;
internal Reducer(string field) => _field = field;
/// <summary>
/// The name of the reducer
/// </summary>
public abstract string Name { get; }
public string Alias { get; set; }
public Reducer As(string alias)
{
Alias = alias;
return this;
}
public Reducer SetAliasAsField()
{
if (string.IsNullOrEmpty(_field)) throw new InvalidOperationException("Cannot set to field name since no field exists");
return As(_field);
}
protected virtual int GetOwnArgsCount() => _field == null ? 0 : 1;
protected virtual void AddOwnArgs(List<object> args)
{
if (_field != null) args.Add(_field);
}
internal void SerializeRedisArgs(List<object> args)
{
int count = GetOwnArgsCount();
args.Add(count.Boxed());
int before = args.Count;
AddOwnArgs(args);
int after = args.Count;
if (count != (after - before))
throw new InvalidOperationException($"Reducer '{ToString()}' incorrectly reported the arg-count as {count}, but added {after - before}");
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Collections.Generic;
namespace NRediSearch.Aggregation.Reducers
{
public static class Reducers
{
public static Reducer Count() => CountReducer.Instance;
sealed class CountReducer : Reducer
{
internal static readonly Reducer Instance = new CountReducer();
private CountReducer() : base(null) { }
public override string Name => "COUNT";
}
sealed class SingleFieldReducer : Reducer
{
private readonly string _name;
internal SingleFieldReducer(string name, string field) : base(field)
{
_name = name;
}
public override string Name => _name;
}
public static Reducer CountDistinct(string field) => new SingleFieldReducer("COUNT_DISTINCT", field);
public static Reducer CountDistinctish(string field) => new SingleFieldReducer("COUNT_DISTINCTISH", field);
public static Reducer Sum(string field) => new SingleFieldReducer("SUM", field);
public static Reducer Min(string field) => new SingleFieldReducer("MIN", field);
public static Reducer Max(string field) => new SingleFieldReducer("MAX", field);
public static Reducer Avg(string field) => new SingleFieldReducer("AVG", field);
public static Reducer StdDev(string field) => new SingleFieldReducer("STDDEV", field);
public static Reducer Quantile(string field, double percentile) => new QuantileReducer(field, percentile);
sealed class QuantileReducer : Reducer
{
private readonly double _percentile;
public QuantileReducer(string field, double percentile) : base(field)
{
_percentile = percentile;
}
protected override int GetOwnArgsCount() => base.GetOwnArgsCount() + 1;
protected override void AddOwnArgs(List<object> args)
{
base.AddOwnArgs(args);
args.Add(_percentile);
}
public override string Name => "QUANTILE";
}
public static Reducer FirstValue(string field, SortedField sortBy) => new FirstValueReducer(field, sortBy);
sealed class FirstValueReducer : Reducer
{
private readonly SortedField? _sortBy;
public FirstValueReducer(string field, SortedField? sortBy) : base(field)
{
_sortBy = sortBy;
}
public override string Name => "FIRST_VALUE";
protected override int GetOwnArgsCount() => base.GetOwnArgsCount() + (_sortBy.HasValue ? 3 : 0);
protected override void AddOwnArgs(List<object> args)
{
base.AddOwnArgs(args);
if (_sortBy != null)
{
var sortBy = _sortBy.GetValueOrDefault();
args.Add("BY".Literal());
args.Add(sortBy.Field);
args.Add(sortBy.OrderAsArg());
}
}
}
public static Reducer FirstValue(string field) => new FirstValueReducer(field, null);
public static Reducer ToList(string field) => new SingleFieldReducer("TOLIST", field);
public static Reducer RandomSample(string field, int size) => new RandomSampleReducer(field, size);
sealed class RandomSampleReducer : Reducer
{
private readonly int _size;
public RandomSampleReducer(string field, int size) : base(field)
{
_size = size;
}
public override string Name => "RANDOM_SAMPLE";
protected override int GetOwnArgsCount() => base.GetOwnArgsCount() + 1;
protected override void AddOwnArgs(List<object> args)
{
base.AddOwnArgs(args);
args.Add(_size.Boxed());
}
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Collections.Generic;
using StackExchange.Redis;
namespace NRediSearch.Aggregation
{
public readonly struct Row
{
private readonly Dictionary<string, RedisValue> _fields;
internal Row(Dictionary<string, RedisValue> fields)
{
_fields = fields;
}
public bool ContainsKey(string key) => _fields.ContainsKey(key);
public RedisValue this[string key] => _fields.TryGetValue(key, out var result) ? result : RedisValue.Null;
public string GetString(string key) => _fields.TryGetValue(key, out var result) ? (string)result : default;
public long GetInt64(string key) => _fields.TryGetValue(key, out var result) ? (long)result : default;
public double GetDouble(string key) => _fields.TryGetValue(key, out var result) ? (double)result : default;
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using StackExchange.Redis;
namespace NRediSearch.Aggregation
{
public readonly struct SortedField
{
public SortedField(string field, Order order)
{
Field = field;
Order = order;
}
public string Field { get; }
public Order Order { get; }
internal object OrderAsArg() => (Order == Order.Ascending ? "ASC" : "DESC").Literal();
public static SortedField Ascending(string field) => new SortedField(field, Order.Ascending);
public static SortedField Descending(string field) => new SortedField(field, Order.Descending);
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Collections.Generic;
using NRediSearch.Aggregation;
using StackExchange.Redis;
namespace NRediSearch
{
public sealed class AggregationResult
{
private readonly Dictionary<string, RedisValue>[] _results;
internal AggregationResult(RedisResult result)
{
var arr = (RedisResult[])result;
_results = new Dictionary<string, RedisValue>[arr.Length - 1];
for (int i = 1; i < arr.Length; i++)
{
var raw = (RedisResult[])arr[i];
var cur = new Dictionary<string, RedisValue>();
for (int j = 0; j < raw.Length;)
{
var key = (string)raw[j++];
var val = raw[j++];
if (val.Type != ResultType.MultiBulk)
cur.Add(key, (RedisValue)val);
}
_results[i - 1] = cur;
}
}
public IReadOnlyList<Dictionary<string, RedisValue>> GetResults() => _results;
public Dictionary<string, RedisValue> this[int index]
=> index >= _results.Length ? null : _results[index];
public Row? GetRow(int index)
{
if (index >= _results.Length) return null;
return new Row(_results[index]);
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("NRediSearch.Test, PublicKey=00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff")]
This diff is collapsed.
......@@ -2,9 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StackExchange.Redis;
namespace NRediSearch
......@@ -17,15 +14,23 @@ public class Document
public string Id { get; }
public double Score { get; }
public byte[] Payload { get; }
private readonly Dictionary<String, RedisValue> properties = new Dictionary<string, RedisValue>();
internal readonly Dictionary<string, RedisValue> _properties;
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, double score, byte[] payload)
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, byte[] payload)
{
Id = id;
_properties = fields ?? new Dictionary<string, RedisValue>();
Score = score;
Payload = payload;
}
public IEnumerable<KeyValuePair<string, RedisValue>> GetProperties() => _properties;
public static Document Load(string id, double score, byte[] payload, RedisValue[] fields)
{
Document ret = new Document(id, score, payload);
......@@ -41,10 +46,29 @@ public static Document Load(string id, double score, byte[] payload, RedisValue[
public RedisValue this[string key]
{
get { return properties.TryGetValue(key, out var val) ? val : default(RedisValue); }
internal set { properties[key] = value; }
get { return _properties.TryGetValue(key, out var val) ? val : default(RedisValue); }
internal set { _properties[key] = value; }
}
public bool HasProperty(string key) => properties.ContainsKey(key);
public bool HasProperty(string key) => _properties.ContainsKey(key);
internal static Document Parse(string docId, RedisResult result)
{
if (result == null || result.IsNull) return null;
var arr = (RedisResult[])result;
var doc = new Document(docId);
for(int i = 0; i < arr.Length; )
{
doc[(string)arr[i++]] = (RedisValue)arr[i++];
}
return doc;
}
public Document Set(string field, RedisValue value)
{
this[field] = value;
return this;
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
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>
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))
{
return "-inf";
}
else if (double.IsPositiveInfinity(value))
{
return "inf";
}
else
{
return value.ToString(forceDecimal ? "#.0" : "G17", NumberFormatInfo.InvariantInfo);
}
}
internal static string AsRedisString(this GeoUnit value)
{
switch (value)
{
case GeoUnit.Feet: return "ft";
case GeoUnit.Kilometers: return "km";
case GeoUnit.Meters: return "m";
case GeoUnit.Miles: return "mi";
default: throw new InvalidOperationException($"Unknown unit: {value}");
}
}
}
}
using StackExchange.Redis;
// .NET port of https://github.com/RedisLabs/JRediSearch/
using StackExchange.Redis;
using System.Collections;
using System.Linq;
namespace NRediSearch
{
/// <summary>
......@@ -33,5 +37,14 @@ public static object Literal(this string value)
}
return boxed;
}
const int BOXED_MIN = -1, BOXED_MAX = 20;
static readonly object[] s_Boxed = Enumerable.Range(BOXED_MIN, BOXED_MAX - BOXED_MIN).Select(i => (object)i).ToArray();
/// <summary>
/// Obtain a pre-boxed integer if possible, else box the inbound value
/// </summary>
public static object Boxed(this int value) => value >= BOXED_MIN && value <= BOXED_MAX ? s_Boxed[value - BOXED_MIN] : value;
}
}
......@@ -4,6 +4,7 @@
<VersionPrefix>0.2</VersionPrefix>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<PackageTags>Redis;Search;Modules;RediSearch</PackageTags>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StackExchange.Redis\StackExchange.Redis.csproj" />
......
// .NET port of https://github.com/RedisLabs/JRediSearch/
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Globalization;
using StackExchange.Redis;
namespace NRediSearch
{
/// <summary>
/// Query represents query parameters and filters to load results from the engine
/// </summary>
public class Query
public sealed class Query
{
/// <summary>
/// Filter represents a filtering rules in a query
......@@ -85,19 +86,11 @@ internal override void SerializeRedisArgs(List<object> args)
args.Add(lon);
args.Add(lat);
args.Add(radius);
switch (unit)
{
case GeoUnit.Feet: args.Add("ft".Literal()); break;
case GeoUnit.Kilometers: args.Add("km".Literal()); break;
case GeoUnit.Meters: args.Add("m".Literal()); break;
case GeoUnit.Miles: args.Add("mi".Literal()); break;
default: throw new InvalidOperationException($"Unknown unit: {unit}");
}
args.Add(unit.AsRedisString().Literal());
}
}
private struct Paging
internal readonly struct Paging
{
public int Offset { get; }
public int Count { get; }
......@@ -112,7 +105,7 @@ public Paging(int offset, int count)
/// <summary>
/// The query's filter list. We only support AND operation on all those filters
/// </summary>
private readonly List<Filter> _filters = new List<Filter>();
internal readonly List<Filter> _filters = new List<Filter>();
/// <summary>
/// The textual part of the query
......@@ -122,7 +115,7 @@ public Paging(int offset, int count)
/// <summary>
/// The sorting parameters
/// </summary>
private Paging _paging = new Paging(0, 10);
internal Paging _paging = new Paging(0, 10);
/// <summary>
/// Set the query to verbatim mode, disabling stemming and query expansion
......@@ -149,7 +142,7 @@ public Paging(int offset, int count)
/// Set the query language, for stemming purposes; see http://redisearch.io for documentation on languages and stemming
/// </summary>
public string Language { get; set; }
protected String[] _fields = null;
internal string[] _fields = null;
/// <summary>
/// Set the query payload to be evaluated by the scoring function
/// </summary>
......@@ -165,6 +158,14 @@ public Paging(int offset, int count)
/// </summary>
public bool SortAscending { get; set; } = true;
// highlight and summarize
internal bool _wantsHighlight = false, _wantsSummarize = false;
internal string[] _highlightFields = null;
internal string[] _summarizeFields = null;
internal HighlightTags? _highlightTags = null;
internal string _summarizeSeparator = null;
internal int _summarizeNumFragments = -1, _summarizeFragmentLen = -1;
/// <summary>
/// Create a new index
/// </summary>
......@@ -206,7 +207,7 @@ internal void SerializeRedisArgs(List<object> args)
if (_fields?.Length > 0)
{
args.Add("INFIELDS".Literal());
args.Add(_fields.Length);
args.Add(_fields.Length.Boxed());
args.AddRange(_fields);
}
......@@ -226,8 +227,8 @@ internal void SerializeRedisArgs(List<object> args)
if (_paging.Offset != 0 || _paging.Count != 10)
{
args.Add("LIMIT".Literal());
args.Add(_paging.Offset);
args.Add(_paging.Count);
args.Add(_paging.Offset.Boxed());
args.Add(_paging.Count.Boxed());
}
if (_filters?.Count > 0)
......@@ -237,6 +238,55 @@ internal void SerializeRedisArgs(List<object> args)
f.SerializeRedisArgs(args);
}
}
if (_wantsHighlight)
{
args.Add("HIGHLIGHT".Literal());
if (_highlightFields != null)
{
args.Add("FIELDS".Literal());
args.Add(_highlightFields.Length.Boxed());
foreach (var s in _highlightFields)
{
args.Add(s);
}
}
if (_highlightTags != null)
{
args.Add("TAGS".Literal());
var tags = _highlightTags.GetValueOrDefault();
args.Add(tags.Open);
args.Add(tags.Close);
}
}
if (_wantsSummarize)
{
args.Add("SUMMARIZE".Literal());
if (_summarizeFields != null)
{
args.Add("FIELDS".Literal());
args.Add(_summarizeFields.Length.Boxed());
foreach (var s in _summarizeFields)
{
args.Add(s);
}
}
if (_summarizeNumFragments != -1)
{
args.Add("FRAGS".Literal());
args.Add(_summarizeNumFragments.Boxed());
}
if (_summarizeFragmentLen != -1)
{
args.Add("LEN".Literal());
args.Add(_summarizeFragmentLen.Boxed());
}
if (_summarizeSeparator != null)
{
args.Add("SEPARATOR".Literal());
args.Add(_summarizeSeparator);
}
}
}
/// <summary>
......@@ -273,6 +323,45 @@ public Query LimitFields(params string[] fields)
return this;
}
public readonly struct HighlightTags
{
public HighlightTags(string open, string close)
{
Open = open;
Close = close;
}
public string Open { get; }
public string Close { get; }
}
public Query HighlightFields(HighlightTags tags, params string[] fields) => HighlightFieldsImpl(tags, fields);
public Query HighlightFields(params string[] fields) => HighlightFieldsImpl(null, fields);
private Query HighlightFieldsImpl(HighlightTags? tags, string[] fields)
{
if (fields == null || fields.Length > 0)
{
_highlightFields = fields;
}
_highlightTags = tags;
_wantsHighlight = true;
return this;
}
public Query SummarizeFields(int contextLen, int fragmentCount, string separator, params string[] fields)
{
if (fields == null || fields.Length > 0)
{
_summarizeFields = fields;
}
_summarizeFragmentLen = contextLen;
_summarizeNumFragments = fragmentCount;
_summarizeSeparator = separator;
_wantsSummarize = true;
return this;
}
public Query SummarizeFields(params string[] fields) => SummarizeFields(-1, -1, null, fields);
/// <summary>
/// Set the query to be sorted by a sortable field defined in the schema
/// </summary>
......@@ -285,5 +374,34 @@ public Query SetSortBy(string field, bool ascending = true)
SortAscending = ascending;
return this;
}
public Query SetWithScores(bool value = true)
{
WithScores = value;
return this;
}
public Query SetNoContent(bool value = true)
{
NoContent = value;
return this;
}
public Query SetVerbatim(bool value = true)
{
Verbatim = value;
return this;
}
public Query SetNoStopwords(bool value = true)
{
NoStopwords = value;
return this;
}
public Query SetLanguage(string language)
{
Language = language;
return this;
}
}
}
\ No newline at end of file
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
namespace NRediSearch.QueryBuilder
{
/// <summary>
/// A disjunct node. evaluates to true if any of its children are false. Conversely, this node evaluates to false
/// only iff <b>all</b> of its children are true, making it the exact inverse of IntersectNode
/// </summary>
/// <remarks>DisjunctUnionNode which evalutes to true if <b>all</b> its children are false.</remarks>
public class DisjunctNode : IntersectNode
{
public override string ToString(ParenMode mode)
{
var ret = base.ToString(ParenMode.Never);
if (ShouldUseParens(mode))
{
return "-(" + ret + ")";
}
else
{
return "-" + ret;
}
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
namespace NRediSearch.QueryBuilder
{
/// <summary>
/// A disjunct union node is the inverse of a UnionNode. It evaluates to true only iff <b>all</b> its
/// children are false. Conversely, it evaluates to false if <b>any</b> of its children are true.
/// </summary>
/// <remarks>see DisjunctNode which evaluates to true if <b>any</b> of its children are false.</remarks>
public class DisjunctUnionNode : DisjunctNode
{
protected override string GetJoinString() => "|";
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Text;
using StackExchange.Redis;
namespace NRediSearch.QueryBuilder
{
public class GeoValue : Value
{
private readonly GeoUnit _unit;
private readonly double _lon, _lat, _radius;
public GeoValue(double lon, double lat, double radius, GeoUnit unit)
{
_lon = lon;
_lat = lat;
_radius = radius;
_unit = unit;
}
public override string ToString()
{
return new StringBuilder("[")
.Append(_lon.AsRedisString(true)).Append(" ")
.Append(_lat.AsRedisString(true)).Append(" ")
.Append(_radius.AsRedisString(true)).Append(" ")
.Append(_unit.AsRedisString())
.Append("]").ToString();
}
public override bool IsCombinable() => false;
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
namespace NRediSearch.QueryBuilder
{
/// <summary>
/// The intersection node evaluates to true if any of its children are true.
/// </summary>
public class IntersectNode : QueryNode
{
protected override string GetJoinString() => " ";
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
namespace NRediSearch.QueryBuilder
{
public enum ParenMode
{
/// <summary>
/// Always encapsulate
/// </summary>
Always,
/// <summary>
/// Never encapsulate. Note that this may be ignored if parentheses are semantically required (e.g.
/// <pre>@foo:(val1|val2)</pre>. However something like <pre>@foo:v1 @bar:v2</pre> need not be parenthesized.
/// </summary>
Never,
/// <summary>
/// Determine encapsulation based on number of children. If the node only has one child, it is not
/// parenthesized, if it has more than one child, it is parenthesized
/// </summary>
Default,
}
public interface INode
{
/// <summary>
/// Returns the string form of this node.
/// </summary>
/// <param name="mode"> Whether the string should be encapsulated in parentheses <pre>(...)</pre></param>
/// <returns>The string query.</returns>
string ToString(ParenMode mode);
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
namespace NRediSearch.QueryBuilder
{
/// <summary>
/// The optional node affects scoring and ordering. If it evaluates to true, the result is ranked
/// higher. It is helpful to combine it with a UnionNode to rank a document higher if it meets
/// one of several criteria.
/// </summary>
public class OptionalNode : IntersectNode
{
public override string ToString(ParenMode mode)
{
var ret = base.ToString(ParenMode.Never);
if (ShouldUseParens(mode))
{
return "~(" + ret + ")";
}
else
{
return "~" + ret;
}
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
namespace NRediSearch.QueryBuilder
{
/// <summary>
/// This class contains methods to construct query nodes. These query nodes can be added to parent query
/// nodes (building a chain) or used as the root query node.
///
/// You can use <pre>using static</pre> for these helper methods.
/// </summary>
public static class QueryBuilder
{
public static QueryNode Intersect() => new IntersectNode();
/// <summary>
/// Create a new intersection node with child nodes. An intersection node is true if all its children
/// are also true
/// </summary>
/// <param name="n">sub-condition to add</param>
/// <returns>The node</returns>
public static QueryNode Intersect(params INode[] n)
{
return Intersect().Add(n);
}
///
/// Create a new intersection node with a field-value pair.
/// @param field The field that should contain this value. If this value is empty, then any field
/// will be checked.
/// @param values Value to check for. The node will be true only if the field (or any field)
/// contains <i>all</i> of the values
/// @return The node
///
public static QueryNode Intersect(string field, params Value[] values)
{
return Intersect().Add(field, values);
}
///
/// Helper method to create a new intersection node with a string value.
/// @param field The field to check. If left null or empty, all fields will be checked.
/// @param stringValue The value to check
/// @return The node
///
public static QueryNode Intersect(string field, string stringValue)
{
return Intersect(field, Values.Value(stringValue));
}
public static QueryNode Union() => new UnionNode();
///
/// Create a union node. Union nodes evaluate to true if <i>any</i> of its children are true
/// @param n Child node
/// @return The union node
///
public static QueryNode Union(params INode[] n)
{
return Union().Add(n);
}
///
/// Create a union node which can match an one or more values
/// @param field Field to check. If empty, all fields are checked
/// @param values Values to search for. The node evaluates to true if {@code field} matches
/// any of the values
/// @return The union node
///
public static QueryNode Union(string field, params Value[] values)
{
return Union().Add(field, values);
}
///
/// Convenience method to match one or more strings. This is equivalent to
/// {@code union(field, value(v1), value(v2), value(v3)) ...}
/// @param field Field to match
/// @param values Strings to check for
/// @return The union node
///
public static QueryNode Union(string field, params string[] values)
{
return Union(field, Values.Value(values));
}
public static QueryNode Disjunct() => new DisjunctNode();
///
/// Create a disjunct node. Disjunct nodes are true iff <b>any</b> of its children are <b>not</b> true.
/// Conversely, this node evaluates to false if <b>all</b> its children are true.
/// @param n Child nodes to add
/// @return The disjunct node
///
public static QueryNode Disjunct(params INode[] n)
{
return Disjunct().Add(n);
}
///
/// Create a disjunct node using one or more values. The node will evaluate to true iff the field does not
/// match <b>any</b> of the values.
/// @param field Field to check for (empty or null for any field)
/// @param values The values to check for
/// @return The node
///
public static QueryNode Disjunct(string field, params Value[] values)
{
return Disjunct().Add(field, values);
}
///
/// Create a disjunct node using one or more values. The node will evaluate to true iff the field does not
/// match <b>any</b> of the values.
/// @param field Field to check for (empty or null for any field)
/// @param values The values to check for
/// @return The node
///
public static QueryNode Disjunct(string field, params string[] values)
{
return Disjunct(field, Values.Value(values));
}
public static QueryNode DisjunctUnion() => new DisjunctUnionNode();
///
/// Create a disjunct union node. This node evaluates to true if <b>all</b> of its children are not true.
/// Conversely, this node evaluates as false if <b>any</b> of its children are true.
/// @param n
/// @return The node
///
public static QueryNode DisjunctUnion(params INode[] n)
{
return DisjunctUnion().Add(n);
}
public static QueryNode DisjunctUnion(string field, params Value[] values)
{
return DisjunctUnion().Add(field, values);
}
public static QueryNode DisjunctUnion(string field, params string[] values)
{
return DisjunctUnion(field, Values.Value(values));
}
public static QueryNode Optional() => new OptionalNode();
///
/// Create an optional node. Optional nodes do not affect which results are returned but they influence
/// ordering and scoring.
/// @param n The node to evaluate as optional
/// @return The new node
///
public static QueryNode Optional(params INode[] n)
{
return Optional().Add(n);
}
public static QueryNode Optional(string field, params Value[] values)
{
return Optional().Add(field, values);
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NRediSearch.QueryBuilder
{
public abstract class QueryNode : INode
{
private readonly List<INode> children = new List<INode>();
protected abstract string GetJoinString();
/**
* Add a match criteria to this node
* @param field The field to check. If null or empty, then any field is checked
* @param values Values to check for.
* @return The current node, for chaining.
*/
public QueryNode Add(string field, params Value[] values)
{
children.Add(new ValueNode(field, GetJoinString(), values));
return this;
}
/**
* Convenience method to add a list of string values
* @param field Field to check for
* @param values One or more string values.
* @return The current node, for chaining.
*/
public QueryNode Add(string field, params string[] values)
{
children.Add(new ValueNode(field, GetJoinString(), values));
return this;
}
/**
* Add a list of values from a collection
* @param field The field to check
* @param values Collection of values to match
* @return The current node for chaining.
*/
public QueryNode Add(string field, IList<Value> values)
{
return Add(field, values.ToArray());
}
/**
* Add children nodes to this node.
* @param nodes Children nodes to add
* @return The current node, for chaining.
*/
public QueryNode Add(params INode[] nodes)
{
children.AddRange(nodes);
return this;
}
protected bool ShouldUseParens(ParenMode mode)
{
if (mode == ParenMode.Always)
{
return true;
}
else if (mode == ParenMode.Never)
{
return false;
}
else
{
return children.Count > 1;
}
}
public virtual string ToString(ParenMode parenMode)
{
StringBuilder sb = new StringBuilder();
if (ShouldUseParens(parenMode))
{
sb.Append("(");
}
var sj = new StringJoiner(sb, GetJoinString());
foreach (var n in children)
{
sj.Add(n.ToString(parenMode));
}
if (ShouldUseParens(parenMode))
{
sb.Append(")");
}
return sb.ToString();
}
public override string ToString() => ToString(ParenMode.Default);
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Text;
namespace NRediSearch.QueryBuilder
{
public sealed class RangeValue : Value
{
private double from, to;
private bool inclusiveMin = true, inclusiveMax = true;
public override bool IsCombinable() => false;
private static void AppendNum(StringBuilder sb, double n, bool inclusive)
{
if (!inclusive)
{
sb.Append("(");
}
sb.Append(n.AsRedisString(true));
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
sb.Append("[");
AppendNum(sb, from, inclusiveMin);
sb.Append(" ");
AppendNum(sb, to, inclusiveMax);
sb.Append("]");
return sb.ToString();
}
public RangeValue(double from, double to)
{
this.from = from;
this.to = to;
}
public RangeValue InclusiveMin(bool val)
{
inclusiveMin = val;
return this;
}
public RangeValue InclusiveMax(bool val)
{
inclusiveMax = val;
return this;
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Text;
namespace NRediSearch.QueryBuilder
{
internal ref struct StringJoiner // this is to replace a Java feature cleanly
{
readonly StringBuilder _sb;
readonly string _delimiter;
bool _isFirst;
public StringJoiner(StringBuilder sb, string delimiter)
{
_sb = sb;
_delimiter = delimiter;
_isFirst = true;
}
public void Add(string value)
{
if (_isFirst) _isFirst = false;
else _sb.Append(_delimiter);
_sb.Append(value);
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
namespace NRediSearch.QueryBuilder
{
public class UnionNode : QueryNode
{
protected override string GetJoinString() => "|";
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
namespace NRediSearch.QueryBuilder
{
public abstract class Value
{
public virtual bool IsCombinable() => false;
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System.Text;
namespace NRediSearch.QueryBuilder
{
public class ValueNode : INode
{
private readonly Value[] _values;
private readonly string _field, _joinString;
public ValueNode(string field, string joinstr, params Value[] values)
{
_field = field;
_values = values;
_joinString = joinstr;
}
private static Value[] fromStrings(string[] values)
{
Value[] objs = new Value[values.Length];
for (int i = 0; i < values.Length; i++)
{
objs[i] = Values.Value(values[i]);
}
return objs;
}
public ValueNode(string field, string joinstr, params string[] values)
: this(field, joinstr, fromStrings(values)) { }
private string FormatField()
{
if (string.IsNullOrWhiteSpace(_field)) return "";
return "@" + _field + ":";
}
private string ToStringCombinable(ParenMode mode)
{
StringBuilder sb = new StringBuilder(FormatField());
if (_values.Length > 1 || mode == ParenMode.Always)
{
sb.Append("(");
}
var sj = new StringJoiner(sb, _joinString);
foreach (var v in _values)
{
sj.Add(v.ToString());
}
if (_values.Length > 1 || mode == ParenMode.Always)
{
sb.Append(")");
}
return sb.ToString();
}
private string ToStringDefault(ParenMode mode)
{
bool useParen = mode == ParenMode.Always;
if (!useParen)
{
useParen = mode != ParenMode.Never && _values.Length > 1;
}
var sb = new StringBuilder();
if (useParen)
{
sb.Append("(");
}
var sj = new StringJoiner(sb, _joinString);
foreach (var v in _values)
{
sj.Add(FormatField() + v.ToString());
}
if (useParen)
{
sb.Append(")");
}
return sb.ToString();
}
public string ToString(ParenMode mode)
{
if (_values[0].IsCombinable())
{
return ToStringCombinable(mode);
}
else
{
return ToStringDefault(mode);
}
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System;
namespace NRediSearch.QueryBuilder
{
public static class Values
{
private abstract class ScalableValue : Value
{
public override bool IsCombinable() => true;
}
private sealed class ValueValue : ScalableValue
{
private readonly string s;
public ValueValue(string s)
{
this.s = s;
}
public override string ToString() => s;
}
public static Value Value(string s) => new ValueValue(s);
internal static Value[] Value(string[] s) => Array.ConvertAll(s, _ => Value(_));
public static RangeValue Between(double from, double to) => new RangeValue(from, to);
public static RangeValue Between(int from, int to) => new RangeValue((double)from, (double)to);
public static RangeValue Equal(double d) => new RangeValue(d, d);
public static RangeValue Equal(int i) => Equal((double)i);
public static RangeValue LessThan(double d) => new RangeValue(double.NegativeInfinity, d).InclusiveMax(false);
public static RangeValue GreaterThan(double d) => new RangeValue(d, double.PositiveInfinity).InclusiveMin(false);
public static RangeValue LessThanOrEqual(double d) => LessThan(d).InclusiveMax(true);
public static RangeValue GreaterThanOrEqual(double d) => GreaterThan(d).InclusiveMin(true);
public static Value Tags(params string[] tags)
{
if (tags.Length == 0)
{
throw new ArgumentException("Must have at least one tag", nameof(tags));
}
return new TagValue("{" + string.Join(" | ", tags) + "}");
}
sealed class TagValue : Value
{
private readonly string s;
public TagValue(string s) { this.s = s; }
public override string ToString() => s;
}
}
}
......@@ -23,13 +23,15 @@ public class Field
{
public String Name { get; }
public FieldType Type { get; }
public bool Sortable {get;}
public bool Sortable { get; }
public bool NoIndex { get; }
internal Field(string name, FieldType type, bool sortable)
internal Field(string name, FieldType type, bool sortable, bool noIndex = false)
{
Name = name;
Type = type;
Sortable = sortable;
NoIndex = noIndex;
}
internal virtual void SerializeRedisArgs(List<object> args)
......@@ -47,21 +49,20 @@ object GetForRedis(FieldType type)
}
args.Add(Name);
args.Add(GetForRedis(Type));
if(Sortable){args.Add("SORTABLE");}
if (Sortable) { args.Add("SORTABLE".Literal()); }
if (NoIndex) { args.Add("NOINDEX".Literal()); }
}
}
public class TextField : Field
{
public double Weight { get; }
internal TextField(string name, double weight = 1.0) : base(name, FieldType.FullText, false)
{
Weight = weight;
}
public bool NoStem { get; }
internal TextField(string name, bool sortable, double weight = 1.0) : base(name, FieldType.FullText, sortable)
public TextField(string name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false) : base(name, FieldType.FullText, sortable, noIndex)
{
Weight = weight;
NoStem = noStem;
}
internal override void SerializeRedisArgs(List<object> args)
......@@ -72,11 +73,22 @@ internal override void SerializeRedisArgs(List<object> args)
args.Add("WEIGHT".Literal());
args.Add(Weight);
}
if (NoStem) args.Add("NOSTEM".Literal());
}
}
public List<Field> Fields { get; } = new List<Field>();
/// <summary>
/// Add a field to the schema
/// </summary>
/// <returns>the schema object</returns>
public Schema AddField(Field field)
{
Fields.Add(field ?? throw new ArgumentNullException(nameof(field)));
return this;
}
/// <summary>
/// Add a text field to the schema with a given weight
/// </summary>
......@@ -97,7 +109,7 @@ public Schema AddTextField(string name, double weight = 1.0)
/// <returns>the schema object</returns>
public Schema AddSortableTextField(string name, double weight = 1.0)
{
Fields.Add(new TextField(name, true, weight));
Fields.Add(new TextField(name, weight, true));
return this;
}
......
// .NET port of https://github.com/RedisLabs/JRediSearch/
using StackExchange.Redis;
using System.Collections.Generic;
......
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