Browse Source

feat: Add an ES expression tree translator

pull/1552/head
colin 1 week ago
parent
commit
a0558aaab3
  1. 1
      aspnet-core/LINGYUN.MicroService.All.slnx
  2. 2
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN.Abp.Elasticsearch.csproj
  3. 15
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/AbpElasticsearchModule.cs
  4. 387
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ElasticsearchIndexMappingProvider.cs
  5. 1356
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs
  6. 52
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/FieldInfo.cs
  7. 47
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/FieldMappingInfo.cs
  8. 9
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IIndexMappingProvider.cs
  9. 139
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IndexMappingInfo.cs
  10. 4
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IsExternalInit.cs
  11. 89
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/NestedMappingInfo.cs
  12. 20
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN.Abp.Elasticsearch.Tests.csproj
  13. 7
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/AbpElasticsearchTestBase.cs
  14. 142
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/AbpElasticsearchTestModule.cs
  15. 1173
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs
  16. 43
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/TestDocument.cs
  17. 6
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/TestDocumentIndexNames.cs

1
aspnet-core/LINGYUN.MicroService.All.slnx

@ -528,6 +528,7 @@
<Project Path="tests/LINGYUN.Abp.BlobStoring.Aliyun.Tests/LINGYUN.Abp.BlobStoring.Aliyun.Tests.csproj" />
<Project Path="tests/LINGYUN.Abp.BlobStoring.Nexus.Tests/LINGYUN.Abp.BlobStoring.Nexus.Tests.csproj" />
<Project Path="tests/LINGYUN.Abp.DataProtection.Tests/LINGYUN.Abp.DataProtection.Tests.csproj" />
<Project Path="tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN.Abp.Elasticsearch.Tests.csproj" Id="056a13a4-8fbb-4437-adc8-fe76d0869880" />
<Project Path="tests/LINGYUN.Abp.Emailing.Platform.Tests/LINGYUN.Abp.Emailing.Platform.Tests.csproj" />
<Project Path="tests/LINGYUN.Abp.EntityFrameworkCore.Tests/LINGYUN.Abp.EntityFrameworkCore.Tests.csproj" />
<Project Path="tests/LINGYUN.Abp.ExceptionHandling.Emailing.Tests/LINGYUN.Abp.ExceptionHandling.Emailing.Tests.csproj" />

2
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN.Abp.Elasticsearch.csproj

@ -17,7 +17,7 @@
<ItemGroup>
<PackageReference Include="Elastic.Clients.Elasticsearch" />
<PackageReference Include="Volo.Abp.Core" />
<PackageReference Include="Volo.Abp.Caching" />
</ItemGroup>
</Project>

15
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<AbpElasticsearchOptions>(configuration.GetSection("Elasticsearch"));
}
var configuration = context.Services.GetConfiguration();
Configure<AbpElasticsearchOptions>(configuration.GetSection("Elasticsearch"));
}
}

387
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<IndexMappingInfo> GetMappingAsync(string indexName, CancellationToken cancellationToken = default)
{
var cacheKey = $"es_mapping_{indexName}";
var cacheItem = _cache.Get<IndexMappingInfo>(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<string, FieldMappingInfo>();
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<string, FieldMappingInfo>()
};
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<string, FieldMappingInfo>();
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<string, FieldMappingInfo>();
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()
};
}
}

1356
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs

File diff suppressed because it is too large

52
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/FieldInfo.cs

@ -0,0 +1,52 @@
using System;
namespace LINGYUN.Abp.Elasticsearch;
/// <summary>
/// 字段信息
/// </summary>
public record FieldInfo
{
public string Path { get; init; }
public Type Type { get; init; }
public string Name { get; init; }
public bool IsKeyword { get; init; }
public bool IsWildcard { get; init; }
public bool IsText { get; init; }
public bool IsNested { get; init; }
public bool IsDate { get; init; }
public bool IsNumeric { get; init; }
public bool IsBoolean { get; init; }
public bool IsRange { get; init; }
public string? Format { get; init; }
public bool HasMultiFields { get; init; }
public FieldInfo(
string path,
Type type,
string name,
bool isKeyword = false,
bool isText = false,
bool isWildcard = false,
bool isNested = false,
bool isDate = false,
bool isNumeric = false,
bool isBoolean = false,
bool isRange = false,
string? format = null,
bool hasMultiFields = false)
{
Path = path;
Type = type;
Name = name;
IsKeyword = isKeyword;
IsText = isText;
IsWildcard = isWildcard;
IsNested = isNested;
IsDate = isDate;
IsNumeric = isNumeric;
IsBoolean = isBoolean;
IsRange = isRange;
Format = format;
HasMultiFields = hasMultiFields;
}
}

47
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/FieldMappingInfo.cs

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
namespace LINGYUN.Abp.Elasticsearch;
public class FieldMappingInfo
{
public string Path { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public Type? ClrType { get; set; }
public bool IsKeyword { get; set; }
public bool IsText { get; set; }
public bool IsWildcard { get; set; }
public bool IsNested { get; set; }
public bool IsObject { get; set; }
public bool IsDate { get; set; }
public bool IsNumeric { get; set; }
public bool IsBoolean { get; set; }
public bool IsRange { get; set; }
public string? Format { get; set; }
public bool? Store { get; set; }
public bool? Index { get; set; }
// 子字段(用于 text 的 keyword 子字段,或 object/nested 的内部字段)
public Dictionary<string, FieldMappingInfo>? Properties { get; set; }
public Dictionary<string, object>? Meta { get; set; }
public bool HasMultiFields => Properties?.Count > 0;
public string GetKeywordPath()
{
if (IsKeyword) return Path;
// 如果是 text 类型且有 keyword 子字段
if (IsText && Properties?.ContainsKey("keyword") == true)
{
return $"{Path}.keyword";
}
return Path;
}
public bool IsComparable => IsDate || IsNumeric || IsRange;
}

9
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IIndexMappingProvider.cs

@ -0,0 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
namespace LINGYUN.Abp.Elasticsearch;
public interface IIndexMappingProvider
{
Task<IndexMappingInfo> GetMappingAsync(string indexName, CancellationToken cancellationToken = default);
}

139
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IndexMappingInfo.cs

@ -0,0 +1,139 @@
using System.Collections.Generic;
using System.Linq;
namespace LINGYUN.Abp.Elasticsearch;
/// <summary>
/// 索引映射信息
/// </summary>
public class IndexMappingInfo
{
/// <summary>
/// 索引名称
/// </summary>
public string IndexName { get; set; } = string.Empty;
/// <summary>
/// 所有字段映射(扁平化)
/// </summary>
public Dictionary<string, FieldMappingInfo> Fields { get; set; } = new();
/// <summary>
/// Nested 字段映射
/// </summary>
public Dictionary<string, NestedMappingInfo> NestedFields { get; set; } = new();
/// <summary>
/// Keyword 字段列表
/// </summary>
public HashSet<string> KeywordFields { get; set; } = new();
/// <summary>
/// Text 字段列表
/// </summary>
public HashSet<string> TextFields { get; set; } = new();
/// <summary>
/// Wildcard 字段列表
/// </summary>
public HashSet<string> WildcardFields { get; set; } = new();
/// <summary>
/// 日期字段列表
/// </summary>
public HashSet<string> DateFields { get; set; } = new();
/// <summary>
/// 数值字段列表
/// </summary>
public HashSet<string> NumericFields { get; set; } = new();
/// <summary>
/// 布尔字段列表
/// </summary>
public HashSet<string> BooleanFields { get; set; } = new();
/// <summary>
/// Nested 字段路径列表
/// </summary>
public HashSet<string> NestedFieldPaths { get; set; } = new();
/// <summary>
/// 获取字段映射信息
/// </summary>
public FieldMappingInfo? GetField(string path)
{
return Fields.GetOrDefault(path);
}
/// <summary>
/// 判断是否为 Nested 字段
/// </summary>
public bool IsNested(string path)
{
return NestedFields.ContainsKey(path) || NestedFieldPaths.Contains(path);
}
/// <summary>
/// 获取 Nested 字段信息
/// </summary>
public NestedMappingInfo? GetNestedField(string path)
{
return NestedFields.GetOrDefault(path);
}
/// <summary>
/// 获取字段的精确匹配路径(处理 text 的 keyword 子字段)
/// </summary>
public string GetExactFieldPath(string path)
{
var field = GetField(path);
if (field == null)
{
return path;
}
return field.GetKeywordPath();
}
/// <summary>
/// 判断字段是否需要 Nested 查询
/// </summary>
public bool ShouldUseNestedQuery(string path)
{
// 检查路径本身是否是 Nested
if (IsNested(path))
{
return true;
}
// 检查路径的父级是否是 Nested
var parts = path.Split('.');
for (int i = 0; i < parts.Length - 1; i++)
{
var parentPath = string.Join(".", parts.Take(i + 1));
if (IsNested(parentPath))
{
return true;
}
}
return false;
}
/// <summary>
/// 获取 Nested 字段路径(如果字段在 Nested 内部,返回最近的 Nested 父级路径)
/// </summary>
public string? GetNestedParentPath(string path)
{
var parts = path.Split('.');
for (int i = parts.Length - 1; i >= 0; i--)
{
var parentPath = string.Join(".", parts.Take(i + 1));
if (IsNested(parentPath))
{
return parentPath;
}
}
return null;
}
}

4
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IsExternalInit.cs

@ -0,0 +1,4 @@
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}

89
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/NestedMappingInfo.cs

@ -0,0 +1,89 @@
using System.Collections.Generic;
namespace LINGYUN.Abp.Elasticsearch;
/// <summary>
/// Nested 字段的映射信息
/// </summary>
public class NestedMappingInfo
{
/// <summary>
/// Nested 字段的完整路径
/// </summary>
public string Path { get; set; } = string.Empty;
/// <summary>
/// Nested 字段名称(最后一段)
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Nested 内部的属性映射
/// </summary>
public Dictionary<string, FieldMappingInfo> Properties { get; set; } = new();
/// <summary>
/// 是否包含指定子字段
/// </summary>
public bool ContainsProperty(string propertyName)
{
return Properties.ContainsKey(propertyName);
}
/// <summary>
/// 获取子字段信息
/// </summary>
public FieldMappingInfo? GetProperty(string propertyName)
{
return Properties.GetOrDefault(propertyName);
}
/// <summary>
/// 获取 nested 内部字段的完整路径
/// </summary>
public string GetFullPath(string propertyName)
{
return $"{Path}.{propertyName}";
}
/// <summary>
/// 获取 nested 内部字段的映射信息(递归)
/// </summary>
public FieldMappingInfo? GetNestedField(string fullPath)
{
// 去掉当前 nested 路径前缀
if (!fullPath.StartsWith(Path + "."))
{
return null;
}
var remainingPath = fullPath.Substring(Path.Length + 1);
var parts = remainingPath.Split('.');
FieldMappingInfo? current = null;
Dictionary<string, FieldMappingInfo>? currentProperties = Properties;
for (var i = 0; i < parts.Length; i++)
{
var part = parts[i];
if (currentProperties == null)
{
return null;
}
if (!currentProperties.TryGetValue(part, out current))
{
return null;
}
// 如果还有下一级,且当前字段是 object 或 nested 类型
if (i < parts.Length - 1)
{
currentProperties = current.Properties;
}
}
return current;
}
}

20
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN.Abp.Elasticsearch.Tests.csproj

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
<IsPackable>false</IsPackable>
<Platforms>AnyCPU</Platforms>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq.AutoMock" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\framework\elasticsearch\LINGYUN.Abp.Elasticsearch\LINGYUN.Abp.Elasticsearch.csproj" />
<ProjectReference Include="..\LINGYUN.Abp.TestBase\LINGYUN.Abp.TestsBase.csproj" />
</ItemGroup>
</Project>

7
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/AbpElasticsearchTestBase.cs

@ -0,0 +1,7 @@
using LINGYUN.Abp.Tests;
namespace LINGYUN.Abp.Elasticsearch;
public abstract class AbpElasticsearchTestBase : AbpTestsBase<AbpElasticsearchTestModule>
{
}

142
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/AbpElasticsearchTestModule.cs

@ -0,0 +1,142 @@
using Elastic.Clients.Elasticsearch.IndexManagement;
using Elastic.Clients.Elasticsearch.Mapping;
using Elastic.Transport.Diagnostics.Auditing;
using LINGYUN.Abp.Tests;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Modularity;
using Volo.Abp.Threading;
namespace LINGYUN.Abp.Elasticsearch;
[DependsOn(
typeof(AbpTestsBaseModule),
typeof(AbpElasticsearchModule))]
public class AbpElasticsearchTestModule : AbpModule
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private const string UserSecretsId = "D4327320-718E-4A7F-A987-85838EDD8675";
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.ReplaceConfiguration(ConfigurationHelper.BuildConfiguration(builderAction: builder =>
{
builder.AddUserSecrets(UserSecretsId);
}));
}
public override void OnPostApplicationInitialization(ApplicationInitializationContext context)
{
AsyncHelper.RunSync(async () => await OnPostApplicationInitializationAsync(context));
}
public async override Task OnPostApplicationInitializationAsync(ApplicationInitializationContext context)
{
var clientFactory = context.ServiceProvider.GetRequiredService<IElasticsearchClientFactory>();
var client = clientFactory.Create();
var indexPatterns = new[] { TestDocumentIndexNames.Index + "*" };
var indexTemplateName = TestDocumentIndexNames.Index + "-generic";
var dateTimeFormat = "yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis";
var indexTemplateExists = await client.Indices.ExistsIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
if (indexTemplateExists.Exists)
{
await client.Indices.DeleteIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
}
var putTemplateResponse = await client.Indices.PutIndexTemplateAsync(indexTemplateName, setup =>
{
setup.IndexPatterns(indexPatterns);
setup.Priority(100);
setup.Version(1);
setup.Template(template =>
{
template.Settings(new IndexSettings()
{
NumberOfReplicas = 1,
NumberOfShards = 3,
Mapping = new MappingLimitSettings
{
TotalFields = new MappingLimitSettingsTotalFields
{
Limit = 1000,
},
NestedFields = new MappingLimitSettingsNestedFields
{
Limit = 50,
},
Depth = new MappingLimitSettingsDepth
{
Limit = 10,
},
}
});
template.Mappings(mp => mp
.Dynamic(DynamicMapping.False)
.Properties<TestDocument>(pd =>
{
pd.IntegerNumber(k => k.Id);
pd.Text(k => k.Name, p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(100))));
pd.Text(t => t.Description);
pd.IntegerNumber(k => k.Age);
pd.DoubleNumber(k => k.Salary);
pd.Boolean(k => k.IsActive);
pd.Date(k => k.CreatedTime, d => d.Format(dateTimeFormat));
pd.Date(k => k.UpdatedTime, d => d.Format(dateTimeFormat));
pd.ByteNumber(k => k.Status);
pd.ByteNumber(k => k.NullableStatus);
pd.Text(k => k.StringValueStatus, p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(100))));
pd.Keyword(k => k.Tags);
pd.Wildcard(k => k.Exceptions);
pd.Nested(n => n.Items, np =>
{
np.Dynamic(DynamicMapping.False);
np.Properties(npd =>
{
npd.IntegerNumber(nameof(SubDocument.Id));
npd.Text(nameof(SubDocument.Name), p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(100))));
npd.DoubleNumber(nameof(SubDocument.Price));
});
});
pd.Nested(n => n.Address, np =>
{
np.Dynamic(DynamicMapping.False);
np.Properties(npd =>
{
npd.Text(nameof(Address.City), p => p.Fields(f => f.Keyword("keyword", k => k.IgnoreAbove(255))));
npd.Text(nameof(Address.Street));
});
});
}));
});
}, _cancellationTokenSource.Token);
await client.Indices.DeleteAsync(TestDocumentIndexNames.Index, _cancellationTokenSource.Token);
await client.IndexAsync(
new TestDocument(),
dsl => dsl.Index(TestDocumentIndexNames.Index),
_cancellationTokenSource.Token);
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
AsyncHelper.RunSync(async () => await OnApplicationShutdownAsync(context));
}
public async override Task OnApplicationShutdownAsync(ApplicationShutdownContext context)
{
var clientFactory = context.ServiceProvider.GetRequiredService<IElasticsearchClientFactory>();
var client = clientFactory.Create();
var indexTemplateName = TestDocumentIndexNames.Index + "-generic";
var indexTemplateExists = await client.Indices.ExistsIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
if (indexTemplateExists.Exists)
{
await client.Indices.DeleteIndexTemplateAsync(indexTemplateName, _cancellationTokenSource.Token);
}
await client.Indices.DeleteAsync(TestDocumentIndexNames.Index, _cancellationTokenSource.Token);
_cancellationTokenSource.Cancel();
}
}

1173
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs

File diff suppressed because it is too large

43
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/TestDocument.cs

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
namespace LINGYUN.Abp.Elasticsearch;
public class TestDocument
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public int Age { get; set; }
public decimal Salary { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime? UpdatedTime { get; set; }
public TestEnum Status { get; set; }
public TestEnum StringValueStatus { get; set; }
public TestEnum? NullableStatus { get; set; }
public List<SubDocument>? Items { get; set; }
public List<string>? Tags { get; set; }
public Address? Address { get; set; }
public string? Exceptions { get; set; }
}
public class SubDocument
{
public int Id { get; set; }
public string? Name { get; set; }
public decimal Price { get; set; }
}
public class Address
{
public string? City { get; set; }
public string? Street { get; set; }
}
public enum TestEnum
{
Active = 1,
Inactive = 2,
Pending = 3
}

6
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/TestDocumentIndexNames.cs

@ -0,0 +1,6 @@
namespace LINGYUN.Abp.Elasticsearch;
public static class TestDocumentIndexNames
{
public const string Index = "abp-elasticsearch-test";
}
Loading…
Cancel
Save