17 changed files with 3484 additions and 8 deletions
@ -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")); |
|||
} |
|||
} |
|||
|
|||
@ -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