17 changed files with 3484 additions and 8 deletions
@ -1,14 +1,15 @@ |
|||||
using Microsoft.Extensions.DependencyInjection; |
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.Caching; |
||||
using Volo.Abp.Modularity; |
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")); |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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() |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
namespace System.Runtime.CompilerServices |
||||
|
{ |
||||
|
internal static class IsExternalInit { } |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -0,0 +1,7 @@ |
|||||
|
using LINGYUN.Abp.Tests; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Elasticsearch; |
||||
|
|
||||
|
public abstract class AbpElasticsearchTestBase : AbpTestsBase<AbpElasticsearchTestModule> |
||||
|
{ |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -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 |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace LINGYUN.Abp.Elasticsearch; |
||||
|
|
||||
|
public static class TestDocumentIndexNames |
||||
|
{ |
||||
|
public const string Index = "abp-elasticsearch-test"; |
||||
|
} |
||||
Loading…
Reference in new issue