From 4d9ac2e69256bf45521f94e8728c2b3db41030bd Mon Sep 17 00:00:00 2001 From: 2881099 <2881099@qq.com> Date: Sat, 19 Oct 2024 20:48:44 +0800 Subject: [PATCH] add RediSearch FT.cmd --- .../RedisClient/Modules/RediSearch.cs | 489 ++---------------- .../Modules/RediSearch/AggregateBuilder.cs | 182 +++++++ .../Modules/RediSearch/CreateBuilder.cs | 412 +++++++++++++++ .../Modules/RediSearch/SearchBuilder.cs | 317 ++++++++++++ 4 files changed, 963 insertions(+), 437 deletions(-) create mode 100644 src/FreeRedis/RedisClient/Modules/RediSearch/AggregateBuilder.cs create mode 100644 src/FreeRedis/RedisClient/Modules/RediSearch/CreateBuilder.cs create mode 100644 src/FreeRedis/RedisClient/Modules/RediSearch/SearchBuilder.cs diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch.cs b/src/FreeRedis/RedisClient/Modules/RediSearch.cs index ae412f7..42e6ce4 100644 --- a/src/FreeRedis/RedisClient/Modules/RediSearch.cs +++ b/src/FreeRedis/RedisClient/Modules/RediSearch.cs @@ -1,48 +1,29 @@ -using System; +using FreeRedis.RediSearch; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; using System.Linq; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using static FreeRedis.FtCreateParams; namespace FreeRedis { partial class RedisClient { - public string[] FtAggregate() => Call("FT._LIST", rt => rt.ThrowOrValue()); - public void FtCreate(string index, FieldInfo[] schema, FtCreateParams param) => Call(FtCreateParams.ToCommandPacket(index, schema, param), rt => rt.ThrowOrValue() == "OK"); - public void FtAlter(string index, bool skipInitialScan = false) => Call("FT.ALTER".Input(index).InputIf(skipInitialScan, "SKIPINITIALSCAN"), rt => rt.ThrowOrValue() == "OK"); - public FtAggregationResult FtCursorRead(string index, long cursor_id, int count = 0) => Call("FT.CURSOR".SubCommand("READ") - .Input(index, cursor_id) - .InputIf(count > 0, "COUNT", count), rt => rt.ThrowOrValueToFtCursorRead()); - public object FtInfo(string index) => Call("FT.INFO".Input(index), rt => rt.ThrowOrValue()); - public object FtProfile(string index) => Call("FT.PROFILE".Input(index), rt => rt.ThrowOrValue()); - public object FtSearch(string index, string query) => Call("FT.SEARCH".Input(index), rt => rt.ThrowOrValue()); - public object FtSpellCheck(string index) => Call("FT.SPELLCHECK".Input(index), rt => rt.ThrowOrValue()); - - - public string[] Ft_List() => Call("FT._LIST", rt => rt.ThrowOrValue()); - - //public string[] FtAggregate() => Call("FT._LIST", rt => rt.ThrowOrValue()); + public AggregateBuilder FtAggregate(string index, string query) => new AggregateBuilder(this, index, query); public void FtAliasAdd(string alias, string index) => Call("FT.ALIASADD".Input(alias, index), rt => rt.ThrowOrValue() == "OK"); public void FtAliasDel(string alias) => Call("FT.ALIASDEL".Input(alias), rt => rt.ThrowOrValue() == "OK"); public void FtAliasUpdate(string alias, string index) => Call("FT.ALIASUPDATE".Input(alias, index), rt => rt.ThrowOrValue() == "OK"); - //public void FtAlter (string index, bool skipInitialScan = false) => Call("FT.ALTER".Input(index).InputIf(skipInitialScan, "SKIPINITIALSCAN"), rt => rt.ThrowOrValue() == "OK"); + public AlterBuilder FtAlter(string index) => new AlterBuilder(this, index); public Dictionary FtConfigGet(string option, string value) => Call("FT.CONFIG".SubCommand("GET").Input(option, value), rt => rt.ThrowOrValue((a, _) => a.MapToHash(rt.Encoding))); public void FtConfigSet(string option, string value) => Call("FT.CONFIG".SubCommand("SET").Input(option, value), rt => rt.ThrowOrValue() == "OK"); - //public void FtCreate(string index, FieldInfo[] schema, FtCreateParams param) => Call(FtCreateParams.ToCommandPacket(index, schema, param), rt => rt.ThrowOrValue() == "OK"); + public CreateBuilder FtCreate(string index) => new CreateBuilder(this, index); public void FtCursorDel(string index, long cursor_id) => Call("FT.CURSOR".SubCommand("DEL").Input(index, cursor_id), rt => rt.ThrowOrValue() == "OK"); - //public FtAggregationResult FtCursorRead(string index, long cursor_id, int count = 0) => Call("FT.CURSOR".SubCommand("READ") - // .Input(index, cursor_id) - // .InputIf(count > 0, "COUNT", count), rt => rt.ThrowOrValueToFtCursorRead()); + public AggregationResult FtCursorRead(string index, long cursorId, int count = 0) => Call("FT.CURSOR".SubCommand("READ") + .Input(index, cursorId) + .InputIf(count > 0, "COUNT", count), rt => rt.ThrowOrValue((a, _) => new AggregationResult(a[0], a[1].ConvertTo()))); public long FtDictAdd(string dict, params string[] terms) => Call("FT.DICTADD".Input(dict).Input(terms.Select(a => (object)a).ToArray()), rt => rt.ThrowOrValue()); public long FtDictDel(string dict, params string[] terms) => Call("FT.DICTDEL".Input(dict).Input(terms.Select(a => (object)a).ToArray()), rt => rt.ThrowOrValue()); @@ -54,427 +35,61 @@ public FtAggregationResult FtCursorRead(string index, long cursor_id, int count //public object FtInfo(string index) => Call("FT.INFO".Input(index), rt => rt.ThrowOrValue()); //public object FtProfile(string index) => Call("FT.PROFILE".Input(index), rt => rt.ThrowOrValue()); - //public object FtSearch(string index, string query) => Call("FT.SEARCH".Input(index), rt => rt.ThrowOrValue()); - //public object FtSpellCheck(string index) => Call("FT.SPELLCHECK".Input(index), rt => rt.ThrowOrValue()); + public SearchBuilder FtSearch(string index, string query) => new SearchBuilder(this, index, query); + public Dictionary> FtSpellCheck(string index, string query, int distance = 1, int? dialect = null) => Call("FT.SPELLCHECK".Input(index, query) + .InputIf(distance > 1, "DISTANCE", distance) + .InputIf((dialect ?? ConnectionString.FtDialect) > 0, "DIALECT", dialect), rt => rt.ThrowOrValueToFtSpellCheckResult()); public Dictionary FtSynDump(string index) => Call("FT.SYNDUMP".Input(index), rt => rt.ThrowOrValue((a, _) => a.MapToHash(rt.Encoding))); - public void FtSynUpdate(string index, string synonym_group_id, bool skipInitialScan, params string[] terms) => Call("FT.SYNUPDATE" - .Input(index, synonym_group_id) + public void FtSynUpdate(string index, string synonymGroupId, bool skipInitialScan, params string[] terms) => Call("FT.SYNUPDATE" + .Input(index, synonymGroupId) .InputIf(skipInitialScan, "SKIPINITIALSCAN") .Input(terms.Select(a => (object)a).ToArray()), rt => rt.ThrowOrValue() == "OK"); - public string[] FtTagVals(string index, string field_name) => Call("FT.TAGVALS".Input(index, field_name), rt => rt.ThrowOrValue()); - + public long FtSugAdd(string key, string str, double score, bool incr = false, string payload = null) => Call("FT.SUGADD".InputKey(key).Input(str, score) + .InputIf(incr, "INCR") + .InputIf(payload != null, "PAYLOAD", payload), rt => rt.ThrowOrValue()); + public void FtSugDel(string key, string str) => Call("FT.SUGDEL".InputKey(key).Input(str), rt => rt.ThrowOrValue()); + public string[] FtSugGet(string key, string prefix, bool fuzzy = false, bool withScores = false, bool withPayloads = false, int? max = null) => Call("FT.SUGGET".InputKey(key) + .Input(prefix) + .InputIf(fuzzy, "FUZZY") + .InputIf(withScores, "WITHSCORES") + .InputIf(withPayloads, "WITHPAYLOADS") + .InputIf(max != null, "MAX", max), rt => rt.ThrowOrValue()); + public void FtSugLen(string key) => Call("FT.SUGLEN".InputKey(key), rt => rt.ThrowOrValue()); + + public string[] FtTagVals(string index, string fieldName) => Call("FT.TAGVALS".Input(index, fieldName), rt => rt.ThrowOrValue()); } static partial class RedisResultThrowOrValueExtensions { - public static FtAggregationResult ThrowOrValueToFtCursorRead(this RedisResult rt) => + public static Dictionary> ThrowOrValueToFtSpellCheckResult(this RedisResult rt) => + rt.ThrowOrValue((rawTerms, _) => + { + var returnTerms = new Dictionary>(rawTerms.Length); + foreach (var term in rawTerms) + { + var rawElements = term as object[]; + string termValue = rawElements[1].ConvertTo(); + + var list = rawElements[2] as object[]; + var entries = new Dictionary(list.Length); + foreach (var entry in list) + { + var entryElements = entry as object[]; + string suggestion = entryElements[1].ConvertTo(); + double score = entryElements[0].ConvertTo(); + entries.Add(suggestion, score); + } + returnTerms.Add(termValue, entries); + } + + return returnTerms; + }); + public static object ThrowOrValueToFtCursorRead(this RedisResult rt) => rt.ThrowOrValue((a, _) => { - return new FtAggregationResult(); + return a; }); } - - public class FtAggregationResult - { - public long TotalResults { get; } - public Dictionary[] Results; - public long CursorId { get; } - } - public class FtSearchBuilder - { - private bool _noContent, _verbatim, _noStopWords, _withScores, _withPayLoads, _withSortKeys; - private List _filter = new List(); - private List _geoFilter = new List(); - private List _inKeys = new List(); - private List _inFields = new List(); - private List _return = new List(); - private bool _sumarize; - private string[] _sumarizeFields; - private long _sumarizeFrags = -1; - private long _sumarizeLen = -1; - private string _sumarizeSeparator; - private bool _highLight; - private List _highLightFields = new List(); - private string[] _highLightTags; - private decimal _slop = -1; - private long _timeout; - private bool _inOrder; - private string _language, _expander, _scorer; - private bool _explainScore; - private string _payLoad; - private string _sortBy; - private bool _sortByDesc; - private bool _sortByWithCount; - private long _limitOffset, _limitNum; - private List _params = new List(); - private int _dialect; - - public FtSearchBuilder NoContent(bool value = true) - { - _noContent = value; - return this; - } - public FtSearchBuilder Verbatim(bool value = true) - { - _verbatim = value; - return this; - } - public FtSearchBuilder NoStopWords(bool value = true) - { - _noStopWords = value; - return this; - } - public FtSearchBuilder WithScores(bool value = true) - { - _withScores = value; - return this; - } - public FtSearchBuilder WithPayLoads(bool value = true) - { - _withPayLoads = value; - return this; - } - public FtSearchBuilder WithSortKeys(bool value = true) - { - _withSortKeys = value; - return this; - } - public FtSearchBuilder Filter(string field, string min, string max) - { - _filter.Add(new object[] { field, min, max }); - return this; - } - public FtSearchBuilder GeoFilter(string field, string lon, string lat, decimal radius, GeoUnit unit = GeoUnit.m) - { - _geoFilter.Add(new object[] { field, lon, lat, radius, unit }); - return this; - } - public FtSearchBuilder InKeys(params string[] key) - { - if (key?.Any() == true) _inKeys.AddRange(key); - return this; - } - public FtSearchBuilder InFields(params string[] field) - { - if (field?.Any() == true) _inFields.AddRange(field); - return this; - } - - public FtSearchBuilder Return(params string[] identifier) - { - if (identifier?.Any() == true) _return.AddRange(identifier.Select(a => new[] { a, null })); - return this; - } - public FtSearchBuilder Return(Dictionary identifier) - { - if (identifier?.Any() == true) _return.AddRange(identifier.Select(a => new[] { a.Key, a.Value })); - return this; - } - public FtSearchBuilder Sumarize(string[] fields, long frags, long len, string separator) - { - _sumarize = true; - _sumarizeFields = fields; - _sumarizeFrags = frags; - _sumarizeLen = len; - _sumarizeSeparator = separator; - return this; - } - public FtSearchBuilder HighLight(string[] fields, string tagsOpen, string tagsClose) - { - _highLight = true; - _highLightTags = new[] { tagsOpen, tagsClose }; - return this; - } - public FtSearchBuilder Slop(decimal value) - { - _slop = value; - return this; - } - public FtSearchBuilder Timeout(long milliseconds) - { - _timeout = milliseconds; - return this; - } - public FtSearchBuilder InOrder(bool value = true) - { - _inOrder = value; - return this; - } - public FtSearchBuilder Language(string value) - { - _language = value; - return this; - } - public FtSearchBuilder Expander(string value) - { - _expander = value; - return this; - } - public FtSearchBuilder Scorer(string value) - { - _scorer = value; - return this; - } - public FtSearchBuilder ExplainScore(bool value) - { - _explainScore = value; - return this; - } - public FtSearchBuilder PayLoad(string value) - { - _payLoad = value; - return this; - } - public FtSearchBuilder SortBy(string sortBy, bool desc = false, bool withCount = false) - { - _sortBy = sortBy; - _sortByDesc = desc; - _sortByWithCount = withCount; - return this; - } - public FtSearchBuilder Limit(long offset, long num) - { - _limitOffset = offset; - _limitNum = num; - return this; - } - public FtSearchBuilder Params(string name, string value) - { - _params.Add(name); - _params.Add(value); - return this; - } - public FtSearchBuilder Dialect(int value) - { - _dialect = value; - return this; - } - } - public class FtSearchParams - { - public class FilterInfo - { - public string Field { get; } - public string Min { get; } - public string Max { get; } - public FilterInfo(string field, string min, string max) - { - this.Field = field; - this.Min = min; - this.Max = max; - } - } - public class GeoFilterInfo - { - public string Field { get; } - public string Lon { get; } - public string Lat { get; } - public decimal Radius { get; } - public GeoUnit Unit { get; } - public GeoFilterInfo(string field, string lon, string lat, decimal radius, GeoUnit unit = GeoUnit.m) - { - this.Field = field; - this.Lon = lon; - this.Lat = lat; - this.Radius = radius; - this.Unit = unit; - } - } - - public bool NoContent { get; set; } - public bool Verbatim { get; set; } - public bool NoStopWords { get; set; } - public bool WithScores { get; set; } - public bool WithPayLoads { get; set; } - public bool WithSortKeys { get; set; } - public FilterInfo[] Filter { get; set; } - public GeoFilterInfo[] GeoFilter { get; set; } - public string[] InKeys { get; set; } - public string[] InFields { get; set; } - public List _return = new List(); - public bool _sumarize; - public string[] _sumarizeFields; - public long _sumarizeFrags = -1; - public long _sumarizeLen = -1; - public string _sumarizeSeparator; - public bool _highLight; - public List _highLightFields = new List(); - public string[] _highLightTags; - public decimal _slop = -1; - public long _timeout; - public bool _inOrder; - public string _language, _expander, _scorer; - public bool _explainScore; - public string _payLoad; - public string _sortBy; - public bool _sortByDesc; - public bool _sortByWithCount; - public long _limitOffset, _limitNum; - public List _params = new List(); - public int _dialect; - - - public FtSearchBuilder Return(params string[] identifier) - { - if (identifier?.Any() == true) _return.AddRange(identifier.Select(a => new[] { a, null })); - return this; - } - public FtSearchBuilder Return(Dictionary identifier) - { - if (identifier?.Any() == true) _return.AddRange(identifier.Select(a => new[] { a.Key, a.Value })); - return this; - } - public FtSearchBuilder Sumarize(string[] fields, long frags, long len, string separator) - { - _sumarize = true; - _sumarizeFields = fields; - _sumarizeFrags = frags; - _sumarizeLen = len; - _sumarizeSeparator = separator; - return this; - } - public FtSearchBuilder HighLight(string[] fields, string tagsOpen, string tagsClose) - { - _highLight = true; - _highLightTags = new[] { tagsOpen, tagsClose }; - return this; - } - public FtSearchBuilder Slop(decimal value) - { - _slop = value; - return this; - } - public FtSearchBuilder Timeout(long milliseconds) - { - _timeout = milliseconds; - return this; - } - public FtSearchBuilder InOrder(bool value = true) - { - _inOrder = value; - return this; - } - public FtSearchBuilder Language(string value) - { - _language = value; - return this; - } - public FtSearchBuilder Expander(string value) - { - _expander = value; - return this; - } - public FtSearchBuilder Scorer(string value) - { - _scorer = value; - return this; - } - public FtSearchBuilder ExplainScore(bool value) - { - _explainScore = value; - return this; - } - public FtSearchBuilder PayLoad(string value) - { - _payLoad = value; - return this; - } - public FtSearchBuilder SortBy(string sortBy, bool desc = false, bool withCount = false) - { - _sortBy = sortBy; - _sortByDesc = desc; - _sortByWithCount = withCount; - return this; - } - public FtSearchBuilder Limit(long offset, long num) - { - _limitOffset = offset; - _limitNum = num; - return this; - } - public FtSearchBuilder Params(string name, string value) - { - _params.Add(name); - _params.Add(value); - return this; - } - public FtSearchBuilder Dialect(int value) - { - _dialect = value; - return this; - } - } - public class FtCreateParams - { - public enum OnType { Hash, Json } - public enum FieldType { Text, Tag, Numeric, Geo, Vector, GeoShape } - public class FieldInfo - { - public string Name { get; set; } - public string Alias { get; set; } - public FieldType Type { get; set; } - public bool Sortable { get; set; } - public bool Unf { get; set; } - public bool NoIndex { get; set; } - } - - public OnType On { get; set; } = OnType.Hash; - public string[] Prefix { get; set; } - public string Filter { get; set; } - public string Language { get; set; } - public string LanguageField { get; set; } - public decimal Score { get; set; } - public decimal ScoreField { get; set; } - public string PayLoadField { get; set; } - public bool MaxTextFields { get; set; } - public long Temporary { get; set; } - public bool NoOffsets { get; set; } - public bool NoHL { get; set; } - public bool NoFields { get; set; } - public bool NoFreqs { get; set; } - public string[] Stopwords { get; set; } - public bool SkipInitialScan { get; set; } - - public static CommandPacket ToCommandPacket(string index, FieldInfo[] schema, FtCreateParams param) - { - var on = param?.On ?? OnType.Hash; - var cmd = "FT.CREATE".Input(index).Input("ON", on.ToString().ToUpper()); - if (param != null) - { - if (param.Prefix?.Any() == true) cmd.Input("PREFIX", param.Prefix.Length).Input(param.Prefix.Select(a => (object)a).ToArray()); - cmd - .InputIf(!string.IsNullOrWhiteSpace(param.Filter), "FILTER", param.Filter) - .InputIf(!string.IsNullOrWhiteSpace(param.Language), "LANGUAGE", param.Language) - .InputIf(!string.IsNullOrWhiteSpace(param.LanguageField), "LANGUAGE_FIELD", param.LanguageField) - .InputIf(param.Score > 0, "SCORE", param.Score) - .InputIf(param.ScoreField > 0, "SCORE_FIELD", param.ScoreField) - .InputIf(!string.IsNullOrWhiteSpace(param.PayLoadField), "PAYLOAD_FIELD", param.PayLoadField) - .InputIf(param.MaxTextFields, "MAXTEXTFIELDS") - .InputIf(param.Temporary > 0, "TEMPORARY", param.Temporary) - .InputIf(param.NoOffsets, "NOOFFSETS") - .InputIf(param.NoHL, "NOHL") - .InputIf(param.NoFields, "NOFIELDS") - .InputIf(param.NoFreqs, "NOFREQS"); - if (param.Stopwords?.Any() == true) cmd.Input("STOPWORDS", param.Stopwords.Length).Input(param.Stopwords.Select(a => (object)a).ToArray()); - cmd - .InputIf(param.SkipInitialScan, "SKIPINITIALSCAN"); - } - if (schema != null) - { - cmd.Input("SCHEMA"); - foreach (var field in schema) - { - cmd.Input(field.Name) - .InputIf(!string.IsNullOrWhiteSpace(field.Alias), "AS", field.Alias) - .Input(field.Type.ToString().ToUpper()) - .InputIf(field.Sortable, "SORTABLE") - .InputIf(field.Sortable && field.Unf, "UNF") - .InputIf(field.NoIndex, "NOINDEX"); - } - } - return cmd; - } - } -} +} \ No newline at end of file diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch/AggregateBuilder.cs b/src/FreeRedis/RedisClient/Modules/RediSearch/AggregateBuilder.cs new file mode 100644 index 0000000..a2bdbb2 --- /dev/null +++ b/src/FreeRedis/RedisClient/Modules/RediSearch/AggregateBuilder.cs @@ -0,0 +1,182 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FreeRedis.RediSearch +{ + public class AggregationResult + { + public long CursorId { get; } + public Dictionary[] Results { get; } + + public AggregationResult(object result, long cursorId = -1) + { + var arr = result as object[]; + Results = new Dictionary[arr.Length - 1]; + for (int i = 1; i < arr.Length; i++) + { + var raw = arr[i] as object[]; + var cur = new Dictionary(); + for (int j = 0; j < raw.Length;) + { + var key = raw[j++].ConvertTo(); + var val = raw[j++]; + cur.Add(key, val); + } + Results[i - 1] = cur; + } + CursorId = cursorId; + } + } + + public class AggregateBuilder + { + RedisClient _redis; + string _index; + string _query; + internal AggregateBuilder(RedisClient redis, string index, string query) + { + _redis = redis; + _index = index; + _query = query; + _dialect = redis.ConnectionString.FtDialect; + } + public AggregationResult Execute() + { + var cmd = "FT.SEARCH".Input(_index).Input(_query) + .InputIf(_verbatim, "VERBATIM"); + if (_load.Any()) cmd.Input("LOAD", _load.Count).Input(_load.ToArray()); + cmd.InputIf(_timeout >= 0, "TIMEOUT", _timeout); + if (_groupBy.Any()) + { + cmd.Input("GROUPBY", _groupBy.Count).Input(_groupBy.ToArray()); + foreach (var reduce in _groupByReduces) + cmd.Input("REDUCE", reduce.Function, reduce.Arguments?.Length ?? 0).InputIf(!string.IsNullOrWhiteSpace(reduce.Alias), reduce.Alias); + } + if (_sortBy.Any()) cmd.InputIf(_sortBy.Any(), "SORTBY").Input(_sortBy.ToArray()).InputIf(_sortByMax > 0, "MAX", _sortByMax); + cmd.InputIf(_applies.Any(), _applies.ToArray()) + .InputIf(_limitOffset > 0 || _limitNum != 10, "LIMIT", _limitOffset, _limitNum) + .InputIf(!string.IsNullOrWhiteSpace(_filter), "FILTER", _filter); + if (_withCursor) cmd.Input("WITHCURSOR").InputIf(_withCursorCount != -1, "COUNT", _withCursorCount).InputIf(_withCursorMaxIdle != -1, "MAXIDLE", _withCursorMaxIdle); + if (_params.Any()) cmd.Input("PARAMS", _params.Count).Input(_params); + cmd + .InputIf(_dialect > 0, "DIALECT", _dialect); + return _redis.Call(cmd, rt => rt.ThrowOrValue((a, _) => + { + if (_withCursor) return new AggregationResult(a[0], a[1].ConvertTo()); + else return new AggregationResult(a); + })); + } + + private bool _verbatim; + private List _load = new List(); + private long _timeout = -1; + private List _groupBy = new List(); + private List _groupByReduces = new List(); + private List _sortBy; + private int _sortByMax; + private List _applies; + private long _limitOffset, _limitNum = 10; + private string _filter; + private bool _withCursor; + private int _withCursorCount = -1; + private long _withCursorMaxIdle = -1; + private List _params = new List(); + private int _dialect; + + public AggregateBuilder Verbatim(bool value = true) + { + _verbatim = value; + return this; + } + public AggregateBuilder Load(params string[] fields) + { + if (fields?.Any() == true) _load.AddRange(fields); + return this; + } + public AggregateBuilder Timeout(long milliseconds) + { + _timeout = milliseconds; + return this; + } + public AggregateBuilder GroupBy(params string[] properties) + { + if (properties?.Any() == true) _groupBy.AddRange(properties); + return this; + } + public AggregateBuilder GroupBy(string[] properties = null, params AggregateReduce[] reduces) + { + if (properties?.Any() == true) _groupBy.AddRange(properties); + if (reduces?.Any() == true) _groupByReduces.AddRange(reduces); + return this; + } + public AggregateBuilder SortBy(string property, bool desc = false) + { + if (!string.IsNullOrWhiteSpace(property)) + { + _sortBy.Add(property); + if (desc) _sortBy.Add("DESC"); + } + return this; + } + public AggregateBuilder SortBy(string[] properties, bool[] desc, int max = 0) + { + if (properties != null) + { + for (var a = 0; a < properties.Length; a++) + { + if (!string.IsNullOrWhiteSpace(properties[a])) + { + _sortBy.Add(properties[a]); + if (desc != null && a < desc.Length && desc[a]) _sortBy.Add("DESC"); + } + } + } + _sortByMax = max; + return this; + } + public AggregateBuilder Apply(string expression, string alias) + { + _applies.Add("APPLY"); + _applies.Add(expression); + _applies.Add("AS"); + _applies.Add(alias); + return this; + } + public AggregateBuilder Limit(long offset, long num) + { + _limitOffset = offset; + _limitNum = num; + return this; + } + public AggregateBuilder Filter(string value) + { + _filter = value; + return this; + } + public AggregateBuilder WithCursor(int count = -1, long maxIdle = -1) + { + _withCursor = true; + _withCursorCount = count; + _withCursorMaxIdle = maxIdle; + return this; + } + public AggregateBuilder Params(string name, string value) + { + _params.Add(name); + _params.Add(value); + return this; + } + public AggregateBuilder Dialect(int value) + { + _dialect = value; + return this; + } + } + + public class AggregateReduce + { + public string Function { get; set; } + public object[] Arguments { get; set; } + public string Alias { get; set; } + } +} diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch/CreateBuilder.cs b/src/FreeRedis/RedisClient/Modules/RediSearch/CreateBuilder.cs new file mode 100644 index 0000000..cd91970 --- /dev/null +++ b/src/FreeRedis/RedisClient/Modules/RediSearch/CreateBuilder.cs @@ -0,0 +1,412 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FreeRedis.RediSearch +{ + public class AlterBuilder : SchemaBuilder + { + RedisClient _redis; + string _index; + internal AlterBuilder(RedisClient redis, string index) + { + _redis = redis; + _index = index; + } + public void Execute() + { + var cmd = "FT.ALTER".Input(_index) + .InputIf(_skipInitialScan, "SKIPINITIALSCAN") + .Input("SCHEMA") + .Input(_schemaArgs.ToArray()); + _redis.Call(cmd, rt => rt.ThrowOrValue() == "OK"); + } + + private bool _skipInitialScan; + public AlterBuilder SkipInitialScan(bool value = true) + { + _skipInitialScan = value; + return this; + } + } + + public class CreateBuilder : SchemaBuilder + { + RedisClient _redis; + string _index; + internal CreateBuilder(RedisClient redis, string index) + { + _redis = redis; + _index = index; + _language = redis.ConnectionString.FtLanguage; + } + public void Execute() + { + var cmd = "FT.CREATE".Input(_index).InputIf(_on.HasValue, "ON", _on.ToString().ToUpper()); + if (_prefix?.Any() == true) cmd.Input("PREFIX", _prefix.Length).Input(_prefix.Select(a => (object)a).ToArray()); + cmd + .InputIf(!string.IsNullOrWhiteSpace(_filter), "FILTER", _filter) + .InputIf(!string.IsNullOrWhiteSpace(_language), "LANGUAGE", _language) + .InputIf(!string.IsNullOrWhiteSpace(_languageField), "LANGUAGE_FIELD", _languageField) + .InputIf(_score > 0, "SCORE", _score) + .InputIf(_scoreField > 0, "SCORE_FIELD", _scoreField) + .InputIf(!string.IsNullOrWhiteSpace(_payloadField), "PAYLOAD_FIELD", _payloadField) + .InputIf(_maxTextFields, "MAXTEXTFIELDS") + .InputIf(_temporary > 0, "TEMPORARY", _temporary) + .InputIf(_noOffsets, "NOOFFSETS") + .InputIf(_noHL, "NOHL") + .InputIf(_noFields, "NOFIELDS") + .InputIf(_noFreqs, "NOFREQS"); + if (_stopwords?.Any() == true) cmd.Input("STOPWORDS", _stopwords.Length).Input(_stopwords.Select(a => (object)a).ToArray()); + cmd.InputIf(_skipInitialScan, "SKIPINITIALSCAN") + .Input("SCHEMA") + .Input(_schemaArgs.ToArray()); + _redis.Call(cmd, rt => rt.ThrowOrValue() == "OK"); + } + + private IndexDataType? _on = IndexDataType.Hash; + private string[] _prefix; + private string _filter; + private string _language; + private string _languageField; + private decimal _score; + private decimal _scoreField; + private string _payloadField; + private bool _maxTextFields; + private long _temporary; + private bool _noOffsets; + private bool _noHL; + private bool _noFields; + private bool _noFreqs; + private string[] _stopwords; + private bool _skipInitialScan; + public CreateBuilder On(IndexDataType value) + { + _on = value; + return this; + } + public CreateBuilder Prefix(params string[] value) + { + _prefix = value; + return this; + } + public CreateBuilder Filter(string value) + { + _filter = value; + return this; + } + public CreateBuilder Language(string value) + { + _language = value; + return this; + } + public CreateBuilder LanguageField(string value) + { + _languageField = value; + return this; + } + public CreateBuilder Score(decimal value) + { + _score = value; + return this; + } + public CreateBuilder ScoreField(decimal value) + { + _scoreField = value; + return this; + } + public CreateBuilder PayloadField(string value) + { + _payloadField = value; + return this; + } + public CreateBuilder MaxTextFields(bool value = true) + { + _maxTextFields = value; + return this; + } + public CreateBuilder Temporary(long seconds) + { + _temporary = seconds; + return this; + } + public CreateBuilder NoOffsets(bool value = true) + { + _noOffsets = value; + return this; + } + public CreateBuilder NoHL(bool value = true) + { + _noHL = value; + return this; + } + public CreateBuilder NoFields(bool value = true) + { + _noFields = value; + return this; + } + public CreateBuilder NoFreqs(bool value = true) + { + _noFreqs = value; + return this; + } + public CreateBuilder Stopwords(params string[] value) + { + _stopwords = value; + return this; + } + public CreateBuilder SkipInitialScan(bool value = true) + { + _skipInitialScan = value; + return this; + } + } + + public class SchemaBuilder where TBuilder : class + { + protected List _schemaArgs = new List(); + public TBuilder AddTextField(string name, string alias = null, double weight = 1.0, bool sortable = false, bool unf = false, bool noStem = false, + string phonetic = null, bool noIndex = false, bool withSuffixTrie = false, bool missingIndex = false, bool emptyIndex = false) => AddTextField(name, new TextFieldOptions + { + Alias = alias, + Weight = weight, + Sortable = sortable, + Unf = unf, + NoStem = noStem, + Phonetic = phonetic, + NoIndex = noIndex, + WithSuffixTrie = withSuffixTrie, + MissingIndex = missingIndex, + EmptyIndex = emptyIndex + }); + public TBuilder AddTextField(string name, TextFieldOptions options) + { + _schemaArgs.Add(name); + if (options != null) + { + if (!string.IsNullOrWhiteSpace(options.Alias)) + { + _schemaArgs.Add("AS"); + _schemaArgs.Add(options.Alias); + } + _schemaArgs.Add("TEXT"); + if (options.NoStem) _schemaArgs.Add("NOSTEM"); + if (options.NoIndex) _schemaArgs.Add("NOINDEX"); + if (options.Phonetic != null) + { + _schemaArgs.Add("PHONETIC"); + _schemaArgs.Add(options.Phonetic); + } + if (options.Weight != 1.0) + { + _schemaArgs.Add("WEIGHT"); + _schemaArgs.Add(options.Weight); + } + if (options.WithSuffixTrie) _schemaArgs.Add("WITHSUFFIXTRIE"); + if (options.Sortable) _schemaArgs.Add("SORTABLE"); + if (options.Unf) _schemaArgs.Add("UNF"); + if (options.MissingIndex) _schemaArgs.Add("INDEXMISSING"); + if (options.EmptyIndex) _schemaArgs.Add("INDEXEMPTY"); + } + return this as TBuilder; + } + public TBuilder AddTagField(string name, string alias = null, bool sortable = false, bool unf = false, bool noIndex = false, string separator = ",", + bool caseSensitive = false, bool withSuffixTrie = false, bool missingIndex = false, bool emptyIndex = false) => AddTagField(name, new TagFieldOptions + { + Alias = alias, + Sortable = sortable, + Unf = unf, + NoIndex = noIndex, + Separator = separator, + CaseSensitive = caseSensitive, + WithSuffixTrie = withSuffixTrie, + MissingIndex = missingIndex, + EmptyIndex = emptyIndex + }); + public TBuilder AddTagField(string name, TagFieldOptions options) + { + _schemaArgs.Add(name); + if (options != null) + { + if (!string.IsNullOrWhiteSpace(options.Alias)) + { + _schemaArgs.Add("AS"); + _schemaArgs.Add(options.Alias); + } + _schemaArgs.Add("TAG"); + if (options.NoIndex) _schemaArgs.Add("NOINDEX"); + if (options.WithSuffixTrie) _schemaArgs.Add("WITHSUFFIXTRIE"); + if (options.Separator != ",") + { + + _schemaArgs.Add("SEPARATOR"); + _schemaArgs.Add(options.Separator); + } + if (options.CaseSensitive) _schemaArgs.Add("CASESENSITIVE"); + if (options.Sortable) _schemaArgs.Add("SORTABLE"); + if (options.Unf) _schemaArgs.Add("UNF"); + if (options.MissingIndex) _schemaArgs.Add("INDEXMISSING"); + if (options.EmptyIndex) _schemaArgs.Add("INDEXEMPTY"); + } + return this as TBuilder; + } + public TBuilder AddNumericField(string name, string alias = null, bool sortable = false, bool noIndex = false, bool missingIndex = false) => AddNumericField(name, new NumbericFieldOptions + { + Alias = alias, + Sortable = sortable, + NoIndex = noIndex, + MissingIndex = missingIndex + }); + public TBuilder AddNumericField(string name, NumbericFieldOptions options) + { + _schemaArgs.Add(name); + if (options != null) + { + if (!string.IsNullOrWhiteSpace(options.Alias)) + { + _schemaArgs.Add("AS"); + _schemaArgs.Add(options.Alias); + } + _schemaArgs.Add("NUMERIC"); + if (options.NoIndex) _schemaArgs.Add("NOINDEX"); + if (options.Sortable) _schemaArgs.Add("SORTABLE"); + if (options.MissingIndex) _schemaArgs.Add("INDEXMISSING"); + } + return this as TBuilder; + } + public TBuilder AddGeoField(string name, string alias = null, bool sortable = false, bool noIndex = false, bool missingIndex = false) => AddGeoField(name, new GeoFieldOptions + { + Alias = alias, + Sortable = sortable, + NoIndex = noIndex, + MissingIndex = missingIndex + }); + public TBuilder AddGeoField(string name, GeoFieldOptions options) + { + _schemaArgs.Add(name); + if (options != null) + { + if (!string.IsNullOrWhiteSpace(options.Alias)) + { + _schemaArgs.Add("AS"); + _schemaArgs.Add(options.Alias); + } + _schemaArgs.Add("GEO"); + if (options.NoIndex) _schemaArgs.Add("NOINDEX"); + if (options.Sortable) _schemaArgs.Add("SORTABLE"); + if (options.MissingIndex) _schemaArgs.Add("INDEXMISSING"); + } + return this as TBuilder; + } + public TBuilder AddGeoShapeField(string name, string alias = null, CoordinateSystem system = CoordinateSystem.FLAT, bool missingIndex = false) => AddGeoShapeField(name, new GeoShapeFieldOptions + { + Alias = alias, + System = system, + MissingIndex = missingIndex + }); + public TBuilder AddGeoShapeField(string name, GeoShapeFieldOptions options) + { + _schemaArgs.Add(name); + if (options != null) + { + if (!string.IsNullOrWhiteSpace(options.Alias)) + { + _schemaArgs.Add("AS"); + _schemaArgs.Add(options.Alias); + } + _schemaArgs.Add("GEOSHAPE"); + _schemaArgs.Add(options.System.ToString().ToUpper()); + if (options.MissingIndex) _schemaArgs.Add("INDEXMISSING"); + } + return this as TBuilder; + } + public TBuilder AddVectorField(string name, string alias = null, VectorAlgo algorithm = VectorAlgo.FLAT, Dictionary attributes = null, bool missingIndex = false) => AddVectorField(name, new VectorFieldOptions + { + Alias = alias, + Algorithm = algorithm, + Attributes = attributes, + MissingIndex = missingIndex + }); + public TBuilder AddVectorField(string name, VectorFieldOptions options) + { + _schemaArgs.Add(name); + if (options != null) + { + if (!string.IsNullOrWhiteSpace(options.Alias)) + { + _schemaArgs.Add("AS"); + _schemaArgs.Add(options.Alias); + } + _schemaArgs.Add("VECTOR"); + _schemaArgs.Add(options.Algorithm.ToString().ToUpper()); + if (options.Attributes != null) + { + _schemaArgs.Add(options.Attributes.Count * 2); + foreach (var attribute in options.Attributes) + { + _schemaArgs.Add(attribute.Key); + _schemaArgs.Add(attribute.Value); + } + } + if (options.MissingIndex) _schemaArgs.Add("INDEXMISSING"); + } + return this as TBuilder; + } + } + + public enum IndexDataType { Hash, Json } + public enum FieldType { Text, Tag, Numeric, Geo, Vector, GeoShape } + public class TextFieldOptions + { + public string Alias { get; set; } + public double Weight { get; set; } = 1.0; + public bool NoStem { get; set; } + public string Phonetic { get; set; } + public bool Sortable { get; set; } + public bool Unf { get; set; } + public bool NoIndex { get; set; } + public bool WithSuffixTrie { get; set; } + public bool MissingIndex { get; set; } + public bool EmptyIndex { get; set; } + } + public class TagFieldOptions + { + public string Alias { get; set; } + public bool Sortable { get; set; } + public bool Unf { get; set; } + public bool NoIndex { get; set; } + public string Separator { get; set; } = ","; + public bool CaseSensitive { get; set; } + public bool WithSuffixTrie { get; set; } + public bool MissingIndex { get; set; } + public bool EmptyIndex { get; set; } + } + public class NumbericFieldOptions + { + public string Alias { get; set; } + public bool Sortable { get; set; } + public bool NoIndex { get; set; } + public bool MissingIndex { get; set; } + } + public class GeoFieldOptions + { + public string Alias { get; set; } + public bool Sortable { get; set; } + public bool NoIndex { get; set; } + public bool MissingIndex { get; set; } + } + public enum CoordinateSystem { FLAT, SPHERICAL } + public class GeoShapeFieldOptions + { + public string Alias { get; set; } + public CoordinateSystem System { get; set; } + public bool MissingIndex { get; set; } + } + public enum VectorAlgo { FLAT, HNSW } + public class VectorFieldOptions + { + public string Alias { get; set; } + public VectorAlgo Algorithm { get; set; } + public Dictionary Attributes { get; set; } + public bool MissingIndex { get; set; } + } +} diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch/SearchBuilder.cs b/src/FreeRedis/RedisClient/Modules/RediSearch/SearchBuilder.cs new file mode 100644 index 0000000..4854754 --- /dev/null +++ b/src/FreeRedis/RedisClient/Modules/RediSearch/SearchBuilder.cs @@ -0,0 +1,317 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FreeRedis.RediSearch +{ + public class Document + { + public string Id { get; } + public double Score { get; set; } + public byte[] Payload { get; } + public string[] ScoreExplained { get; private set; } + public Dictionary Body { get; } = new Dictionary(); + public object this[string key] + { + get => Body.TryGetValue(key, out var val) ? val : null; + internal set => Body[key] = value; + } + + public Document(string id, double score, byte[] payload) + { + Id = id; + Score = score; + Payload = payload; + } + public static Document Load(string id, double score, byte[] payload, object[] fieldValues, string[] scoreExplained) + { + Document ret = new Document(id, score, payload); + if (fieldValues != null) + { + for (int i = 0; i < fieldValues.Length; i += 2) + { + string fieldName = fieldValues[i].ConvertTo(); + if (fieldName == "$") + ret.Body["json"] = fieldValues[i + 1]; + else + ret.Body[fieldName] = fieldValues[i + 1]; + } + } + ret.ScoreExplained = scoreExplained; + return ret; + } + } + + public class SearchBuilder + { + RedisClient _redis; + string _index; + string _query; + internal SearchBuilder(RedisClient redis, string index, string query) + { + _redis = redis; + _index = index; + _query = query; + _dialect = redis.ConnectionString.FtDialect; + _language = redis.ConnectionString.FtLanguage; + } + public List Execute() + { + var cmd = "FT.SEARCH".Input(_index).Input(_query) + .InputIf(_noContent, "NOCONTENT") + .InputIf(_verbatim, "VERBATIM") + .InputIf(_noStopwords, "NOSTOPWORDS") + .InputIf(_withScores, "WITHSCORES") + .InputIf(_withPayloads, "WITHPAYLOADS") + .InputIf(_withSortKeys, "WITHSORTKEYS"); + if (_filters.Any()) cmd.Input("FILTER").Input(_filters.ToArray()); + if (_geoFilter.Any()) cmd.Input("GEOFILTER").Input(_geoFilter.ToArray()); + if (_inKeys.Any()) cmd.Input("INKEYS", _inKeys.Count).Input(_inKeys.ToArray()); + if (_inFields.Any()) cmd.Input("INFIELDS", _inFields.Count).Input(_inFields.ToArray()); + if (_return.Any()) + { + cmd.Input("RETURN", _return.Sum(a => a[1] == null ? 1 : 2)); + foreach (var ret in _return) + if (ret[1] == null) cmd.Input(ret[0]); + else cmd.Input(ret[0], "AS", ret[1]); + } + if (_sumarize) + { + cmd.Input("SUMMARIZE"); + if (_sumarizeFields.Any()) cmd.Input("FIELDS", _sumarizeFields.Count).Input(_sumarizeFields.ToArray()); + if (_sumarizeFrags != -1) cmd.Input("FRAGS", _sumarizeFrags); + if (_sumarizeLen != -1) cmd.Input("LEN", _sumarizeLen); + if (_sumarizeSeparator != null) cmd.Input("SEPARATOR", _sumarizeSeparator); + } + if (_highLight) + { + cmd.Input("HIGHLIGHT"); + if (_highLightFields.Any()) cmd.Input("FIELDS", _highLightFields.Count).Input(_highLightFields.ToArray()); + if (_highLightTags != null) cmd.Input("TAGS").Input(_highLightTags); + } + cmd + .InputIf(_slop >= 0, "SLOP", _slop) + .InputIf(_timeout >= 0, "TIMEOUT", _timeout) + .InputIf(_inOrder, "INORDER") + .InputIf(!string.IsNullOrWhiteSpace(_language), "LANGUAGE", _language) + .InputIf(!string.IsNullOrWhiteSpace(_expander), "EXPANDER", _expander) + .InputIf(!string.IsNullOrWhiteSpace(_scorer), "SCORER", _scorer) + .InputIf(_explainScore, "EXPLAINSCORE") + .InputIf(!string.IsNullOrWhiteSpace(_payLoad), "PAYLOAD", _payLoad) + .InputIf(!string.IsNullOrWhiteSpace(_sortBy), "SORTBY", _sortBy).InputIf(_sortByDesc, "DESC") + .InputIf(_limitOffset > 0 || _limitNum != 10, "LIMIT", _limitOffset, _limitNum); + if (_params.Any()) cmd.Input("PARAMS", _params.Count).Input(_params); + cmd + .InputIf(_dialect > 0, "DIALECT", _dialect); + return _redis.Call(cmd, rt => rt.ThrowOrValue((a, _) => + { + var docs = new List(); + if (a.Any() != true) return docs; + var totalResults = a[0].ConvertTo(); + if (totalResults < 1) return docs; + + int step = 1; + int scoreOffset = 0; + int contentOffset = 1; + int payloadOffset = 0; + if (_withScores) + { + step++; + scoreOffset = 1; + contentOffset++; + + } + if (_noContent == false) + { + step++; + if (_withPayloads) + { + payloadOffset = scoreOffset + 1; + step++; + contentOffset++; + } + } + for (var x = 1; x < a.Length; x += step) + { + var id = a[x].ConvertTo(); + double score = 1.0; + byte[] payload = null; + object[] fieldValues = null; + string[] scoreExplained = null; + if (_withScores) score = a[x + scoreOffset].ConvertTo(); + if (_withPayloads) payload = a[x + payloadOffset].ConvertTo(); + if (_noContent == false) fieldValues = a[x + contentOffset].ConvertTo(); + docs.Add(Document.Load(id, score, payload, fieldValues, scoreExplained)); + } + return docs; + })); + } + + private bool _noContent, _verbatim, _noStopwords, _withScores, _withPayloads, _withSortKeys; + private List _filters = new List(); + private List _geoFilter = new List(); + private List _inKeys = new List(); + private List _inFields = new List(); + private List _return = new List(); + private bool _sumarize; + private List _sumarizeFields = new List(); + private long _sumarizeFrags = -1; + private long _sumarizeLen = -1; + private string _sumarizeSeparator; + private bool _highLight; + private List _highLightFields = new List(); + private object[] _highLightTags; + private decimal _slop = -1; + private long _timeout = -1; + private bool _inOrder; + private string _language; + private string _expander, _scorer; + private bool _explainScore; + private string _payLoad; + private string _sortBy; + private bool _sortByDesc; + private long _limitOffset, _limitNum = 10; + private List _params = new List(); + private int _dialect; + + public SearchBuilder NoContent(bool value = true) + { + _noContent = value; + return this; + } + public SearchBuilder Verbatim(bool value = true) + { + _verbatim = value; + return this; + } + public SearchBuilder NoStopwords(bool value = true) + { + _noStopwords = value; + return this; + } + public SearchBuilder WithScores(bool value = true) + { + _withScores = value; + return this; + } + public SearchBuilder WithPayloads(bool value = true) + { + _withPayloads = value; + return this; + } + public SearchBuilder WithSortKeys(bool value = true) + { + _withSortKeys = value; + return this; + } + public SearchBuilder Filter(string field, object min, object max) + { + _filters.AddRange(new object[] { field, min, max }); + return this; + } + public SearchBuilder GeoFilter(string field, decimal lon, decimal lat, decimal radius, GeoUnit unit = GeoUnit.m) + { + _geoFilter.AddRange(new object[] { field, lon, lat, radius, unit }); + return this; + } + public SearchBuilder InKeys(params string[] keys) + { + if (keys?.Any() == true) _inKeys.AddRange(keys); + return this; + } + public SearchBuilder InFields(params string[] fields) + { + if (fields?.Any() == true) _inFields.AddRange(fields); + return this; + } + + public SearchBuilder Return(params string[] identifiers) + { + if (identifiers?.Any() == true) _return.AddRange(identifiers.Select(a => new[] { a, null })); + return this; + } + public SearchBuilder Return(params KeyValuePair[] identifierProperties) + { + if (identifierProperties?.Any() == true) _return.AddRange(identifierProperties.Select(a => new[] { a.Key, a.Value })); + return this; + } + public SearchBuilder Sumarize(string[] fields, long frags, long len, string separator) + { + _sumarize = true; + _sumarizeFields.AddRange(fields); + _sumarizeFrags = frags; + _sumarizeLen = len; + _sumarizeSeparator = separator; + return this; + } + public SearchBuilder HighLight(string[] fields, string tagsOpen, string tagsClose) + { + _highLight = true; + _highLightFields.AddRange(fields); + _highLightTags = new[] { tagsOpen, tagsClose }; + return this; + } + public SearchBuilder Slop(decimal value) + { + _slop = value; + return this; + } + public SearchBuilder Timeout(long milliseconds) + { + _timeout = milliseconds; + return this; + } + public SearchBuilder InOrder(bool value = true) + { + _inOrder = value; + return this; + } + public SearchBuilder Language(string value) + { + _language = value; + return this; + } + public SearchBuilder Expander(string value) + { + _expander = value; + return this; + } + public SearchBuilder Scorer(string value) + { + _scorer = value; + return this; + } + public SearchBuilder ExplainScore(bool value = true) + { + _explainScore = value; + return this; + } + public SearchBuilder Payload(string value) + { + _payLoad = value; + return this; + } + public SearchBuilder SortBy(string sortBy, bool desc = false) + { + _sortBy = sortBy; + _sortByDesc = desc; + return this; + } + public SearchBuilder Limit(long offset, long num) + { + _limitOffset = offset; + _limitNum = num; + return this; + } + public SearchBuilder Params(string name, string value) + { + _params.Add(name); + _params.Add(value); + return this; + } + public SearchBuilder Dialect(int value) + { + _dialect = value; + return this; + } + } +}