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" />
......
// .NET port of https://github.com/RedisLabs/JRediSearch/
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace StackExchange.Redis.Modules.RediSearch
{
public sealed class Client
{
[Flags]
public enum IndexOptions
{
/// <summary>
/// All options disabled
/// </summary>
None = 0,
/// <summary>
/// Set this to tell the index not to save term offset vectors. This reduces memory consumption but does not
/// allow performing exact matches, and reduces overall relevance of multi-term queries
/// </summary>
UseTermOffsets = 1,
/// <summary>
/// If set (default), we keep flags per index record telling us what fields the term appeared on,
/// and allowing us to filter results by field
/// </summary>
KeepFieldFlags = 2,
/// <summary>
/// If set, we keep an index of the top entries per term, allowing extremely fast single word queries
/// regardless of index size, at the cost of more memory
/// </summary>
UseScoreIndexes = 4,
/// <summary>
/// The default indexing options - use term offsets and keep fields flags
/// </summary>
Default = UseTermOffsets | KeepFieldFlags
}
private static void SerializeRedisArgs(IndexOptions flags, List<object> args)
{
if ((flags & IndexOptions.UseTermOffsets) == 0)
{
args.Add("NOOFFSETS".Literal());
}
if ((flags & IndexOptions.KeepFieldFlags) == 0)
{
args.Add("NOFIELDS".Literal());
}
if ((flags & IndexOptions.UseScoreIndexes) == 0)
{
args.Add("NOSCOREIDX".Literal());
}
}
private readonly IDatabaseAsync _db;
private IDatabase DbSync
=> (_db as IDatabase) ?? throw new InvalidOperationException("Synchronous operations are not available on this database instance");
private readonly object _boxedIndexName;
public RedisKey IndexName => (RedisKey)_boxedIndexName;
public Client(RedisKey indexName, IDatabaseAsync db)
{
_db = db ?? throw new ArgumentNullException(nameof(db));
_boxedIndexName = indexName; // only box once, not per-command
}
public Client(RedisKey indexName, IDatabase db) : this(indexName, (IDatabaseAsync)db) { }
/// <summary>
/// Create the index definition in redis
/// </summary>
/// <param name="schema">a schema definition <seealso cref="Schema"/></param>
/// <param name="options">index option flags <seealso cref="IndexOptions"/></param>
/// <returns>true if successful</returns>
public bool CreateIndex(Schema schema, IndexOptions options)
{
var args = new List<object>();
args.Add(_boxedIndexName);
SerializeRedisArgs(options, args);
args.Add("SCHEMA".Literal());
foreach (var f in schema.Fields)
{
f.SerializeRedisArgs(args);
}
return (string)DbSync.Execute("FT.CREATE", args) == "OK";
}
/// <summary>
/// Create the index definition in redis
/// </summary>
/// <param name="schema">a schema definition <seealso cref="Schema"/></param>
/// <param name="options">index option flags <seealso cref="IndexOptions"/></param>
/// <returns>true if successful</returns>
public async Task<bool> CreateIndexAsync(Schema schema, IndexOptions options)
{
var args = new List<object>();
args.Add(_boxedIndexName);
SerializeRedisArgs(options, args);
args.Add("SCHEMA".Literal());
foreach (var f in schema.Fields)
{
f.SerializeRedisArgs(args);
}
return (string)await _db.ExecuteAsync("FT.CREATE", args).ConfigureAwait(false) == "OK";
}
/// <summary>
/// Search the index
/// </summary>
/// <param name="q">a <see cref="Query"/> object with the query string and optional parameters</param>
/// <returns>a <see cref="SearchResult"/> object with the results</returns>
public SearchResult Search(Query q)
{
var args = new List<object>();
args.Add(_boxedIndexName);
q.SerializeRedisArgs(args);
var resp = (RedisResult[])DbSync.Execute("FT.SEARCH", args);
return new SearchResult(resp, !q.NoContent, q.WithScores, q.WithPayloads);
}
/// <summary>
/// Search the index
/// </summary>
/// <param name="q">a <see cref="Query"/> object with the query string and optional parameters</param>
/// <returns>a <see cref="SearchResult"/> object with the results</returns>
public async Task<SearchResult> SearchAsync(Query q)
{
var args = new List<object>();
args.Add(_boxedIndexName);
q.SerializeRedisArgs(args);
var resp = (RedisResult[])await _db.ExecuteAsync("FT.SEARCH", args).ConfigureAwait(false);
return new SearchResult(resp, !q.NoContent, q.WithScores, q.WithPayloads);
}
/// <summary>
/// Add a single document to the query
/// </summary>
/// <param name="docId">the id of the document. It cannot belong to a document already in the index unless replace is set</param>
/// <param name="score">the document's score, floating point number between 0 and 1</param>
/// <param name="fields">a map of the document's fields</param>
/// <param name="noSave">if set, we only index the document and do not save its contents. This allows fetching just doc ids</param>
/// <param name="replace">if set, and the document already exists, we reindex and update it</param>
/// <param name="payload">if set, we can save a payload in the index to be retrieved or evaluated by scoring functions on the server</param>
public bool AddDocument(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, bool noSave = false, bool replace = false, byte[] payload = null)
{
var args = BuildAddDocumentArgs(docId, fields, score, noSave, replace, payload);
return (string)DbSync.Execute("FT.ADD", args) == "OK";
}
/// <summary>
/// Add a single document to the query
/// </summary>
/// <param name="docId">the id of the document. It cannot belong to a document already in the index unless replace is set</param>
/// <param name="score">the document's score, floating point number between 0 and 1</param>
/// <param name="fields">a map of the document's fields</param>
/// <param name="noSave">if set, we only index the document and do not save its contents. This allows fetching just doc ids</param>
/// <param name="replace">if set, and the document already exists, we reindex and update it</param>
/// <param name="payload">if set, we can save a payload in the index to be retrieved or evaluated by scoring functions on the server</param>
public async Task<bool> AddDocumentAsync(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, bool noSave = false, bool replace = false, byte[] payload = null)
{
var args = BuildAddDocumentArgs(docId, fields, score, noSave, replace, payload);
return (string)await _db.ExecuteAsync("FT.ADD", args).ConfigureAwait(false) == "OK";
}
private List<object> BuildAddDocumentArgs(string docId, Dictionary<string, RedisValue> fields, double score, bool noSave, bool replace, byte[] payload)
{
var args = new List<object> { _boxedIndexName, docId, score };
if (noSave)
{
args.Add("NOSAVE".Literal());
}
if (replace)
{
args.Add("REPLACE".Literal());
}
if (payload != null)
{
args.Add("PAYLOAD".Literal());
// TODO: Fix this
args.Add(payload);
}
args.Add("FIELDS".Literal());
foreach (var ent in fields)
{
args.Add(ent.Key);
args.Add(ent.Value);
}
return args;
}
/// <summary>
/// replaceDocument is a convenience for calling addDocument with replace=true
/// </summary>
public bool ReplaceDocument(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, byte[] payload = null)
=> AddDocument(docId, fields, score, false, true, payload);
/// <summary>
/// replaceDocument is a convenience for calling addDocument with replace=true
/// </summary>
public Task<bool> ReplaceDocumentAsync(string docId, Dictionary<string, RedisValue> fields, double score = 1.0, byte[] payload = null)
=> AddDocumentAsync(docId, fields, score, false, true, payload);
/// <summary>
/// Index a document already in redis as a HASH key.
/// </summary>
/// <param name="docId">the id of the document in redis. This must match an existing, unindexed HASH key</param>
/// <param name="score">the document's index score, between 0 and 1</param>
/// <param name="replace">if set, and the document already exists, we reindex and update it</param>
/// <returns>true on success</returns>
public bool AddHash(string docId, double score, bool replace)
{
var args = new List<object> { _boxedIndexName, docId, score };
if (replace)
{
args.Add("REPLACE".Literal());
}
return (string)DbSync.Execute("FT.ADDHASH", args) == "OK";
}
/// <summary>
/// Index a document already in redis as a HASH key.
/// </summary>
/// <param name="docId">the id of the document in redis. This must match an existing, unindexed HASH key</param>
/// <param name="score">the document's index score, between 0 and 1</param>
/// <param name="replace">if set, and the document already exists, we reindex and update it</param>
/// <returns>true on success</returns>
public async Task<bool> AddHashAsync(string docId, double score, bool replace)
{
var args = new List<object> { _boxedIndexName, docId, score };
if (replace)
{
args.Add("REPLACE".Literal());
}
return (string)await _db.ExecuteAsync("FT.ADDHASH", args).ConfigureAwait(false) == "OK";
}
/// <summary>
/// Get the index info, including memory consumption and other statistics.
/// </summary>
/// <remarks>TODO: Make a class for easier access to the index properties</remarks>
/// <returns>a map of key/value pairs</returns>
public Dictionary<string, RedisValue> GetInfo()
{
return ParseGetInfo(DbSync.Execute("FT.INFO", _boxedIndexName));
}
/// <summary>
/// Get the index info, including memory consumption and other statistics.
/// </summary>
/// <remarks>TODO: Make a class for easier access to the index properties</remarks>
/// <returns>a map of key/value pairs</returns>
public async Task<Dictionary<string, RedisValue>> GetInfoAsync()
{
return ParseGetInfo(await _db.ExecuteAsync("FT.INFO", _boxedIndexName).ConfigureAwait(false));
}
static Dictionary<string, RedisValue> ParseGetInfo(RedisResult value)
{
var res = (RedisValue[])value;
var info = new Dictionary<string, RedisValue>();
for (int i = 0; i < res.Length; i += 2)
{
var key = (string)res[i];
var val = res[i + 1];
info.Add(key, val);
}
return info;
}
/// <summary>
/// Delete a document from the index.
/// </summary>
/// <param name="docId">the document's id</param>
/// <returns>true if it has been deleted, false if it did not exist</returns>
public bool DeleteDocument(string docId)
{
return (long)DbSync.Execute("FT.DEL", _boxedIndexName, docId) == 1;
}
/// <summary>
/// Delete a document from the index.
/// </summary>
/// <param name="docId">the document's id</param>
/// <returns>true if it has been deleted, false if it did not exist</returns>
public async Task<bool> DeleteDocumentAsync(string docId)
{
return (long)await _db.ExecuteAsync("FT.DEL", _boxedIndexName, docId).ConfigureAwait(false) == 1;
}
/// <summary>
/// Drop the index and all associated keys, including documents
/// </summary>
/// <returns>true on success</returns>
public bool DropIndex()
{
return (string)DbSync.Execute("FT.DROP", _boxedIndexName) == "OK";
}
/// <summary>
/// Drop the index and all associated keys, including documents
/// </summary>
/// <returns>true on success</returns>
public async Task<bool> DropIndexAsync()
{
return (string) await _db.ExecuteAsync("FT.DROP", _boxedIndexName).ConfigureAwait(false) == "OK";
}
/// <summary>
/// Optimize memory consumption of the index by removing extra saved capacity. This does not affect speed
/// </summary>
public long OptimizeIndex()
{
return (long)DbSync.Execute("FT.OPTIMIZE", _boxedIndexName);
}
/// <summary>
/// Optimize memory consumption of the index by removing extra saved capacity. This does not affect speed
/// </summary>
public async Task<long> OptimizeIndexAsync()
{
return (long) await _db.ExecuteAsync("FT.OPTIMIZE", _boxedIndexName).ConfigureAwait(false);
}
/// <summary>
/// Get the size of an autoc-complete suggestion dictionary
/// </summary>
public long CountSuggestions()
=> (long)DbSync.Execute("FT.SUGLEN", _boxedIndexName);
/// <summary>
/// Get the size of an autoc-complete suggestion dictionary
/// </summary>
public async Task<long> CountSuggestionsAsync()
=> (long)await _db.ExecuteAsync("FT.SUGLEN", _boxedIndexName).ConfigureAwait(false);
/// <summary>
/// Add a suggestion string to an auto-complete suggestion dictionary. This is disconnected from the index definitions, and leaves creating and updating suggestino dictionaries to the user.
/// </summary>
/// <param name="value">the suggestion string we index</param>
/// <param name="score">a floating point number of the suggestion string's weight</param>
/// <param name="increment">if set, we increment the existing entry of the suggestion by the given score, instead of replacing the score. This is useful for updating the dictionary based on user queries in real time</param>
/// <returns>the current size of the suggestion dictionary.</returns>
public long AddSuggestion(string value, double score, bool increment = false)
{
object args = increment
? new object[] { _boxedIndexName, value, score, "INCR".Literal() }
: new object[] { _boxedIndexName, value, score };
return (long)DbSync.Execute("FT.SUGADD", args);
}
/// <summary>
/// Add a suggestion string to an auto-complete suggestion dictionary. This is disconnected from the index definitions, and leaves creating and updating suggestino dictionaries to the user.
/// </summary>
/// <param name="value">the suggestion string we index</param>
/// <param name="score">a floating point number of the suggestion string's weight</param>
/// <param name="increment">if set, we increment the existing entry of the suggestion by the given score, instead of replacing the score. This is useful for updating the dictionary based on user queries in real time</param>
/// <returns>the current size of the suggestion dictionary.</returns>
public async Task<long> AddSuggestionAsync(string value, double score, bool increment = false)
{
object args = increment
? new object[] { _boxedIndexName, value, score, "INCR".Literal() }
: new object[] { _boxedIndexName, value, score };
return (long)await _db.ExecuteAsync("FT.SUGADD", args).ConfigureAwait(false);
}
/// <summary>
/// Delete a string from a suggestion index.
/// </summary>
/// <param name="value">the string to delete</param>
public bool DeleteSuggestion(string value)
=> (long)DbSync.Execute("FT.SUGDEL", _boxedIndexName, value) == 1;
/// <summary>
/// Delete a string from a suggestion index.
/// </summary>
/// <param name="value">the string to delete</param>
public async Task<bool> DeleteSuggestionAsync(string value)
=> (long)await _db.ExecuteAsync("FT.SUGDEL", _boxedIndexName, value).ConfigureAwait(false) == 1;
/// <summary>
/// Get completion suggestions for a prefix
/// </summary>
/// <param name="prefix">the prefix to complete on</param>
/// <param name="fuzzy"> if set,we do a fuzzy prefix search, including prefixes at levenshtein distance of 1 from the prefix sent</param>
/// <param name="max">If set, we limit the results to a maximum of num. (Note: The default is 5, and the number cannot be greater than 10).</param>
/// <returns>a list of the top suggestions matching the prefix</returns>
public string[] GetSuggestions(string prefix, bool fuzzy = false, int max = 5)
{
var args = new List<object> { _boxedIndexName, prefix};
if (fuzzy) args.Add("FUZZY".Literal());
if (max != 5)
{
args.Add("MAX".Literal());
args.Add(max);
}
return (string[])DbSync.Execute("FT.SUGGET", args);
}
/// <summary>
/// Get completion suggestions for a prefix
/// </summary>
/// <param name="prefix">the prefix to complete on</param>
/// <param name="fuzzy"> if set,we do a fuzzy prefix search, including prefixes at levenshtein distance of 1 from the prefix sent</param>
/// <param name="max">If set, we limit the results to a maximum of num. (Note: The default is 5, and the number cannot be greater than 10).</param>
/// <returns>a list of the top suggestions matching the prefix</returns>
public async Task<string[]> GetSuggestionsAsync(string prefix, bool fuzzy = false, int max = 5)
{
var args = new List<object> { _boxedIndexName, prefix };
if (fuzzy) args.Add("FUZZY".Literal());
if (max != 5)
{
args.Add("MAX".Literal());
args.Add(max);
}
return (string[])await _db.ExecuteAsync("FT.SUGGET", args).ConfigureAwait(false);
}
}
}
// .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