38 changed files with 840 additions and 253 deletions
@ -0,0 +1,14 @@ |
|||
// Global using directives
|
|||
|
|||
global using System.Collections.Concurrent; |
|||
global using System.Runtime.Serialization; |
|||
global using Elasticsearch.Net; |
|||
global using Microsoft.Extensions.Logging; |
|||
global using Microsoft.Extensions.Options; |
|||
global using Nest; |
|||
global using Volo.Abp.Autofac; |
|||
global using Volo.Abp.DependencyInjection; |
|||
global using Volo.Abp.Modularity; |
|||
global using Microsoft.Extensions.DependencyInjection; |
|||
global using Volo.Abp; |
|||
global using LogLevel = Microsoft.Extensions.Logging.LogLevel; |
|||
@ -0,0 +1,13 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<ImplicitUsings>enable</ImplicitUsings> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="NEST" /> |
|||
<PackageReference Include="Volo.Abp.Autofac" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,10 @@ |
|||
namespace Lion.AbpPro.ElasticSearch; |
|||
|
|||
[DependsOn(typeof(AbpAutofacModule))] |
|||
public class AbpProElasticSearchModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.Configure<AbpProElasticSearchOptions>(context.Services.GetConfiguration().GetSection("ElasticSearch")); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
namespace Lion.AbpPro.ElasticSearch; |
|||
|
|||
public class AbpProElasticSearchOptions |
|||
{ |
|||
/// <summary>
|
|||
/// es地址
|
|||
/// </summary>
|
|||
public string Host { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 用户名
|
|||
/// </summary>
|
|||
public string UserName { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 密码
|
|||
/// </summary>
|
|||
public string Password { get; set; } |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
using Lion.AbpPro.ElasticSearch.Exceptions; |
|||
|
|||
namespace Lion.AbpPro.ElasticSearch; |
|||
|
|||
public abstract class ElasticSearchRepository<TEntity> : IBasicElasticSearchRepository<TEntity> |
|||
where TEntity : class, IElasticSearchEntity |
|||
{ |
|||
protected abstract string IndexName { get; } |
|||
|
|||
private readonly IElasticsearchProvider _elasticsearchProvider; |
|||
|
|||
protected ElasticSearchRepository(IElasticsearchProvider elasticsearchProvider) |
|||
{ |
|||
_elasticsearchProvider = elasticsearchProvider; |
|||
} |
|||
|
|||
|
|||
protected IElasticClient Client => _elasticsearchProvider.GetClient(); |
|||
|
|||
/// <summary>
|
|||
/// 根据id查询实体
|
|||
/// </summary>
|
|||
public virtual async Task<TEntity> FindAsync(Guid id, CancellationToken cancellationToken = default) |
|||
{ |
|||
var result = await Client.GetAsync<TEntity>(id, e => e.Index(IndexName), cancellationToken); |
|||
if (!result.IsValid) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
return result.Source; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 根据id查询实体
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchEntityNotFoundException"></exception>
|
|||
/// <returns>如果没有查询到,会抛异常</returns>
|
|||
public virtual async Task<TEntity> GetAsync(Guid id, CancellationToken cancellationToken = default) |
|||
{ |
|||
var result = await Client.GetAsync<TEntity>(id, e => e.Index(IndexName), cancellationToken); |
|||
if (!result.IsValid) |
|||
{ |
|||
throw new AbpProElasticSearchEntityNotFoundException(innerException: result.OriginalException); |
|||
} |
|||
|
|||
return result.Source; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 新增数据时,如果文档的唯一id在索引里已存在,那么会更新掉原数据
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchException"></exception>
|
|||
public virtual async Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default) |
|||
{ |
|||
var result = await Client.IndexAsync(entity, x => x.Index(IndexName), cancellationToken); |
|||
if (!result.IsValid) |
|||
{ |
|||
throw new AbpProElasticSearchException(innerException: result.OriginalException); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 批量新增数据时,如果文档的唯一id在索引里已存在,那么会更新掉原数据
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchException"></exception>
|
|||
public async Task InsertManyAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default) |
|||
{ |
|||
// var result = await Client.BulkAsync(b => b.Index(IndexName).IndexMany(entities), cancellationToken);
|
|||
var result = await Client.IndexManyAsync(entities, IndexName, cancellationToken); |
|||
if (!result.IsValid) |
|||
{ |
|||
throw new AbpProElasticSearchException(innerException: result.OriginalException); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 根据Id删除实体
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchException"></exception>
|
|||
public virtual async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) |
|||
{ |
|||
var result = await Client.DeleteAsync<TEntity>(id, x => x.Index(IndexName), cancellationToken); |
|||
if (!result.IsValid) |
|||
{ |
|||
throw new AbpProElasticSearchException(innerException: result.OriginalException); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 根据Id更新实体
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchException"></exception>
|
|||
public virtual async Task UpdateAsync(TEntity TEntity) |
|||
{ |
|||
var result = await Client.UpdateAsync<TEntity>(TEntity.Id, x => x.Index(IndexName).Doc(TEntity)); |
|||
if (!result.IsValid) |
|||
{ |
|||
throw new UserFriendlyException(result.OriginalException.Message); |
|||
} |
|||
} |
|||
|
|||
|
|||
public async Task<Tuple<long, IList<TEntity>>> PageAsync(List<Func<QueryContainerDescriptor<TEntity>, QueryContainer>> predicates, int pageIndex = 1, int pageSize = 10) |
|||
{ |
|||
predicates ??= new List<Func<QueryContainerDescriptor<TEntity>, QueryContainer>>(); |
|||
var query = await Client.SearchAsync<TEntity>(x => x.Index(IndexName) |
|||
.Query(q => q.Bool(qb => qb.Filter(predicates))) |
|||
.From((pageIndex - 1) * pageSize) |
|||
.Size(pageSize) |
|||
.Sort(s => s.Descending(v => v.CreationTime))); |
|||
return new Tuple<long, IList<TEntity>>(query.Total, query.Documents.ToList()); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
namespace Lion.AbpPro.ElasticSearch; |
|||
|
|||
public class ElasticsearchProvider : IElasticsearchProvider, ISingletonDependency |
|||
{ |
|||
private readonly AbpProElasticSearchOptions _options; |
|||
|
|||
public ElasticsearchProvider(IOptions<AbpProElasticSearchOptions> options) |
|||
{ |
|||
_options = options.Value; |
|||
} |
|||
|
|||
public virtual IElasticClient GetClient() |
|||
{ |
|||
var connectionPool = new SingleNodeConnectionPool(new Uri(_options.Host)); |
|||
var settings = new ConnectionSettings(connectionPool); |
|||
settings.BasicAuthentication(_options.UserName, _options.Password); |
|||
return new ElasticClient(settings); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
namespace Lion.AbpPro.ElasticSearch.Exceptions; |
|||
|
|||
public class AbpProElasticSearchEntityNotFoundException : BusinessException |
|||
{ |
|||
public AbpProElasticSearchEntityNotFoundException( |
|||
string code = null, |
|||
string message = null, |
|||
string details = null, |
|||
Exception innerException = null, |
|||
LogLevel logLevel = LogLevel.Error) |
|||
: base( |
|||
code, |
|||
message, |
|||
details, |
|||
innerException, |
|||
logLevel |
|||
) |
|||
{ |
|||
} |
|||
|
|||
public AbpProElasticSearchEntityNotFoundException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
namespace Lion.AbpPro.ElasticSearch.Exceptions; |
|||
|
|||
public class AbpProElasticSearchException : BusinessException |
|||
{ |
|||
public AbpProElasticSearchException( |
|||
string code = null, |
|||
string message = null, |
|||
string details = null, |
|||
Exception innerException = null, |
|||
LogLevel logLevel = LogLevel.Error) |
|||
: base( |
|||
code, |
|||
message, |
|||
details, |
|||
innerException, |
|||
logLevel |
|||
) |
|||
{ |
|||
} |
|||
|
|||
public AbpProElasticSearchException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
using Lion.AbpPro.ElasticSearch.Exceptions; |
|||
|
|||
namespace Lion.AbpPro.ElasticSearch; |
|||
|
|||
public interface IBasicElasticSearchRepository<TEntity> where TEntity : class, IElasticSearchEntity |
|||
{ |
|||
/// <summary>
|
|||
/// 根据id查询实体
|
|||
/// </summary>
|
|||
Task<TEntity> FindAsync(Guid id, CancellationToken cancellationToken = default); |
|||
|
|||
/// <summary>
|
|||
/// 根据id查询实体
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchEntityNotFoundException"></exception>
|
|||
/// <returns>如果没有查询到,会抛异常</returns>
|
|||
Task<TEntity> GetAsync(Guid id, CancellationToken cancellationToken = default); |
|||
|
|||
|
|||
/// <summary>
|
|||
/// 新增数据时,如果文档的唯一id在索引里已存在,那么会更新掉原数据
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchException"></exception>
|
|||
Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default); |
|||
|
|||
/// <summary>
|
|||
/// 批量新增数据时,如果文档的唯一id在索引里已存在,那么会更新掉原数据
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchException"></exception>
|
|||
Task InsertManyAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default); |
|||
|
|||
/// <summary>
|
|||
/// 根据Id删除实体
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchException"></exception>
|
|||
Task UpdateAsync(TEntity TEntity); |
|||
|
|||
/// <summary>
|
|||
/// 根据Id更新实体
|
|||
/// </summary>
|
|||
/// <exception cref="AbpProElasticSearchException"></exception>
|
|||
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); |
|||
|
|||
Task<Tuple<long, IList<TEntity>>> PageAsync(List<Func<QueryContainerDescriptor<TEntity>, QueryContainer>> predicates, int pageIndex = 1, int pageSize = 10); |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
namespace Lion.AbpPro.ElasticSearch; |
|||
|
|||
|
|||
public interface IElasticSearchEntity |
|||
{ |
|||
/// <summary>
|
|||
/// 主键Id
|
|||
/// </summary>
|
|||
Guid Id { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 创建时间
|
|||
/// </summary>
|
|||
DateTime CreationTime { get; set; } |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace Lion.AbpPro.ElasticSearch; |
|||
|
|||
public interface IElasticsearchProvider |
|||
{ |
|||
IElasticClient GetClient(); |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<ImplicitUsings>enable</ImplicitUsings> |
|||
<IsPackable>false</IsPackable> |
|||
<IsTestProject>true</IsTestProject> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" /> |
|||
<PackageReference Include="NSubstitute" /> |
|||
<PackageReference Include="Shouldly" /> |
|||
<PackageReference Include="xunit" /> |
|||
<PackageReference Include="xunit.extensibility.execution" /> |
|||
<PackageReference Include="xunit.runner.visualstudio" /> |
|||
<PackageReference Include="coverlet.collector" /> |
|||
<PackageReference Include="JunitXml.TestLogger" /> |
|||
<PackageReference Include="Volo.Abp.TestBase" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Lion.AbpPro.ElasticSearch\Lion.AbpPro.ElasticSearch.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Content Include="appsettings.json"> |
|||
<CopyToOutputDirectory>Always</CopyToOutputDirectory> |
|||
</Content> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,13 @@ |
|||
using Volo.Abp.Testing; |
|||
|
|||
namespace Lion.AbpPro.ElasticSearch |
|||
{ |
|||
|
|||
public abstract class AbpProElasticSearchTestBase : AbpIntegratedTest<AbpProElasticSearchTestBaseModule> |
|||
{ |
|||
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
|||
{ |
|||
options.UseAutofac(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Lion.AbpPro.ElasticSearch |
|||
{ |
|||
|
|||
[DependsOn(typeof(AbpProElasticSearchModule))] |
|||
[DependsOn(typeof(AbpTestBaseModule))] |
|||
public class AbpProElasticSearchTestBaseModule : AbpModule |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,412 @@ |
|||
using Lion.AbpPro.ElasticSearch.Exceptions; |
|||
using Lion.AbpPro.ElasticSearch.Students; |
|||
using Nest; |
|||
using Shouldly; |
|||
|
|||
namespace Lion.AbpPro.ElasticSearch |
|||
{ |
|||
public sealed class StudentElasticSearchRepositoryTests : AbpProElasticSearchTestBase |
|||
{ |
|||
private readonly IStudentElasticSearchRepository _studentElasticSearchRepository; |
|||
|
|||
|
|||
public StudentElasticSearchRepositoryTests() |
|||
{ |
|||
_studentElasticSearchRepository = GetRequiredService<IStudentElasticSearchRepository>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task FindAsync_Should_Find_Student() |
|||
{ |
|||
// Arrange
|
|||
var student = new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "John Doe", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.3, |
|||
Gender = Gender.Man |
|||
}; |
|||
await _studentElasticSearchRepository.InsertAsync(student); |
|||
|
|||
// Act
|
|||
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
|||
|
|||
// Assert
|
|||
result.ShouldNotBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task GetAsync_Should_Exception() |
|||
{ |
|||
await Should.ThrowAsync<AbpProElasticSearchEntityNotFoundException>(async () => { await _studentElasticSearchRepository.GetAsync(Guid.NewGuid()); }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task InsertAsync_Should_Insert_Student() |
|||
{ |
|||
// Arrange
|
|||
var student = new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "John Doe", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.3, |
|||
Gender = Gender.Man |
|||
}; |
|||
|
|||
// Act
|
|||
await _studentElasticSearchRepository.InsertAsync(student); |
|||
|
|||
// Assert
|
|||
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
|||
result.ShouldNotBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task InsertAsync_Should_RepeatInsert_Student() |
|||
{ |
|||
// Arrange
|
|||
var student = new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "John Doe", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.3, |
|||
Gender = Gender.Man |
|||
}; |
|||
|
|||
// Act
|
|||
await _studentElasticSearchRepository.InsertAsync(student); |
|||
|
|||
// Assert
|
|||
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
|||
result.ShouldNotBeNull(); |
|||
|
|||
// Act
|
|||
student.Name = "abp"; |
|||
student.Age = 20; |
|||
student.Gender = Gender.WoMan; |
|||
await _studentElasticSearchRepository.InsertAsync(student); |
|||
|
|||
// Assert
|
|||
var result1 = await _studentElasticSearchRepository.FindAsync(student.Id); |
|||
result1.ShouldNotBeNull(); |
|||
result1.Name.ShouldBe(student.Name); |
|||
result1.Age.ShouldBe(student.Age); |
|||
result1.Gender.ShouldBe(student.Gender); |
|||
result1.Price.ShouldBe(student.Price); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task InsertManyAsync_Should_Insert_Student() |
|||
{ |
|||
// Arrange
|
|||
var students = new List<Student> |
|||
{ |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "John Doe", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.3, |
|||
Gender = Gender.Man |
|||
}, |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "John Wang", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.3, |
|||
Gender = Gender.WoMan |
|||
} |
|||
}; |
|||
// Act
|
|||
await _studentElasticSearchRepository.InsertManyAsync(students); |
|||
|
|||
// Assert
|
|||
foreach (var student in students) |
|||
{ |
|||
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
|||
result.ShouldNotBeNull(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task DeleteAsync_Should_Delete_Student() |
|||
{ |
|||
// Arrange
|
|||
var student = new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "John Doe", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.3, |
|||
Gender = Gender.Man |
|||
}; |
|||
await _studentElasticSearchRepository.InsertAsync(student); |
|||
|
|||
// Act
|
|||
await _studentElasticSearchRepository.DeleteAsync(student.Id); |
|||
|
|||
// Assert
|
|||
var result = await _studentElasticSearchRepository.FindAsync(student.Id); |
|||
result.ShouldBeNull(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task UpdateAsync_Should_Update_Student() |
|||
{ |
|||
// Arrange
|
|||
var student = new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "John Doe", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.3, |
|||
Gender = Gender.Man |
|||
}; |
|||
await _studentElasticSearchRepository.InsertAsync(student); |
|||
|
|||
// Act
|
|||
student.Name = "update"; |
|||
student.Age = 20; |
|||
await _studentElasticSearchRepository.UpdateAsync(student); |
|||
|
|||
// Assert
|
|||
var exiStudent = await _studentElasticSearchRepository.FindAsync(student.Id); |
|||
exiStudent.ShouldNotBeNull(); |
|||
exiStudent.Name.ShouldBe(student.Name); |
|||
exiStudent.Age.ShouldBe(student.Age); |
|||
} |
|||
|
|||
[Fact(DisplayName = "DataRange时间范围查询")] |
|||
public async Task PageAsync_Should_Return_Students() |
|||
{ |
|||
// Arrange
|
|||
var students = new List<Student> |
|||
{ |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "韩立", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.1, |
|||
Gender = Gender.Man |
|||
}, |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "南宫婉", |
|||
Age = 18, |
|||
CreationTime = DateTime.Now.AddDays(-1), |
|||
Price = 100.2, |
|||
Gender = Gender.WoMan |
|||
}, |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "Test001", |
|||
Age = 19, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100, |
|||
Gender = Gender.WoMan |
|||
}, |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "Test", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now.AddDays(-10), |
|||
Price = 100, |
|||
Gender = Gender.WoMan |
|||
} |
|||
}; |
|||
// Act
|
|||
await _studentElasticSearchRepository.InsertManyAsync(students); |
|||
var TimeZone = "Asia/Shanghai"; |
|||
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
|||
|
|||
// // 查询日期
|
|||
// mustFilters.Add(e =>
|
|||
// e.DateRange(f =>
|
|||
// f.Field(fd => fd.CreationTime)
|
|||
// .TimeZone(TimeZone)
|
|||
// // 小于等于LessThanOrEquals
|
|||
// // 大于等于GreaterThanOrEquals
|
|||
// .GreaterThanOrEquals(DateTime.Now.AddDays(-1))));
|
|||
//
|
|||
// 查询日期区间 3天之前到现在
|
|||
mustFilters.Add(a => a |
|||
.Bool(b => b |
|||
.Must( |
|||
m => m.DateRange(r => r.Field(f => f.CreationTime).TimeZone(TimeZone).GreaterThanOrEquals(DateTime.Now.AddDays(-3))), |
|||
m => m.DateRange(r => r.Field(f => f.CreationTime).TimeZone(TimeZone).LessThanOrEquals(DateTime.Now)) |
|||
) |
|||
) |
|||
); |
|||
|
|||
var query = new QueryContainerDescriptor<Student>(); |
|||
// https://zhuanlan.zhihu.com/p/592767668
|
|||
|
|||
// Act
|
|||
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
|||
|
|||
// Assert
|
|||
Assert.NotNull(result); |
|||
Assert.True(result.Item1 >= 0); |
|||
Assert.NotNull(result.Item2); |
|||
} |
|||
|
|||
[Fact(DisplayName = "Term精准查询")] |
|||
public async Task PageAsync_Term_Should_OK() |
|||
{ |
|||
// Arrange
|
|||
var students = new List<Student> |
|||
{ |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "韩立", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.1, |
|||
Gender = Gender.Man |
|||
}, |
|||
}; |
|||
await _studentElasticSearchRepository.InsertManyAsync(students); |
|||
// Act
|
|||
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
|||
// 因为text类型会自动生成keyword类型,所以此时这样可以查询出来
|
|||
mustFilters.Add(e => e.Term(f => f.Field(b => b.Name.Suffix("keyword")).Value("韩立"))); |
|||
// Act
|
|||
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
|||
|
|||
// Assert
|
|||
Assert.NotNull(result); |
|||
Assert.True(result.Item1 >= 0); |
|||
Assert.NotNull(result.Item2); |
|||
|
|||
// Act
|
|||
var mustFilters1 = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
|||
// 如果name是中文是无法查询到的,text类型会自动分词
|
|||
mustFilters1.Add(e => e.Term(f => f.Field(b => b.Name).Value("韩立"))); |
|||
// Act
|
|||
var result1 = await _studentElasticSearchRepository.PageAsync(mustFilters1); |
|||
|
|||
Assert.True(result1.Item1 == 0); |
|||
Assert.True(result1.Item2.Count == 0); |
|||
} |
|||
|
|||
[Fact(DisplayName = "Wildcard模糊查询")] |
|||
public async Task PageAsync_Wildcard_Should_OK() |
|||
{ |
|||
// Arrange
|
|||
var students = new List<Student> |
|||
{ |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "Mock", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.1, |
|||
Gender = Gender.Man |
|||
}, |
|||
}; |
|||
await _studentElasticSearchRepository.InsertManyAsync(students); |
|||
|
|||
// Act
|
|||
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
|||
// * 代表匹配多个字符
|
|||
// ?代表匹配单个字符
|
|||
mustFilters.Add(e => e.Wildcard(f => f.Field(b => b.Name).Value("Moc?"))); |
|||
// Act
|
|||
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
|||
|
|||
// Assert
|
|||
Assert.NotNull(result); |
|||
Assert.True(result.Item1 >= 0); |
|||
Assert.NotNull(result.Item2); |
|||
} |
|||
|
|||
[Fact(DisplayName = "Match查询")] |
|||
public async Task PageAsync_Match_Should_OK() |
|||
{ |
|||
// Arrange
|
|||
var students = new List<Student> |
|||
{ |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
//Name = "杀人防火历飞雨,万人敬仰韩天尊",
|
|||
Name = "Student1", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.1, |
|||
Gender = Gender.Man |
|||
}, |
|||
}; |
|||
await _studentElasticSearchRepository.InsertManyAsync(students); |
|||
|
|||
// Act
|
|||
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
|||
|
|||
// Student1 不会进行分词, 所以参数是Student 或者 1 都会查询不到数据
|
|||
mustFilters.Add(e => e.Match(f => f.Field(b => b.Name).Query("Student"))); |
|||
// Act
|
|||
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
|||
|
|||
// Assert
|
|||
Assert.True(result.Item1 == 0); |
|||
Assert.True(result.Item2.Count == 0); |
|||
} |
|||
|
|||
[Fact(DisplayName = "数值区间查询")] |
|||
public async Task PageAsync_Range_Should_OK() |
|||
{ |
|||
// Arrange
|
|||
var students = new List<Student> |
|||
{ |
|||
new Student |
|||
{ |
|||
Id = Guid.NewGuid(), |
|||
Name = "My Name Is Match", |
|||
Age = 10, |
|||
CreationTime = DateTime.Now, |
|||
Price = 100.1, |
|||
Gender = Gender.Man |
|||
}, |
|||
}; |
|||
await _studentElasticSearchRepository.InsertManyAsync(students); |
|||
|
|||
// Act
|
|||
var mustFilters = new List<Func<QueryContainerDescriptor<Student>, QueryContainer>>(); |
|||
|
|||
// 查询数值区间
|
|||
mustFilters.Add(a => a |
|||
.Bool(b => b |
|||
.Must( |
|||
m => m.Range(r => r.Field(f => f.Age).GreaterThanOrEquals(0)), |
|||
m => m.Range(r => r.Field(f => f.Age).LessThanOrEquals(20)) |
|||
) |
|||
) |
|||
); |
|||
// Act
|
|||
var result = await _studentElasticSearchRepository.PageAsync(mustFilters); |
|||
|
|||
// Assert
|
|||
Assert.NotNull(result); |
|||
Assert.True(result.Item1 >= 0); |
|||
Assert.NotNull(result.Item2); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Lion.AbpPro.ElasticSearch.Students; |
|||
|
|||
public enum Gender |
|||
{ |
|||
Man = 10, |
|||
WoMan = 20 |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
namespace Lion.AbpPro.ElasticSearch.Students; |
|||
|
|||
public interface IStudentElasticSearchRepository : IBasicElasticSearchRepository<Student> |
|||
{ |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
namespace Lion.AbpPro.ElasticSearch.Students; |
|||
|
|||
public class Student : IElasticSearchEntity |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public DateTime CreationTime { get; set; } |
|||
|
|||
public double Price { get; set; } |
|||
|
|||
public string Name { get; set; } |
|||
|
|||
public int Age { get; set; } |
|||
|
|||
public Gender Gender { get; set; } |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Lion.AbpPro.ElasticSearch.Students; |
|||
|
|||
public class StudentElasticSearchRepository : ElasticSearchRepository<Student>, IStudentElasticSearchRepository, ITransientDependency |
|||
{ |
|||
public StudentElasticSearchRepository(IElasticsearchProvider elasticsearchProvider) : base(elasticsearchProvider) |
|||
{ |
|||
} |
|||
|
|||
// index 只能是小写
|
|||
protected override string IndexName => "Students20230701".ToLower(); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
global using Xunit; |
|||
global using Volo.Abp; |
|||
global using Volo.Abp.Modularity; |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"ElasticSearch": { |
|||
"Host": "http://localhost:9200", |
|||
"UserName": "admin", |
|||
"Password": "changeme" |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
namespace Lion.AbpPro.ElasticSearches.Dto |
|||
{ |
|||
public class PagingElasticSearchLogInput : PagingBase |
|||
{ |
|||
public string Filter { get; set; } |
|||
|
|||
public DateTime? StartCreationTime { get; set; } |
|||
|
|||
public DateTime? EndCreationTime { get; set; } |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
namespace Lion.AbpPro.ElasticSearches.Dto |
|||
{ |
|||
|
|||
public class PagingElasticSearchLogOutput |
|||
{ |
|||
/// <summary>
|
|||
/// 日志级别
|
|||
/// </summary>
|
|||
public string Level { get; set; } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// 日志内容
|
|||
/// </summary>
|
|||
public string Message { get; set; } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// 创建时间
|
|||
/// </summary>
|
|||
public DateTime CreationTime { get; set; } |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
using Lion.AbpPro.ElasticSearches.Dto; |
|||
|
|||
namespace Lion.AbpPro.ElasticSearches |
|||
{ |
|||
public interface ILionAbpProLogAppService : IApplicationService |
|||
{ |
|||
/// <summary>
|
|||
/// 分页查询es日志
|
|||
/// </summary>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task<CustomPagedResultDto<PagingElasticSearchLogOutput>> PaingAsync(PagingElasticSearchLogInput input); |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
namespace Lion.AbpPro.ElasticSearches.Dto |
|||
{ |
|||
/// <summary>
|
|||
/// Dto为什么在Service层
|
|||
/// 因为NEST 类库的坑 PropertyName必须用这个
|
|||
/// 不想在契约层添加NEST 包引用
|
|||
/// </summary>
|
|||
[Serializable] |
|||
public class PagingElasticSearchLogDto |
|||
{ |
|||
/// <summary>
|
|||
/// 日志级别
|
|||
/// </summary>
|
|||
public string Level { get; set; } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// 日志内容
|
|||
/// </summary>
|
|||
public string Message { get; set; } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// 创建时间
|
|||
/// </summary>
|
|||
[PropertyName("@timestamp")] |
|||
public DateTime CreationTime { get; set; } |
|||
} |
|||
} |
|||
@ -1,10 +0,0 @@ |
|||
namespace Lion.AbpPro.ElasticSearches |
|||
{ |
|||
public class ElasticSearchApplicationAutoMapperProfile : Profile |
|||
{ |
|||
public ElasticSearchApplicationAutoMapperProfile() |
|||
{ |
|||
CreateMap<PagingElasticSearchLogDto, PagingElasticSearchLogOutput>(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,73 +0,0 @@ |
|||
namespace Lion.AbpPro.ElasticSearches |
|||
{ |
|||
[Authorize] |
|||
public class LionAbpProLogAppService : ElasticsearchBasicService, ILionAbpProLogAppService |
|||
{ |
|||
private readonly IConfiguration _configuration; |
|||
|
|||
// 时区
|
|||
private const string TimeZone = "Asia/Shanghai"; |
|||
|
|||
|
|||
public LionAbpProLogAppService( |
|||
IElasticsearchProvider elasticsearchProvider, |
|||
IConfiguration configuration) : base(elasticsearchProvider) |
|||
{ |
|||
_configuration = configuration; |
|||
} |
|||
|
|||
[Authorize(Policy = AbpProPermissions.SystemManagement.ES)] |
|||
public async Task<CustomPagedResultDto<PagingElasticSearchLogOutput>> PaingAsync(PagingElasticSearchLogInput input) |
|||
{ |
|||
var IndexName = _configuration.GetValue<string>("ElasticSearch:SearchIndexFormat"); |
|||
// 默认查询当天
|
|||
input.StartCreationTime = input.StartCreationTime?.AddMilliseconds(-1) ?? Clock.Now.Date.AddMilliseconds(-1); |
|||
input.EndCreationTime = input.EndCreationTime?.AddDays(1).AddMilliseconds(-1) ?? Clock.Now.Date.AddDays(1).AddMilliseconds(-1); |
|||
var mustFilters = new List<Func<QueryContainerDescriptor<PagingElasticSearchLogDto>, QueryContainer>>(); |
|||
if (input.StartCreationTime.HasValue) |
|||
{ |
|||
input.StartCreationTime = input.StartCreationTime.ToCurrentDateMaxDateTime(); |
|||
mustFilters.Add(e => e.DateRange(f => f.Field(fd => fd.CreationTime).TimeZone(TimeZone).GreaterThanOrEquals(input.StartCreationTime))); |
|||
} |
|||
|
|||
if (input.EndCreationTime.HasValue) |
|||
{ |
|||
input.EndCreationTime = input.EndCreationTime.ToNextSecondDateTime(); |
|||
mustFilters.Add(e => e.DateRange(f => f.Field(fd => fd.CreationTime).TimeZone(TimeZone).LessThanOrEquals(input.EndCreationTime))); |
|||
} |
|||
|
|||
if (!string.IsNullOrWhiteSpace(input.Filter)) |
|||
{ |
|||
mustFilters.Add |
|||
( |
|||
t => t.Match(f => f.Field(fd => fd.Message).Query(input.Filter.Trim()).Fuzziness(Fuzziness.Auto)) |
|||
); |
|||
} |
|||
|
|||
var result = await Client.SearchAsync<PagingElasticSearchLogDto> |
|||
( |
|||
e => e |
|||
.Index(IndexName) |
|||
.From(input.SkipCount) |
|||
.Size(input.PageSize) |
|||
.Sort(s => s.Descending(sd => sd.CreationTime)) |
|||
.Query(q => q.Bool(qb => qb.Filter(mustFilters))) |
|||
); |
|||
|
|||
if (result.HitsMetadata != null) |
|||
{ |
|||
return new CustomPagedResultDto<PagingElasticSearchLogOutput> |
|||
( |
|||
result.HitsMetadata.Total.Value, |
|||
ObjectMapper |
|||
.Map<List<PagingElasticSearchLogDto>, List<PagingElasticSearchLogOutput>> |
|||
( |
|||
result.Documents.ToList() |
|||
) |
|||
); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
namespace Lion.AbpPro.ElasticSearches.Providers |
|||
{ |
|||
public abstract class ElasticsearchBasicService : AbpProAppService |
|||
{ |
|||
private readonly IElasticsearchProvider _elasticsearchProvider; |
|||
|
|||
// ReSharper disable once PublicConstructorInAbstractClass
|
|||
public ElasticsearchBasicService(IElasticsearchProvider elasticsearchProvider) |
|||
{ |
|||
_elasticsearchProvider = elasticsearchProvider; |
|||
} |
|||
|
|||
protected IElasticClient Client => _elasticsearchProvider.GetElasticClient(); |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
namespace Lion.AbpPro.ElasticSearches.Providers |
|||
{ |
|||
public class ElasticsearchProvider : IElasticsearchProvider, ISingletonDependency |
|||
{ |
|||
private readonly IConfiguration _configuration; |
|||
|
|||
public ElasticsearchProvider(IConfiguration configuration) |
|||
{ |
|||
_configuration = configuration; |
|||
} |
|||
|
|||
public IElasticClient GetElasticClient() |
|||
{ |
|||
var pool = new SingleNodeConnectionPool(new Uri(_configuration.GetValue<string>("ElasticSearch:Url"))); |
|||
var connectionSettings = |
|||
new ConnectionSettings(pool); |
|||
connectionSettings.EnableHttpCompression(); |
|||
connectionSettings.BasicAuthentication(_configuration.GetValue<string>("ElasticSearch:UserName"), |
|||
_configuration.GetValue<string>("ElasticSearch:Password")); |
|||
return new ElasticClient(connectionSettings); |
|||
} |
|||
} |
|||
} |
|||
@ -1,7 +0,0 @@ |
|||
namespace Lion.AbpPro.ElasticSearches.Providers |
|||
{ |
|||
public interface IElasticsearchProvider : ISingletonDependency |
|||
{ |
|||
IElasticClient GetElasticClient(); |
|||
} |
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
namespace Lion.AbpPro.Controllers.Systems |
|||
{ |
|||
[Route("EsLog")] |
|||
public class LionAbpProLogController: AbpProController,ILionAbpProLogAppService |
|||
{ |
|||
private readonly ILionAbpProLogAppService _companyNameAbpProLogAppService; |
|||
|
|||
public LionAbpProLogController(ILionAbpProLogAppService companyNameAbpProLogAppService) |
|||
{ |
|||
_companyNameAbpProLogAppService = companyNameAbpProLogAppService; |
|||
} |
|||
|
|||
[HttpPost("page")] |
|||
[SwaggerOperation(summary: "分页获取Es日志", Tags = new[] { "EsLog" })] |
|||
public Task<CustomPagedResultDto<PagingElasticSearchLogOutput>> PaingAsync(PagingElasticSearchLogInput input) |
|||
{ |
|||
return _companyNameAbpProLogAppService.PaingAsync(input); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue