diff --git a/src/FreeRedis/ConnectionStringBuilder.cs b/src/FreeRedis/ConnectionStringBuilder.cs index 41cc8448..f7912c76 100644 --- a/src/FreeRedis/ConnectionStringBuilder.cs +++ b/src/FreeRedis/ConnectionStringBuilder.cs @@ -34,6 +34,8 @@ public string Host public int Retry { get; set; } = 0; public bool ExitAutoDisposePool { get; set; } = true; public bool SubscribeReadbytes { get; set; } = false; + public int FtDialect { get; set; } = 0; + public string FtLanguage { get; set; } public RemoteCertificateValidationCallback CertificateValidation; public LocalCertificateSelectionCallback CertificateSelection; @@ -64,6 +66,8 @@ public override string ToString() if (Retry != 0) sb.Append(",retry=").Append(Retry); if (ExitAutoDisposePool != true) sb.Append(",exitAutoDisposePool=false"); if (SubscribeReadbytes != false) sb.Append(",subscribeReadbytes=true"); + if (FtDialect != 0) sb.Append(",ftdialect=").Append(FtDialect); + if (!string.IsNullOrWhiteSpace(FtLanguage)) sb.Append(",ftlanguage=").Append(FtLanguage); return sb.ToString(); } @@ -106,6 +110,8 @@ public static ConnectionStringBuilder Parse(string connectionString) case "exitautodisposepool": if (kv.Length > 1 && new[] { "false", "0" }.Contains(kv[1].Trim())) ret.ExitAutoDisposePool = false; break; case "subscriblereadbytes": //history error case "subscribereadbytes": if (kv.Length > 1 && kv[1].ToLower().Trim() == "true") ret.SubscribeReadbytes = true; break; + case "ftdialect": if (kv.Length > 1 && int.TryParse(kv[1].Trim(), out var dialect) && dialect > 0) ret.FtDialect = dialect; break; + case "ftlanguage": if (kv.Length > 1) ret.FtLanguage = kv[1].Trim(); break; } } return ret; diff --git a/src/FreeRedis/Internal/RespHelper.cs b/src/FreeRedis/Internal/RespHelper.cs index dcfb4b03..181507ae 100644 --- a/src/FreeRedis/Internal/RespHelper.cs +++ b/src/FreeRedis/Internal/RespHelper.cs @@ -580,13 +580,13 @@ public static void SetPropertyOrFieldValue(this Type entityType, object entity, [typeof(decimal)] = 3, [typeof(decimal?)] = 3 }); - static bool IsIntegerType(this Type that) => that == null ? false : (_dicIsNumberType.Value.TryGetValue(that, out var tryval) ? tryval == 1 : false); - static bool IsNumberType(this Type that) => that == null ? false : _dicIsNumberType.Value.ContainsKey(that); - static bool IsNullableType(this Type that) => that.IsArray == false && that?.FullName.StartsWith("System.Nullable`1[") == true; - static bool IsAnonymousType(this Type that) => that?.FullName.StartsWith("<>f__AnonymousType") == true; - static bool IsArrayOrList(this Type that) => that == null ? false : (that.IsArray || typeof(IList).IsAssignableFrom(that)); - static Type NullableTypeOrThis(this Type that) => that?.IsNullableType() == true ? that.GetGenericArguments().First() : that; - static string DisplayCsharp(this Type type, bool isNameSpace = true) + internal static bool IsIntegerType(this Type that) => that == null ? false : (_dicIsNumberType.Value.TryGetValue(that, out var tryval) ? tryval == 1 : false); + internal static bool IsNumberType(this Type that) => that == null ? false : _dicIsNumberType.Value.ContainsKey(that); + internal static bool IsNullableType(this Type that) => that.IsArray == false && that?.FullName.StartsWith("System.Nullable`1[") == true; + internal static bool IsAnonymousType(this Type that) => that?.FullName.StartsWith("<>f__AnonymousType") == true; + internal static bool IsArrayOrList(this Type that) => that == null ? false : (that.IsArray || typeof(IList).IsAssignableFrom(that)); + internal static Type NullableTypeOrThis(this Type that) => that?.IsNullableType() == true ? that.GetGenericArguments().First() : that; + internal static string DisplayCsharp(this Type type, bool isNameSpace = true) { if (type == null) return null; if (type == typeof(void)) return "void"; @@ -743,7 +743,7 @@ public static Type GetPropertyOrFieldType(this MemberInfo that) } #endregion -#region 类型转换 + #region 类型转换 internal static string ToInvariantCultureToString(this object obj) => obj is string objstr ? objstr : string.Format(CultureInfo.InvariantCulture, @"{0}", obj); public static void MapSetListValue(this object[] list, Dictionary> valueHandlers) { @@ -758,6 +758,21 @@ public static void MapSetListValue(this object[] list, Dictionary(this Dictionary dict) + { + if (dict == null) return default(T); + var ttype = typeof(T); + var ret = (T)ttype.CreateInstanceGetDefaultValue(); + foreach(var kv in dict) + { + var name = kv.Key.Replace("-", "_"); + var prop = ttype.GetPropertyOrFieldIgnoreCase(name); + if (prop == null) continue; // throw new ArgumentException($"{typeof(T).DisplayCsharp()} undefined Property {list[a]}"); + if (kv.Value == null) continue; + ttype.SetPropertyOrFieldValue(ret, prop.Name, prop.GetPropertyOrFieldType().FromObject(kv.Value)); + } + return ret; + } public static T MapToClass(this object[] list, Encoding encoding) { if (list == null) return default(T); diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch.cs b/src/FreeRedis/RedisClient/Modules/RediSearch.cs index 42e6ce43..fb6f9188 100644 --- a/src/FreeRedis/RedisClient/Modules/RediSearch.cs +++ b/src/FreeRedis/RedisClient/Modules/RediSearch.cs @@ -1,11 +1,14 @@ using FreeRedis.RediSearch; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; namespace FreeRedis { partial class RedisClient { + public FtDocumentRepository FtDocumentRepository() => new FtDocumentRepository(this); + public string[] Ft_List() => Call("FT._LIST", rt => rt.ThrowOrValue()); public AggregateBuilder FtAggregate(string index, string query) => new AggregateBuilder(this, index, query); @@ -91,5 +94,42 @@ public static object ThrowOrValueToFtCursorRead(this RedisResult rt) => { return a; }); + + public static bool IsParameter(this Expression exp) + { + var test = new TestParameterExpressionVisitor(); + test.Visit(exp); + return test.Result; + } + } + + class ReplaceVisitor : ExpressionVisitor + { + private Expression _oldexp; + private Expression _newexp; + public Expression Modify(Expression find, Expression oldexp, Expression newexp) + { + this._oldexp = oldexp; + this._newexp = newexp; + return Visit(find); + } + protected override Expression VisitMember(MemberExpression node) + { + if (node.Expression == _oldexp) + return Expression.Property(_newexp, node.Member.Name); + if (node == _oldexp) + return _newexp; + return base.VisitMember(node); + } + } + class TestParameterExpressionVisitor : ExpressionVisitor + { + public bool Result { get; private set; } + + protected override Expression VisitParameter(ParameterExpression node) + { + if (!Result) Result = true; + return node; + } } } \ No newline at end of file diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch/CreateBuilder.cs b/src/FreeRedis/RedisClient/Modules/RediSearch/CreateBuilder.cs index cd91970e..3cf2ff16 100644 --- a/src/FreeRedis/RedisClient/Modules/RediSearch/CreateBuilder.cs +++ b/src/FreeRedis/RedisClient/Modules/RediSearch/CreateBuilder.cs @@ -235,7 +235,7 @@ public TBuilder AddTagField(string name, TagFieldOptions options) _schemaArgs.Add("TAG"); if (options.NoIndex) _schemaArgs.Add("NOINDEX"); if (options.WithSuffixTrie) _schemaArgs.Add("WITHSUFFIXTRIE"); - if (options.Separator != ",") + if (!string.IsNullOrWhiteSpace(options.Separator) && options.Separator != ",") { _schemaArgs.Add("SEPARATOR"); diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch/FtDocumentRepository.cs b/src/FreeRedis/RedisClient/Modules/RediSearch/FtDocumentRepository.cs new file mode 100644 index 00000000..d69cedd5 --- /dev/null +++ b/src/FreeRedis/RedisClient/Modules/RediSearch/FtDocumentRepository.cs @@ -0,0 +1,716 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace FreeRedis.RediSearch +{ + public class FtDocumentRepository + { + static ConcurrentDictionary _schemaFactories = new ConcurrentDictionary(); + internal protected class DocumentSchemaInfo + { + public Type DocumentType { get; set; } + public FtDocumentAttribute DocumentAttribute { get; set; } + public PropertyInfo KeyProperty { get; set; } + public List Fields { get; set; } + public Dictionary FieldsMap { get; set; } + } + internal protected class DocumentSchemaFieldInfo + { + public DocumentSchemaInfo DocumentSchema { get; set; } + public PropertyInfo Property { get; set; } + public FtFieldAttribute FieldAttribute { get; set; } + public FieldType FieldType { get; set; } + } + + internal protected RedisClient _client; + internal protected DocumentSchemaInfo _schema; + public FtDocumentRepository(RedisClient client) + { + var type = typeof(T); + _client = client; + _schema = _schemaFactories.GetOrAdd(type, t => + { + var fieldProprties = type.GetProperties().Select(p => new + { + attribute = p.GetCustomAttributes(false).FirstOrDefault(a => a is FtFieldAttribute) as FtFieldAttribute, + property = p + }).Where(a => a.attribute != null).ToList(); + if (fieldProprties.Any() == false) throw new Exception($"Not found: [FtFieldAttribute]"); + var schema = new DocumentSchemaInfo + { + DocumentType = type, + DocumentAttribute = type.GetCustomAttributes(false).FirstOrDefault(a => a is FtDocumentAttribute) as FtDocumentAttribute, + KeyProperty = type.GetProperties().FirstOrDefault(p => p.GetCustomAttributes(false).FirstOrDefault(a => a is FtKeyAttribute) != null), + }; + var fields = fieldProprties.Select(a => new DocumentSchemaFieldInfo + { + DocumentSchema = schema, + Property = a.property, + FieldAttribute = a.attribute, + FieldType = GetMapFieldType(a.property, a.attribute) + }).ToList(); + schema.Fields = fields; + schema.FieldsMap = fields.ToDictionary(a => a.Property.Name, a => a); + return schema; + }); + } + protected FieldType GetMapFieldType(PropertyInfo property, FtFieldAttribute ftattr) + { + //Text, Tag, Numeric, Geo, Vector, GeoShape + if (ftattr is FtTextFieldAttribute) return FieldType.Text; + if (ftattr is FtTagFieldAttribute) return FieldType.Tag; + if (ftattr is FtNumericFieldAttribute) return FieldType.Numeric; + return FieldType.Text; + } + + public void DropIndex(bool dd = false) + { + _client.FtDropIndex(_schema.DocumentAttribute.Name, dd); + } + public void CreateIndex() + { + var attr = _schema.DocumentAttribute; + var createBuilder = _client.FtCreate(attr.Name); + if (!string.IsNullOrWhiteSpace(attr.Prefix)) createBuilder.Prefix(attr.Prefix); + if (!string.IsNullOrWhiteSpace(attr.Filter)) createBuilder.Prefix(attr.Filter); + if (!string.IsNullOrWhiteSpace(attr.Language)) createBuilder.Language(attr.Language); + foreach (var field in _schema.Fields) + { + switch (field.FieldType) + { + case FieldType.Text: + { + var ftattr = field.FieldAttribute as FtTextFieldAttribute; + createBuilder.AddTextField(ftattr.Name, new TextFieldOptions + { + Alias = ftattr.Alias, + EmptyIndex = ftattr.EmptyIndex, + MissingIndex = ftattr.MissingIndex, + NoIndex = ftattr.NoIndex, + NoStem = ftattr.NoStem, + Phonetic = ftattr.Phonetic, + Sortable = ftattr.Sortable, + Unf = ftattr.Unf, + Weight = ftattr.Weight, + WithSuffixTrie = ftattr.WithSuffixTrie, + }); + } + break; + case FieldType.Tag: + { + var ftattr = field.FieldAttribute as FtTagFieldAttribute; + createBuilder.AddTagField(ftattr.Name, new TagFieldOptions + { + Alias = ftattr.Alias, + CaseSensitive = ftattr.CaseSensitive, + EmptyIndex = ftattr.EmptyIndex, + MissingIndex = ftattr.MissingIndex, + NoIndex = ftattr.NoIndex, + Separator = ftattr.Separator, + Sortable = ftattr.Sortable, + Unf = ftattr.Unf, + WithSuffixTrie = ftattr.WithSuffixTrie, + }); + } + break; + case FieldType.Numeric: + { + var ftattr = field.FieldAttribute as FtNumericFieldAttribute; + createBuilder.AddNumericField(ftattr.Name, new NumbericFieldOptions + { + Alias = ftattr.Alias, + MissingIndex = ftattr.MissingIndex, + NoIndex = ftattr.NoIndex, + Sortable = ftattr.Sortable, + }); + } + break; + } + } + createBuilder.Execute(); + } + + void Save(T doc, RedisClient.PipelineHook pipe) + { + var key = $"{_schema.DocumentAttribute.Prefix}{_schema.KeyProperty.GetValue(doc, null)}"; + var opts = _schema.Fields.Where((a, b) => b > 0).Select((a, b) => new object[] { a.FieldAttribute.FieldName, a.Property.GetValue(doc, null) }).SelectMany(a => a).ToArray(); + var field = _schema.Fields[0].FieldAttribute.FieldName; + var value = _schema.Fields[0].Property.GetValue(doc, null); + if (pipe != null) pipe.HMSet(key, field, value, opts); + else _client.HMSet(key, field, value, opts); + } + public void Save(T doc) => Save(doc, null); + public void Save(T[] docs) => Save(docs as IEnumerable); + public void Save(IEnumerable docs) + { + if (docs == null) return; + using (var pipe = _client.StartPipe()) + { + foreach (var doc in docs) + Save(doc, pipe); + pipe.EndPipe(); + } + } + + public long Delete(params long[] id) + { + if (id == null || id.Length == 0) return 0; + return _client.Del(id.Select(a => $"{_schema.DocumentAttribute.Prefix}{a}").ToArray()); + } + public long Delete(params string[] id) + { + if (id == null || id.Length == 0) return 0; + return _client.Del(id.Select(a => $"{_schema.DocumentAttribute.Prefix}{a}").ToArray()); + } + + public FtDocumentRepositorySearchBuilder Search(Expression> query) => Search(ParseQueryExpression(query.Body, null)); + public FtDocumentRepositorySearchBuilder Search(string query = "*") + { + if (string.IsNullOrEmpty(query)) query = "*"; + var q = _client.FtSearch(_schema.DocumentAttribute.Name, query); + if (!string.IsNullOrEmpty(_schema.DocumentAttribute.Language)) q.Language(_schema.DocumentAttribute.Language); + return new FtDocumentRepositorySearchBuilder(this, _schema.DocumentAttribute.Name, query); + } + + internal protected int ToTimestamp(DateTime dt) => (int)dt.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; + internal protected List> ParseSelectorExpression(Expression selector) + { + var fieldValues = new List>(); + if (selector is LambdaExpression lambdaExp) selector = lambdaExp.Body; + + if (selector.NodeType == ExpressionType.New) + { + var newExp = selector as NewExpression; + for (var a = 0; a < newExp?.Members?.Count; a++) + { + var left = newExp.Members[a].Name; + var right = ParseQueryExpression(newExp.Arguments[a], new ParseQueryExpressionOptions { IsQuoteFieldName = false }); + fieldValues.Add(new KeyValuePair(left, right)); + } + } + else if (selector.NodeType == ExpressionType.MemberInit) + { + var initExp = selector as MemberInitExpression; + for (var a = 0; a < initExp?.Bindings.Count; a++) + { + var initAssignExp = (initExp.Bindings[a] as MemberAssignment); + if (initAssignExp == null) continue; + var left = initAssignExp.Member.Name; + var right = ParseQueryExpression(initAssignExp.Expression, new ParseQueryExpressionOptions { IsQuoteFieldName = false }); + fieldValues.Add(new KeyValuePair(left, right)); + } + } + return fieldValues; + } + internal protected class ParseQueryExpressionOptions + { + public bool IsQuoteFieldName { get; set; } = true; + } + internal protected string ParseQueryExpression(Expression exp, ParseQueryExpressionOptions options) + { + if (options == null) options = new ParseQueryExpressionOptions(); + string parseExp(Expression thenExp) => ParseQueryExpression(thenExp, options); + string toFt(object obj) => string.Format(CultureInfo.InvariantCulture, "{0}", toFtObject(obj)); + object toFtObject(object param) + { + if (param == null) return "NULL"; + + if (param is bool || param is bool?) + return (bool)param ? 1 : 0; + else if (param is string str) + return string.Concat("\"", str.Replace("\\", "\\\\").Replace("\"", "\\\""), "\""); + else if (param is char chr) + return string.Concat("\"", chr.ToString().Replace("\\", "\\\\").Replace("\"", "\\\"").Replace('\0', ' '), "\""); + else if (param is Enum enm) + return string.Concat("\"", enm.ToString().Replace("\\", "\\\\").Replace("\"", "\\\"").Replace(", ", ","), "\""); + else if (decimal.TryParse(string.Concat(param), out var trydec)) + return param; + + else if (param is DateTime || param is DateTime?) + return ToTimestamp((DateTime)param); + + return string.Concat("\"", param.ToString().Replace("\\", "\\\\").Replace("\"", "\\\""), "\""); + } + if (exp == null) return ""; + + switch (exp.NodeType) + { + case ExpressionType.Not: + var notExp = (exp as UnaryExpression)?.Operand; + return $"-({parseExp(notExp)})"; + case ExpressionType.Quote: return parseExp((exp as UnaryExpression)?.Operand); + case ExpressionType.Lambda: return parseExp((exp as LambdaExpression)?.Body); + case ExpressionType.Invoke: + var invokeExp = exp as InvocationExpression; + var invokeReplaceExp = invokeExp.Expression; + var invokeLambdaExp = invokeReplaceExp as LambdaExpression; + if (invokeLambdaExp == null) return toFt(Expression.Lambda(exp).Compile().DynamicInvoke()); + var invokeReplaceVistor = new ReplaceVisitor(); + var len = Math.Min(invokeExp.Arguments.Count, invokeLambdaExp.Parameters.Count); + for (var a = 0; a < len; a++) + invokeReplaceExp = invokeReplaceVistor.Modify(invokeReplaceExp, invokeLambdaExp.Parameters[a], invokeExp.Arguments[a]); + return parseExp(invokeReplaceExp); + case ExpressionType.TypeAs: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + var expOperand = (exp as UnaryExpression)?.Operand; + if (expOperand.Type.NullableTypeOrThis().IsEnum && exp.IsParameter() == false) + return toFt(Expression.Lambda(exp).Compile().DynamicInvoke()); + return parseExp(expOperand); + case ExpressionType.Negate: + case ExpressionType.NegateChecked: return $"-({parseExp((exp as UnaryExpression)?.Operand)})"; + case ExpressionType.Constant: return toFt((exp as ConstantExpression)?.Value); + case ExpressionType.Conditional: + var condExp = exp as ConditionalExpression; + if (condExp.Test.IsParameter()) + return ""; + if ((bool)Expression.Lambda(condExp.Test).Compile().DynamicInvoke()) + return parseExp(condExp.IfTrue); + else + return parseExp(condExp.IfFalse); + case ExpressionType.MemberAccess: + var memberExp = exp as MemberExpression; + var memberType = memberExp.Expression?.Type ?? memberExp.Type; + var memberParseResult = ""; + switch (memberType.FullName) + { + case "System.String": memberParseResult = ParseMemberAccessString(); break; + case "System.DateTime": memberParseResult = ParseMemberAccessDateTime(); break; + } + if (string.IsNullOrEmpty(memberParseResult) == false) return memberParseResult; + + if (memberExp.IsParameter() == false) return toFt(Expression.Lambda(exp).Compile().DynamicInvoke()); + if (_schema.FieldsMap.TryGetValue(memberExp.Member.Name, out var field)) + return options.IsQuoteFieldName ? $"@{field.FieldAttribute.FieldName}" : field.FieldAttribute.FieldName; + break; + + string ParseMemberAccessString() + { + if (memberExp.Expression != null) + { + var left = parseExp(memberExp.Expression); + switch (memberExp.Member.Name) + { + case "Length": return $"strlen({left})"; + } + } + return null; + } + string ParseMemberAccessDateTime() + { + if (memberExp.Expression == null) + { + switch (memberExp.Member.Name) + { + case "Now": return ToTimestamp(DateTime.Now).ToString(); + case "UtcNow": return ToTimestamp(DateTime.UtcNow).ToString(); + case "Today": return ToTimestamp(DateTime.Today).ToString(); + case "MinValue": return "0"; + case "MaxValue": return ToTimestamp(new DateTime(2050, 1, 1)).ToString(); + } + return null; + } + var left = parseExp(memberExp.Expression); + switch (memberExp.Member.Name) + { + case "Date": return $"timefmt({left},'%Y-%m-%d')"; + case "TimeOfDay": return $"timefmt({left},'%H:%M:%S')"; + case "DayOfWeek": return $"dayofweek({left})"; + case "Day": return $"dayofmonth({left})"; + case "DayOfYear": return $"dayofyear({left})+1"; + case "Month": return $"month({left})"; + case "Year": return $"year({left})"; + case "Hour": return $"hour({left})"; + case "Minute": return $"minute({left})"; + case "Second": return $"timefmt({left},'%S')"; + } + return null; + } + case ExpressionType.Call: + var callExp = exp as MethodCallExpression; + var callType = callExp.Object?.Type ?? callExp.Method.DeclaringType; + var callParseResult = ""; + switch (callType.FullName) + { + case "System.String": callParseResult = ParseCallString(); break; + case "System.Math": callParseResult = ParseCallMath(); break; + case "System.DateTime": callParseResult = ParseCallDateTime(); break; + } + if (!string.IsNullOrEmpty(callParseResult)) return callParseResult; + break; + + string ParseCallString() + { + if (callExp.Object != null) + { + var left = parseExp(callExp.Object); + switch (callExp.Method.Name) + { + case "StartsWith": return $"startswith({left},{parseExp(callExp.Arguments[0])})"; + case "EndsWith": return $"endswith({left},{parseExp(callExp.Arguments[0])})"; + case "Contains": return $"contains({left},{parseExp(callExp.Arguments[0])})"; + case "ToLower": return $"lower({left})"; + case "ToUpper": return $"upper({left})"; + case "Substring": + var substrArgs1 = parseExp(callExp.Arguments[0]); + if (callExp.Arguments.Count == 1) return $"substr({left},{substrArgs1},-1)"; + return $"substr({left},{substrArgs1},{parseExp(callExp.Arguments[1])})"; + case "Equals": + var equalRight = parseExp(callExp.Arguments[0]); + return $"{left}:[{equalRight} {equalRight}]"; + } + } + return null; + } + string ParseCallMath() + { + switch (callExp.Method.Name) + { + case "Abs": return $"abs({parseExp(callExp.Arguments[0])})"; + case "Sign": return $"sign({parseExp(callExp.Arguments[0])})"; + case "Floor": return $"floor({parseExp(callExp.Arguments[0])})"; + case "Ceiling": return $"ceiling({parseExp(callExp.Arguments[0])})"; + case "Round": + if (callExp.Arguments.Count > 1 && callExp.Arguments[1].Type.FullName == "System.Int32") return $"round({parseExp(callExp.Arguments[0])}, {parseExp(callExp.Arguments[1])})"; + return $"round({parseExp(callExp.Arguments[0])})"; + case "Exp": return $"exp({parseExp(callExp.Arguments[0])})"; + case "Log": return $"log({parseExp(callExp.Arguments[0])})"; + case "Log10": return $"log10({parseExp(callExp.Arguments[0])})"; + case "Pow": return $"pow({parseExp(callExp.Arguments[0])}, {parseExp(callExp.Arguments[1])})"; + case "Sqrt": return $"sqrt({parseExp(callExp.Arguments[0])})"; + case "Cos": return $"cos({parseExp(callExp.Arguments[0])})"; + case "Sin": return $"sin({parseExp(callExp.Arguments[0])})"; + case "Tan": return $"tan({parseExp(callExp.Arguments[0])})"; + case "Acos": return $"acos({parseExp(callExp.Arguments[0])})"; + case "Asin": return $"asin({parseExp(callExp.Arguments[0])})"; + case "Atan": return $"atan({parseExp(callExp.Arguments[0])})"; + case "Atan2": return $"atan2({parseExp(callExp.Arguments[0])}, {parseExp(callExp.Arguments[1])})"; + case "Truncate": return $"truncate({parseExp(callExp.Arguments[0])}, 0)"; + } + return null; + } + string ParseCallDateTime() + { + if (callExp.Object != null) + { + var left = parseExp(callExp.Object); + var args1 = callExp.Arguments.Count == 0 ? null : parseExp(callExp.Arguments[0]); + switch (callExp.Method.Name) + { + case "Equals": return $"{left}:[{args1} {args1}]"; + case "ToString": + if (callExp.Arguments.Count == 0) return $"timefmt({left},'%Y-%m-%d %H:%M:%S')"; + switch (args1) + { + case "'yyyy-MM-dd HH:mm:ss'": return $"timefmt({left},'%Y-%m-%d %H:%M:%S')"; + case "'yyyy-MM-dd HH:mm'": return $"timefmt({left},'%Y-%m-%d %H:%M')"; + case "'yyyy-MM-dd HH'": return $"timefmt({left},'%Y-%m-%d %H')"; + case "'yyyy-MM-dd'": return $"timefmt({left},'%Y-%m-%d')"; + case "'yyyy-MM'": return $"timefmt({left},'%Y-%m')"; + case "'yyyyMMddHHmmss'": return $"timefmt({left},'%Y%m%d%H%M%S')"; + case "'yyyyMMddHHmm'": return $"timefmt({left},'%Y%m%d%H%M')"; + case "'yyyyMMddHH'": return $"timefmt({left},'%Y%m%d%H')"; + case "'yyyyMMdd'": return $"timefmt({left},'%Y%m%d')"; + case "'yyyyMM'": return $"timefmt({left},'%Y%m')"; + case "'yyyy'": return $"timefmt({left},'%Y')"; + case "'HH:mm:ss'": return $"timefmt({left},'%H:%M:%S')"; + } + args1 = Regex.Replace(args1, "(yyyy|MM|dd|HH|mm|ss)", m => + { + switch (m.Groups[1].Value) + { + case "yyyy": return $"%Y"; + case "MM": return $"%m"; + case "dd": return $"%d"; + case "HH": return $"%H"; + case "mm": return $"%M"; + case "ss": return $"%S"; + } + return m.Groups[0].Value; + }); + return args1; + } + } + return null; + } + } + if (exp is BinaryExpression expBinary && expBinary != null) + { + switch (expBinary.NodeType) + { + case ExpressionType.OrElse: + case ExpressionType.Or: + return $"({parseExp(expBinary.Left)}|{parseExp(expBinary.Right)})"; + case ExpressionType.AndAlso: + case ExpressionType.And: + return $"{parseExp(expBinary.Left)} {parseExp(expBinary.Right)}"; + + case ExpressionType.GreaterThan: + return $"{parseExp(expBinary.Left)}:[({parseExp(expBinary.Right)} +inf]"; + case ExpressionType.GreaterThanOrEqual: + return $"{parseExp(expBinary.Left)}:[{parseExp(expBinary.Right)} +inf]"; + case ExpressionType.LessThan: + return $"{parseExp(expBinary.Left)}:[-inf ({parseExp(expBinary.Right)}]"; + case ExpressionType.LessThanOrEqual: + return $"{parseExp(expBinary.Left)}:[-inf {parseExp(expBinary.Right)}]"; + case ExpressionType.NotEqual: + case ExpressionType.Equal: + var equalRight = parseExp(expBinary.Right); + if (ParseTryGetField(expBinary.Left, out var field)) + { + if (field.FieldType == FieldType.Text) + return $"{(expBinary.NodeType == ExpressionType.NotEqual ? "-" : "")}{parseExp(expBinary.Left)}:{equalRight}"; + else if (field.FieldType == FieldType.Tag) + return $"{(expBinary.NodeType == ExpressionType.NotEqual ? "-" : "")}{parseExp(expBinary.Left)}:{{{equalRight}}}"; + } + return $"{(expBinary.NodeType == ExpressionType.NotEqual ? "-" : "")}{parseExp(expBinary.Left)}:[{equalRight} {equalRight}]"; + case ExpressionType.Add: + case ExpressionType.AddChecked: + return $"({parseExp(expBinary.Left)}+{parseExp(expBinary.Right)})"; + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + return $"({parseExp(expBinary.Left)}-{parseExp(expBinary.Right)})"; + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + return $"{parseExp(expBinary.Left)}*{parseExp(expBinary.Right)}"; + case ExpressionType.Divide: + return $"{parseExp(expBinary.Left)}/{parseExp(expBinary.Right)}"; + case ExpressionType.Modulo: + return $"{parseExp(expBinary.Left)}%{parseExp(expBinary.Right)}"; + } + } + if (exp.IsParameter() == false) return toFt(Expression.Lambda(exp).Compile().DynamicInvoke()); + throw new Exception($"Unable to parse this expression: {exp}"); + } + internal protected bool ParseTryGetField(Expression exp, out DocumentSchemaFieldInfo field) + { + field = null; + if (exp == null) return false; + if (exp.NodeType != ExpressionType.MemberAccess) return false; + var memberExp = exp as MemberExpression; + if (memberExp == null) return false; + if (memberExp.Expression.IsParameter() == false) return false; + return _schema.FieldsMap.TryGetValue(memberExp.Member.Name, out field); + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class FtDocumentAttribute : Attribute + { + public string Name { get; set; } + public string Prefix { get; set; } + public string Filter { get; set; } + public string Language { get; set; } = "chinese"; + public FtDocumentAttribute(string name) + { + Name = name; + } + } + [AttributeUsage(AttributeTargets.Property)] + public class FtKeyAttribute : Attribute { } + + public abstract class FtFieldAttribute : Attribute + { + public string Name { get; set; } + public string Alias { get; set; } + public string FieldName => string.IsNullOrEmpty(Alias) ? Name : Alias; + } + [AttributeUsage(AttributeTargets.Property)] + public class FtTextFieldAttribute : FtFieldAttribute + { + public double Weight { get; set; } + 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 FtTextFieldAttribute() { } + public FtTextFieldAttribute(string name) + { + Name = name; + } + } + [AttributeUsage(AttributeTargets.Property)] + public class FtTagFieldAttribute : FtFieldAttribute + { + 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 FtTagFieldAttribute() { } + public FtTagFieldAttribute(string name) + { + Name = name; + } + } + [AttributeUsage(AttributeTargets.Property)] + public class FtNumericFieldAttribute : FtFieldAttribute + { + public bool Sortable { get; set; } + public bool NoIndex { get; set; } + public bool MissingIndex { get; set; } + public FtNumericFieldAttribute() { } + public FtNumericFieldAttribute(string name) + { + Name = name; + } + } + + public class FtDocumentRepositorySearchBuilder + { + SearchBuilder _searchBuilder; + FtDocumentRepository _repository; + internal FtDocumentRepositorySearchBuilder(FtDocumentRepository repository, string index, string query) + { + _repository = repository; + _searchBuilder = new SearchBuilder(_repository._client, index, query); + } + + public List ToList() + { + var prefix = _repository._schema.DocumentAttribute.Prefix; + var keyProperty = _repository._schema.KeyProperty; + var docs = _searchBuilder.Execute(); + return docs.Select(doc => { + var item = doc.Body.MapToClass(); + var itemId = doc.Id; + if (!string.IsNullOrEmpty(prefix)) + if (itemId.StartsWith(prefix)) + itemId = itemId.Substring(prefix.Length); + typeof(T).SetPropertyOrFieldValue(item, keyProperty.Name, keyProperty.PropertyType.FromObject(itemId)); + return item; + }).ToList(); + } + + public FtDocumentRepositorySearchBuilder NoContent(bool value = true) + { + _searchBuilder.NoContent(value); + return this; + } + public FtDocumentRepositorySearchBuilder Verbatim(bool value = true) + { + _searchBuilder.Verbatim(value); + return this; + } + public FtDocumentRepositorySearchBuilder Filter(Expression> selector, object min, object max) + { + var fields = _repository.ParseSelectorExpression(selector.Body); + if (fields.Any()) _searchBuilder.Filter(fields.FirstOrDefault().Value, min, max); + return this; + } + public FtDocumentRepositorySearchBuilder Filter(string field, object min, object max) + { + _searchBuilder.Filter(field, min, max); + return this; + } + public FtDocumentRepositorySearchBuilder InKeys(params string[] keys) + { + _searchBuilder.InKeys(keys); + return this; + } + public FtDocumentRepositorySearchBuilder InFields(Expression> selector) + { + var fields = _repository.ParseSelectorExpression(selector.Body).Select(a => a.Value).ToArray(); + if (fields.Any()) _searchBuilder.InFields(fields); + return this; + } + + public FtDocumentRepositorySearchBuilder Return(Expression> selector) + { + var identifiers = _repository.ParseSelectorExpression(selector.Body) + .Select(a => new KeyValuePair(a.Value, a.Key)).ToArray(); + if (identifiers.Any()) _searchBuilder.Return(identifiers); + return this; + } + public FtDocumentRepositorySearchBuilder Sumarize(Expression> selector, long frags = -1, long len = -1, string separator = null) + { + var fields = _repository.ParseSelectorExpression(selector.Body).Select(a => a.Value).ToArray(); + if (fields.Any()) _searchBuilder.Sumarize(fields, frags, len, separator); + return this; + } + public FtDocumentRepositorySearchBuilder Sumarize(string[] fields, long frags = -1, long len = -1, string separator = null) + { + _searchBuilder.Sumarize(fields, frags, len, separator); + return this; + } + public FtDocumentRepositorySearchBuilder HighLight(Expression> selector, string tagsOpen = null, string tagsClose = null) + { + var fields = _repository.ParseSelectorExpression(selector.Body).Select(a => a.Value).ToArray(); + if (fields.Any()) _searchBuilder.HighLight(fields, tagsOpen, tagsClose); + return this; + } + public FtDocumentRepositorySearchBuilder HighLight(string[] fields, string tagsOpen = null, string tagsClose = null) + { + _searchBuilder.HighLight(fields, tagsOpen, tagsClose); + return this; + } + public FtDocumentRepositorySearchBuilder Slop(decimal value) + { + _searchBuilder.Slop(value); + return this; + } + public FtDocumentRepositorySearchBuilder Timeout(long milliseconds) + { + _searchBuilder.Timeout(milliseconds); + return this; + } + public FtDocumentRepositorySearchBuilder InOrder(bool value = true) + { + _searchBuilder.InOrder(value); + return this; + } + public FtDocumentRepositorySearchBuilder Language(string value) + { + _searchBuilder.Language(value); + return this; + } + public FtDocumentRepositorySearchBuilder Scorer(string value) + { + _searchBuilder.Scorer(value); + return this; + } + public FtDocumentRepositorySearchBuilder SortBy(Expression> selector) + { + _searchBuilder.SortBy(_repository.ParseQueryExpression(selector, new FtDocumentRepository.ParseQueryExpressionOptions { IsQuoteFieldName = false }), false); + return this; + } + public FtDocumentRepositorySearchBuilder SortByDesc(Expression> selector) + { + _searchBuilder.SortBy(_repository.ParseQueryExpression(selector, new FtDocumentRepository.ParseQueryExpressionOptions { IsQuoteFieldName = false }), true); + return this; + } + public FtDocumentRepositorySearchBuilder SortBy(string sortBy, bool desc = false) + { + _searchBuilder.SortBy(sortBy, desc); + return this; + } + public FtDocumentRepositorySearchBuilder Limit(long offset, long num) + { + _searchBuilder.Limit(offset, num); + return this; + } + public FtDocumentRepositorySearchBuilder Params(string name, string value) + { + _searchBuilder.Params(name, value); + return this; + } + public FtDocumentRepositorySearchBuilder Dialect(int value) + { + _searchBuilder.Dialect(value); + return this; + } + } +} diff --git a/src/FreeRedis/RedisClient/Modules/RediSearch/SearchBuilder.cs b/src/FreeRedis/RedisClient/Modules/RediSearch/SearchBuilder.cs index 48547541..c11f1116 100644 --- a/src/FreeRedis/RedisClient/Modules/RediSearch/SearchBuilder.cs +++ b/src/FreeRedis/RedisClient/Modules/RediSearch/SearchBuilder.cs @@ -54,7 +54,7 @@ internal SearchBuilder(RedisClient redis, string index, string query) _dialect = redis.ConnectionString.FtDialect; _language = redis.ConnectionString.FtLanguage; } - public List Execute() + internal CommandPacket GetCommandPacket() { var cmd = "FT.SEARCH".Input(_index).Input(_query) .InputIf(_noContent, "NOCONTENT") @@ -69,7 +69,7 @@ public List Execute() 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)); + cmd.Input("RETURN", _return.Sum(a => a[1] == null ? 1 : 3)); foreach (var ret in _return) if (ret[1] == null) cmd.Input(ret[0]); else cmd.Input(ret[0], "AS", ret[1]); @@ -102,6 +102,11 @@ public List Execute() if (_params.Any()) cmd.Input("PARAMS", _params.Count).Input(_params); cmd .InputIf(_dialect > 0, "DIALECT", _dialect); + return cmd; + } + public List Execute() + { + var cmd = GetCommandPacket(); return _redis.Call(cmd, rt => rt.ThrowOrValue((a, _) => { var docs = new List(); @@ -234,7 +239,7 @@ public SearchBuilder Return(params KeyValuePair[] identifierProp 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) + public SearchBuilder Sumarize(string[] fields, long frags = -1, long len = -1, string separator = null) { _sumarize = true; _sumarizeFields.AddRange(fields); @@ -243,7 +248,7 @@ public SearchBuilder Sumarize(string[] fields, long frags, long len, string sepa _sumarizeSeparator = separator; return this; } - public SearchBuilder HighLight(string[] fields, string tagsOpen, string tagsClose) + public SearchBuilder HighLight(string[] fields, string tagsOpen = null, string tagsClose = null) { _highLight = true; _highLightFields.AddRange(fields); diff --git a/test/Unit/FreeRedis.Tests/RedisClientTests/ModulesTests/RediSearchTests.cs b/test/Unit/FreeRedis.Tests/RedisClientTests/ModulesTests/RediSearchTests.cs new file mode 100644 index 00000000..65e68b92 --- /dev/null +++ b/test/Unit/FreeRedis.Tests/RedisClientTests/ModulesTests/RediSearchTests.cs @@ -0,0 +1,273 @@ +using FreeRedis.RediSearch; +using Newtonsoft.Json; +using System; +using System.Diagnostics; +using System.Xml.Linq; +using Xunit; + +namespace FreeRedis.Tests.RedisClientTests.Other +{ + public class RediSearchTests : TestBase + { + + protected static ConnectionStringBuilder Connection = new ConnectionStringBuilder() + { + Host = "8.154.26.119", + MaxPoolSize = 10, + Protocol = RedisProtocol.RESP2, + ClientName = "FreeRedis", + //FtDialect = 4, + FtLanguage = "chinese" + }; + static Lazy _cliLazy = new Lazy(() => + { + var r = new RedisClient(Connection); + r.Serialize = obj => JsonConvert.SerializeObject(obj); + r.Deserialize = (json, type) => JsonConvert.DeserializeObject(json, type); + r.Notice += (s, e) => Trace.WriteLine(e.Log); + return r; + }); + public static RedisClient cli => _cliLazy.Value; + + [FtDocument("index_post", Prefix = "blog:post:")] + class TestDoc + { + [FtKey] + public int Id { get; set; } + + [FtTextField("title", Weight = 5.0)] + public string Title { get; set; } + + [FtTextField("category")] + public string Category { get; set; } + + [FtTextField("content", Weight = 1.0, NoIndex = true)] + public string Content { get; set; } + + [FtTagField("tags")] + public string Tags { get; set; } + + [FtNumericField("views")] + public int Views { get; set; } + } + + [Fact] + public void FtDocumentRepository() + { + var repo = cli.FtDocumentRepository(); + + try + { + repo.DropIndex(); + } + catch { } + repo.CreateIndex(); + + repo.Save(new TestDoc { Id = 1, Title = "测试标题1 word", Category = "一级分类", Content = "测试内容1suffix", Tags = "作者1,作者2", Views = 101 }); + repo.Save(new TestDoc { Id = 2, Title = "prefix测试标题2", Category = "二级分类", Content = "测试infix内容2", Tags = "作者2,作者3", Views = 201 }); + repo.Save(new TestDoc { Id = 3, Title = "测试标题3 word", Category = "一级分类", Content = "测试word内容3", Tags = "作者2,作者5", Views = 301 }); + + repo.Delete(1, 2, 3); + + repo.Save(new[]{ + new TestDoc { Id = 1, Title = "测试标题1 word", Category = "一级分类", Content = "测试内容1suffix", Tags = "作者1,作者2", Views = 101 }, + new TestDoc { Id = 2, Title = "prefix测试标题2", Category = "二级分类", Content = "测试infix内容2", Tags = "作者2,作者3", Views = 201 }, + new TestDoc { Id = 3, Title = "测试标题3 word", Category = "一级分类", Content = "测试word内容3", Tags = "作者2,作者5", Views = 301 } + }); + + var list = repo.Search("*").InFields(a => new { a.Title }).ToList(); + list = repo.Search("*").Return(a => new { a.Title, a.Tags }).ToList(); + list = repo.Search("*").Return(a => new { tit1 = a.Title, tgs1 = a.Tags, a.Title, a.Tags }).ToList(); + + list = repo.Search(a => a.Title == "word").Filter(a => a.Views, 1, 1000).ToList(); + list = repo.Search("word").ToList(); + list = repo.Search("@title:word").ToList(); + list = repo.Search("prefix*").ToList(); + list = repo.Search("@title:prefix*").ToList(); + list = repo.Search("*suffix").ToList(); + list = repo.Search("*infix*").ToList(); + list = repo.Search("%word%").ToList(); + + list = repo.Search("@views:[200 300]").ToList(); + list = repo.Search("@views:[-inf 2000]").SortBy(a => a.Views).Limit(0, 5).ToList(); + list = repo.Search("@views:[(200 (300]").ToList(); + list = repo.Search("@views>=200").Dialect(4).ToList(); + list = repo.Search("@views:[200 +inf]").ToList(); + list = repo.Search("@views<=300").Dialect(4).ToList(); + list = repo.Search("@views:[-inf 300]").ToList(); + list = repo.Search("@views==200").Dialect(4).ToList(); + list = repo.Search("@views:[200 200]").ToList(); + list = repo.Search("@views!=200").Dialect(4).ToList(); + list = repo.Search("-@views:[200 200]").ToList(); + list = repo.Search("@views==200 | @views==300").Dialect(4).ToList(); + list = repo.Search("*").Filter("views", 200, 300).Dialect(4).ToList(); + + + list = repo.Search("word").ToList(); + list = repo.Search("@title:word").ToList(); + list = repo.Search("prefix*").ToList(); + list = repo.Search("@title:prefix*").ToList(); + list = repo.Search("*suffix").ToList(); + list = repo.Search("*infix*").ToList(); + list = repo.Search("%word%").ToList(); + + list = repo.Search("@views:[200 300]").ToList(); + list = repo.Search("@views:[-inf 2000]").SortBy(a => a.Views).Limit(0, 5).ToList(); + list = repo.Search("@views:[(200 (300]").ToList(); + list = repo.Search("@views>=200").Dialect(4).ToList(); + list = repo.Search("@views:[200 +inf]").ToList(); + list = repo.Search("@views<=300").Dialect(4).ToList(); + list = repo.Search("@views:[-inf 300]").ToList(); + list = repo.Search("@views==200").Dialect(4).ToList(); + list = repo.Search("@views:[200 200]").ToList(); + list = repo.Search("@views!=200").Dialect(4).ToList(); + list = repo.Search("-@views:[200 200]").ToList(); + list = repo.Search("@views==200 | @views==300").Dialect(4).ToList(); + list = repo.Search("*").Filter("views", 200, 300).Dialect(4).ToList(); + } + + [Fact] + public void FtSearch() + { + var idxName = Guid.NewGuid().ToString(); + cli.FtCreate(idxName) + .On(IndexDataType.Hash) + .Prefix("blog:post:") + .AddTextField("title", weight: 5.0) + .AddTextField("content") + .AddTagField("author") + .AddNumericField("created_date", sortable: true) + .AddNumericField("views") + .Execute(); + + cli.HSet("blog:post:1", "title", "测试标题1 word", "content", "测试内容1suffix", "author", "作者1,作者2", "created_date", "10000", "views", 10); + cli.HSet("blog:post:2", "title", "prefix测试标题2", "content", "测试infix内容2", "author", "作者2,作者3", "created_date", "10001", "views", 201); + cli.HSet("blog:post:3", "title", "测试标题3 word", "content", "测试word内容3", "author", "作者2,作者5", "created_date", "10002", "views", 301); + + var list = cli.FtSearch(idxName, "word").Execute(); + list = cli.FtSearch(idxName, "@title:word").Execute(); + list = cli.FtSearch(idxName, "prefix*").Execute(); + list = cli.FtSearch(idxName, "@title:prefix*").Execute(); + list = cli.FtSearch(idxName, "*suffix").Execute(); + list = cli.FtSearch(idxName, "*infix*").Execute(); + list = cli.FtSearch(idxName, "%word%").Execute(); + + + list = cli.FtSearch(idxName, "@views:[200 300]").Execute(); + list = cli.FtSearch(idxName, "@views:[-inf 2000]").SortBy("views").Limit(0, 5).Execute(); + list = cli.FtSearch(idxName, "@views:[(200 (300]").Execute(); + list = cli.FtSearch(idxName, "@views>=200").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "@views:[200 +inf]").Execute(); + list = cli.FtSearch(idxName, "@views<=300").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "@views:[-inf 300]").Execute(); + list = cli.FtSearch(idxName, "@views==200").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "@views:[200 200]").Execute(); + list = cli.FtSearch(idxName, "@views!=200").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "-@views:[200 200]").Execute(); + list = cli.FtSearch(idxName, "@views==200 | @views==300").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "*").Filter("views", 200, 300).Dialect(4).Execute(); + } + + + [Fact] + public void FtAggregate() + { + var idxName = Guid.NewGuid().ToString(); + cli.FtCreate(idxName) + .On(IndexDataType.Hash) + .Prefix("blog:post:") + .AddTextField("title", weight: 5.0) + .AddTextField("content") + .AddTagField("author") + .AddNumericField("created_date", sortable: true) + .AddNumericField("views") + .Execute(); + + cli.HSet("blog:post:1", "title", "测试标题1 word", "content", "测试内容1suffix", "author", "作者1,作者2", "created_date", "10000", "views", 10); + cli.HSet("blog:post:2", "title", "prefix测试标题2", "content", "测试infix内容2", "author", "作者2,作者3", "created_date", "10001", "views", 201); + cli.HSet("blog:post:3", "title", "测试标题3 word", "content", "测试word内容3", "author", "作者2,作者5", "created_date", "10002", "views", 301); + + var list = cli.FtSearch(idxName, "word").Execute(); + list = cli.FtSearch(idxName, "@title:word").Execute(); + list = cli.FtSearch(idxName, "prefix*").Execute(); + list = cli.FtSearch(idxName, "@title:prefix*").Execute(); + list = cli.FtSearch(idxName, "*suffix").Execute(); + list = cli.FtSearch(idxName, "*infix*").Execute(); + list = cli.FtSearch(idxName, "%word%").Execute(); + + + list = cli.FtSearch(idxName, "@views:[200 300]").Execute(); + list = cli.FtSearch(idxName, "@views:[-inf 2000]").SortBy("views").Limit(0, 5).Execute(); + list = cli.FtSearch(idxName, "@views:[(200 (300]").Execute(); + list = cli.FtSearch(idxName, "@views>=200").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "@views:[200 +inf]").Execute(); + list = cli.FtSearch(idxName, "@views<=300").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "@views:[-inf 300]").Execute(); + list = cli.FtSearch(idxName, "@views==200").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "@views:[200 200]").Execute(); + list = cli.FtSearch(idxName, "@views!=200").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "-@views:[200 200]").Execute(); + list = cli.FtSearch(idxName, "@views==200 | @views==300").Dialect(4).Execute(); + list = cli.FtSearch(idxName, "*").Filter("views", 200, 300).Dialect(4).Execute(); + } + + [Fact] + public void FtCreate() + { + cli.FtCreate("idx1") + .On(IndexDataType.Hash) + .Prefix("blog:post:") + .AddTextField("title", weight: 5.0) + .AddTextField("content") + .AddTagField("author") + .AddNumericField("created_date", sortable: true) + .AddNumericField("views") + .Execute(); + + cli.FtCreate("idx2") + .On(IndexDataType.Hash) + .Prefix("book:details:") + .AddTextField("title") + .AddTagField("categories", separator: ";") + .Execute(); + + cli.FtCreate("idx3") + .On(IndexDataType.Hash) + .Prefix("blog:post:") + .AddTextField("sku", alias: "sku_text") + .AddTagField("sku", alias: "sku_tag", sortable: true) + .Execute(); + + cli.FtCreate("idx4") + .On(IndexDataType.Hash) + .Prefix("author:details:", "book:details:") + .AddTagField("author_id", sortable: true) + .AddTagField("author_ids") + .AddTextField("title") + .AddTextField("name") + .Execute(); + + cli.FtCreate("idx5") + .On(IndexDataType.Hash) + .Prefix("author:details") + .Filter("startswith(@name, 'G')") + .AddTextField("name") + .Execute(); + + cli.FtCreate("idx6") + .On(IndexDataType.Hash) + .Prefix("book:details") + .Filter("@subtitle != ''") + .AddTextField("title") + .Execute(); + + cli.FtCreate("idx7") + .On(IndexDataType.Json) + .Prefix("book:details") + .Filter("@subtitle != ''") + .AddTextField("$.title", alias: "title") + .AddTagField("$.categories", alias: "categories") + .Execute(); + } + } +}