Commit eba20f84 authored by Marc Gravell's avatar Marc Gravell

NRediSearch => SE.Redis.Modules; implement CL.THROTTLE

parent 69897ca9
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<TargetFrameworks>$(LibraryTargetFrameworks)</TargetFrameworks> <TargetFrameworks>$(LibraryTargetFrameworks)</TargetFrameworks>
<VersionPrefix>0.1</VersionPrefix> <VersionPrefix>0.1</VersionPrefix>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<PackageTags>Redis;Search;RediSearch</PackageTags> <PackageTags>Redis;Search;Modules;RediSearch</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\StackExchange.Redis\StackExchange.Redis.csproj" /> <ProjectReference Include="..\StackExchange.Redis\StackExchange.Redis.csproj" />
......
This diff is collapsed.
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StackExchange.Redis;
namespace StackExchange.Redis.Modules.RediSearch
{
/// <summary>
/// Document represents a single indexed document or entity in the engine
/// </summary>
public class Document
{
public string Id { get; }
public double Score { get; }
public byte[] Payload { get; }
private Dictionary<String, RedisValue> properties = new Dictionary<string, RedisValue>();
public Document(string id, double score, byte[] payload)
{
Id = id;
Score = score;
Payload = payload;
}
public static Document Load(string id, double score, byte[] payload, RedisValue[] fields)
{
Document ret = new Document(id, score, payload);
if (fields != null)
{
for (int i = 0; i < fields.Length; i += 2)
{
ret[(string)fields[i]] = fields[i + 1];
}
}
return ret;
}
public RedisValue this[string key]
{
get { return properties.TryGetValue(key, out var val) ? val : default(RedisValue); }
internal set { properties[key] = value; }
}
public bool HasProperty(string key) => properties.ContainsKey(key);
}
}
using StackExchange.Redis;
using System.Collections;
namespace StackExchange.Redis.Modules.RediSearch
{
/// <summary>
/// Cache to ensure we encode and box literals once only
/// </summary>
internal static class Literals
{
private static Hashtable _boxed = new Hashtable();
private static object _null = RedisValue.Null;
/// <summary>
/// Obtain a lazily-cached pre-encoded and boxed representation of a string
/// </summary>
/// <remarks>This shoul donly be used for fixed values, not user data (the cache is never reclaimed, so it will be a memory leak)</remarks>
public static object Literal(this string value)
{
if (value == null) return _null;
object boxed = _boxed[value];
if (boxed == null)
{
lock (_boxed)
{
boxed = _boxed[value];
if (boxed == null)
{
boxed = (RedisValue)value;
_boxed.Add(value, boxed);
}
}
}
return boxed;
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Globalization;
namespace StackExchange.Redis.Modules.RediSearch
{
/// <summary>
/// Query represents query parameters and filters to load results from the engine
/// </summary>
public class Query
{
/// <summary>
/// Filter represents a filtering rules in a query
/// </summary>
public abstract class Filter
{
public string Property { get; }
internal abstract void SerializeRedisArgs(List<object> args);
internal Filter(string property)
{
Property = property;
}
}
/// <summary>
/// NumericFilter wraps a range filter on a numeric field. It can be inclusive or exclusive
/// </summary>
public class NumericFilter : Filter
{
private readonly double min, max;
private readonly bool exclusiveMin, exclusiveMax;
public NumericFilter(string property, double min, bool exclusiveMin, double max, bool exclusiveMax) : base(property)
{
this.min = min;
this.max = max;
this.exclusiveMax = exclusiveMax;
this.exclusiveMin = exclusiveMin;
}
public NumericFilter(string property, double min, double max) : this(property, min, false, max, false) { }
internal override void SerializeRedisArgs(List<object> args)
{
RedisValue FormatNum(double num, bool exclude)
{
if (!exclude || double.IsInfinity(num))
{
return (RedisValue)num; // can use directly
}
// need to add leading bracket
return "(" + num.ToString("G17", NumberFormatInfo.InvariantInfo);
}
args.Add("FILTER".Literal());
args.Add(Property);
args.Add(FormatNum(min, exclusiveMin));
args.Add(FormatNum(max, exclusiveMax));
}
}
/// <summary>
/// GeoFilter encapsulates a radius filter on a geographical indexed fields
/// </summary>
public class GeoFilter : Filter
{
private readonly double lon, lat, radius;
private GeoUnit unit;
public GeoFilter(string property, double lon, double lat, double radius, GeoUnit unit) : base(property)
{
this.lon = lon;
this.lat = lat;
this.radius = radius;
this.unit = unit;
}
internal override void SerializeRedisArgs(List<object> args)
{
args.Add("GEOFILTER".Literal());
args.Add(Property);
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}");
}
}
}
private struct Paging
{
public int Offset { get; }
public int Count { get; }
public Paging(int offset, int count)
{
Offset = offset;
Count = count;
}
}
/// <summary>
/// The query's filter list. We only support AND operation on all those filters
/// </summary>
List<Filter> _filters = new List<Filter>();
/// <summary>
/// The textual part of the query
/// </summary>
public string QueryString { get; }
/// <summary>
/// The sorting parameters
/// </summary>
Paging _paging = new Paging(0, 10);
/// <summary>
/// Set the query to verbatim mode, disabling stemming and query expansion
/// </summary>
public bool Verbatim { get; set; }
/// <summary>
/// Set the query not to return the contents of documents, and rather just return the ids
/// </summary>
public bool NoContent { get; set; }
/// <summary>
/// Set the query not to filter for stopwords. In general this should not be used
/// </summary>
public bool NoStopwords { get; set; }
/// <summary>
/// Set the query to return a factored score for each results. This is useful to merge results from multiple queries.
/// </summary>
public bool WithScores { get; set; }
/// <summary>
/// Set the query to return object payloads, if any were given
/// </summary>
public bool WithPayloads { get; set; }
/// <summary>
/// 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;
/// <summary>
/// Set the query payload to be evaluated by the scoring function
/// </summary>
public byte[] Payload { get; set; }
/// <summary>
/// Create a new index
/// </summary>
public Query(String queryString)
{
QueryString = queryString;
}
internal void SerializeRedisArgs(List<object> args)
{
args.Add(QueryString);
if (Verbatim)
{
args.Add("VERBATIM".Literal());
}
if (NoContent)
{
args.Add("NOCONTENT".Literal());
}
if (NoStopwords)
{
args.Add("NOSTOPWORDS".Literal());
}
if (WithScores)
{
args.Add("WITHSCORES".Literal());
}
if (WithPayloads)
{
args.Add("WITHPAYLOADS".Literal());
}
if (Language != null)
{
args.Add("LANGUAGE".Literal());
args.Add(Language);
}
if (_fields != null && _fields.Length > 0)
{
args.Add("INFIELDS".Literal());
args.Add(_fields.Length);
args.AddRange(_fields);
}
if (Payload != null)
{
args.Add("PAYLOAD".Literal());
args.Add(Payload);
}
if (_paging.Offset != 0 || _paging.Count != 10)
{
args.Add("LIMIT".Literal());
args.Add(_paging.Offset);
args.Add(_paging.Count);
}
if (_filters != null && _filters.Count > 0)
{
foreach (var f in _filters)
{
f.SerializeRedisArgs(args);
}
}
}
/// <summary>
/// Limit the results to a certain offset and limit
/// </summary>
/// <param name="offset">the first result to show, zero based indexing</param>
/// <param name="limit">how many results we want to show</param>
/// <returns>the query itself, for builder-style syntax</returns>
public Query Limit(int offset, int count)
{
_paging = new Paging(offset, count);
return this;
}
/// <summary>
/// Add a filter to the query's filter list
/// </summary>
/// <param name="f">either a numeric or geo filter object</param>
/// <returns>the query itself</returns>
public Query AddFilter(Filter f)
{
_filters.Add(f);
return this;
}
/// <summary>
/// Limit the query to results that are limited to a specific set of fields
/// </summary>
/// <param name="fields">a list of TEXT fields in the schemas</param>
/// <returns>the query object itself</returns>
public Query LimitFields(params string[] fields)
{
this._fields = fields;
return this;
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using System;
using System.Collections.Generic;
namespace StackExchange.Redis.Modules.RediSearch
{
/// <summary>
/// Schema abstracts the schema definition when creating an index.
/// Documents can contain fields not mentioned in the schema, but the index will only index pre-defined fields
/// </summary>
public sealed class Schema
{
public enum FieldType
{
FullText,
Geo,
Numeric
}
public class Field
{
public String Name { get; }
public FieldType Type { get; }
internal Field(string name, FieldType type)
{
Name = name;
Type = type;
}
internal virtual void SerializeRedisArgs(List<object> args)
{
object GetForRedis(FieldType type)
{
switch (type)
{
case FieldType.FullText: return "TEXT".Literal();
case FieldType.Geo: return "GEO".Literal();
case FieldType.Numeric: return "NUMERIC".Literal();
default: throw new ArgumentOutOfRangeException(nameof(type));
}
}
args.Add(Name);
args.Add(GetForRedis(Type));
}
}
public class TextField : Field
{
public double Weight { get; }
internal TextField(string name, double weight = 1.0) : base(name, FieldType.FullText)
{
Weight = weight;
}
internal override void SerializeRedisArgs(List<object> args)
{
base.SerializeRedisArgs(args);
if (Weight != 1.0)
{
args.Add("WEIGHT".Literal());
args.Add(Weight);
}
}
}
public List<Field> Fields { get; } = new List<Field>();
/// <summary>
/// Add a text field to the schema with a given weight
/// </summary>
/// <param name="name">the field's name</param>
/// <param name="weight">its weight, a positive floating point number</param>
/// <returns>the schema object</returns>
public Schema AddTextField(string name, double weight = 1.0)
{
Fields.Add(new TextField(name, weight));
return this;
}
/// <summary>
/// Add a numeric field to the schema
/// </summary>
/// <param name="name">the field's name</param>
/// <returns>the schema object</returns>
public Schema AddGeoField(string name)
{
Fields.Add(new Field(name, FieldType.Geo));
return this;
}
/// <summary>
/// Add a numeric field to the schema
/// </summary>
/// <param name="name">the field's name</param>
/// <returns>the schema object</returns>
public Schema AddNumericField(string name)
{
Fields.Add(new Field(name, FieldType.Numeric));
return this;
}
}
}
// .NET port of https://github.com/RedisLabs/JRediSearch/
using StackExchange.Redis;
using System.Collections.Generic;
namespace StackExchange.Redis.Modules.RediSearch
{
/// <summary>
/// SearchResult encapsulates the returned result from a search query.
/// It contains publically accessible fields for the total number of results, and an array of <see cref="Document"/>
/// objects conatining the actual returned documents.
/// </summary>
public class SearchResult
{
public long TotalResults { get; }
public List<Document> Documents { get; }
internal SearchResult(RedisResult[] resp, bool hasContent, bool hasScores, bool hasPayloads)
{
// Calculate the step distance to walk over the results.
// The order of results is id, score (if withScore), payLoad (if hasPayloads), fields
int step = 1;
int scoreOffset = 0;
int contentOffset = 1;
int payloadOffset = 0;
if (hasScores)
{
step += 1;
scoreOffset = 1;
contentOffset += 1;
}
if (hasContent)
{
step += 1;
if (hasPayloads)
{
payloadOffset = scoreOffset + 1;
step += 1;
contentOffset += 1;
}
}
// the first element is always the number of results
TotalResults = (long)resp[0];
var docs = new List<Document>((resp.Length - 1) / step);
Documents = docs;
for (int i = 1; i < resp.Length; i += step)
{
var id = (string)resp[i];
double score = 1.0;
byte[] payload = null;
RedisValue[] fields = null;
if (hasScores)
{
score = (double)resp[i + scoreOffset];
}
if (hasPayloads)
{
payload = (byte[])resp[i + payloadOffset];
}
if (hasContent)
{
fields = (RedisValue[])resp[i + contentOffset];
}
docs.Add(Document.Load(id, score, payload, fields));
}
}
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(LibraryTargetFrameworks)</TargetFrameworks>
<VersionPrefix>0.1</VersionPrefix>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<PackageTags>Redis;Search;Modules;RediSearch</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StackExchange.Redis\StackExchange.Redis.csproj" />
</ItemGroup>
</Project>
\ No newline at end of file
using System;
using System.Linq;
using System.Threading.Tasks;
namespace StackExchange.Redis.Modules.Throttling
{
public static class ThrottlingExtensions
{
public static ThrottleResult Throttle(
this IDatabase db, RedisKey key, int maxBurst,
int maxPerInterval,
int intervalSeconds = 60, int count = 1)
{
return new ThrottleResult(db.Execute("CL.THROTTLE",
key, maxBurst.Boxed(), maxPerInterval.Boxed(), intervalSeconds.Boxed(), count.Boxed()));
}
public async static Task<ThrottleResult> ThrottleAsync(
this IDatabaseAsync db, RedisKey key, int maxBurst,
int maxPerInterval,
int intervalSeconds = 60, int count = 1)
{
return new ThrottleResult(await db.ExecuteAsync("CL.THROTTLE",
key, maxBurst.Boxed(), maxPerInterval.Boxed(), intervalSeconds.Boxed(), count.Boxed()));
}
static readonly object[] _boxedInt32 = Enumerable.Range(-1, 128).Select(i => (object)i).ToArray();
internal static object Boxed(this int value)
=> value >= -1 && value <= 126 ? _boxedInt32[value + 1] : (object)value;
}
public struct ThrottleResult
{
internal ThrottleResult(RedisResult result)
{
var arr = (int[])result;
Permitted = arr[0] == 0;
TotalLimit = arr[1];
RemainingLimit = arr[2];
RetryAfterSeconds = arr[3];
ResetAfterSeconds = arr[4];
}
/// <summary>Whether the action was limited</summary>
public bool Permitted {get;}
/// <summary>The total limit of the key (max_burst + 1). This is equivalent to the common `X-RateLimit-Limit` HTTP header.</summary>
public int TotalLimit {get;}
/// <summary>The remaining limit of the key. Equivalent to `X-RateLimit-Remaining`.</summary>
public int RemainingLimit {get;}
/// <summary>The number of seconds until the user should retry, and always -1 if the action was allowed. Equivalent to `Retry-After`.</summary>
public int RetryAfterSeconds {get;}
/// <summary>The number of seconds until the limit will reset to its maximum capacity. Equivalent to `X-RateLimit-Reset`.</summary>
public int ResetAfterSeconds {get;}
}
}
\ No newline at end of file
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