13 changed files with 1032 additions and 595 deletions
@ -0,0 +1,98 @@ |
|||
using Elastic.Clients.Elasticsearch.QueryDsl; |
|||
using System; |
|||
using System.Collections; |
|||
using System.Linq; |
|||
using System.Linq.Expressions; |
|||
|
|||
namespace LINGYUN.Abp.Elasticsearch; |
|||
|
|||
public partial class ExpressionQueryTranslator |
|||
{ |
|||
/// <summary>
|
|||
/// 翻译集合 Contains
|
|||
/// </summary>
|
|||
private Query TranslateCollectionContains(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
// list.Contains(value) 或 hashSet.Contains(value)
|
|||
// 有两种情况:
|
|||
// 1. 外部集合.Contains(字段) - 例如:new[]{"a","b"}.Contains(x.xxx)
|
|||
// 2. 字段.Contains(值) - 例如:x.Y.Contains("B")
|
|||
|
|||
Expression collectionExpr = node.Object!; |
|||
Expression valueExpr = node.Arguments[0]; |
|||
|
|||
// 判断哪个是字段,哪个是值
|
|||
var collectionIsField = IsFieldLike(collectionExpr); |
|||
var valueIsField = IsFieldLike(valueExpr); |
|||
|
|||
if (collectionIsField && !valueIsField) |
|||
{ |
|||
// 情况 2:字段.Contains(值)
|
|||
// 这通常用于集合字段,如 x.Tags.Contains("B")
|
|||
var field = ResolveField(collectionExpr, prefix, mappingInfo); |
|||
var value = Evaluate(valueExpr); |
|||
|
|||
if (value == null) |
|||
{ |
|||
return new MatchNoneQuery(); |
|||
} |
|||
|
|||
// 对于集合字段,使用 Terms 查询或 Term 查询
|
|||
// 如果字段是数组或集合类型,单个值的 Contains 实际上就是 Term 查询
|
|||
return BuildEquality(field, value); |
|||
} |
|||
else if (!collectionIsField && valueIsField) |
|||
{ |
|||
// 情况 1:外部集合.Contains(字段)
|
|||
var field = ResolveField(valueExpr, prefix, mappingInfo); |
|||
|
|||
// 尝试获取集合的值
|
|||
var collectionValue = Evaluate(collectionExpr); |
|||
|
|||
if (collectionValue is IEnumerable enumerable && collectionValue is not string) |
|||
{ |
|||
var values = enumerable.Cast<object>().Select(NormalizeValue).ToList(); |
|||
if (values.Count == 0) |
|||
{ |
|||
return new MatchNoneQuery(); |
|||
} |
|||
if (values.Count == 1) |
|||
{ |
|||
// 如果集合只有一个值,使用 TermQuery
|
|||
return BuildEquality(field, values[0]); |
|||
} |
|||
// 多个值使用 TermsQuery
|
|||
return new TermsQuery { Field = field.Path, Terms = new TermsQueryField(values) }; |
|||
} |
|||
|
|||
// 如果集合值无法获取,使用默认处理
|
|||
var defaultValue = Evaluate(valueExpr); |
|||
return BuildEquality(field, defaultValue!); |
|||
} |
|||
else if (collectionIsField && valueIsField) |
|||
{ |
|||
// 两个都是字段,这种情况较少见
|
|||
throw new NotSupportedException($"Unsupported Contains with two field expressions: {node}"); |
|||
} |
|||
else |
|||
{ |
|||
// 两个都不是字段,尝试直接求值
|
|||
var collectionValue = Evaluate(collectionExpr); |
|||
var value = Evaluate(valueExpr); |
|||
|
|||
if (collectionValue is IEnumerable enumerable && collectionValue is not string) |
|||
{ |
|||
if (enumerable.Cast<object>().Contains(value)) |
|||
{ |
|||
return new MatchAllQuery(); |
|||
} |
|||
else |
|||
{ |
|||
return new MatchNoneQuery(); |
|||
} |
|||
} |
|||
|
|||
throw new NotSupportedException($"Unsupported Contains expression: {node}"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using Elastic.Clients.Elasticsearch.QueryDsl; |
|||
using System; |
|||
using System.Linq.Expressions; |
|||
|
|||
namespace LINGYUN.Abp.Elasticsearch; |
|||
|
|||
public partial class ExpressionQueryTranslator |
|||
{ |
|||
/// <summary>
|
|||
/// 翻译 Enum.HasFlag
|
|||
/// </summary>
|
|||
private Query TranslateEnumHasFlag(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var field = ResolveField(node.Object!, prefix, mappingInfo); |
|||
var flag = Evaluate(node.Arguments[0]); |
|||
|
|||
if (flag == null) |
|||
{ |
|||
throw new NotSupportedException("Cannot use null flag in Enum.HasFlag"); |
|||
} |
|||
|
|||
var flagValue = Convert.ToInt64(flag); |
|||
return new TermQuery { Field = field.Path, Value = flagValue }; |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
using Elastic.Clients.Elasticsearch.QueryDsl; |
|||
using System; |
|||
using System.Linq; |
|||
using System.Linq.Expressions; |
|||
|
|||
namespace LINGYUN.Abp.Elasticsearch; |
|||
|
|||
public partial class ExpressionQueryTranslator |
|||
{ |
|||
/// <summary>
|
|||
/// 翻译 Enumerable 方法
|
|||
/// </summary>
|
|||
private Query TranslateEnumerableMethod(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
switch (node.Method.Name) |
|||
{ |
|||
case nameof(Enumerable.Any): |
|||
return TranslateEnumerableAny(node, prefix, mappingInfo); |
|||
|
|||
case nameof(Enumerable.Contains): |
|||
return TranslateEnumerableContains(node, prefix, mappingInfo); |
|||
|
|||
case nameof(Enumerable.All): |
|||
return TranslateEnumerableAll(node, prefix, mappingInfo); |
|||
|
|||
default: |
|||
throw new NotSupportedException($"Unsupported Enumerable method {node.Method.Name}"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 Enumerable.Any
|
|||
/// </summary>
|
|||
private Query TranslateEnumerableAny(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var collectionField = ResolveField(node.Arguments[0], prefix, mappingInfo); |
|||
|
|||
Query inner; |
|||
if (node.Arguments.Count == 1) |
|||
{ |
|||
// .Any() 检查集合是否存在
|
|||
inner = new ExistsQuery { Field = collectionField.Path }; |
|||
} |
|||
else |
|||
{ |
|||
// .Any(predicate)
|
|||
var predicate = UnwrapLambda(node.Arguments[1]); |
|||
inner = TranslateNode(predicate.Body, prefix: collectionField.Path, mappingInfo); |
|||
} |
|||
|
|||
var shouldUseNested = collectionField.IsNested || (mappingInfo?.IsNested(collectionField.Path) ?? false); |
|||
|
|||
return shouldUseNested |
|||
? new NestedQuery(collectionField.Path, inner) |
|||
: inner; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 Enumerable.Contains
|
|||
/// </summary>
|
|||
private Query TranslateEnumerableContains(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
Expression collectionExpr; |
|||
Expression valueExpr; |
|||
|
|||
if (node.Object != null) |
|||
{ |
|||
// list.Contains(value)
|
|||
collectionExpr = node.Object; |
|||
valueExpr = node.Arguments[0]; |
|||
} |
|||
else |
|||
{ |
|||
// Enumerable.Contains(list, value)
|
|||
collectionExpr = node.Arguments[0]; |
|||
valueExpr = node.Arguments[1]; |
|||
} |
|||
|
|||
var field = ResolveField(collectionExpr, prefix, mappingInfo); |
|||
var value = Evaluate(valueExpr); |
|||
|
|||
return BuildTermsQuery(field, value!); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 Enumerable.All
|
|||
/// </summary>
|
|||
private Query TranslateEnumerableAll(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var collectionField = ResolveField(node.Arguments[0], prefix, mappingInfo); |
|||
var predicate = UnwrapLambda(node.Arguments[1]); |
|||
|
|||
var inner = TranslateNode(predicate.Body, prefix: collectionField.Path, mappingInfo); |
|||
|
|||
return new NestedQuery(collectionField.Path, inner); |
|||
} |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq.Expressions; |
|||
using System.Reflection; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.Elasticsearch; |
|||
|
|||
public partial class ExpressionQueryTranslator |
|||
{ |
|||
/// <summary>
|
|||
/// 解析字段
|
|||
/// </summary>
|
|||
protected virtual FieldInfo ResolveField(Expression expression, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var current = expression; |
|||
while (current is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary) |
|||
{ |
|||
current = unary.Operand; |
|||
} |
|||
|
|||
var names = new Stack<string>(); |
|||
Type? leafType = null; |
|||
string? leafName = null; |
|||
|
|||
// 收集成员路径
|
|||
while (current is MemberExpression member) |
|||
{ |
|||
leafName ??= member.Member.Name; |
|||
leafType ??= GetMemberType(member.Member); |
|||
names.Push(ResolveFieldName(member.Member)); |
|||
current = member.Expression!; |
|||
} |
|||
|
|||
if (current is not ParameterExpression && current is not ConstantExpression) |
|||
{ |
|||
throw new NotSupportedException($"Unable to parse as field path: {expression}"); |
|||
} |
|||
|
|||
var path = string.Join(".", names); |
|||
|
|||
// 应用前缀
|
|||
if (!string.IsNullOrEmpty(prefix) && path.Length > 0) |
|||
{ |
|||
path = prefix + "." + path; |
|||
} |
|||
else if (path.Length == 0) |
|||
{ |
|||
path = prefix ?? string.Empty; |
|||
} |
|||
|
|||
// 获取字段映射信息
|
|||
var finalMapping = mappingInfo?.GetField(path); |
|||
|
|||
// 如果是 text 类型且有 keyword 子字段,自动使用 .keyword
|
|||
if (finalMapping?.IsText == true && finalMapping.Properties?.ContainsKey("keyword") == true) |
|||
{ |
|||
path = $"{path}.keyword"; |
|||
finalMapping = mappingInfo?.GetField(path); |
|||
} |
|||
|
|||
// 如果 leafType 为 null,使用 expression.Type
|
|||
var type = leafType ?? expression.Type; |
|||
var underlyingType = Nullable.GetUnderlyingType(type) ?? type; |
|||
|
|||
return new FieldInfo( |
|||
path, |
|||
underlyingType, |
|||
leafName ?? string.Empty, |
|||
finalMapping?.IsKeyword ?? false, |
|||
finalMapping?.IsText ?? false, |
|||
finalMapping?.IsWildcard ?? false, |
|||
finalMapping?.IsNested ?? false || (mappingInfo?.IsNested(path) ?? false), |
|||
finalMapping?.IsDate ?? false, |
|||
finalMapping?.IsNumeric ?? false, |
|||
finalMapping?.IsBoolean ?? false, |
|||
finalMapping?.IsRange ?? false, |
|||
finalMapping?.Format, |
|||
finalMapping?.HasMultiFields ?? false |
|||
); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取成员的实际类型
|
|||
/// </summary>
|
|||
private static Type GetMemberType(MemberInfo member) |
|||
{ |
|||
return member switch |
|||
{ |
|||
System.Reflection.FieldInfo field => field.FieldType, |
|||
PropertyInfo property => property.PropertyType, |
|||
MethodInfo method => method.ReturnType, |
|||
_ => typeof(object) |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 解析字段名称
|
|||
/// </summary>
|
|||
private static string ResolveFieldName(MemberInfo member) |
|||
{ |
|||
// 检查 JsonPropertyName 属性
|
|||
if (member is PropertyInfo property) |
|||
{ |
|||
var jsonName = property.GetCustomAttribute<JsonPropertyNameAttribute>(); |
|||
if (jsonName != null && !string.IsNullOrWhiteSpace(jsonName.Name)) |
|||
{ |
|||
return jsonName.Name; |
|||
} |
|||
} |
|||
|
|||
return member.Name; |
|||
} |
|||
} |
|||
@ -0,0 +1,317 @@ |
|||
using Elastic.Clients.Elasticsearch.QueryDsl; |
|||
using System; |
|||
using System.Linq.Expressions; |
|||
|
|||
namespace LINGYUN.Abp.Elasticsearch; |
|||
|
|||
public partial class ExpressionQueryTranslator |
|||
{ |
|||
/// <summary>
|
|||
/// 翻译字符串方法
|
|||
/// </summary>
|
|||
private Query TranslateStringMethod(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
return node.Method.Name switch |
|||
{ |
|||
// 实例方法
|
|||
nameof(string.Contains) => TranslateStringContains(node, prefix, mappingInfo), |
|||
nameof(string.StartsWith) => TranslateStringStartsWith(node, prefix, mappingInfo), |
|||
nameof(string.EndsWith) => TranslateStringEndsWith(node, prefix, mappingInfo), |
|||
nameof(string.Equals) => TranslateStringEquals(node, prefix, mappingInfo), |
|||
nameof(string.CompareTo) => TranslateStringCompareTo(node, prefix, mappingInfo), |
|||
nameof(string.IndexOf) => TranslateStringIndexOf(node, prefix, mappingInfo), |
|||
|
|||
// 静态方法
|
|||
nameof(string.IsNullOrEmpty) => TranslateStringIsNullOrEmpty(node, prefix, mappingInfo), |
|||
nameof(string.IsNullOrWhiteSpace) => TranslateStringIsNullOrWhiteSpace(node, prefix, mappingInfo), |
|||
|
|||
// 其他方法不支持
|
|||
_ => throw new NotSupportedException($"Unsupported string method {node.Method.Name}"), |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 Contains
|
|||
/// </summary>
|
|||
private Query TranslateStringContains(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo); |
|||
var fieldMapping = mappingInfo?.GetField(field.Path); |
|||
return TranslateStringContains(field, fieldMapping, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 StartsWith
|
|||
/// </summary>
|
|||
private Query TranslateStringStartsWith(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo); |
|||
var fieldMapping = mappingInfo?.GetField(field.Path); |
|||
return TranslateStartsWith(field, fieldMapping, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 EndsWith
|
|||
/// </summary>
|
|||
private Query TranslateStringEndsWith(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo); |
|||
var fieldMapping = mappingInfo?.GetField(field.Path); |
|||
return TranslateEndsWith(field, fieldMapping, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 StartsWith
|
|||
/// </summary>
|
|||
private Query TranslateStartsWith(FieldInfo field, FieldMappingInfo? fieldMapping, string value) |
|||
{ |
|||
var pattern = EscapeWildcard(value) + "*"; |
|||
|
|||
// Wildcard 类型
|
|||
if (field.IsWildcard || fieldMapping?.IsWildcard == true) |
|||
{ |
|||
return new WildcardQuery { Field = field.Path, Value = pattern }; |
|||
} |
|||
|
|||
// Keyword 类型或 Text 有 keyword 子字段
|
|||
if (field.IsKeyword || fieldMapping?.IsKeyword == true || |
|||
(fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true)) |
|||
{ |
|||
var fieldPath = fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true |
|||
? $"{field.Path}.keyword" |
|||
: field.Path; |
|||
return new WildcardQuery { Field = fieldPath, Value = pattern }; |
|||
} |
|||
|
|||
// Text 类型无 keyword 子字段
|
|||
if (field.IsText || fieldMapping?.IsText == true) |
|||
{ |
|||
return new MatchPhrasePrefixQuery |
|||
{ |
|||
Field = field.Path, |
|||
Query = value |
|||
}; |
|||
} |
|||
|
|||
return new WildcardQuery { Field = field.Path, Value = pattern }; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 EndsWith
|
|||
/// </summary>
|
|||
private Query TranslateEndsWith(FieldInfo field, FieldMappingInfo? fieldMapping, string value) |
|||
{ |
|||
var pattern = "*" + EscapeWildcard(value); |
|||
|
|||
// Wildcard 类型
|
|||
if (field.IsWildcard || fieldMapping?.IsWildcard == true) |
|||
{ |
|||
return new WildcardQuery { Field = field.Path, Value = pattern }; |
|||
} |
|||
|
|||
// Keyword 类型或 Text 有 keyword 子字段
|
|||
if (field.IsKeyword || fieldMapping?.IsKeyword == true || |
|||
(fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true)) |
|||
{ |
|||
var fieldPath = fieldMapping?.IsText == true && fieldMapping?.Properties?.ContainsKey("keyword") == true |
|||
? $"{field.Path}.keyword" |
|||
: field.Path; |
|||
return new WildcardQuery { Field = fieldPath, Value = pattern }; |
|||
} |
|||
|
|||
// Text 类型无 keyword 子字段
|
|||
if (field.IsText || fieldMapping?.IsText == true) |
|||
{ |
|||
return new MatchPhraseQuery |
|||
{ |
|||
Field = field.Path, |
|||
Query = value |
|||
}; |
|||
} |
|||
|
|||
return new WildcardQuery { Field = field.Path, Value = pattern }; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 string.Equals
|
|||
/// </summary>
|
|||
private Query TranslateStringEquals(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo); |
|||
|
|||
if (string.IsNullOrEmpty(value)) |
|||
{ |
|||
return new BoolQuery { MustNot = new Query[] { new ExistsQuery { Field = field.Path } } }; |
|||
} |
|||
|
|||
return BuildEquality(field, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 string.CompareTo
|
|||
/// </summary>
|
|||
private Query TranslateStringCompareTo(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo); |
|||
|
|||
// CompareTo 通常用于比较,这里简化为相等比较
|
|||
// 如果需要更复杂的比较逻辑,可以在这里扩展
|
|||
return BuildEquality(field, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 string.IndexOf
|
|||
/// </summary>
|
|||
private Query TranslateStringIndexOf(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var (field, value) = GetStringMethodOperands(node, prefix, mappingInfo); |
|||
var fieldMapping = mappingInfo?.GetField(field.Path); |
|||
|
|||
// IndexOf >= 0 等价于 Contains
|
|||
return TranslateStringContains(field, fieldMapping, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 Contains
|
|||
/// </summary>
|
|||
private Query TranslateStringContains(FieldInfo field, FieldMappingInfo? fieldMapping, string value) |
|||
{ |
|||
// 1. Wildcard 类型 - 直接使用通配符查询(最优)
|
|||
if (field.IsWildcard || fieldMapping?.IsWildcard == true) |
|||
{ |
|||
return new WildcardQuery |
|||
{ |
|||
Field = field.Path, |
|||
Value = "*" + EscapeWildcard(value) + "*" |
|||
}; |
|||
} |
|||
|
|||
// 2. Keyword 类型 - 使用通配符查询
|
|||
if (field.IsKeyword || fieldMapping?.IsKeyword == true) |
|||
{ |
|||
return new WildcardQuery |
|||
{ |
|||
Field = field.Path, |
|||
Value = "*" + EscapeWildcard(value) + "*" |
|||
}; |
|||
} |
|||
|
|||
// 3. Text 类型
|
|||
if (field.IsText || fieldMapping?.IsText == true) |
|||
{ |
|||
// 3.1 如果有 keyword 子字段,使用 .keyword 进行通配符查询
|
|||
if (fieldMapping?.Properties?.ContainsKey("keyword") == true) |
|||
{ |
|||
return new WildcardQuery |
|||
{ |
|||
Field = $"{field.Path}.keyword", |
|||
Value = "*" + EscapeWildcard(value) + "*" |
|||
}; |
|||
} |
|||
|
|||
// 3.2 没有 keyword 子字段,使用 MatchPhrase 进行全文搜索
|
|||
// 注意:这不是精确的 Contains,而是分词后的短语匹配
|
|||
return new MatchPhraseQuery |
|||
{ |
|||
Field = field.Path, |
|||
Query = value |
|||
}; |
|||
} |
|||
|
|||
// 4. 默认 - 尝试使用通配符
|
|||
return new WildcardQuery |
|||
{ |
|||
Field = field.Path, |
|||
Value = "*" + EscapeWildcard(value) + "*" |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 string.IsNullOrEmpty
|
|||
/// </summary>
|
|||
private Query TranslateStringIsNullOrEmpty(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var field = ResolveField(node.Arguments[0], prefix, mappingInfo); |
|||
|
|||
// field == null || field == ""
|
|||
return new BoolQuery |
|||
{ |
|||
Should = new Query[] |
|||
{ |
|||
// null 或不存在
|
|||
new BoolQuery |
|||
{ |
|||
MustNot = new Query[] { new ExistsQuery { Field = field.Path } } |
|||
}, |
|||
// 空字符串
|
|||
new TermQuery |
|||
{ |
|||
Field = field.Path, |
|||
Value = string.Empty |
|||
} |
|||
}, |
|||
MinimumShouldMatch = 1 |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译 string.IsNullOrWhiteSpace
|
|||
/// </summary>
|
|||
private Query TranslateStringIsNullOrWhiteSpace(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
var field = ResolveField(node.Arguments[0], prefix, mappingInfo); |
|||
|
|||
// field == null || field == "" || Regex.IsMatch(field, @"^\s*$")
|
|||
return new BoolQuery |
|||
{ |
|||
Should = new Query[] |
|||
{ |
|||
// null 或不存在
|
|||
new BoolQuery |
|||
{ |
|||
MustNot = new Query[] { new ExistsQuery { Field = field.Path } } |
|||
}, |
|||
// 空字符串
|
|||
new TermQuery |
|||
{ |
|||
Field = field.Path, |
|||
Value = string.Empty |
|||
}, |
|||
// 使用正则表达式匹配只有空白字符的字符串
|
|||
new RegexpQuery |
|||
{ |
|||
Field = field.Path, |
|||
Value = @"^\s*$" |
|||
} |
|||
}, |
|||
MinimumShouldMatch = 1 |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 翻译字符串实例方法(获取字段和值)
|
|||
/// </summary>
|
|||
private (FieldInfo Field, string Value) GetStringMethodOperands(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo) |
|||
{ |
|||
Expression fieldExpression; |
|||
Expression valueExpression; |
|||
|
|||
if (node.Object != null) |
|||
{ |
|||
// 实例方法:obj.Method(value)
|
|||
fieldExpression = node.Object; |
|||
valueExpression = node.Arguments[0]; |
|||
} |
|||
else |
|||
{ |
|||
// 静态方法:string.Method(field, value)
|
|||
fieldExpression = node.Arguments[0]; |
|||
valueExpression = node.Arguments.Count > 1 ? node.Arguments[1] : node.Arguments[0]; |
|||
} |
|||
|
|||
var field = ResolveField(fieldExpression, prefix, mappingInfo); |
|||
var value = Evaluate(valueExpression)?.ToString() ?? string.Empty; |
|||
|
|||
return (field, value); |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
namespace LINGYUN.Abp.Elasticsearch; |
|||
|
|||
public class DefaultExpressionQueryService_Tests : ExpressionQueryService_Tests<AbpElasticsearchTestModule> |
|||
{ |
|||
} |
|||
@ -0,0 +1,169 @@ |
|||
using Elastic.Clients.Elasticsearch; |
|||
using Shouldly; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Testing; |
|||
using Volo.Abp.Threading; |
|||
using Xunit; |
|||
|
|||
namespace LINGYUN.Abp.Elasticsearch; |
|||
|
|||
public abstract class ExpressionQueryService_Tests<TStartupModule> : AbpIntegratedTest<TStartupModule> |
|||
where TStartupModule : IAbpModule |
|||
{ |
|||
private List<TestDocument> _documents = new List<TestDocument>(); |
|||
protected IExpressionQueryService ExpressionQueryService { get; } |
|||
protected ExpressionQueryService_Tests() |
|||
{ |
|||
ExpressionQueryService = GetRequiredService<IExpressionQueryService>(); |
|||
} |
|||
|
|||
#region 索引初始化
|
|||
|
|||
protected override void AfterInitialize() |
|||
{ |
|||
AsyncHelper.RunSync(async () => await ApplicationInitializationAsync()); |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
AsyncHelper.RunSync(async () => await ApplicationShutdownAsync()); |
|||
} |
|||
|
|||
protected async virtual Task ApplicationInitializationAsync() |
|||
{ |
|||
var clientFactory = GetRequiredService<IElasticsearchClientFactory>(); |
|||
var client = clientFactory.Create(); |
|||
|
|||
_documents.AddRange(new[] |
|||
{ |
|||
new TestDocument |
|||
{ |
|||
Name = "Name1", |
|||
Age = 20, |
|||
Address = new Address |
|||
{ |
|||
City = "HANGZHOU", |
|||
}, |
|||
CreatedTime = new DateTime(2026, 8, 1, 0, 0, 0), |
|||
Status = TestEnum.Active, |
|||
StringValueStatus = TestEnum.Pending, |
|||
IsActive = true, |
|||
Id = 1, |
|||
Salary = 3m, |
|||
}, |
|||
new TestDocument |
|||
{ |
|||
Name = "Name2", |
|||
Age = 10, |
|||
Address = new Address |
|||
{ |
|||
City = "GUANGZHOU", |
|||
}, |
|||
CreatedTime = new DateTime(2026, 5, 1, 0, 0, 0), |
|||
Status = TestEnum.Inactive, |
|||
StringValueStatus = TestEnum.Active, |
|||
IsActive = false, |
|||
Id = 2, |
|||
Salary = 7m, |
|||
Tags = new List<string>{ "B" }, |
|||
}, |
|||
new TestDocument |
|||
{ |
|||
Name = "Test1", |
|||
Age = 30, |
|||
Address = new Address |
|||
{ |
|||
City = "BEIJING", |
|||
}, |
|||
CreatedTime = new DateTime(2026, 3, 1, 0, 0, 0), |
|||
Status = TestEnum.Pending, |
|||
StringValueStatus = TestEnum.Inactive, |
|||
IsActive = true, |
|||
Id = 3, |
|||
Salary = 10m, |
|||
Tags = new List<string> { "B", "C" }, |
|||
}, |
|||
}); |
|||
|
|||
await client.BulkAsync(b => |
|||
b.Index(TestDocumentIndexNames.Index) |
|||
.Refresh(Refresh.WaitFor) |
|||
.IndexMany(_documents)); |
|||
} |
|||
|
|||
protected async virtual Task ApplicationShutdownAsync() |
|||
{ |
|||
var clientFactory = GetRequiredService<IElasticsearchClientFactory>(); |
|||
var client = clientFactory.Create(); |
|||
|
|||
await client.Indices.DeleteAsync(TestDocumentIndexNames.Index); |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
[Fact] |
|||
public async Task Should_Get_Count() |
|||
{ |
|||
(await ExpressionQueryService.GetCountAsync<TestDocument>( |
|||
TestDocumentIndexNames.Index, |
|||
x => x.Name!.StartsWith("Name") && x.Salary <= 10m)).ShouldBe(2); |
|||
|
|||
(await ExpressionQueryService.GetCountAsync<TestDocument>( |
|||
TestDocumentIndexNames.Index, |
|||
x => !string.IsNullOrWhiteSpace(x.Name) && x.Tags != null && x.Tags.Contains("B"))).ShouldBe(2); |
|||
|
|||
(await ExpressionQueryService.GetCountAsync<TestDocument>( |
|||
TestDocumentIndexNames.Index, |
|||
x => |
|||
x.Name != null && x.Name.Contains("1") && |
|||
(x.Status == TestEnum.Active || x.Status == TestEnum.Pending))).ShouldBe(2); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_List() |
|||
{ |
|||
var list1 = await ExpressionQueryService.GetListAsync<TestDocument>( |
|||
TestDocumentIndexNames.Index, |
|||
x => x.Name!.StartsWith("Name") && x.Salary <= 10m); |
|||
list1.Count.ShouldBe(2); |
|||
list1[0].Name.ShouldBe("Name1"); |
|||
list1[0].Age.ShouldBe(20); |
|||
list1[0].Status.ShouldBe(TestEnum.Active); |
|||
list1[0].StringValueStatus.ShouldBe(TestEnum.Pending); |
|||
list1[0].IsActive.ShouldBeTrue(); |
|||
list1[0].Tags.ShouldBeNull(); |
|||
list1[0].Address.ShouldNotBeNull(); |
|||
list1[0].Address!.City.ShouldBe("HANGZHOU"); |
|||
|
|||
var list2 = await ExpressionQueryService.GetListAsync<TestDocument>( |
|||
TestDocumentIndexNames.Index, |
|||
x => !string.IsNullOrWhiteSpace(x.Name) && x.Tags != null && x.Tags.Contains("B")); |
|||
list2.Count.ShouldBe(2); |
|||
list2[0].Name.ShouldBe("Name2"); |
|||
list2[0].Age.ShouldBe(10); |
|||
list2[0].Status.ShouldBe(TestEnum.Inactive); |
|||
list2[0].StringValueStatus.ShouldBe(TestEnum.Active); |
|||
list2[0].IsActive.ShouldBeFalse(); |
|||
list2[0].Tags.ShouldNotBeEmpty(); |
|||
list2[0].Tags!.ShouldContain("B"); |
|||
list2[0].Address!.City.ShouldBe("GUANGZHOU"); |
|||
|
|||
var list3 = await ExpressionQueryService.GetListAsync<TestDocument>( |
|||
TestDocumentIndexNames.Index, |
|||
x => |
|||
x.Name != null && x.Name.Contains("1") && |
|||
(x.Status == TestEnum.Active || x.Status == TestEnum.Pending)); |
|||
list3.Count.ShouldBe(2); |
|||
list3[1].Name.ShouldBe("Test1"); |
|||
list3[1].Age.ShouldBe(30); |
|||
list3[1].Status.ShouldBe(TestEnum.Pending); |
|||
list3[1].StringValueStatus.ShouldBe(TestEnum.Inactive); |
|||
list3[1].IsActive.ShouldBeTrue(); |
|||
list3[1].Tags.ShouldNotBeEmpty(); |
|||
list3[1].Tags!.ShouldContain("C"); |
|||
list3[1].Address!.City.ShouldBe("BEIJING"); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue