diff --git a/aspnet-core/LINGYUN.MicroService.All.slnx b/aspnet-core/LINGYUN.MicroService.All.slnx
index 19b0fa78a..b531257ce 100644
--- a/aspnet-core/LINGYUN.MicroService.All.slnx
+++ b/aspnet-core/LINGYUN.MicroService.All.slnx
@@ -528,6 +528,7 @@
+
diff --git a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN.Abp.Elasticsearch.csproj b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN.Abp.Elasticsearch.csproj
index 92d366e88..539a0f42f 100644
--- a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN.Abp.Elasticsearch.csproj
+++ b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN.Abp.Elasticsearch.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/AbpElasticsearchModule.cs b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/AbpElasticsearchModule.cs
index 64dd9b46f..6389a318d 100644
--- a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/AbpElasticsearchModule.cs
+++ b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/AbpElasticsearchModule.cs
@@ -1,14 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Caching;
using Volo.Abp.Modularity;
-namespace LINGYUN.Abp.Elasticsearch
+namespace LINGYUN.Abp.Elasticsearch;
+
+[DependsOn(typeof(AbpCachingModule))]
+public class AbpElasticsearchModule : AbpModule
{
- public class AbpElasticsearchModule : AbpModule
+ public override void ConfigureServices(ServiceConfigurationContext context)
{
- public override void ConfigureServices(ServiceConfigurationContext context)
- {
- var configuration = context.Services.GetConfiguration();
- Configure(configuration.GetSection("Elasticsearch"));
- }
+ var configuration = context.Services.GetConfiguration();
+ Configure(configuration.GetSection("Elasticsearch"));
}
}
diff --git a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ElasticsearchIndexMappingProvider.cs b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ElasticsearchIndexMappingProvider.cs
new file mode 100644
index 000000000..38000d182
--- /dev/null
+++ b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ElasticsearchIndexMappingProvider.cs
@@ -0,0 +1,387 @@
+using Elastic.Clients.Elasticsearch;
+using Elastic.Clients.Elasticsearch.Mapping;
+using Elastic.Transport.Products.Elasticsearch;
+using Microsoft.Extensions.Caching.Memory;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.Elasticsearch;
+
+public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransientDependency
+{
+ private readonly IMemoryCache _cache;
+ private readonly IElasticsearchClientFactory _clientFactory;
+ private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(10);
+
+ public ElasticsearchIndexMappingProvider(
+ IElasticsearchClientFactory clientFactory,
+ IMemoryCache cache)
+ {
+ _clientFactory = clientFactory;
+ _cache = cache;
+ }
+
+ public async Task GetMappingAsync(string indexName, CancellationToken cancellationToken = default)
+ {
+ var cacheKey = $"es_mapping_{indexName}";
+
+ var cacheItem = _cache.Get(cacheKey);
+ if (cacheItem == null)
+ {
+ var client = _clientFactory.Create();
+ var response = await client.Indices.GetMappingAsync(indexName, cancellationToken);
+
+ if (!response.IsSuccess())
+ {
+ var errorBuilder = new StringBuilder();
+ if (response.TryGetOriginalException(out var ex) && ex != null)
+ {
+ errorBuilder.AppendLine(ex.Message);
+ }
+ else if (response.TryGetElasticsearchServerError(out var error) && error != null)
+ {
+ errorBuilder.AppendLine(error.ToString());
+ }
+ else
+ {
+ errorBuilder.AppendLine(response.DebugInformation);
+ }
+ throw new Exception($"Failed to get mapping for index {indexName}: {errorBuilder.ToString()}");
+ }
+
+ if (!response.Mappings.TryGetValue(indexName, out var indexMappingRecord))
+ {
+ throw new Exception($"Index {indexName} not found in response");
+ }
+
+ cacheItem = ParseMapping(indexMappingRecord.Mappings, indexName);
+
+ _cache.Set(cacheKey, cacheItem, _cacheDuration);
+ }
+
+ return cacheItem;
+ }
+
+ private IndexMappingInfo ParseMapping(TypeMapping mappings, string indexName)
+ {
+ var mappingInfo = new IndexMappingInfo { IndexName = indexName };
+
+ if (mappings?.Properties != null)
+ {
+ ParseProperties(mappings.Properties, mappingInfo, string.Empty);
+ }
+
+ return mappingInfo;
+ }
+
+ private void ParseProperties(Properties? properties, IndexMappingInfo mappingInfo, string parentPath)
+ {
+ if (properties == null) return;
+
+ foreach (var kvp in properties)
+ {
+ var propertyName = kvp.Key.ToString();
+ var property = kvp.Value;
+ var fullPath = string.IsNullOrEmpty(parentPath)
+ ? propertyName
+ : $"{parentPath}.{propertyName}";
+
+ var fieldInfo = new FieldMappingInfo
+ {
+ Path = fullPath,
+ Name = propertyName,
+ Type = GetPropertyType(property)
+ };
+
+ switch (property)
+ {
+ // Keyword 类型
+ case KeywordProperty keyword:
+ fieldInfo.IsKeyword = true;
+ mappingInfo.KeywordFields.Add(fullPath);
+ break;
+
+ // Text 类型 - 包含多字段支持
+ case TextProperty text:
+ fieldInfo.IsText = true;
+ mappingInfo.TextFields.Add(fullPath);
+
+ // 处理 Text 的 Fields(多字段)
+ if (text.Fields != null && text.Fields.Count() > 0)
+ {
+ fieldInfo.Properties = new Dictionary();
+
+ foreach (var subFieldKvp in text.Fields)
+ {
+ var subFieldName = subFieldKvp.Key.ToString();
+ var subFieldProperty = subFieldKvp.Value;
+ var subFieldPath = $"{fullPath}.{subFieldName}";
+
+ var subFieldInfo = new FieldMappingInfo
+ {
+ Path = subFieldPath,
+ Name = subFieldName,
+ Type = GetPropertyType(subFieldProperty)
+ };
+
+ // 处理子字段的类型
+ if (subFieldProperty is KeywordProperty)
+ {
+ subFieldInfo.IsKeyword = true;
+ mappingInfo.KeywordFields.Add(subFieldPath);
+ }
+ else if (subFieldProperty is TextProperty)
+ {
+ subFieldInfo.IsText = true;
+ mappingInfo.TextFields.Add(subFieldPath);
+ }
+
+ fieldInfo.Properties[subFieldName] = subFieldInfo;
+ mappingInfo.Fields[subFieldPath] = subFieldInfo;
+ }
+ }
+ break;
+
+ // 日期类型
+ case DateProperty date:
+ fieldInfo.IsDate = true;
+ fieldInfo.Format = date.Format;
+ mappingInfo.DateFields.Add(fullPath);
+ break;
+
+ // 日期纳秒类型
+ case DateNanosProperty dateNanos:
+ fieldInfo.IsDate = true;
+ fieldInfo.Format = dateNanos.Format;
+ mappingInfo.DateFields.Add(fullPath);
+ break;
+
+ // 数值类型
+ case ByteNumberProperty:
+ case DoubleNumberProperty:
+ case FloatNumberProperty:
+ case HalfFloatNumberProperty:
+ case IntegerNumberProperty:
+ case LongNumberProperty:
+ case ScaledFloatNumberProperty:
+ case ShortNumberProperty:
+ case UnsignedLongNumberProperty:
+ fieldInfo.IsNumeric = true;
+ mappingInfo.NumericFields.Add(fullPath);
+ break;
+
+ // 布尔类型
+ case BooleanProperty:
+ fieldInfo.IsBoolean = true;
+ mappingInfo.BooleanFields.Add(fullPath);
+ break;
+
+ // Nested 类型
+ case NestedProperty nested:
+ fieldInfo.IsNested = true;
+ fieldInfo.IsObject = true;
+ mappingInfo.NestedFieldPaths.Add(fullPath);
+
+ var nestedInfo = new NestedMappingInfo
+ {
+ Path = fullPath,
+ Name = propertyName,
+ Properties = new Dictionary()
+ };
+
+ if (nested.Properties != null)
+ {
+ // 先递归解析内部字段
+ ParseProperties(nested.Properties, mappingInfo, fullPath);
+
+ // 收集 nested 内部的字段信息
+ foreach (var innerKvp in nested.Properties)
+ {
+ var innerName = innerKvp.Key.ToString();
+ var innerFullPath = $"{fullPath}.{innerName}";
+
+ if (mappingInfo.Fields.TryGetValue(innerFullPath, out var innerFieldInfo))
+ {
+ nestedInfo.Properties[innerName] = innerFieldInfo;
+ }
+ else
+ {
+ innerFieldInfo = new FieldMappingInfo
+ {
+ Path = innerFullPath,
+ Name = innerName,
+ Type = GetPropertyType(innerKvp.Value)
+ };
+ nestedInfo.Properties[innerName] = innerFieldInfo;
+ mappingInfo.Fields[innerFullPath] = innerFieldInfo;
+ }
+ }
+ }
+
+ mappingInfo.NestedFields[fullPath] = nestedInfo;
+ break;
+
+ // Object 类型
+ case ObjectProperty obj:
+ fieldInfo.IsObject = true;
+ fieldInfo.Properties = new Dictionary();
+
+ if (obj.Properties != null)
+ {
+ ParseProperties(obj.Properties, mappingInfo, fullPath);
+ }
+ break;
+
+ // 范围类型
+ case DateRangeProperty:
+ case DoubleRangeProperty:
+ case FloatRangeProperty:
+ case IntegerRangeProperty:
+ case LongRangeProperty:
+ case IpRangeProperty:
+ fieldInfo.IsRange = true;
+ break;
+
+ // 其他类型
+ case FlattenedProperty:
+ fieldInfo.Type = "flattened";
+ break;
+
+ case GeoPointProperty:
+ fieldInfo.Type = "geo_point";
+ break;
+
+ case GeoShapeProperty:
+ fieldInfo.Type = "geo_shape";
+ break;
+
+ case IpProperty:
+ fieldInfo.Type = "ip";
+ break;
+
+ case VersionProperty:
+ fieldInfo.Type = "version";
+ break;
+
+ case MatchOnlyTextProperty matchOnlyText:
+ fieldInfo.IsText = true;
+ fieldInfo.Type = "match_only_text";
+ mappingInfo.TextFields.Add(fullPath);
+
+ // MatchOnlyText 也可能有 Fields
+ if (matchOnlyText.Fields != null && matchOnlyText.Fields.Count() > 0)
+ {
+ fieldInfo.Properties = new Dictionary();
+ foreach (var subFieldKvp in matchOnlyText.Fields)
+ {
+ var subFieldName = subFieldKvp.Key.ToString();
+ var subFieldPath = $"{fullPath}.{subFieldName}";
+ var subFieldInfo = new FieldMappingInfo
+ {
+ Path = subFieldPath,
+ Name = subFieldName,
+ Type = GetPropertyType(subFieldKvp.Value)
+ };
+ if (subFieldKvp.Value is KeywordProperty)
+ {
+ subFieldInfo.IsKeyword = true;
+ mappingInfo.KeywordFields.Add(subFieldPath);
+ }
+ fieldInfo.Properties[subFieldName] = subFieldInfo;
+ mappingInfo.Fields[subFieldPath] = subFieldInfo;
+ }
+ }
+ break;
+
+ case WildcardProperty:
+ fieldInfo.IsWildcard = true;
+ fieldInfo.Type = "wildcard";
+ mappingInfo.WildcardFields.Add(fullPath);
+ break;
+
+ case CompletionProperty:
+ fieldInfo.Type = "completion";
+ break;
+
+ case JoinProperty:
+ fieldInfo.Type = "join";
+ break;
+
+ case PercolatorProperty:
+ fieldInfo.Type = "percolator";
+ break;
+
+ case RankFeatureProperty:
+ fieldInfo.Type = "rank_feature";
+ break;
+
+ case RankFeaturesProperty:
+ fieldInfo.Type = "rank_features";
+ break;
+
+ case DenseVectorProperty:
+ fieldInfo.Type = "dense_vector";
+ break;
+
+ case SparseVectorProperty:
+ fieldInfo.Type = "sparse_vector";
+ break;
+
+ default:
+ fieldInfo.Type = property.GetType().Name.Replace("Property", "").ToLowerInvariant();
+ break;
+ }
+
+ mappingInfo.Fields[fullPath] = fieldInfo;
+ }
+ }
+
+ private string GetPropertyType(IProperty property)
+ {
+ return property switch
+ {
+ KeywordProperty => "keyword",
+ TextProperty => "text",
+ DateProperty => "date",
+ DateNanosProperty => "date_nanos",
+ ByteNumberProperty => "byte",
+ DoubleNumberProperty => "double",
+ FloatNumberProperty => "float",
+ HalfFloatNumberProperty => "half_float",
+ IntegerNumberProperty => "integer",
+ LongNumberProperty => "long",
+ ScaledFloatNumberProperty => "scaled_float",
+ ShortNumberProperty => "short",
+ UnsignedLongNumberProperty => "unsigned_long",
+ BooleanProperty => "boolean",
+ NestedProperty => "nested",
+ ObjectProperty => "object",
+ FlattenedProperty => "flattened",
+ GeoPointProperty => "geo_point",
+ GeoShapeProperty => "geo_shape",
+ IpProperty => "ip",
+ VersionProperty => "version",
+ MatchOnlyTextProperty => "match_only_text",
+ WildcardProperty => "wildcard",
+ CompletionProperty => "completion",
+ JoinProperty => "join",
+ PercolatorProperty => "percolator",
+ RankFeatureProperty => "rank_feature",
+ RankFeaturesProperty => "rank_features",
+ DenseVectorProperty => "dense_vector",
+ SparseVectorProperty => "sparse_vector",
+ DateRangeProperty => "date_range",
+ DoubleRangeProperty => "double_range",
+ FloatRangeProperty => "float_range",
+ IntegerRangeProperty => "integer_range",
+ LongRangeProperty => "long_range",
+ IpRangeProperty => "ip_range",
+ _ => property.GetType().Name.Replace("Property", "").ToLowerInvariant()
+ };
+ }
+}
diff --git a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs
new file mode 100644
index 000000000..fff6a0190
--- /dev/null
+++ b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs
@@ -0,0 +1,1356 @@
+using Elastic.Clients.Elasticsearch;
+using Elastic.Clients.Elasticsearch.QueryDsl;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using Volo.Abp;
+
+namespace LINGYUN.Abp.Elasticsearch;
+
+///
+/// 表达式查询转换器 - 将 LINQ 表达式转换为 Elasticsearch Query
+///
+/// 文档类型
+public class ExpressionQueryTranslator
+{
+ private readonly bool _defaultNestedBehavior;
+ private readonly IndexMappingInfo _mappingInfo;
+
+ ///
+ /// 构造函数(使用已有的映射信息)
+ ///
+ public ExpressionQueryTranslator(
+ IndexMappingInfo mappingInfo,
+ bool defaultNestedBehavior = false)
+ {
+ _mappingInfo = mappingInfo;
+ _defaultNestedBehavior = defaultNestedBehavior;
+ }
+
+ ///
+ /// 同步翻译表达式(需要已加载映射)
+ ///
+ public virtual Query Translate(Expression> expression)
+ {
+ Check.NotNull(expression, nameof(expression));
+
+ return TranslateNode(expression.Body, prefix: null, _mappingInfo);
+ }
+
+ #region 节点翻译
+
+ ///
+ /// 翻译表达式节点
+ ///
+ protected virtual Query TranslateNode(Expression node, string? prefix, IndexMappingInfo? mappingInfo)
+ {
+ return node switch
+ {
+ ConstantExpression { Value: bool value } =>
+ value ? new MatchAllQuery() : new MatchNoneQuery(),
+
+ UnaryExpression { NodeType: ExpressionType.Not } unary =>
+ Negate(TranslateNode(unary.Operand, prefix, mappingInfo)),
+
+ UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary =>
+ TranslateNode(unary.Operand, prefix, mappingInfo),
+
+ BinaryExpression binary => TranslateBinary(binary, prefix, mappingInfo),
+
+ MethodCallExpression method => TranslateMethodCall(method, prefix, mappingInfo),
+
+ MemberExpression member when IsHasValueAccess(member) =>
+ new ExistsQuery { Field = ResolveField(member.Expression!, prefix, mappingInfo).Path },
+
+ MemberExpression member when member.Type == typeof(bool) =>
+ new TermQuery { Field = ResolveField(member, prefix, mappingInfo).Path, Value = true },
+
+ MemberExpression member when IsNullableHasValue(member) =>
+ new ExistsQuery { Field = ResolveField(member.Expression!, prefix, mappingInfo).Path },
+
+ MemberExpression member when IsNullableValueAccess(member) =>
+ TranslateNode(member.Expression!, prefix, mappingInfo),
+
+ _ => throw new NotSupportedException($"Unsupported expression node {node.NodeType}: {node}"),
+ };
+ }
+ #endregion
+
+ #region 二元表达式翻译
+
+ ///
+ /// 翻译二元表达式
+ ///
+ protected virtual Query TranslateBinary(BinaryExpression node, string? prefix, IndexMappingInfo? mappingInfo)
+ {
+ return node.NodeType switch
+ {
+ ExpressionType.AndAlso or ExpressionType.And => TranslateAndAlso(node, prefix, mappingInfo),
+
+ ExpressionType.OrElse or ExpressionType.Or => TranslateOrElse(node, prefix, mappingInfo),
+
+ ExpressionType.Equal => TranslateComparison(node, prefix, mappingInfo),
+
+ ExpressionType.NotEqual => TranslateComparison(node, prefix, mappingInfo),
+
+ ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual or
+ ExpressionType.LessThan or ExpressionType.LessThanOrEqual => TranslateComparison(node, prefix, mappingInfo),
+
+ _ => throw new NotSupportedException($"Unsupported binary operator {node.NodeType}: {node}"),
+ };
+ }
+
+
+ ///
+ /// 构建 And 查询(展平 BoolQuery)
+ ///
+ private Query TranslateAndAlso(BinaryExpression node, string? prefix, IndexMappingInfo? mappingInfo)
+ {
+ var filters = new List();
+
+ // 收集左侧所有 Filter
+ CollectFilters(node.Left, prefix, mappingInfo, filters);
+
+ // 收集右侧 Filter
+ CollectFilters(node.Right, prefix, mappingInfo, filters);
+
+ return new BoolQuery
+ {
+ Filter = filters.ToArray()
+ };
+ }
+
+ ///
+ /// 递归收集 Filter 查询
+ ///
+ private void CollectFilters(Expression node, string? prefix, IndexMappingInfo? mappingInfo, List filters)
+ {
+ if (node is BinaryExpression binary &&
+ (binary.NodeType == ExpressionType.AndAlso || binary.NodeType == ExpressionType.And))
+ {
+ // 递归收集左右子节点
+ CollectFilters(binary.Left, prefix, mappingInfo, filters);
+ CollectFilters(binary.Right, prefix, mappingInfo, filters);
+ }
+ else
+ {
+ // 非 And 表达式,直接翻译并添加到列表
+ var query = TranslateNode(node, prefix, mappingInfo);
+
+ // 如果翻译结果是 BoolQuery 且有 Filter,展平它
+ if (query.Bool != null && query.Bool.Filter != null && query.Bool.Filter.Count > 0)
+ {
+ foreach (var subQuery in query.Bool.Filter)
+ {
+ filters.Add(subQuery);
+ }
+ }
+ else if (query.Bool != null && query.Bool.Must != null && query.Bool.Must.Count > 0)
+ {
+ foreach (var subQuery in query.Bool.Must)
+ {
+ filters.Add(subQuery);
+ }
+ }
+ else
+ {
+ filters.Add(query);
+ }
+ }
+ }
+
+ ///
+ /// 构建 Or 查询(展平 BoolQuery)
+ ///
+ private Query TranslateOrElse(BinaryExpression node, string? prefix, IndexMappingInfo? mappingInfo)
+ {
+ var shouldQueries = new List();
+
+ CollectShouldQueries(node.Left, prefix, mappingInfo, shouldQueries);
+ CollectShouldQueries(node.Right, prefix, mappingInfo, shouldQueries);
+
+ return new BoolQuery
+ {
+ Should = shouldQueries.ToArray(),
+ MinimumShouldMatch = 1
+ };
+ }
+
+ ///
+ /// 递归收集 Should 查询
+ ///
+ private void CollectShouldQueries(Expression node, string? prefix, IndexMappingInfo? mappingInfo, List shouldQueries)
+ {
+ if (node is BinaryExpression binary &&
+ (binary.NodeType == ExpressionType.OrElse || binary.NodeType == ExpressionType.Or))
+ {
+ CollectShouldQueries(binary.Left, prefix, mappingInfo, shouldQueries);
+ CollectShouldQueries(binary.Right, prefix, mappingInfo, shouldQueries);
+ }
+ else
+ {
+ var query = TranslateNode(node, prefix, mappingInfo);
+
+ // 如果翻译结果是 BoolQuery 且有 Should,展平它
+ if (query.Bool != null && query.Bool.Should != null && query.Bool.Should.Count > 0)
+ {
+ foreach (var subQuery in query.Bool.Should)
+ {
+ shouldQueries.Add(subQuery);
+ }
+ }
+ else
+ {
+ shouldQueries.Add(query);
+ }
+ }
+ }
+
+ ///
+ /// 翻译比较表达式
+ ///
+ protected virtual Query TranslateComparison(BinaryExpression node, string? prefix, IndexMappingInfo? mappingInfo)
+ {
+ var (fieldExpression, valueExpression) = ResolveOperands(node);
+ var field = ResolveField(fieldExpression, prefix, mappingInfo);
+
+ // 处理 null 比较
+ if (IsNullConstant(valueExpression))
+ {
+ return node.NodeType == ExpressionType.NotEqual
+ ? new ExistsQuery { Field = field.Path }
+ : new BoolQuery
+ {
+ MustNot = new Query[] { new ExistsQuery { Field = field.Path } }
+ };
+ }
+
+ var value = Evaluate(valueExpression);
+
+ if (value == null)
+ {
+ throw new NotSupportedException("The null value is only supported for the == null / != null comparison.");
+ }
+
+ // 处理集合包含
+ if (field.Type.IsArray || (field.Type.IsGenericType && typeof(IEnumerable).IsAssignableFrom(field.Type)))
+ {
+ return BuildTermsQuery(field, value);
+ }
+
+ return node.NodeType switch
+ {
+ ExpressionType.Equal => BuildEquality(field, value),
+ ExpressionType.NotEqual => BuildNotEqualQuery(field, value),
+ ExpressionType.GreaterThan => BuildRange(field, greaterThan: value),
+ ExpressionType.GreaterThanOrEqual => BuildRange(field, greaterThanOrEqualTo: value),
+ ExpressionType.LessThan => BuildRange(field, lessThan: value),
+ ExpressionType.LessThanOrEqual => BuildRange(field, lessThanOrEqualTo: value),
+ _ => throw new NotSupportedException($"Unsupported comparison operator {node.NodeType}"),
+ };
+ }
+
+ ///
+ /// 解析操作数,确定字段和值
+ ///
+ private static (Expression Field, Expression Value) ResolveOperands(BinaryExpression node)
+ {
+ var leftIsField = IsFieldLike(node.Left);
+ var rightIsField = IsFieldLike(node.Right);
+
+ if (leftIsField == rightIsField)
+ {
+ throw new NotSupportedException($"The comparison expression must have one side as a field and the other side as a value: {node}");
+ }
+
+ return leftIsField ? (node.Left, node.Right) : (node.Right, node.Left);
+ }
+
+ ///
+ /// 判断表达式是否为字段
+ ///
+ private static bool IsFieldLike(Expression expression)
+ {
+ var current = expression;
+ while (current is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary)
+ {
+ current = unary.Operand;
+ }
+
+ while (current is MemberExpression member)
+ {
+ current = member.Expression!;
+ }
+
+ return current is ParameterExpression;
+ }
+
+ ///
+ /// 判断表达式是否为 null 常量
+ ///
+ private static bool IsNullConstant(Expression expression)
+ {
+ while (expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary)
+ {
+ expression = unary.Operand;
+ }
+
+ return expression is ConstantExpression { Value: null };
+ }
+
+ #endregion
+
+ #region 方法调用翻译
+
+ ///
+ /// 取反查询
+ ///
+ private static Query Negate(Query query)
+ {
+ // 检查是否为 BoolQuery(通过 Bool 属性)
+ if (query.Bool != null)
+ {
+ var boolQuery = query.Bool;
+ var newBool = new BoolQuery();
+
+ // MustNot -> Must
+ if (boolQuery.MustNot != null && boolQuery.MustNot.Count > 0)
+ {
+ newBool.Must = boolQuery.MustNot.ToArray();
+ return newBool;
+ }
+
+ // Must -> MustNot
+ if (boolQuery.Must != null && boolQuery.Must.Count > 0)
+ {
+ newBool.MustNot = boolQuery.Must.ToArray();
+ return newBool;
+ }
+
+ // Filter -> MustNot
+ if (boolQuery.Filter != null && boolQuery.Filter.Count > 0)
+ {
+ newBool.MustNot = boolQuery.Filter.ToArray();
+ return newBool;
+ }
+
+ // Should -> MustNot
+ if (boolQuery.Should != null && boolQuery.Should.Count > 0)
+ {
+ newBool.MustNot = boolQuery.Should.ToArray();
+ return newBool;
+ }
+
+ // 空 BoolQuery -> MatchAll
+ return new MatchAllQuery();
+ }
+
+ // 对于 ExistsQuery,取反后变成 MustNot + Exists
+ if (query.Exists != null)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query.Exists }
+ };
+ }
+
+ // 对于 MatchAllQuery,取反后变成 MatchNoneQuery
+ if (query.MatchAll != null)
+ {
+ return new MatchNoneQuery();
+ }
+
+ // 对于 MatchNoneQuery,取反后变成 MatchAllQuery
+ if (query.MatchNone != null)
+ {
+ return new MatchAllQuery();
+ }
+
+ // 对于 TermQuery,取反后变成 MustNot + Term
+ if (query.Term != null)
+ {
+ if (query.Term.Value.IsBool && query.Term.Value.TryGetBool(out var boolValue))
+ {
+ return new TermQuery
+ {
+ Field = query.Term.Field,
+ Value = FieldValue.FromValue(!boolValue)
+ };
+ }
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query.Term }
+ };
+ }
+
+ // 对于 WildcardQuery,取反后变成 MustNot + Wildcard
+ if (query.Wildcard != null)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query.Wildcard }
+ };
+ }
+
+ // 对于 MatchQuery,取反后变成 MustNot + Match
+ if (query.Match != null)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query.Match }
+ };
+ }
+
+ // 对于 MatchPhraseQuery,取反后变成 MustNot + MatchPhrase
+ if (query.MatchPhrase != null)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query.MatchPhrase }
+ };
+ }
+
+ // 对于 MatchPhrasePrefixQuery,取反后变成 MustNot + MatchPhrasePrefix
+ if (query.MatchPhrasePrefix != null)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query.MatchPhrasePrefix }
+ };
+ }
+
+ // 对于 NumberRangeQuery
+ if (query.Range != null && query.Range is NumberRangeQuery numberRange)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { numberRange }
+ };
+ }
+
+ // 对于 DateRangeQuery
+ if (query.Range != null && query.Range is DateRangeQuery dateRange)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { dateRange }
+ };
+ }
+
+ // 对于 TermsQuery
+ if (query.Terms != null)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query.Terms }
+ };
+ }
+
+ // 对于 NestedQuery
+ if (query.Nested != null)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query.Nested }
+ };
+ }
+
+ // 对于 ScriptQuery
+ if (query.Script != null)
+ {
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query.Script }
+ };
+ }
+
+ // 默认:对于未知类型,使用 MustNot 包装
+ return new BoolQuery
+ {
+ MustNot = new Query[] { query }
+ };
+ }
+
+ ///
+ /// 翻译方法调用表达式
+ ///
+ protected virtual Query TranslateMethodCall(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
+ {
+ // Enumerable.Any / Enumerable.Contains / Enumerable.All
+ if (node.Method.DeclaringType == typeof(Enumerable))
+ {
+ return TranslateEnumerableMethod(node, prefix, mappingInfo);
+ }
+
+ // string.Equals 需放在其他方法前
+ if (node.Method.Name == nameof(string.Equals))
+ {
+ return TranslateStringEquals(node, prefix, mappingInfo);
+ }
+
+ // 字符串方法
+ if (node.Method.DeclaringType == typeof(string) && node.Object != null)
+ {
+ return TranslateStringMethod(node, prefix, mappingInfo);
+ }
+
+ // Enum.HasFlag
+ if (node.Method.Name == nameof(Enum.HasFlag))
+ {
+ return TranslateEnumHasFlag(node, prefix, mappingInfo);
+ }
+
+ // List/Collection.Contains (实例方法)
+ if (node.Method.Name == nameof(ICollection<>.Contains) && node.Object != null)
+ {
+ return TranslateCollectionContains(node, prefix, mappingInfo);
+ }
+
+ throw new NotSupportedException(
+ $"Unsupported method invocation {node.Method.DeclaringType?.Name}.{node.Method.Name}");
+ }
+
+ ///
+ /// 翻译 Enumerable 方法
+ ///
+ 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}");
+ }
+ }
+
+ ///
+ /// 翻译 Enumerable.Any
+ ///
+ 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) ||
+ _defaultNestedBehavior;
+
+ return shouldUseNested
+ ? new NestedQuery(collectionField.Path, inner)
+ : inner;
+ }
+
+ ///
+ /// 翻译 Enumerable.Contains
+ ///
+ 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!);
+ }
+
+ ///
+ /// 翻译 Enumerable.All
+ ///
+ 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);
+ }
+
+ ///
+ /// 翻译字符串方法
+ ///
+ private Query TranslateStringMethod(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
+ {
+ var field = ResolveField(node.Object!, prefix, mappingInfo);
+ var value = (string)Evaluate(node.Arguments[0])!;
+ var fieldMapping = mappingInfo?.GetField(field.Path);
+ var escapedValue = EscapeWildcard(value);
+
+ return node.Method.Name switch
+ {
+ nameof(string.Contains) => TranslateContains(field, fieldMapping, value),
+ nameof(string.StartsWith) => TranslateStartsWith(field, fieldMapping, value),
+ nameof(string.EndsWith) => TranslateEndsWith(field, fieldMapping, value),
+ _ => throw new NotSupportedException($"Unsupported string method {node.Method.Name}"),
+ };
+ }
+
+ ///
+ /// 翻译 Contains
+ ///
+ private Query TranslateContains(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) + "*"
+ };
+ }
+
+ ///
+ /// 翻译 StartsWith
+ ///
+ 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 };
+ }
+
+ ///
+ /// 翻译 EndsWith
+ ///
+ 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 };
+ }
+
+ ///
+ /// 翻译 string.Equals
+ ///
+ private Query TranslateStringEquals(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
+ {
+ Expression fieldExpression;
+ Expression valueExpression;
+
+ if (node.Object != null)
+ {
+ fieldExpression = node.Object;
+ valueExpression = node.Arguments[0];
+ }
+ else
+ {
+ fieldExpression = node.Arguments[0];
+ valueExpression = node.Arguments[1];
+ }
+
+ var field = ResolveField(fieldExpression, prefix, mappingInfo);
+ var value = Evaluate(valueExpression);
+
+ if (value == null)
+ {
+ return new BoolQuery { MustNot = new Query[] { new ExistsQuery { Field = field.Path } } };
+ }
+
+ return BuildEquality(field, value);
+ }
+
+ ///
+ /// 翻译 Enum.HasFlag
+ ///
+ 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 };
+ }
+
+ ///
+ /// 翻译集合 Contains
+ ///
+ private Query TranslateCollectionContains(MethodCallExpression node, string? prefix, IndexMappingInfo? mappingInfo)
+ {
+ // list.Contains(value) 或 hashSet.Contains(value)
+ // node.Object = 集合实例 (可能是一个变量或常量)
+ // node.Arguments[0] = value (要检查的值)
+
+ // 尝试获取集合的值
+ var collectionValue = Evaluate(node.Object!);
+
+ // 如果集合是常量且可枚举,构建 TermsQuery
+ if (collectionValue is IEnumerable enumerable && collectionValue is not string)
+ {
+ var values = enumerable.Cast