Browse Source

feat: Optimize the log query module

- Use query specifications
- Add type-based index field mapping
pull/1558/head
colin 3 weeks ago
parent
commit
4b0b267ff8
  1. 459
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs
  2. 32
      aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/System/Linq/Expressions/ExpressionFuncExtensions.cs
  3. 8
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/Elastic/Clients/Elasticsearch/SortOptionsExtenssions.cs
  4. 253
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ElasticsearchIndexMappingProvider.cs
  5. 8
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/EmptyDocument.cs
  6. 266
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryService.cs
  7. 2
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Comparison.cs
  8. 85
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs
  9. 3
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs
  10. 7
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/FieldMappingInfo.cs
  11. 4
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IIndexMappingProvider.cs
  12. 84
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IndexMappingInfo.cs
  13. 29
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IndexMappingInfoExtensions.cs
  14. 540
      aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN/Abp/AuditLogging/Serilog/Elasticsearch/SerilogElasticsearchLoggingManager.cs
  15. 32
      aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/System/Linq/Expressions/ExpressionFuncExtensions.cs
  16. 1
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/DefaultLoggingManager.cs
  17. 1
      aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/ILoggingManager.cs

459
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/LINGYUN/Abp/AuditLogging/Elasticsearch/ElasticsearchAuditLogManager.cs

@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -67,7 +68,7 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
var sortingField = sorting; var sortingField = sorting;
if (sortingField.IsNullOrWhiteSpace()) if (sortingField.IsNullOrWhiteSpace())
{ {
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); var indexMapping = await _indexMappingProvider.GetMappingAsync<AuditLog>(indexName, cancellationToken);
if (indexMapping != null) if (indexMapping != null)
{ {
var sortingFieldMap = indexMapping.Fields var sortingFieldMap = indexMapping.Fields
@ -87,13 +88,13 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
sortingField, sortingField,
maxResultCount, maxResultCount,
skipCount, skipCount,
sourceExcludes: includeDetails == true sourceExcludes: includeDetails == false
? Fields.FromFields( ? Fields.FromFields(
[ [
new Field("Actions"), new Field(nameof(AuditLog.Actions)),
new Field("Comments"), new Field(nameof(AuditLog.Comments)),
new Field("EntityChanges"), new Field(nameof(AuditLog.EntityChanges)),
new Field("Exceptions"), new Field(nameof(AuditLog.Exceptions)),
]) ])
: null, : null,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
@ -116,41 +117,31 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
HttpStatusCode? httpStatusCode = null, HttpStatusCode? httpStatusCode = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var indexName = CreateIndex();
var client = _clientFactory.Create(); var client = _clientFactory.Create();
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken);
var querys = BuildQueryDescriptor( Expression<Func<AuditLog, bool>> expression = _ => true;
indexMapping,
startTime, expression = expression
endTime, .AndIf(startTime.HasValue, x => x.ExecutionTime >= _clock.Normalize(startTime!.Value))
httpMethod, .AndIf(endTime.HasValue, x => x.ExecutionTime <= _clock.Normalize(endTime!.Value))
url, .AndIf(!httpMethod.IsNullOrWhiteSpace(), x => x.HttpMethod == httpMethod)
userId, .AndIf(!url.IsNullOrWhiteSpace(), x => x.Url!.Contains(url!))
userName, .AndIf(userId.HasValue, x => x.UserId == userId)
applicationName, .AndIf(!userName.IsNullOrWhiteSpace(), x => x.UserName == userName)
correlationId, .AndIf(!applicationName.IsNullOrWhiteSpace(), x => x.ApplicationName == applicationName)
clientId, .AndIf(!correlationId.IsNullOrWhiteSpace(), x => x.CorrelationId == correlationId)
clientIpAddress, .AndIf(!clientId.IsNullOrWhiteSpace(), x => x.ClientId == clientId)
maxExecutionDuration, .AndIf(!clientIpAddress.IsNullOrWhiteSpace(), x => x.ClientIpAddress == clientIpAddress)
minExecutionDuration, .AndIf(maxExecutionDuration.HasValue, x => x.ExecutionDuration >= maxExecutionDuration)
hasException, .AndIf(minExecutionDuration.HasValue, x => x.ExecutionDuration <= minExecutionDuration)
httpStatusCode); .AndIf(hasException == true, x => x.Exceptions != null)
.AndIf(hasException == false, x => x.Exceptions == null)
.AndIf(httpStatusCode.HasValue, x => x.HttpStatusCode == (int)httpStatusCode!);
var response = await client.CountAsync<AuditLog>(dsl => return await _expressionQueryService.GetCountAsync(
dsl.Indices(indexName) CreateIndex(),
.Query(new BoolQuery expression,
{
Must = querys
}),
cancellationToken); cancellationToken);
if (response.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query audit log count failed: {errorMessage}", errorMessage);
}
return response.Count;
} }
public async virtual Task<List<AuditLog>> GetListAsync( public async virtual Task<List<AuditLog>> GetListAsync(
@ -174,51 +165,47 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
bool includeDetails = false, bool includeDetails = false,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var indexName = CreateIndex();
var client = _clientFactory.Create(); var client = _clientFactory.Create();
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); if (sorting.IsNullOrWhiteSpace())
{
var sorts = GetOrDefaultSort(indexMapping, sorting); sorting = $"{nameof(AuditLog.ExecutionTime)} DESC";
}
var querys = BuildQueryDescriptor(
indexMapping,
startTime,
endTime,
httpMethod,
url,
userId,
userName,
applicationName,
correlationId,
clientId,
clientIpAddress,
maxExecutionDuration,
minExecutionDuration,
hasException,
httpStatusCode);
var query = new BoolQuery { Must = querys }; Expression<Func<AuditLog, bool>> expression = _ => true;
expression = expression
.AndIf(startTime.HasValue, x => x.ExecutionTime >= _clock.Normalize(startTime!.Value))
.AndIf(endTime.HasValue, x => x.ExecutionTime <= _clock.Normalize(endTime!.Value))
.AndIf(!httpMethod.IsNullOrWhiteSpace(), x => x.HttpMethod == httpMethod)
.AndIf(!url.IsNullOrWhiteSpace(), x => x.Url!.Contains(url!))
.AndIf(userId.HasValue, x => x.UserId == userId)
.AndIf(!userName.IsNullOrWhiteSpace(), x => x.UserName == userName)
.AndIf(!applicationName.IsNullOrWhiteSpace(), x => x.ApplicationName == applicationName)
.AndIf(!correlationId.IsNullOrWhiteSpace(), x => x.CorrelationId == correlationId)
.AndIf(!clientId.IsNullOrWhiteSpace(), x => x.ClientId == clientId)
.AndIf(!clientIpAddress.IsNullOrWhiteSpace(), x => x.ClientIpAddress == clientIpAddress)
.AndIf(maxExecutionDuration.HasValue, x => x.ExecutionDuration >= maxExecutionDuration)
.AndIf(minExecutionDuration.HasValue, x => x.ExecutionDuration <= minExecutionDuration)
.AndIf(hasException == true, x => x.Exceptions != null)
.AndIf(hasException == false, x => x.Exceptions == null)
.AndIf(httpStatusCode.HasValue, x => x.HttpStatusCode == (int)httpStatusCode!);
// ES最大支持10000, 超出这个长度后升级为使用Search_After方案 return await _expressionQueryService.GetListAsync(
return skipCount >= 10000 && sorts != null CreateIndex(),
? await SearchAfterAuditLogs( expression,
client, sorting: sorting,
indexName, maxResultCount: maxResultCount,
query, skipCount: skipCount,
sorts, sourceExcludes: includeDetails == false
maxResultCount, ? Fields.FromFields(
skipCount, [
includeDetails, new Field(nameof(AuditLog.Actions)),
cancellationToken) new Field(nameof(AuditLog.Comments)),
: await SearchFromSizeAuditLogs( new Field(nameof(AuditLog.EntityChanges)),
client, new Field(nameof(AuditLog.Exceptions)),
indexName, ])
query, : null,
sorts, cancellationToken: cancellationToken);
maxResultCount,
skipCount,
includeDetails,
cancellationToken);
} }
public async virtual Task<AuditLog?> GetAsync( public async virtual Task<AuditLog?> GetAsync(
@ -271,322 +258,8 @@ public class ElasticsearchAuditLogManager : IAuditLogManager, ITransientDependen
cancellationToken); cancellationToken);
} }
protected virtual List<Query> BuildQueryDescriptor(
IndexMappingInfo indexMappingInfo,
DateTime? startTime = null,
DateTime? endTime = null,
string? httpMethod = null,
string? url = null,
Guid? userId = null,
string? userName = null,
string? applicationName = null,
string? correlationId = null,
string? clientId = null,
string? clientIpAddress = null,
int? maxExecutionDuration = null,
int? minExecutionDuration = null,
bool? hasException = null,
HttpStatusCode? httpStatusCode = null)
{
var queries = new List<Query>();
if (startTime.HasValue)
{
queries.Add(new DateRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionTime)))
{
Gte = _clock.Normalize(startTime.Value)
});
}
if (endTime.HasValue)
{
queries.Add(new DateRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionTime)))
{
Lte = _clock.Normalize(endTime.Value)
});
}
if (!httpMethod.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.HttpMethod)), httpMethod));
}
if (!url.IsNullOrWhiteSpace())
{
queries.Add(new WildcardQuery(GetField(indexMappingInfo, nameof(AuditLog.Url)))
{
Value = $"*{url}*"
});
}
if (userId.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.UserId)), userId.Value.ToString()));
}
if (!userName.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.UserName)), userName));
}
if (!applicationName.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.ApplicationName)), applicationName));
}
if (!correlationId.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.CorrelationId)), correlationId));
}
if (!clientId.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.ClientId)), clientId));
}
if (!clientIpAddress.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.ClientIpAddress)), clientIpAddress));
}
if (maxExecutionDuration.HasValue)
{
queries.Add(new NumberRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionDuration)))
{
Lte = maxExecutionDuration.Value
});
}
if (minExecutionDuration.HasValue)
{
queries.Add(new NumberRangeQuery(GetField(indexMappingInfo, nameof(AuditLog.ExecutionDuration)))
{
Gte = minExecutionDuration.Value
});
}
if (hasException.HasValue)
{
if (hasException.Value)
{
queries.Add(new ExistsQuery(GetField(indexMappingInfo, nameof(AuditLog.Exceptions))));
}
else
{
queries.Add(new BoolQuery
{
MustNot = new List<Query>
{
new ExistsQuery(GetField(indexMappingInfo, nameof(AuditLog.Exceptions)))
}
});
}
}
if (httpStatusCode.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, nameof(AuditLog.HttpStatusCode)), ((int)httpStatusCode.Value).ToString()));
}
return queries;
}
private async Task<List<AuditLog>> SearchFromSizeAuditLogs(
ElasticsearchClient client,
string indexName,
Query query,
SortOptions[]? sorts = null,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var searchResponse = await client.SearchAsync<AuditLog>(dsl =>
{
dsl.Indices(indexName)
.Query(query)
.From(skipCount)
.Size(maxResultCount);
if (sorts != null)
{
dsl.Sort(sorts);
}
if (!includeDetails)
{
dsl.SourceExcludes(
ex => ex.Actions,
ex => ex.Comments,
ex => ex.Exceptions,
ex => ex.EntityChanges);
}
}, cancellationToken);
if (searchResponse.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query audit log failed: {errorMessage}", errorMessage);
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<AuditLog>> SearchAfterAuditLogs(
ElasticsearchClient client,
string indexName,
Query query,
SortOptions[] sorts,
int maxResultCount = 50,
int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var searchAfter = await GetSearchAfterValue(
client,
indexName,
query,
sorts,
skipCount,
cancellationToken);
if (searchAfter == null || !searchAfter.Any())
{
return [];
}
var searchResponse = await client.SearchAsync<AuditLog>(dsl =>
{
dsl.Indices(indexName)
.Query(query)
.Sort(sorts)
.Size(maxResultCount)
.SearchAfter(searchAfter);
if (!includeDetails)
{
dsl.SourceExcludes(
ex => ex.Actions,
ex => ex.Comments,
ex => ex.Exceptions,
ex => ex.EntityChanges);
}
}, cancellationToken);
if (searchResponse.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query audit log failed: {errorMessage}", errorMessage);
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<FieldValue>?> GetSearchAfterValue(
ElasticsearchClient client,
string indexName,
Query query,
SortOptions[] sorts,
int skipCount,
CancellationToken cancellationToken = default)
{
// 10000以内直接取最后一条数据
if (skipCount < 10000)
{
var response = await client.SearchAsync<AuditLog>(
dsl => dsl.Indices(indexName)
.Query(query)
.Sort(sorts)
.SourceIncludes(x => x.Id)
.From(skipCount)
.Size(1),
cancellationToken);
if (!response.IsSuccess() || response.Hits == null || !response.Hits.Any())
{
return null;
}
var hit = response.Hits.FirstOrDefault();
return hit?.Sort?.ToList();
}
// 获取第9999条数据Hits作为searchAfter
var firstResponse = await client.SearchAsync<AuditLog>(
dsl => dsl.Indices(indexName)
.Query(query)
.Sort(sorts)
.SourceIncludes(x => x.Id)
.From(9999)
.Size(1),
cancellationToken);
if (!firstResponse.IsSuccess() || firstResponse.Hits == null || !firstResponse.Hits.Any())
{
return null;
}
var firstHit = firstResponse.Hits.FirstOrDefault();
if (firstHit?.Sort == null || !firstHit.Sort.Any())
{
return null;
}
// 获取skipCount最近一条数据作为searchAfter
var secondResponse = await client.SearchAsync<AuditLog>(
dsl => dsl.Indices(indexName)
.Query(query)
// 反转排序取第一个数据作为起始索引
.Sort(sorts.ReverseSort()!.ToArray())
.SourceIncludes(x => x.Id)
.SearchAfter(firstHit.Sort.ToList())
.Size(1),
cancellationToken);
if (!secondResponse.IsSuccess() || secondResponse.Hits == null || !secondResponse.Hits.Any())
{
return null;
}
var lastHit = secondResponse.Hits.LastOrDefault();
if (lastHit?.Sort == null || !lastHit.Sort.Any())
{
return null;
}
return lastHit.Sort.ToList();
}
protected virtual string CreateIndex() protected virtual string CreateIndex()
{ {
return _indexNameNormalizer.NormalizeIndex("audit-log"); return _indexNameNormalizer.NormalizeIndex("audit-log");
} }
private static SortOptions[]? GetOrDefaultSort(IndexMappingInfo indexMappingInfo, string? sorting = null)
{
var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase)
? SortOrder.Asc : SortOrder.Desc;
sorting = !sorting.IsNullOrWhiteSpace()
? sorting.Split()[0]
: nameof(AuditLog.ExecutionTime);
SortOptions[]? sorts = null;
var sortingFieldMap = indexMappingInfo.Fields
.Where(x => x.Key.Equals(sorting, StringComparison.CurrentCultureIgnoreCase))
.Select(x => x.Value)
.FirstOrDefault();
if (sortingFieldMap != null)
{
sorting = sortingFieldMap.Path;
}
if (!sorting.IsNullOrWhiteSpace())
{
sorts = new SortOptions[1]
{
new SortOptions
{
Field = new FieldSort(new Field(sorting))
{
Order = sortOrder,
},
}
};
}
return sorts;
}
private static string GetField(IndexMappingInfo indexMappingInfo, string fieldFullPath)
{
return indexMappingInfo.GetExactFieldPath(fieldFullPath);
}
} }

32
aspnet-core/framework/auditing/LINGYUN.Abp.AuditLogging.Elasticsearch/System/Linq/Expressions/ExpressionFuncExtensions.cs

@ -0,0 +1,32 @@
using Volo.Abp.Specifications;
namespace System.Linq.Expressions;
internal static class ExpressionFuncExtensions
{
public static Expression<Func<T, bool>> AndIf<T>(
this Expression<Func<T, bool>> first,
bool condition,
Expression<Func<T, bool>> second)
{
if (condition)
{
return ExpressionFuncExtender.And(first, second);
}
return first;
}
public static Expression<Func<T, bool>> OrIf<T>(
this Expression<Func<T, bool>> first,
bool condition,
Expression<Func<T, bool>> second)
{
if (condition)
{
return ExpressionFuncExtender.Or(first, second);
}
return first;
}
}

8
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/Elastic/Clients/Elasticsearch/SortOptionsExtenssions.cs

@ -12,7 +12,7 @@ public static class SortOptionsExtenssions
{ {
return sortOptions; return sortOptions;
} }
var newSort = sortOptions.Select(sort => return sortOptions.Select(sort =>
{ {
if (sort.Field != null) if (sort.Field != null)
{ {
@ -21,10 +21,6 @@ public static class SortOptionsExtenssions
: SortOrder.Asc; : SortOrder.Asc;
} }
return sort; return sort;
}).ToArray(); }).Reverse();
newSort.Reverse();
return newSort;
} }
} }

253
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ElasticsearchIndexMappingProvider.cs

@ -2,10 +2,11 @@
using Elastic.Clients.Elasticsearch.IndexManagement; using Elastic.Clients.Elasticsearch.IndexManagement;
using Elastic.Clients.Elasticsearch.Mapping; using Elastic.Clients.Elasticsearch.Mapping;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileSystemGlobbing.Internal;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp; using Volo.Abp;
@ -27,9 +28,26 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
_cache = cache; _cache = cache;
} }
public async Task<IndexMappingInfo> GetMappingAsync(string indexPattern, CancellationToken cancellationToken = default) public async virtual Task<IndexMappingInfo> GetMappingAsync<TDocument>(
string indexPattern,
CancellationToken cancellationToken = default)
{ {
var cacheKey = $"es_mapping_{indexPattern}"; return await GetMappingAsync(indexPattern, typeof(TDocument), cancellationToken);
}
public async virtual Task<IndexMappingInfo> GetMappingAsync(string indexPattern, CancellationToken cancellationToken = default)
{
return await GetMappingAsync(indexPattern, null, cancellationToken);
}
private async Task<IndexMappingInfo> GetMappingAsync(
string indexPattern,
Type? documentType,
CancellationToken cancellationToken = default)
{
var cacheKey = documentType == null
? $"es_mapping_{indexPattern}"
: $"es_mapping_{indexPattern}_{documentType.FullName}";
var cacheItem = _cache.Get<IndexMappingInfo>(cacheKey); var cacheItem = _cache.Get<IndexMappingInfo>(cacheKey);
if (cacheItem == null) if (cacheItem == null)
@ -48,7 +66,10 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
throw new AbpException($"Failed to get mapping for index {indexPattern}: {errorMessage}"); throw new AbpException($"Failed to get mapping for index {indexPattern}: {errorMessage}");
} }
var indexName = indexPattern.EndsWith("*") ? indexPattern.Substring(0, indexPattern.Length - 1) : indexPattern; var indexName = indexPattern.EndsWith("*")
? indexPattern.Substring(0, indexPattern.Length - 1)
: indexPattern;
var indexMappings = response.GetMappingFor(indexName); var indexMappings = response.GetMappingFor(indexName);
if (indexMappings == null) if (indexMappings == null)
{ {
@ -66,7 +87,7 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
} }
} }
cacheItem = ParseMapping(indexMappings, indexPattern); cacheItem = ParseMapping(indexMappings, indexPattern, documentType);
_cache.Set(cacheKey, cacheItem, _cacheDuration); _cache.Set(cacheKey, cacheItem, _cacheDuration);
} }
@ -74,35 +95,68 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
return cacheItem; return cacheItem;
} }
private IndexMappingInfo ParseMapping(TypeMapping mappings, string indexName) private static IndexMappingInfo ParseMapping(
TypeMapping mappings,
string indexName,
Type? documentType = null)
{ {
var mappingInfo = new IndexMappingInfo { IndexName = indexName }; var mappingInfo = new IndexMappingInfo
{
IndexName = indexName,
DocumentType = documentType
};
if (mappings?.Properties != null) if (mappings?.Properties != null)
{ {
ParseProperties(mappings.Properties, mappingInfo, string.Empty); ParseProperties(
mappings.Properties,
mappingInfo,
string.Empty,
documentType,
string.Empty);
} }
return mappingInfo; return mappingInfo;
} }
private void ParseProperties(Properties? properties, IndexMappingInfo mappingInfo, string parentPath) private static void ParseProperties(
Properties? properties,
IndexMappingInfo mappingInfo,
string esParentPath,
Type? parentClrType,
string clrParentPath)
{ {
if (properties == null) return; if (properties == null)
{
return;
}
foreach (var kvp in properties) foreach (var kvp in properties)
{ {
var propertyName = kvp.Key.ToString(); var propertyName = kvp.Key.ToString();
var property = kvp.Value; var property = kvp.Value;
var fullPath = string.IsNullOrEmpty(parentPath)
// ES 完整路径
var esFullPath = string.IsNullOrEmpty(esParentPath)
? propertyName
: $"{esParentPath}.{propertyName}";
// CLR 完整路径
var clrFullPath = string.IsNullOrEmpty(clrParentPath)
? propertyName ? propertyName
: $"{parentPath}.{propertyName}"; : $"{clrParentPath}.{propertyName}";
// 解析 CLR 类型
var clrType = ResolveClrType(parentClrType, propertyName);
var fieldInfo = new FieldMappingInfo var fieldInfo = new FieldMappingInfo
{ {
Path = fullPath, Path = esFullPath,
Name = propertyName, Name = propertyName,
Type = GetPropertyType(property) Type = GetPropertyType(property),
ClrType = clrType,
ClrPath = clrFullPath
}; };
switch (property) switch (property)
@ -110,13 +164,13 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
// Keyword 类型 // Keyword 类型
case KeywordProperty keyword: case KeywordProperty keyword:
fieldInfo.IsKeyword = true; fieldInfo.IsKeyword = true;
mappingInfo.KeywordFields.Add(fullPath); mappingInfo.KeywordFields.Add(esFullPath);
break; break;
// Text 类型 - 包含多字段支持 // Text 类型 - 包含多字段支持
case TextProperty text: case TextProperty text:
fieldInfo.IsText = true; fieldInfo.IsText = true;
mappingInfo.TextFields.Add(fullPath); mappingInfo.TextFields.Add(esFullPath);
// 处理 Text 的 Fields(多字段) // 处理 Text 的 Fields(多字段)
if (text.Fields != null && text.Fields.Count() > 0) if (text.Fields != null && text.Fields.Count() > 0)
@ -127,29 +181,34 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
{ {
var subFieldName = subFieldKvp.Key.ToString(); var subFieldName = subFieldKvp.Key.ToString();
var subFieldProperty = subFieldKvp.Value; var subFieldProperty = subFieldKvp.Value;
var subFieldPath = $"{fullPath}.{subFieldName}"; var subFieldEsPath = $"{esFullPath}.{subFieldName}";
var subFieldClrPath = $"{clrFullPath}.{subFieldName}";
var subFieldInfo = new FieldMappingInfo var subFieldInfo = new FieldMappingInfo
{ {
Path = subFieldPath, Path = subFieldEsPath,
Name = subFieldName, Name = subFieldName,
Type = GetPropertyType(subFieldProperty) Type = GetPropertyType(subFieldProperty),
ClrType = clrType,
ClrPath = subFieldClrPath,
IsMultiField = true
}; };
// 处理子字段的类型 // 处理子字段的类型
if (subFieldProperty is KeywordProperty) if (subFieldProperty is KeywordProperty)
{ {
subFieldInfo.IsKeyword = true; subFieldInfo.IsKeyword = true;
mappingInfo.KeywordFields.Add(subFieldPath); mappingInfo.KeywordFields.Add(subFieldEsPath);
} }
else if (subFieldProperty is TextProperty) else if (subFieldProperty is TextProperty)
{ {
subFieldInfo.IsText = true; subFieldInfo.IsText = true;
mappingInfo.TextFields.Add(subFieldPath); mappingInfo.TextFields.Add(subFieldEsPath);
} }
fieldInfo.Properties[subFieldName] = subFieldInfo; fieldInfo.Properties[subFieldName] = subFieldInfo;
mappingInfo.Fields[subFieldPath] = subFieldInfo; mappingInfo.Fields[subFieldEsPath] = subFieldInfo;
mappingInfo.ClrFields[subFieldClrPath] = subFieldInfo;
} }
} }
break; break;
@ -158,14 +217,14 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
case DateProperty date: case DateProperty date:
fieldInfo.IsDate = true; fieldInfo.IsDate = true;
fieldInfo.Format = date.Format; fieldInfo.Format = date.Format;
mappingInfo.DateFields.Add(fullPath); mappingInfo.DateFields.Add(esFullPath);
break; break;
// 日期纳秒类型 // 日期纳秒类型
case DateNanosProperty dateNanos: case DateNanosProperty dateNanos:
fieldInfo.IsDate = true; fieldInfo.IsDate = true;
fieldInfo.Format = dateNanos.Format; fieldInfo.Format = dateNanos.Format;
mappingInfo.DateFields.Add(fullPath); mappingInfo.DateFields.Add(esFullPath);
break; break;
// 数值类型 // 数值类型
@ -179,58 +238,71 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
case ShortNumberProperty: case ShortNumberProperty:
case UnsignedLongNumberProperty: case UnsignedLongNumberProperty:
fieldInfo.IsNumeric = true; fieldInfo.IsNumeric = true;
mappingInfo.NumericFields.Add(fullPath); mappingInfo.NumericFields.Add(esFullPath);
break; break;
// 布尔类型 // 布尔类型
case BooleanProperty: case BooleanProperty:
fieldInfo.IsBoolean = true; fieldInfo.IsBoolean = true;
mappingInfo.BooleanFields.Add(fullPath); mappingInfo.BooleanFields.Add(esFullPath);
break; break;
// Nested 类型 // Nested 类型
case NestedProperty nested: case NestedProperty nested:
fieldInfo.IsNested = true; fieldInfo.IsNested = true;
fieldInfo.IsObject = true; fieldInfo.IsObject = true;
mappingInfo.NestedFieldPaths.Add(fullPath); mappingInfo.NestedFieldPaths.Add(esFullPath);
var nestedInfo = new NestedMappingInfo var nestedInfo = new NestedMappingInfo
{ {
Path = fullPath, Path = esFullPath,
Name = propertyName, Name = propertyName,
Properties = new Dictionary<string, FieldMappingInfo>() Properties = new Dictionary<string, FieldMappingInfo>()
}; };
// 获取 nested 集合的元素类型
var elementType = GetElementType(clrType);
if (nested.Properties != null) if (nested.Properties != null)
{ {
// 先递归解析内部字段 // 先递归解析内部字段
ParseProperties(nested.Properties, mappingInfo, fullPath); ParseProperties(
nested.Properties,
mappingInfo,
esFullPath,
elementType ?? clrType,
clrFullPath);
// 收集 nested 内部的字段信息 // 收集 nested 内部的字段信息
foreach (var innerKvp in nested.Properties) foreach (var innerKvp in nested.Properties)
{ {
var innerName = innerKvp.Key.ToString(); var innerName = innerKvp.Key.ToString();
var innerFullPath = $"{fullPath}.{innerName}"; var innerEsFullPath = $"{esFullPath}.{innerName}";
var innerClrFullPath = $"{clrFullPath}.{innerName}";
if (mappingInfo.Fields.TryGetValue(innerFullPath, out var innerFieldInfo)) if (mappingInfo.Fields.TryGetValue(innerEsFullPath, out var innerFieldInfo))
{ {
nestedInfo.Properties[innerName] = innerFieldInfo; nestedInfo.Properties[innerName] = innerFieldInfo;
} }
else else
{ {
var innerClrType = ResolveClrType(elementType ?? clrType, innerName);
innerFieldInfo = new FieldMappingInfo innerFieldInfo = new FieldMappingInfo
{ {
Path = innerFullPath, Path = innerEsFullPath,
Name = innerName, Name = innerName,
Type = GetPropertyType(innerKvp.Value) Type = GetPropertyType(innerKvp.Value),
ClrType = innerClrType,
ClrPath = innerClrFullPath
}; };
nestedInfo.Properties[innerName] = innerFieldInfo; nestedInfo.Properties[innerName] = innerFieldInfo;
mappingInfo.Fields[innerFullPath] = innerFieldInfo; mappingInfo.Fields[innerEsFullPath] = innerFieldInfo;
mappingInfo.ClrFields[innerClrFullPath] = innerFieldInfo;
} }
} }
} }
mappingInfo.NestedFields[fullPath] = nestedInfo; mappingInfo.NestedFields[esFullPath] = nestedInfo;
break; break;
// Object 类型 // Object 类型
@ -240,7 +312,12 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
if (obj.Properties != null) if (obj.Properties != null)
{ {
ParseProperties(obj.Properties, mappingInfo, fullPath); ParseProperties(
obj.Properties,
mappingInfo,
esFullPath,
clrType,
clrFullPath);
} }
break; break;
@ -278,7 +355,7 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
case MatchOnlyTextProperty matchOnlyText: case MatchOnlyTextProperty matchOnlyText:
fieldInfo.IsText = true; fieldInfo.IsText = true;
fieldInfo.Type = "match_only_text"; fieldInfo.Type = "match_only_text";
mappingInfo.TextFields.Add(fullPath); mappingInfo.TextFields.Add(esFullPath);
// MatchOnlyText 也可能有 Fields // MatchOnlyText 也可能有 Fields
if (matchOnlyText.Fields != null && matchOnlyText.Fields.Count() > 0) if (matchOnlyText.Fields != null && matchOnlyText.Fields.Count() > 0)
@ -287,20 +364,25 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
foreach (var subFieldKvp in matchOnlyText.Fields) foreach (var subFieldKvp in matchOnlyText.Fields)
{ {
var subFieldName = subFieldKvp.Key.ToString(); var subFieldName = subFieldKvp.Key.ToString();
var subFieldPath = $"{fullPath}.{subFieldName}"; var subFieldEsPath = $"{esFullPath}.{subFieldName}";
var subFieldClrPath = $"{clrFullPath}.{subFieldName}";
var subFieldInfo = new FieldMappingInfo var subFieldInfo = new FieldMappingInfo
{ {
Path = subFieldPath, Path = subFieldEsPath,
Name = subFieldName, Name = subFieldName,
Type = GetPropertyType(subFieldKvp.Value) Type = GetPropertyType(subFieldKvp.Value),
ClrType = clrType,
ClrPath = subFieldClrPath,
IsMultiField = true
}; };
if (subFieldKvp.Value is KeywordProperty) if (subFieldKvp.Value is KeywordProperty)
{ {
subFieldInfo.IsKeyword = true; subFieldInfo.IsKeyword = true;
mappingInfo.KeywordFields.Add(subFieldPath); mappingInfo.KeywordFields.Add(subFieldEsPath);
} }
fieldInfo.Properties[subFieldName] = subFieldInfo; fieldInfo.Properties[subFieldName] = subFieldInfo;
mappingInfo.Fields[subFieldPath] = subFieldInfo; mappingInfo.Fields[subFieldEsPath] = subFieldInfo;
mappingInfo.ClrFields[subFieldClrPath] = subFieldInfo;
} }
} }
break; break;
@ -308,7 +390,7 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
case WildcardProperty: case WildcardProperty:
fieldInfo.IsWildcard = true; fieldInfo.IsWildcard = true;
fieldInfo.Type = "wildcard"; fieldInfo.Type = "wildcard";
mappingInfo.WildcardFields.Add(fullPath); mappingInfo.WildcardFields.Add(esFullPath);
break; break;
case CompletionProperty: case CompletionProperty:
@ -344,11 +426,92 @@ public class ElasticsearchIndexMappingProvider : IIndexMappingProvider, ITransie
break; break;
} }
mappingInfo.Fields[fullPath] = fieldInfo; // 添加到映射集合
mappingInfo.Fields[esFullPath] = fieldInfo;
// 只有在有 CLR 类型信息时才添加到 CLR 字段集合
if (clrType != null || parentClrType != null)
{
mappingInfo.ClrFields[clrFullPath] = fieldInfo;
}
}
}
private static Type? ResolveClrType(Type? parentType, string propertyName)
{
if (parentType == null)
{
return null;
} }
var propertyInfo = parentType.GetProperty(
propertyName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
return propertyInfo?.PropertyType;
}
private static Type? GetElementType(Type? type)
{
if (type == null)
{
return null;
}
// 处理数组
if (type.IsArray)
{
return type.GetElementType();
}
// 处理 IEnumerable<T>
if (type.IsGenericType)
{
var genericTypeDefinition = type.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(IEnumerable<>) ||
genericTypeDefinition == typeof(ICollection<>) ||
genericTypeDefinition == typeof(IList<>) ||
genericTypeDefinition == typeof(List<>))
{
return type.GetGenericArguments()[0];
}
}
// 处理实现了 IEnumerable<T> 的接口
var enumerableInterface = type.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
return enumerableInterface?.GetGenericArguments()[0];
}
private static string GetJsonPropertyName(Type? type, string propertyName)
{
if (type == null)
{
return propertyName;
}
var propertyInfo = type.GetProperty(
propertyName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (propertyInfo == null)
{
return propertyName;
}
// 检查 JsonPropertyName 特性
var jsonPropertyNameAttribute = propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>();
if (jsonPropertyNameAttribute != null && !string.IsNullOrEmpty(jsonPropertyNameAttribute.Name))
{
return jsonPropertyNameAttribute.Name;
}
return propertyName;
} }
private string GetPropertyType(IProperty property) private static string GetPropertyType(IProperty property)
{ {
return property switch return property switch
{ {

8
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/EmptyDocument.cs

@ -0,0 +1,8 @@
using System;
namespace LINGYUN.Abp.Elasticsearch;
[Serializable]
public class EmptyDocument
{
}

266
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryService.cs

@ -1,5 +1,7 @@
using Elastic.Clients.Elasticsearch; using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl; using Elastic.Clients.Elasticsearch.QueryDsl;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -12,15 +14,21 @@ namespace LINGYUN.Abp.Elasticsearch;
public class ExpressionQueryService : IExpressionQueryService, ITransientDependency public class ExpressionQueryService : IExpressionQueryService, ITransientDependency
{ {
public ILogger<ExpressionQueryService> Logger { protected get; set; }
protected IElasticsearchClientFactory ClientFactory { get; } protected IElasticsearchClientFactory ClientFactory { get; }
protected IIndexMappingProvider IndexMappingProvider { get; }
protected IExpressionQueryTranslator ExpressionQueryTranslator { get; } protected IExpressionQueryTranslator ExpressionQueryTranslator { get; }
public ExpressionQueryService( public ExpressionQueryService(
IElasticsearchClientFactory clientFactory, IElasticsearchClientFactory clientFactory,
IIndexMappingProvider indexMappingProvider,
IExpressionQueryTranslator expressionQueryTranslator) IExpressionQueryTranslator expressionQueryTranslator)
{ {
ClientFactory = clientFactory; ClientFactory = clientFactory;
IndexMappingProvider = indexMappingProvider;
ExpressionQueryTranslator = expressionQueryTranslator; ExpressionQueryTranslator = expressionQueryTranslator;
Logger = NullLogger<ExpressionQueryService>.Instance;
} }
public async virtual Task<long> GetCountAsync<TDocument>( public async virtual Task<long> GetCountAsync<TDocument>(
@ -55,19 +63,11 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
SortOptions[]? sorts = null; SortOptions[]? sorts = null;
if (!sorting.IsNullOrWhiteSpace()) if (!sorting.IsNullOrWhiteSpace())
{ {
var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase) var indexMapping = await IndexMappingProvider.GetMappingAsync<TDocument>(indexName, cancellationToken);
? SortOrder.Asc : SortOrder.Desc; if (indexMapping != null)
sorts = new SortOptions[1]
{ {
new SortOptions sorts = ResolveDefaultSorts(indexMapping, sorting);
{ }
Field = new FieldSort(new Field(sorting))
{
Order = sortOrder,
},
}
};
} }
// 数量超过10000且存在排序时才可以使用SearchAfter特性 // 数量超过10000且存在排序时才可以使用SearchAfter特性
@ -126,8 +126,9 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
} }
}, cancellationToken); }, cancellationToken);
if (!searchResponse.IsSuccess()) if (searchResponse.TryGetErrorMessage(out var errorMessage))
{ {
Logger.LogWarning("Query document failed: {errorMessage}", errorMessage);
return []; return [];
} }
@ -153,7 +154,7 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
} }
else else
{ {
searchAfter = await GetSearchAfterValue<TDocument>( searchAfter = await GetSearchAfterValue(
client, client,
indexName, indexName,
query, query,
@ -184,15 +185,16 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
} }
}, cancellationToken); }, cancellationToken);
if (!searchResponse.IsSuccess()) if (searchResponse.TryGetErrorMessage(out var errorMessage))
{ {
Logger.LogWarning("Query document failed: {errorMessage}", errorMessage);
return []; return [];
} }
return searchResponse.Documents.ToList(); return searchResponse.Documents.ToList();
} }
private async Task<List<FieldValue>?> GetSearchAfterValue<TDocument>( private async Task<List<FieldValue>?> GetSearchAfterValue(
ElasticsearchClient client, ElasticsearchClient client,
string indexName, string indexName,
Query query, Query query,
@ -203,16 +205,19 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
// 10000以内直接取最后一条数据 // 10000以内直接取最后一条数据
if (skipCount < 10000) if (skipCount < 10000)
{ {
var response = await client.SearchAsync<TDocument>( var response = await client.SearchAsync<EmptyDocument>(
dsl => dsl.Indices(indexName) dsl => dsl.Indices(indexName)
.Query(query) .Query(query)
.Sort(sorts) .Sort(sorts)
.From(skipCount) .From(skipCount)
.Size(1), .Size(1)
.Source(false)
.TrackScores(false),
cancellationToken); cancellationToken);
if (!response.IsSuccess() || response.Hits == null || !response.Hits.Any()) if (response.TryGetErrorMessage(out var oneError))
{ {
Logger.LogWarning("Failed to obtain the {skipCount}th sorting record. error: {error}", skipCount, oneError);
return null; return null;
} }
@ -221,48 +226,235 @@ public class ExpressionQueryService : IExpressionQueryService, ITransientDepende
} }
// 获取第9999条数据Hits作为searchAfter // 获取第9999条数据Hits作为searchAfter
var firstResponse = await client.SearchAsync<TDocument>( var firstResponse = await client.SearchAsync<EmptyDocument>(
dsl => dsl.Indices(indexName) dsl => dsl.Indices(indexName)
.Query(query) .Query(query)
.Sort(sorts) .Sort(sorts)
.SourceIncludes([])
.From(9999) .From(9999)
.Size(1), .Size(1)
.Source(false)
.TrackScores(false),
cancellationToken); cancellationToken);
if (!firstResponse.IsSuccess() || firstResponse.Hits == null || !firstResponse.Hits.Any()) if (firstResponse.TryGetErrorMessage(out var firstError))
{ {
Logger.LogWarning("Failed to obtain the first sorted record after the {skipCount}th item. error: {error}", skipCount, firstError);
return null; return null;
} }
var firstHit = firstResponse.Hits.FirstOrDefault(); var firstHit = firstResponse.Hits?.FirstOrDefault();
if (firstHit?.Sort == null || !firstHit.Sort.Any()) if (firstHit?.Sort == null || !firstHit.Sort.Any())
{ {
Logger.LogWarning("The first sorted record after the {skipCount}th item is empty!", skipCount);
return null; return null;
} }
// 获取skipCount最近一条数据作为searchAfter var remaining = skipCount - 10000;
var secondResponse = await client.SearchAsync<TDocument>(
dsl => dsl.Indices(indexName) return await GetBatchSearchAfterValue(
.Query(query) client,
// 反转排序取第一个数据作为起始索引 indexName,
.Sort(sorts.ReverseSort()!.ToArray()) query,
.SourceIncludes([]) sorts,
.SearchAfter(firstHit.Sort.ToList()) [.. firstHit.Sort],
.Size(1), remaining,
remaining > 10000 ? 5000 : 1000,
cancellationToken); cancellationToken);
}
private async Task<List<FieldValue>?> GetBatchSearchAfterValue(
ElasticsearchClient client,
string indexName,
Query query,
SortOptions[] sorts,
FieldValue[] searchAfter,
int remaining,
int batchSize = 1000,
CancellationToken cancellationToken = default)
{
List<FieldValue>? lastSort = null;
while (remaining > 0)
{
var batch = Math.Min(remaining, batchSize);
var response = await client.SearchAsync<EmptyDocument>(
dsl => dsl.Indices(indexName)
.Query(query)
.Sort(sorts)
.SearchAfter(searchAfter)
.Size(batch)
.Source(false)
.TrackScores(false),
cancellationToken);
if (response.TryGetErrorMessage(out var error))
{
Logger.LogWarning("Failed to get batch records. remaining: {remaining}, error: {error}", remaining, error);
return null;
}
if (response.Hits == null || !response.Hits.Any())
{
Logger.LogWarning("No more records available. remaining: {remaining}", remaining);
return null;
}
var hits = response.Hits.ToList();
var hitCount = hits.Count;
remaining -= hitCount;
if (remaining <= 0)
{
var targetIndex = hitCount + remaining;
if (targetIndex == hitCount)
{
lastSort = hits.LastOrDefault()?.Sort?.ToList();
}
else if (targetIndex >= 0 && targetIndex < hitCount)
{
lastSort = hits[targetIndex]?.Sort?.ToList();
}
else
{
return null;
}
return lastSort;
}
var lastHit = hits.LastOrDefault();
if (lastHit?.Sort == null || !lastHit.Sort.Any())
{
return null;
}
if (!secondResponse.IsSuccess() || secondResponse.Hits == null || !secondResponse.Hits.Any()) searchAfter = [.. lastHit.Sort];
if (hitCount < batch)
{
return null;
}
}
return lastSort;
}
private static SortOptions[]? ResolveDefaultSorts(IndexMappingInfo indexMappingInfo, string? sorting = null)
{
if (sorting.IsNullOrWhiteSpace())
{ {
return null; return null;
} }
var lastHit = secondResponse.Hits.LastOrDefault(); // eg: a desc, b.c asc; d+desc; e-asc; +f; -g
if (lastHit?.Sort == null || !lastHit.Sort.Any()) var sortFields = sorting.Split([';', ','], StringSplitOptions.RemoveEmptyEntries);
var sorts = new List<SortOptions>();
foreach (var sortField in sortFields)
{
var trimmedSortField = sortField.Trim();
if (trimmedSortField.IsNullOrWhiteSpace())
{
continue;
}
// [a, desc]
// [b.c, asc]
// [d, desc]
// [e, asc]
var parts = trimmedSortField.Split([' ', ':', '-', '+'], StringSplitOptions.RemoveEmptyEntries);
string fieldName;
SortOrder sortOrder;
if (parts.Length >= 2)
{
// b.c
fieldName = parts[0].Trim();
// desc
var orderStr = parts[1].Trim();
sortOrder = orderStr.Equals("desc", StringComparison.InvariantCultureIgnoreCase) ||
orderStr.Equals("descending", StringComparison.InvariantCultureIgnoreCase)
? SortOrder.Desc
: SortOrder.Asc;
}
else
{
fieldName = parts[0].Trim();
// +f
if (fieldName.StartsWith("+"))
{
sortOrder = SortOrder.Asc;
fieldName = fieldName.Substring(1);
}
// -g
else if (fieldName.StartsWith("-"))
{
sortOrder = SortOrder.Desc;
fieldName = fieldName.Substring(1);
}
else
{
sortOrder = SortOrder.Asc;
}
}
var resolvedField = ResolveSortField(indexMappingInfo, fieldName);
if (resolvedField != null)
{
var fieldPath = resolvedField.GetKeywordPath();
if (!sorts.Any(x => x.Field?.Field?.Name == fieldPath))
{
sorts.Add(new SortOptions
{
Field = new FieldSort(Field.FromString(fieldPath))
{
Order = sortOrder,
}
});
}
}
}
return sorts?.ToArray();
}
private static FieldMappingInfo? ResolveSortField(
IndexMappingInfo indexMappingInfo,
string fieldPath)
{
if (fieldPath.IsNullOrWhiteSpace())
{ {
return null; return null;
} }
return lastHit.Sort.ToList(); var directField = indexMappingInfo.GetField(fieldPath);
if (directField != null)
{
return directField;
}
var clrField = indexMappingInfo.GetFieldByClrPath(fieldPath);
if (clrField != null)
{
return clrField;
}
var caseInsensitiveEsField = indexMappingInfo.Fields
.FirstOrDefault(kvp => kvp.Key.Equals(fieldPath, StringComparison.InvariantCultureIgnoreCase))
.Value;
if (caseInsensitiveEsField != null)
{
return caseInsensitiveEsField;
}
var caseInsensitiveClrField = indexMappingInfo.ClrFields
.FirstOrDefault(kvp => kvp.Key.Equals(fieldPath, StringComparison.InvariantCultureIgnoreCase))
.Value;
return caseInsensitiveClrField;
} }
} }

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

@ -39,7 +39,7 @@ public partial class ExpressionQueryTranslator
IndexMappingInfo? mappingInfo) IndexMappingInfo? mappingInfo)
{ {
// 限定string方法调用 // 限定string方法调用
if (methodCall.Method.DeclaringType != typeof(string)) if (methodCall.Method.DeclaringType == typeof(string))
{ {
return methodCall.Method.Name switch return methodCall.Method.Name switch
{ {

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

@ -298,45 +298,34 @@ public partial class ExpressionQueryTranslator
string? prefix, string? prefix,
IndexMappingInfo? mappingInfo) IndexMappingInfo? mappingInfo)
{ {
var compareValue = Evaluate(constantExpression)?.ToString() ?? string.Empty; var (field, compareValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
var compareResult = Convert.ToInt32(Evaluate(constantExpression));
// CompareTo 返回值:
// 0: 相等
// > 0: 当前字符串在排序顺序中位于参数之后
// < 0: 当前字符串在排序顺序中位于参数之前
switch (comparisonType) switch (comparisonType)
{ {
case ExpressionType.Equal: case ExpressionType.Equal when compareResult == 0:
// CompareTo == 0 表示相等 // CompareTo == 0 表示相等
var (field, value) = GetStringMethodOperands(methodCall, prefix, mappingInfo); return BuildEquality(field, compareValue);
return BuildEquality(field, value);
case ExpressionType.NotEqual: case ExpressionType.NotEqual when compareResult == 0:
// CompareTo != 0 表示不相等 // CompareTo != 0 表示不相等
var (notEqualField, notEqualValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo); return BuildNotEqualQuery(field, compareValue);
var notEqualQuery = BuildEquality(notEqualField, notEqualValue);
return new BoolQuery
{
MustNot = new Query[] { notEqualQuery }
};
case ExpressionType.GreaterThan: case ExpressionType.GreaterThan when compareResult == 0:
// CompareTo > 0 表示当前字段值大于比较值 // CompareTo > 0 表示当前字段值大于比较值
// 这可以简化处理,但这里先返回 null 让默认处理逻辑处理 return BuildRange(field, greaterThan: compareValue);
return null;
case ExpressionType.GreaterThanOrEqual: case ExpressionType.GreaterThanOrEqual when compareResult == 0:
// CompareTo >= 0 表示当前字段值大于或等于比较值 // CompareTo >= 0 表示当前字段值大于或等于比较值
return null; return BuildRange(field, greaterThanOrEqualTo: compareValue);
case ExpressionType.LessThan: case ExpressionType.LessThan when compareResult == 0:
// CompareTo < 0 表示当前字段值小于比较值 // CompareTo < 0 表示当前字段值小于比较值
return null; return BuildRange(field, lessThan: compareValue);
case ExpressionType.LessThanOrEqual: case ExpressionType.LessThanOrEqual when compareResult == 0:
// CompareTo <= 0 表示当前字段值小于或等于比较值 // CompareTo <= 0 表示当前字段值小于或等于比较值
return null; return BuildRange(field, lessThanOrEqualTo: compareValue);
default: default:
return null; return null;
@ -353,46 +342,28 @@ public partial class ExpressionQueryTranslator
string? prefix, string? prefix,
IndexMappingInfo? mappingInfo) IndexMappingInfo? mappingInfo)
{ {
var indexValue = Evaluate(constantExpression)?.ToString() ?? string.Empty; // 获取字段和搜索值
var (field, searchValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
// IndexOf 返回值: var indexResult = Convert.ToInt32(Evaluate(constantExpression));
// >= 0: 找到了子字符串 var fieldMapping = mappingInfo?.GetField(field.Path);
// < 0: 没有找到子字符串
switch (comparisonType) switch (comparisonType)
{ {
case ExpressionType.GreaterThanOrEqual: case ExpressionType.GreaterThanOrEqual when indexResult >= 0:
case ExpressionType.GreaterThan: case ExpressionType.GreaterThan when indexResult > -1:
// IndexOf >= 0 表示包含 case ExpressionType.NotEqual when indexResult == -1:
var (field, value) = GetStringMethodOperands(methodCall, prefix, mappingInfo); // IndexOf >= 0, IndexOf > -1, IndexOf != -1 表示包含
var fieldMapping = mappingInfo?.GetField(field.Path); return TranslateStringContains(field, fieldMapping, searchValue);
return TranslateStringContains(field, fieldMapping, value);
case ExpressionType.LessThan when indexResult <= 0:
case ExpressionType.LessThan: case ExpressionType.Equal when indexResult == -1:
case ExpressionType.LessThanOrEqual: // IndexOf == -1, IndexOf < 0 表示不包含
// IndexOf < 0 表示不包含 var containsQuery = TranslateStringContains(field, fieldMapping, searchValue);
var (notContainsField, notContainsValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
var notContainsFieldMapping = mappingInfo?.GetField(notContainsField.Path);
var containsQuery = TranslateStringContains(notContainsField, notContainsFieldMapping, notContainsValue);
return new BoolQuery return new BoolQuery
{ {
MustNot = new Query[] { containsQuery } MustNot = new Query[] { containsQuery }
}; };
case ExpressionType.Equal:
// IndexOf == 某个值,这里简化处理,只处理 == -1(不包含)的情况
if (indexValue == "-1")
{
var (eqField, eqValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo);
var eqFieldMapping = mappingInfo?.GetField(eqField.Path);
var eqContainsQuery = TranslateStringContains(eqField, eqFieldMapping, eqValue);
return new BoolQuery
{
MustNot = new Query[] { eqContainsQuery }
};
}
return null;
default: default:
return null; return null;
} }

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

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
@ -34,7 +33,7 @@ public partial class ExpressionQueryTranslator : IExpressionQueryTranslator, ISi
Check.NotNullOrWhiteSpace(indexName, nameof(indexName)); Check.NotNullOrWhiteSpace(indexName, nameof(indexName));
Check.NotNull(expression, nameof(expression)); Check.NotNull(expression, nameof(expression));
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName); var indexMapping = await _indexMappingProvider.GetMappingAsync<TDocument>(indexName);
return TranslateNode(expression.Body, prefix: null, indexMapping); return TranslateNode(expression.Body, prefix: null, indexMapping);
} }

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

@ -9,7 +9,9 @@ public class FieldMappingInfo
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty; public string Type { get; set; } = string.Empty;
public Type? ClrType { get; set; } public Type? ClrType { get; set; }
public string ClrPath { get; set; } = string.Empty;
public bool IsMultiField { get; set; }
public bool IsKeyword { get; set; } public bool IsKeyword { get; set; }
public bool IsText { get; set; } public bool IsText { get; set; }
public bool IsWildcard { get; set; } public bool IsWildcard { get; set; }
@ -32,7 +34,10 @@ public class FieldMappingInfo
public string GetKeywordPath() public string GetKeywordPath()
{ {
if (IsKeyword) return Path; if (IsKeyword)
{
return Path;
}
// 如果是 text 类型且有 keyword 子字段 // 如果是 text 类型且有 keyword 子字段
if (IsText && Properties?.ContainsKey("keyword") == true) if (IsText && Properties?.ContainsKey("keyword") == true)

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

@ -8,4 +8,8 @@ public interface IIndexMappingProvider
Task<IndexMappingInfo> GetMappingAsync( Task<IndexMappingInfo> GetMappingAsync(
string indexPattern, string indexPattern,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<IndexMappingInfo> GetMappingAsync<TDocument>(
string indexPattern,
CancellationToken cancellationToken = default);
} }

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

@ -1,5 +1,7 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
namespace LINGYUN.Abp.Elasticsearch; namespace LINGYUN.Abp.Elasticsearch;
/// <summary> /// <summary>
@ -12,15 +14,25 @@ public class IndexMappingInfo
/// </summary> /// </summary>
public string IndexName { get; set; } = string.Empty; public string IndexName { get; set; } = string.Empty;
/// <summary>
/// 文档类型
/// </summary>
public Type? DocumentType { get; set; }
/// <summary> /// <summary>
/// 所有字段映射(扁平化) /// 所有字段映射(扁平化)
/// </summary> /// </summary>
public Dictionary<string, FieldMappingInfo> Fields { get; set; } = new(); public Dictionary<string, FieldMappingInfo> Fields { get; set; } = new Dictionary<string, FieldMappingInfo>(StringComparer.CurrentCultureIgnoreCase);
/// <summary>
/// 按 CLR 属性路径索引的字段映射
/// </summary>
public Dictionary<string, FieldMappingInfo> ClrFields { get; set; } = new Dictionary<string, FieldMappingInfo>(StringComparer.CurrentCultureIgnoreCase);
/// <summary> /// <summary>
/// Nested 字段映射 /// Nested 字段映射
/// </summary> /// </summary>
public Dictionary<string, NestedMappingInfo> NestedFields { get; set; } = new(); public Dictionary<string, NestedMappingInfo> NestedFields { get; set; } = new Dictionary<string, NestedMappingInfo>(StringComparer.CurrentCultureIgnoreCase);
/// <summary> /// <summary>
/// Keyword 字段列表 /// Keyword 字段列表
@ -64,6 +76,33 @@ public class IndexMappingInfo
return Fields.GetOrDefault(path); return Fields.GetOrDefault(path);
} }
/// <summary>
/// 根据 CLR 属性路径获取字段映射信息
/// </summary>
public FieldMappingInfo? GetFieldByClrPath(string clrPath)
{
return ClrFields.GetOrDefault(clrPath);
}
/// <summary>
/// 根据 CLR 属性表达式获取字段映射信息
/// </summary>
public FieldMappingInfo? GetFieldByExpression<TDocument>(Expression<Func<TDocument, object?>> expression)
{
var clrPath = GetPropertyPath(expression);
return GetFieldByClrPath(clrPath);
}
/// <summary>
/// 根据 CLR 属性表达式获取 ES 字段路径
/// </summary>
public string? GetElasticsearchFieldPath<TDocument>(Expression<Func<TDocument, object?>> expression)
{
var clrPath = GetPropertyPath(expression);
var field = GetFieldByClrPath(clrPath);
return field?.Path;
}
/// <summary> /// <summary>
/// 判断是否为 Nested 字段 /// 判断是否为 Nested 字段
/// </summary> /// </summary>
@ -136,4 +175,43 @@ public class IndexMappingInfo
return null; return null;
} }
/// <summary>
/// 将 CLR 属性路径转换为 ES 字段路径
/// </summary>
public string? ConvertClrPathToElasticsearchPath(string clrPath)
{
var field = GetFieldByClrPath(clrPath);
return field?.Path;
}
/// <summary>
/// 将 ES 字段路径转换为 CLR 属性路径
/// </summary>
public string? ConvertElasticsearchPathToClrPath(string esPath)
{
var field = GetField(esPath);
return field?.ClrPath;
}
private static string GetPropertyPath<TDocument>(Expression<Func<TDocument, object?>> expression)
{
var parts = new List<string>();
var currentExpression = expression.Body;
while (currentExpression is MemberExpression memberExpression)
{
parts.Insert(0, memberExpression.Member.Name);
currentExpression = memberExpression.Expression!;
}
// 处理转换表达式
if (currentExpression is UnaryExpression unaryExpression &&
unaryExpression.NodeType == ExpressionType.Convert &&
unaryExpression.Operand is MemberExpression convertMemberExpression)
{
parts.Insert(0, convertMemberExpression.Member.Name);
}
return string.Join(".", parts);
}
} }

29
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/IndexMappingInfoExtensions.cs

@ -0,0 +1,29 @@
using System;
using System.Linq.Expressions;
namespace LINGYUN.Abp.Elasticsearch;
public static class IndexMappingInfoExtensions
{
public static string? GetElasticsearchFieldPath<TDocument>(
this IndexMappingInfo mappingInfo,
Expression<Func<TDocument, object?>> expression)
{
return mappingInfo.GetElasticsearchFieldPath(expression);
}
public static FieldMappingInfo? GetFieldMapping<TDocument>(
this IndexMappingInfo mappingInfo,
Expression<Func<TDocument, object?>> expression)
{
return mappingInfo.GetFieldByExpression(expression);
}
public static string? GetKeywordPath<TDocument>(
this IndexMappingInfo mappingInfo,
Expression<Func<TDocument, object?>> expression)
{
var field = mappingInfo.GetFieldByExpression(expression);
return field?.GetKeywordPath();
}
}

540
aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/LINGYUN/Abp/AuditLogging/Serilog/Elasticsearch/SerilogElasticsearchLoggingManager.cs

@ -1,5 +1,5 @@
using Elastic.Clients.Elasticsearch; using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl; using Elastic.Transport.Diagnostics.Auditing;
using LINGYUN.Abp.Elasticsearch; using LINGYUN.Abp.Elasticsearch;
using LINGYUN.Linq.Dynamic.Queryable; using LINGYUN.Linq.Dynamic.Queryable;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -10,6 +10,7 @@ using Serilog.Formatting.Elasticsearch;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -37,7 +38,6 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
private readonly AbpLoggingSerilogElasticsearchOptions _options; private readonly AbpLoggingSerilogElasticsearchOptions _options;
private readonly IElasticsearchClientFactory _clientFactory; private readonly IElasticsearchClientFactory _clientFactory;
private readonly IIndexMappingProvider _indexMappingProvider;
private readonly IExpressionQueryService _expressionQueryService; private readonly IExpressionQueryService _expressionQueryService;
private readonly IObjectMapper<AbpLoggingSerilogElasticsearchModule> _objectMapper; private readonly IObjectMapper<AbpLoggingSerilogElasticsearchModule> _objectMapper;
@ -48,7 +48,6 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IOptions<AbpLoggingSerilogElasticsearchOptions> options, IOptions<AbpLoggingSerilogElasticsearchOptions> options,
IElasticsearchClientFactory clientFactory, IElasticsearchClientFactory clientFactory,
IIndexMappingProvider indexMappingProvider,
IExpressionQueryService expressionQueryService, IExpressionQueryService expressionQueryService,
IObjectMapper<AbpLoggingSerilogElasticsearchModule> objectMapper) IObjectMapper<AbpLoggingSerilogElasticsearchModule> objectMapper)
{ {
@ -56,7 +55,6 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
_objectMapper = objectMapper; _objectMapper = objectMapper;
_currentTenant = currentTenant; _currentTenant = currentTenant;
_clientFactory = clientFactory; _clientFactory = clientFactory;
_indexMappingProvider = indexMappingProvider;
_expressionQueryService = expressionQueryService; _expressionQueryService = expressionQueryService;
_options = options.Value; _options = options.Value;
@ -67,12 +65,11 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
ISpecification<LogInfo> specification, ISpecification<LogInfo> specification,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var indexName = CreateIndex();
var converter = new ExpressionQueryConverter<LogInfo, SerilogInfo>(_defaultTypeMap); var converter = new ExpressionQueryConverter<LogInfo, SerilogInfo>(_defaultTypeMap);
var expression = converter.Convert(specification.ToExpression()); var expression = converter.Convert(specification.ToExpression());
return await _expressionQueryService.GetCountAsync( return await _expressionQueryService.GetCountAsync(
indexName, CreateIndex(),
expression, expression,
cancellationToken); cancellationToken);
} }
@ -82,35 +79,28 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
string? sorting = null, string? sorting = null,
int maxResultCount = 50, int maxResultCount = 50,
int skipCount = 0, int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var indexName = CreateIndex();
var converter = new ExpressionQueryConverter<LogInfo, SerilogInfo>(_defaultTypeMap); var converter = new ExpressionQueryConverter<LogInfo, SerilogInfo>(_defaultTypeMap);
var expression = converter.Convert(specification.ToExpression()); var expression = converter.Convert(specification.ToExpression());
if (sorting.IsNullOrWhiteSpace())
var sortingField = sorting;
if (sortingField.IsNullOrWhiteSpace())
{ {
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); sorting = ElasticsearchJsonFormatter.TimestampPropertyName;
if (indexMapping != null)
{
var sortingFieldMap = indexMapping.Fields
.Where(x => x.Key.Equals(sortingField, StringComparison.CurrentCultureIgnoreCase))
.Select(x => x.Value)
.FirstOrDefault();
if (sortingFieldMap != null)
{
sortingField = sortingFieldMap.Path;
}
}
} }
var serilogLogs = await _expressionQueryService.GetListAsync( var serilogLogs = await _expressionQueryService.GetListAsync(
indexName, CreateIndex(),
expression, expression,
sortingField, sorting,
maxResultCount, maxResultCount,
skipCount, skipCount,
sourceExcludes: includeDetails == false
? Fields.FromFields(
[
new Field("exceptions"),
])
: null,
cancellationToken: cancellationToken); cancellationToken: cancellationToken);
return _objectMapper.Map<List<SerilogInfo>, List<LogInfo>>(serilogLogs); return _objectMapper.Map<List<SerilogInfo>, List<LogInfo>>(serilogLogs);
@ -126,83 +116,18 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
string id, string id,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var indexName = CreateIndex();
var client = _clientFactory.Create(); var client = _clientFactory.Create();
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); Expression<Func<SerilogInfo, bool>> expression = x => x.Fields.UniqueId == long.Parse(id);
expression = expression.AndIf(_currentTenant.IsAvailable, x => x.Fields.TenantId == _currentTenant.Id);
SearchResponse<SerilogInfo> response;
var serilogs = await _expressionQueryService.GetListAsync<SerilogInfo>(
if (_currentTenant.IsAvailable) CreateIndex(),
{ x => x.Fields.UniqueId == long.Parse(id),
/* sorting: $"{ElasticsearchJsonFormatter.TimestampPropertyName} DESC",
"query": { maxResultCount: 1,
"bool": { cancellationToken: cancellationToken);
"must": [
{
"term": {
"fields.TenantId.keyword": {
"value": _currentTenant.GetId()
}
}
},
{
"term": {
"fields.UniqueId": {
"value": "1474021081433481216"
}
}
}
]
}
}
*/
response = await client.SearchAsync<SerilogInfo>(
dsl =>
dsl.Indices(CreateIndex())
.Query(
(q) => q.Bool(
(b) => b.Must(
(s) => s.Term(
(t) => t.Field(GetField(indexMapping, "fields.UniqueId")).Value(id)),
(s) => s.Term(
(t) => t.Field(GetField(indexMapping, "fields.TenantId")).Value(_currentTenant.GetId().ToString())))))
.Size(1),
cancellationToken);
}
else
{
/*
"query": {
"bool": {
"must": [
{
"term": {
"fields.UniqueId": {
"value": "1474021081433481216"
}
}
}
]
}
}
*/
response = await client.SearchAsync<SerilogInfo>(
dsl =>
dsl.Indices(CreateIndex())
.Query(
(q) => q.Bool(
(b) => b.Must(
(s) => s.Term(
(t) => t.Field(GetField(indexMapping, "fields.UniqueId")).Value(id)))))
.Size(1),
cancellationToken);
if (response.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query logs failed: {errorMessage}", errorMessage);
}
}
return _objectMapper.Map<SerilogInfo?, LogInfo?>(response.Documents.FirstOrDefault()); return _objectMapper.Map<SerilogInfo?, LogInfo?>(serilogs.FirstOrDefault());
} }
public async virtual Task<long> GetCountAsync( public async virtual Task<long> GetCountAsync(
@ -221,36 +146,30 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
bool? hasException = null, bool? hasException = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var indexName = CreateIndex();
var client = _clientFactory.Create(); var client = _clientFactory.Create();
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken);
var querys = BuildQueryDescriptor( Expression<Func<SerilogInfo, bool>> expression = _ => true;
indexMapping,
startTime, expression = expression
endTime, .AndIf(startTime.HasValue, x => x.TimeStamp >= _clock.Normalize(startTime!.Value))
level, .AndIf(endTime.HasValue, x => x.TimeStamp <= _clock.Normalize(endTime!.Value))
machineName, .AndIf(level.HasValue, x => x.Level == GetLogEventLevel(level!.Value))
environment, .AndIf(!machineName.IsNullOrWhiteSpace(), x => x.Fields.MachineName!.Contains(machineName!))
application, .AndIf(!environment.IsNullOrWhiteSpace(), x => x.Fields.Environment!.Contains(environment!))
context, .AndIf(!application.IsNullOrWhiteSpace(), x => x.Fields.Application!.Contains(application!))
requestId, .AndIf(!context.IsNullOrWhiteSpace(), x => x.Fields.Context == context)
requestPath, .AndIf(!requestId.IsNullOrWhiteSpace(), x => x.Fields.RequestId == requestId)
correlationId, .AndIf(!requestPath.IsNullOrWhiteSpace(), x => x.Fields.RequestPath!.StartsWith(requestPath!))
processId, .AndIf(!correlationId.IsNullOrWhiteSpace(), x => x.Fields.CorrelationId!.Contains(correlationId!))
threadId, .AndIf(processId.HasValue, x => x.Fields.ProcessId == processId)
hasException); .AndIf(threadId.HasValue, x => x.Fields.ThreadId == threadId)
.AndIf(hasException == true, x => x.Exceptions != null)
.AndIf(hasException == false, x => x.Exceptions == null);
var response = await client.CountAsync<SerilogInfo>((dsl) => return await _expressionQueryService.GetCountAsync(
dsl.Indices(indexName) CreateIndex(),
.Query(log => log.Bool(b => b.Must(querys.ToArray()))), expression,
cancellationToken); cancellationToken);
if (response.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query log count failed: {errorMessage}", errorMessage);
}
return response.Count;
} }
/// <summary> /// <summary>
@ -295,308 +214,52 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
bool includeDetails = false, bool includeDetails = false,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
var indexName = CreateIndex();
var client = _clientFactory.Create(); var client = _clientFactory.Create();
var indexMapping = await _indexMappingProvider.GetMappingAsync(indexName, cancellationToken); if (sorting.IsNullOrWhiteSpace())
var sorts = GetOrDefaultSort(indexMapping, sorting);
var querys = BuildQueryDescriptor(
indexMapping,
startTime,
endTime,
level,
machineName,
environment,
application,
context,
requestId,
requestPath,
correlationId,
processId,
threadId,
hasException);
var query = new BoolQuery { Must = querys };
var serilogLogs = skipCount >= 10000 && sorts != null
? await SearchAfterSerilogLogs(client, query, sorts, maxResultCount, skipCount, cancellationToken)
: await SearchFromSizeSerilogLogs(client, query, sorts, maxResultCount, skipCount, cancellationToken);
return _objectMapper.Map<List<SerilogInfo>, List<LogInfo>>(serilogLogs);
}
protected virtual List<Query> BuildQueryDescriptor(
IndexMappingInfo indexMappingInfo,
DateTime? startTime = null,
DateTime? endTime = null,
LogLevel? level = null,
string? machineName = null,
string? environment = null,
string? application = null,
string? context = null,
string? requestId = null,
string? requestPath = null,
string? correlationId = null,
int? processId = null,
int? threadId = null,
bool? hasException = null)
{
var queries = new List<Query>();
if (_currentTenant.IsAvailable)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.TenantId"), _currentTenant.GetId().ToString()));
}
if (startTime.HasValue)
{
queries.Add(new DateRangeQuery(GetField(indexMappingInfo, "@timestamp"))
{
Gte = _clock.Normalize(startTime.Value),
});
}
if (endTime.HasValue)
{
queries.Add(new DateRangeQuery(GetField(indexMappingInfo, "@timestamp"))
{
Lte = _clock.Normalize(endTime.Value),
});
}
if (level.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "level"), GetLogEventLevel(level.Value).ToString()));
}
if (!machineName.IsNullOrWhiteSpace())
{
// 模糊匹配
queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.MachineName"))
{
Value = $"*{machineName}*"
});
}
if (!environment.IsNullOrWhiteSpace())
{
// 模糊匹配
queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.EnvironmentName"))
{
Value = $"*{environment}*"
});
}
if (!application.IsNullOrWhiteSpace())
{
// 模糊匹配
queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.ApplicationName"))
{
Value = $"*{application}*"
});
}
if (!context.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.SourceContext"), context));
}
if (!requestId.IsNullOrWhiteSpace())
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.RequestId"), requestId));
}
if (!requestPath.IsNullOrWhiteSpace())
{
// 前缀匹配
queries.Add(new MatchPhrasePrefixQuery(GetField(indexMappingInfo, "fields.RequestPath"), requestPath));
}
if (!correlationId.IsNullOrWhiteSpace())
{
// 模糊匹配
queries.Add(new WildcardQuery(GetField(indexMappingInfo, "fields.CorrelationId"))
{
Value = $"*{correlationId}*"
});
}
if (processId.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.ProcessId"), FieldValue.FromValue(processId.Value)));
}
if (threadId.HasValue)
{
queries.Add(new TermQuery(GetField(indexMappingInfo, "fields.ThreadId"), FieldValue.FromValue(threadId.Value)));
}
if (hasException.HasValue)
{
if (hasException.Value)
{
/* exceptions字段则就是有异常信息
* "exists": {
"field": "exceptions"
}
*/
queries.Add(new ExistsQuery(GetField(indexMappingInfo, "fields.Exceptions")));
}
else
{
// 不存在 exceptions字段就是没有异常信息的消息
/*
* "bool": {
"must_not": [
{
"exists": {
"field": "exceptions"
}
}
]
}
*/
queries.Add(new BoolQuery
{
MustNot = new List<Query>
{
new ExistsQuery(GetField(indexMappingInfo, "fields.Exceptions"))
}
});
}
}
return queries;
}
private async Task<List<SerilogInfo>> SearchFromSizeSerilogLogs(
ElasticsearchClient client,
Query query,
SortOptions[]? sorts = null,
int maxResultCount = 50,
int skipCount = 0,
CancellationToken cancellationToken = default)
{
var searchResponse = await client.SearchAsync<SerilogInfo>(dsl =>
{
dsl.Indices(CreateIndex())
.Query(query)
.From(skipCount)
.Size(maxResultCount);
if (sorts != null)
{
dsl.Sort(sorts);
}
}, cancellationToken);
if (searchResponse.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query log failed: {errorMessage}", errorMessage);
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<SerilogInfo>> SearchAfterSerilogLogs(
ElasticsearchClient client,
Query query,
SortOptions[] sorts,
int maxResultCount = 50,
int skipCount = 0,
CancellationToken cancellationToken = default)
{
var searchAfter = await GetSearchAfterValue(
client,
query,
sorts,
skipCount,
cancellationToken);
if (searchAfter == null || !searchAfter.Any())
{
return [];
}
var searchResponse = await client.SearchAsync<SerilogInfo>(dsl =>
{
dsl.Indices(CreateIndex())
.Query(query)
.Sort(sorts)
.Size(maxResultCount)
.SearchAfter(searchAfter);
}, cancellationToken);
if (searchResponse.TryGetErrorMessage(out var errorMessage))
{
Logger.LogWarning("Query log failed: {errorMessage}", errorMessage);
return [];
}
return searchResponse.Documents.ToList();
}
private async Task<List<FieldValue>?> GetSearchAfterValue(
ElasticsearchClient client,
Query query,
SortOptions[] sorts,
int skipCount,
CancellationToken cancellationToken = default)
{
// 10000以内直接取最后一条数据
if (skipCount < 10000)
{
var response = await client.SearchAsync<SerilogInfo>(
dsl => dsl.Indices(CreateIndex())
.Query(query)
.Sort(sorts)
.SourceIncludes(x => x.Level)
.From(skipCount)
.Size(1),
cancellationToken);
if (!response.IsSuccess() || response.Hits == null || !response.Hits.Any())
{
return null;
}
var hit = response.Hits.FirstOrDefault();
return hit?.Sort?.ToList();
}
// 获取第9999条数据Hits作为searchAfter
var firstResponse = await client.SearchAsync<SerilogInfo>(
dsl => dsl.Indices(CreateIndex())
.Query(query)
.Sort(sorts)
.SourceIncludes(x => x.Level)
.From(9999)
.Size(1),
cancellationToken);
if (!firstResponse.IsSuccess() || firstResponse.Hits == null || !firstResponse.Hits.Any())
{ {
return null; sorting = $"{ElasticsearchJsonFormatter.TimestampPropertyName} DESC";
} }
// 额外处理一下timestamp字段
var firstHit = firstResponse.Hits.FirstOrDefault(); else if (sorting.Contains("timestamp", StringComparison.CurrentCultureIgnoreCase))
if (firstHit?.Sort == null || !firstHit.Sort.Any())
{ {
return null; sorting = sorting
.Replace("timestamp", ElasticsearchJsonFormatter.TimestampPropertyName, StringComparison.CurrentCultureIgnoreCase)
.Replace("@@", "@");
} }
// 获取skipCount最近一条数据作为searchAfter Expression<Func<SerilogInfo, bool>> expression = _ => true;
var secondResponse = await client.SearchAsync<SerilogInfo>(
dsl => dsl.Indices(CreateIndex()) expression = expression
.Query(query) .AndIf(startTime.HasValue, x => x.TimeStamp >= _clock.Normalize(startTime!.Value))
// 反转排序取第一个数据作为起始索引 .AndIf(endTime.HasValue, x => x.TimeStamp <= _clock.Normalize(endTime!.Value))
.Sort(sorts.ReverseSort()!.ToArray()) .AndIf(level.HasValue, x => x.Level == GetLogEventLevel(level!.Value))
.SourceIncludes(x => x.Level) .AndIf(!machineName.IsNullOrWhiteSpace(), x => x.Fields.MachineName!.Contains(machineName!))
.SearchAfter(firstHit.Sort.ToList()) .AndIf(!environment.IsNullOrWhiteSpace(), x => x.Fields.Environment!.Contains(environment!))
.Size(1), .AndIf(!application.IsNullOrWhiteSpace(), x => x.Fields.Application!.Contains(application!))
cancellationToken); .AndIf(!context.IsNullOrWhiteSpace(), x => x.Fields.Context == context)
.AndIf(!requestId.IsNullOrWhiteSpace(), x => x.Fields.RequestId == requestId)
if (!secondResponse.IsSuccess() || secondResponse.Hits == null || !secondResponse.Hits.Any()) .AndIf(!requestPath.IsNullOrWhiteSpace(), x => x.Fields.RequestPath!.StartsWith(requestPath!))
{ .AndIf(!correlationId.IsNullOrWhiteSpace(), x => x.Fields.CorrelationId!.Contains(correlationId!))
return null; .AndIf(processId.HasValue, x => x.Fields.ProcessId == processId)
} .AndIf(threadId.HasValue, x => x.Fields.ThreadId == threadId)
.AndIf(hasException == true, x => x.Exceptions != null)
.AndIf(hasException == false, x => x.Exceptions == null);
var lastHit = secondResponse.Hits.LastOrDefault(); var serilogLogs = await _expressionQueryService.GetListAsync(
if (lastHit?.Sort == null || !lastHit.Sort.Any()) CreateIndex(),
{ expression,
return null; sorting: sorting,
} maxResultCount: maxResultCount,
skipCount: skipCount,
sourceExcludes: includeDetails == false
? Fields.FromFields(
[
new Field("exceptions"),
])
: null,
cancellationToken: cancellationToken);
return lastHit.Sort.ToList(); return _objectMapper.Map<List<SerilogInfo>, List<LogInfo>>(serilogLogs);
} }
protected virtual string CreateIndex(DateTimeOffset? offset = null) protected virtual string CreateIndex(DateTimeOffset? offset = null)
@ -620,43 +283,4 @@ public class SerilogElasticsearchLoggingManager : ILoggingManager, ISingletonDep
_ => LogEventLevel.Verbose, _ => LogEventLevel.Verbose,
}; };
} }
private static SortOptions[]? GetOrDefaultSort(IndexMappingInfo indexMappingInfo, string? sorting = null)
{
var sortOrder = !sorting.IsNullOrWhiteSpace() && sorting.EndsWith("asc", StringComparison.InvariantCultureIgnoreCase)
? SortOrder.Asc : SortOrder.Desc;
sorting = !sorting.IsNullOrWhiteSpace()
? sorting.Split()[0]
: ElasticsearchJsonFormatter.TimestampPropertyName;
SortOptions[]? sorts = null;
var sortingFieldMap = indexMappingInfo.Fields
.Where(x => x.Key.Equals(sorting, StringComparison.CurrentCultureIgnoreCase))
.Select(x => x.Value)
.FirstOrDefault();
if (sortingFieldMap != null)
{
sorting = sortingFieldMap.Path;
}
if (!sorting.IsNullOrWhiteSpace())
{
sorts = new SortOptions[1]
{
new SortOptions
{
Field = new FieldSort(new Field(sorting))
{
Order = sortOrder,
},
}
};
}
return sorts;
}
private static string GetField(IndexMappingInfo indexMappingInfo, string fieldFullPath)
{
return indexMappingInfo.GetExactFieldPath(fieldFullPath);
}
} }

32
aspnet-core/framework/logging/LINGYUN.Abp.Logging.Serilog.Elasticsearch/System/Linq/Expressions/ExpressionFuncExtensions.cs

@ -0,0 +1,32 @@
using Volo.Abp.Specifications;
namespace System.Linq.Expressions;
internal static class ExpressionFuncExtensions
{
public static Expression<Func<T, bool>> AndIf<T>(
this Expression<Func<T, bool>> first,
bool condition,
Expression<Func<T, bool>> second)
{
if (condition)
{
return ExpressionFuncExtender.And(first, second);
}
return first;
}
public static Expression<Func<T, bool>> OrIf<T>(
this Expression<Func<T, bool>> first,
bool condition,
Expression<Func<T, bool>> second)
{
if (condition)
{
return ExpressionFuncExtender.Or(first, second);
}
return first;
}
}

1
aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/DefaultLoggingManager.cs

@ -83,6 +83,7 @@ public class DefaultLoggingManager : ILoggingManager, ISingletonDependency
string? sorting = null, string? sorting = null,
int maxResultCount = 50, int maxResultCount = 50,
int skipCount = 0, int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
Logger.LogDebug("No logging manager is available!"); Logger.LogDebug("No logging manager is available!");

1
aspnet-core/framework/logging/LINGYUN.Abp.Logging/LINGYUN/Abp/AuditLogging/ILoggingManager.cs

@ -58,5 +58,6 @@ public interface ILoggingManager
string? sorting = null, string? sorting = null,
int maxResultCount = 50, int maxResultCount = 50,
int skipCount = 0, int skipCount = 0,
bool includeDetails = false,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
} }

Loading…
Cancel
Save