AdiElasticSugar.Core 1.1.3

dotnet add package AdiElasticSugar.Core --version 1.1.3
                    
NuGet\Install-Package AdiElasticSugar.Core -Version 1.1.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AdiElasticSugar.Core" Version="1.1.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AdiElasticSugar.Core" Version="1.1.3" />
                    
Directory.Packages.props
<PackageReference Include="AdiElasticSugar.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AdiElasticSugar.Core --version 1.1.3
                    
#r "nuget: AdiElasticSugar.Core, 1.1.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AdiElasticSugar.Core@1.1.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AdiElasticSugar.Core&version=1.1.3
                    
Install as a Cake Addin
#tool nuget:?package=AdiElasticSugar.Core&version=1.1.3
                    
Install as a Cake Tool

ElasticSugar.Core - ElasticSearch ORM 风格查询构建器

一个功能完整的 ElasticSearch .NET 客户端库,提供类似数据库 ORM(如 SqlSugar)的使用体验。支持自动索引管理、文档推送和强大的 LINQ 风格查询构建。

特性

核心功能

  • ORM 风格查询:使用 Where 方法构建查询条件,类似 Entity Framework 或 SqlSugar
  • 自动索引管理:推送文档时自动检查并创建索引,无需手动管理
  • 智能索引命名:支持基于年、年月的自动索引命名,支持自定义生成器
  • Lambda 表达式:通过 Lambda 表达式自动构建字段路径,无需手动指定字符串
  • 逻辑操作符:使用 || 操作符实现 OR 逻辑,使用 && 操作符实现 AND 逻辑
  • 条件判断:支持 WhereIf 方法,根据条件动态添加查询条件
  • 类型安全:编译时检查,减少运行时错误
  • 批量操作:支持批量推送文档,自动分批处理,提高性能
  • 自动映射:根据 C# 类型自动生成 Elasticsearch 字段映射

查询功能

  • 丰富的操作符:支持 >, <, >=, <=, ==, != 等比较操作符
  • 字符串扩展:支持 Contains, StartsWith, EndsWith 等字符串方法
  • 集合查询:支持 In 查询(通过 Contains 方法)
  • 排序和分页:支持 OrderByOrderByDescSkipTake 等方法
  • 总记录数:支持 TrackTotalHits 获取准确的分页总数

项目结构

目录结构

Adi.ElasticSugar.Core/
├── Models/                          # 数据模型和特性
│   ├── BaseEsModel.cs              # ElasticSearch 文档基类
│   ├── EsFieldAttribute.cs         # 字段映射特性
│   └── EsIndexAttribute.cs         # 索引配置特性
│
├── Index/                           # 索引管理模块
│   ├── ElasticsearchClientIndexExtensions.cs  # 索引扩展方法
│   ├── ElasticsearchIndexManager.cs           # 索引管理器(缓存、创建、删除)
│   ├── IndexMappingBuilder.cs                # 索引映射构建器(自动生成字段映射)
│   └── IndexName/                            # 索引名称生成
│       ├── IIndexNameGenerator.cs            # 索引名称生成器接口
│       ├── IndexFormat.cs                    # 索引格式枚举
│       ├── IndexNameGenerator.cs             # 索引名称生成器(统一入口)
│       ├── YearIndexNameGenerator.cs         # 年格式生成器
│       └── YearMonthIndexNameGenerator.cs    # 年月格式生成器
│
├── Search/                          # 查询构建模块
│   ├── ElasticsearchClientExtensions.cs     # 查询扩展方法
│   ├── EsQueryable.cs                        # 基础查询构建器
│   ├── EsQueryableExtensions.cs             # 查询构建器扩展方法
│   ├── EsSearchQueryable.cs                  # 搜索查询构建器(完整功能)
│   └── ExpressionParser.cs                   # 表达式树解析器(Lambda转ES查询)
│
├── Document/                        # 文档推送模块
│   └── ElasticsearchClientDocumentExtensions.cs  # 文档推送扩展方法
│
└── Utils/                           # 工具类
    ├── EnumDeserializationHelper.cs         # 枚举反序列化辅助
    ├── EnumFieldHelper.cs                   # 枚举字段辅助
    ├── FieldNameHelper.cs                   # 字段名辅助(PascalCase转camelCase)
    ├── StringHelper.cs                      # 字符串辅助
    └── TypeHelper.cs                         # 类型辅助

架构说明

项目采用模块化设计,各模块职责清晰:

  • Models: 定义数据模型基类和配置特性,提供元数据支持
  • Index: 负责索引的创建、管理和映射配置的自动生成
  • Search: 提供链式查询构建器,将 LINQ 表达式转换为 Elasticsearch 查询
  • Document: 处理文档的推送(单个和批量),自动处理索引创建
  • Utils: 提供各种辅助工具,如字段名转换、枚举处理等

类图

核心类关系图

classDiagram
    class BaseEsModel {
        <<abstract>>
        +string Id
        +DateTime EsDateTime
        +GetIndexName() string
        +GetIndexNameFromAttribute() string
    }
    
    class EsIndexAttribute {
        +string IndexPrefix
        +IndexFormat Format
        +Type CustomGeneratorType
    }
    
    class EsFieldAttribute {
        +string FieldType
        +bool IsNested
        +bool NeedKeyword
        +bool Ignore
        +string Analyzer
        +string FieldName
    }
    
    class ElasticsearchClient {
        <<external>>
    }
    
    class EsSearchQueryable~T~ {
        -ElasticsearchClient _client
        -string _index
        -List~Expression~ _whereExpressions
        +Where(Expression) EsSearchQueryable
        +WhereIf(bool, Expression) EsSearchQueryable
        +OrderBy(Expression) EsSearchQueryable
        +OrderByDesc(Expression) EsSearchQueryable
        +Skip(int) EsSearchQueryable
        +Take(int) EsSearchQueryable
        +TrackTotalHits() EsSearchQueryable
        +ToListAsync(int? pageSize) Task~IReadOnlyList~
        +ToSearchResponseAsync() Task~SearchResponse~
        +ToPageAsync(int, int) Task~SearchResponse~
    }
    
    class ExpressionParser {
        <<static>>
        +ParseExpression~T~(Expression) Action
    }
    
    class IndexMappingBuilder {
        <<static>>
        +BuildMapping~T~(PropertiesDescriptor) void
    }
    
    class IndexNameGenerator {
        <<static>>
        +RegisterGenerator~T~(IIndexNameGenerator) void
        +GenerateIndexNameFromAttribute~T~(T) string
        +GenerateIndexPatternFromAttribute~T~() string
    }
    
    class IIndexNameGenerator~T~ {
        <<interface>>
        +GenerateIndexName(T) string
        +GenerateIndexName(DateTime) string
        +GenerateIndexPattern() string
    }
    
    class ElasticsearchIndexManager {
        -ElasticsearchClient _client
        -ConcurrentDictionary~string, bool~ _indexCache
        +CreateIndexIfNotExistsAsync~T~(string) Task~bool~
        +IndexExistsAsync(string) Task~bool~
        +DeleteIndexAsync(string) Task~bool~
        +ClearCache() void
    }
    
    class ElasticsearchClientDocumentExtensions {
        <<static>>
        +PushDocumentAsync~T~(T) Task~IndexResponse~
        +PushDocumentsAsync~T~(IEnumerable~T~) Task~BulkResponse~
    }
    
    class ElasticsearchClientIndexExtensions {
        <<static>>
        +IndexManager() ElasticsearchIndexManager
        +CreateIndexForDocumentAsync~T~(T) Task~string~
        +CreateIndexesForDocumentsAsync~T~(IEnumerable~T~) Task~Dictionary~
    }
    
    class ElasticsearchClientExtensions {
        <<static>>
        +Search~T~() EsSearchQueryable~T~
        +Search~T~(string) EsSearchQueryable~T~
    }
    
    BaseEsModel <|-- UserDocument : 继承
    UserDocument ..> EsIndexAttribute : 使用
    UserDocument ..> EsFieldAttribute : 使用
    
    EsSearchQueryable~T~ --> ExpressionParser : 使用
    EsSearchQueryable~T~ --> ElasticsearchClient : 使用
    
    ExpressionParser --> FieldNameHelper : 使用
    ExpressionParser --> EnumFieldHelper : 使用
    
    IndexMappingBuilder --> FieldNameHelper : 使用
    IndexMappingBuilder --> EnumFieldHelper : 使用
    
    IndexNameGenerator --> IIndexNameGenerator~T~ : 使用
    IIndexNameGenerator~T~ <|.. YearIndexNameGenerator~T~ : 实现
    IIndexNameGenerator~T~ <|.. YearMonthIndexNameGenerator~T~ : 实现
    
    ElasticsearchIndexManager --> IndexMappingBuilder : 使用
    ElasticsearchIndexManager --> IndexNameGenerator : 使用
    
    ElasticsearchClientDocumentExtensions --> ElasticsearchIndexManager : 使用
    ElasticsearchClientIndexExtensions --> ElasticsearchIndexManager : 使用
    ElasticsearchClientExtensions --> EsSearchQueryable~T~ : 创建

查询构建流程

sequenceDiagram
    participant User
    participant EsSearchQueryable
    participant ExpressionParser
    participant ElasticsearchClient
    participant Elasticsearch
    
    User->>EsSearchQueryable: Search<T>("index*")
    User->>EsSearchQueryable: Where(x => x.Status == 1)
    User->>EsSearchQueryable: OrderBy(x => x.CreatedDate)
    User->>EsSearchQueryable: Skip(0).Take(20)
    User->>EsSearchQueryable: ToListAsync()
    
    EsSearchQueryable->>ExpressionParser: ParseExpression(lambda)
    ExpressionParser->>ExpressionParser: ConvertToDnf(expression)
    ExpressionParser->>ExpressionParser: BuildQueryFromDnf()
    ExpressionParser-->>EsSearchQueryable: Action<QueryDescriptor>
    
    EsSearchQueryable->>EsSearchQueryable: BuildSearchDescriptor()
    EsSearchQueryable->>ElasticsearchClient: SearchAsync(descriptor)
    ElasticsearchClient->>Elasticsearch: HTTP Request
    Elasticsearch-->>ElasticsearchClient: SearchResponse
    ElasticsearchClient-->>EsSearchQueryable: SearchResponse<T>
    EsSearchQueryable->>EsSearchQueryable: ProcessEnumFieldsDeserialization()
    EsSearchQueryable-->>User: SearchResponse<T>

文档推送流程

sequenceDiagram
    participant User
    participant DocumentExtensions
    participant IndexManager
    participant IndexMappingBuilder
    participant ElasticsearchClient
    participant Elasticsearch
    
    User->>DocumentExtensions: PushDocumentAsync(document)
    DocumentExtensions->>DocumentExtensions: document.GetIndexNameFromAttribute()
    DocumentExtensions->>IndexManager: CreateIndexIfNotExistsAsync()
    
    IndexManager->>IndexManager: IndexExistsAsync() [检查缓存]
    alt 索引不存在
        IndexManager->>ElasticsearchClient: Indices.Create()
        ElasticsearchClient->>IndexMappingBuilder: BuildMapping<T>()
        IndexMappingBuilder->>IndexMappingBuilder: 分析类型特性
        IndexMappingBuilder-->>ElasticsearchClient: PropertiesDescriptor
        ElasticsearchClient->>Elasticsearch: 创建索引请求
        Elasticsearch-->>ElasticsearchClient: 创建成功
        IndexManager->>IndexManager: 更新缓存
    end
    
    DocumentExtensions->>DocumentExtensions: SerializeDocumentForEnumFields()
    DocumentExtensions->>ElasticsearchClient: IndexAsync(document)
    ElasticsearchClient->>Elasticsearch: 推送文档请求
    Elasticsearch-->>ElasticsearchClient: IndexResponse
    ElasticsearchClient-->>DocumentExtensions: IndexResponse
    DocumentExtensions-->>User: IndexResponse

数据模型关系图(ER图)

文档模型关系

erDiagram
    BaseEsModel ||--o{ UserDocument : "继承"
    BaseEsModel ||--o{ OrderDocument : "继承"
    BaseEsModel ||--o{ ProductDocument : "继承"
    
    BaseEsModel {
        string Id PK
        DateTime EsDateTime
    }
    
    UserDocument {
        string UserName
        string Email
        int Age
        DateTime CreatedDate
        UserRole Role
        NestedAddress Address
        List~NestedItem~ Items
    }
    
    OrderDocument {
        string OrderNo
        decimal Amount
        DateTime CreatedDate
        OrderStatus Status
        NestedAddress ShippingAddress
    }
    
    ProductDocument {
        string ProductName
        decimal Price
        int Stock
        List~string~ Tags
    }
    
    NestedAddress {
        string Street
        string City
        string ZipCode
        string Country
    }
    
    NestedItem {
        string ProductName
        int Quantity
        decimal Price
        bool IsAvailable
    }
    
    OrderStatus {
        int Pending
        int Processing
        int Completed
        int Cancelled
    }
    
    UserRole {
        int Admin
        int User
        int Guest
    }
    
    UserDocument ||--o{ NestedAddress : "包含"
    UserDocument ||--o{ NestedItem : "包含多个"
    OrderDocument ||--o{ NestedAddress : "包含"
    UserDocument }o--|| OrderStatus : "关联"
    UserDocument }o--|| UserRole : "关联"
    OrderDocument }o--|| OrderStatus : "关联"

索引与文档关系

erDiagram
    EsIndexAttribute ||--|| DocumentType : "配置"
    DocumentType ||--o{ IndexInstance : "生成"
    IndexInstance ||--o{ Document : "存储"
    
    EsIndexAttribute {
        string IndexPrefix
        IndexFormat Format
        Type CustomGeneratorType
    }
    
    DocumentType {
        string TypeName
        EsIndexAttribute Config
    }
    
    IndexInstance {
        string IndexName PK
        DateTime CreatedDate
        int NumberOfShards
        int NumberOfReplicas
        MappingProperties Properties
    }
    
    Document {
        string Id PK
        string IndexName FK
        DateTime EsDateTime
        object Data
    }
    
    IndexInstance ||--o{ MappingProperties : "包含"
    MappingProperties ||--o{ FieldMapping : "包含"
    
    FieldMapping {
        string FieldName PK
        string FieldType
        bool IsNested
        bool NeedKeyword
        string Analyzer
    }

安装

使用 .NET CLI

dotnet add package Adi.ElasticSugar.Core

使用 Package Manager

Install-Package Adi.ElasticSugar.Core

使用 PackageReference

.csproj 文件中添加:

<ItemGroup>
  <PackageReference Include="Adi.ElasticSugar.Core" Version="1.0.0" />
</ItemGroup>

快速开始

1. 定义文档模型

using Adi.ElasticSugar.Core.Models;

// 使用特性配置索引
[EsIndex(IndexPrefix = "orders", Format = IndexFormat.YearMonth)]
public class OrderDto : BaseEsModel
{
    public string OrderNo { get; set; }
    public decimal Amount { get; set; }
    public DateTime CreatedDate { get; set; }
    public int Status { get; set; }
}

2. 推送文档

var order = new OrderDto
{
    Id = Guid.NewGuid(),
    EsDateTime = DateTime.Now,
    OrderNo = "ORD-001",
    Amount = 1000.00m,
    CreatedDate = DateTime.Now,
    Status = 1
};

// 推送单个文档(自动创建索引)
await _elasticsearchClient.PushDocumentAsync(order);

// 批量推送文档(自动创建索引,自动分批处理)
var orders = new List<OrderDto> { /* ... */ };
await _elasticsearchClient.PushDocumentsAsync(orders, batchSize: 1000);

3. 查询文档

// 从 ElasticsearchClient 开始链式调用
var result = await _elasticsearchClient.Search<OrderDto>("orders*")
    .Where(x => x.Status == 1)
    .Where(x => x.CreatedDate >= DateTime.Now.AddDays(-7))
    .OrderByDesc(x => x.CreatedDate)
    .Skip(0)
    .Take(20)
    .TrackTotalHits()
    .ToListAsync();

详细功能说明

一、文档模型定义

1.1 基础模型

所有需要存储到 Elasticsearch 的文档类型都应该继承 BaseEsModel

public abstract class BaseEsModel
{
    /// <summary>
    /// 文档 ID
    /// </summary>
    public object? Id { get; set; }

    /// <summary>
    /// ElasticSearch 时间字段
    /// 用于索引名称自动生成(基于年月)
    /// </summary>
    public DateTime EsDateTime { get; set; }
}

1.2 索引配置特性

使用 EsIndexAttribute 特性配置索引:

[EsIndex(IndexPrefix = "orders", Format = IndexFormat.YearMonth)]
public class OrderDto : BaseEsModel
{
    // ...
}

特性参数说明:

  • IndexPrefix:索引前缀,如果未设置则使用类名的小写形式
  • Format:索引格式,支持 YearMonth(年月,如 orders-2024-01)和 Year(年,如 orders-2024
  • CustomGeneratorType:自定义索引名称生成器类型

1.3 字段映射特性

使用 EsFieldAttribute 特性配置字段映射:

public class OrderDto : BaseEsModel
{
    [EsField(FieldType = "text", Analyzer = "ik_max_word")]
    public string Description { get; set; }

    [EsField(FieldType = "keyword")]
    public string OrderNo { get; set; }

    [EsField(Ignore = true)]
    public string InternalField { get; set; }
}

特性参数说明:

  • FieldType:字段类型(text, keyword, long, integer, date, boolean 等)
  • IsNested:是否为嵌套文档
  • NeedKeyword:是否需要 keyword 子字段(用于 text 类型)
  • Ignore:是否忽略该字段
  • Analyzer:字段分析器
  • SearchAnalyzer:搜索分析器
  • Index:是否启用索引
  • Store:是否存储字段值

重要说明:

  • 字段映射配置只能通过 EsFieldAttribute 特性完成,不支持在运行时手动配置
  • 所有字段映射都会根据特性自动生成,确保配置的一致性和可维护性
  • 如果字段没有特性,系统会根据字段类型自动推断映射配置

二、文档推送

2.1 推送单个文档

var order = new OrderDto
{
    Id = Guid.NewGuid(),
    EsDateTime = DateTime.Now,
    // ... 其他属性
};

// 推送文档(自动检查并创建索引)
// 字段映射配置通过 EsFieldAttribute 特性完成,无需手动配置
var response = await _elasticsearchClient.PushDocumentAsync(order);

方法签名:

Task<IndexResponse> PushDocumentAsync<T>(
    this ElasticsearchClient client,
    T document,
    int numberOfShards = 3,
    int numberOfReplicas = 1) where T : BaseEsModel

参数说明:

  • document:要推送的文档
  • numberOfShards:分片数量,仅在创建索引时使用,默认 3
  • numberOfReplicas:副本数量,仅在创建索引时使用,默认 1

注意: 字段映射配置只能通过 EsFieldAttribute 特性完成,不支持手动配置。

2.2 批量推送文档

var orders = new List<OrderDto> { /* ... */ };

// 批量推送(自动检查并创建索引,自动分批处理)
// 字段映射配置通过 EsFieldAttribute 特性完成,无需手动配置
var response = await _elasticsearchClient.PushDocumentsAsync(
    orders,
    batchSize: 1000  // 每批处理 1000 条
);

方法签名:

Task<BulkResponse> PushDocumentsAsync<T>(
    this ElasticsearchClient client,
    IEnumerable<T> documents,
    int numberOfShards = 3,
    int numberOfReplicas = 1,
    int batchSize = 1000) where T : BaseEsModel

注意: 字段映射配置只能通过 EsFieldAttribute 特性完成,不支持手动配置。

特性:

  • 自动按索引名称分组文档
  • 相同的索引名称只会创建一次
  • 超过 batchSize 的文档会自动分批处理
  • 使用 Bulk API 提高性能

三、索引管理

3.1 根据文档创建索引

var order = new OrderDto { EsDateTime = DateTime.Now };

// 根据文档创建索引(如果已存在则不创建)
// 字段映射配置通过 EsFieldAttribute 特性完成,无需手动配置
var indexName = await _elasticsearchClient.CreateIndexForDocumentAsync(order);

3.2 批量创建索引

var orders = new List<OrderDto> { /* ... */ };

// 批量创建索引(自动去重)
var documentsByIndex = await _elasticsearchClient.CreateIndexesForDocumentsAsync(orders);

// 返回字典:索引名称 -> 文档列表
foreach (var kvp in documentsByIndex)
{
    Console.WriteLine($"索引: {kvp.Key}, 文档数量: {kvp.Value.Count}");
}

3.3 索引管理器

var manager = _elasticsearchClient.IndexManager();

// 检查索引是否存在
bool exists = await manager.IndexExistsAsync("orders-2024-01");

// 删除索引
await manager.DeleteIndexAsync("orders-2024-01");

// 清除缓存
manager.ClearCache();
manager.ClearCache("orders-2024-01");

索引管理器特性:

  • 内置缓存机制,减少重复检查
  • 线程安全的索引创建
  • 支持并行创建多个索引

四、文档查询

4.1 创建查询构建器

// 单个索引
var query = _elasticsearchClient.Search<OrderDto>("orders-2024-01");

// 多个索引(使用通配符)
var query = _elasticsearchClient.Search<OrderDto>("orders*");

// 多个索引(使用逗号分隔)
var query = _elasticsearchClient.Search<OrderDto>("orders-2024-01,orders-2024-02");

4.2 Where 条件查询

4.2.1 基本比较操作
// 等于查询
query.Where(x => x.Status == 1);

// 不等于查询
query.Where(x => x.Status != 0);

// 大于查询
query.Where(x => x.CreatedDate > DateTime.Now.AddDays(-30));

// 小于等于查询
query.Where(x => x.Amount <= 1000);

// 大于等于查询
query.Where(x => x.Amount >= 100);

// 小于查询
query.Where(x => x.Amount < 5000);
4.2.2 空值判断
// 判断字段为 null
query.Where(x => x.OrderNo == null);

// 判断字段不为 null
query.Where(x => x.OrderNo != null);

// 判断字符串为空或空字符串
query.Where(x => x.OrderNo == null || x.OrderNo == "");
4.2.3 条件判断(WhereIf)
// 当 Status 不为空时才添加条件
query.WhereIf(status.HasValue, x => x.Status == status.Value);

// 当 StartDate 有值时才添加范围查询
query.WhereIf(req.StartDate.HasValue, 
    x => x.CreatedDate >= req.StartDate.Value);

// 当 EndDate 有值时才添加范围查询
query.WhereIf(req.EndDate.HasValue, 
    x => x.CreatedDate <= req.EndDate.Value);

4.3 逻辑操作符

4.3.1 AND 逻辑

多个 Where 方法链式调用时,它们之间是 AND 关系。

// 多个 Where 之间是 AND 关系
query
    .Where(x => x.Status == 1)
    .Where(x => x.CreatedDate > DateTime.Now.AddDays(-7))
    .Where(x => x.Amount > 100);
// 等价于:Status == 1 AND CreatedDate > ... AND Amount > 100

在同一个 Where 中使用 && 操作符也可以实现 AND 逻辑。

// 在同一个 Where 中使用 && 实现 AND 逻辑
query.Where(x => 
    x.Status == 1 
    && x.CreatedDate > DateTime.Now.AddDays(-7)
    && x.Amount > 100);
4.3.2 OR 逻辑

在同一个 Where 中使用 || 操作符实现 OR 逻辑。

// 使用 || 操作符实现 OR 逻辑
query.Where(x => 
    x.Status == 1 || x.Status == 2);
// 等价于:Status == 1 OR Status == 2
4.3.3 复杂逻辑组合

可以组合使用 &&|| 操作符,使用括号控制优先级。

// 复杂组合:使用括号控制优先级
query.Where(x => 
    (x.Status == 1 || x.Status == 2) 
    && x.CreatedDate > DateTime.Now.AddDays(-7)
    && x.Amount > 100);
// 等价于:(Status == 1 OR Status == 2) AND CreatedDate > ... AND Amount > 100

4.4 字符串查询

4.4.1 Contains - 包含查询
// 查询订单号包含 "ORD" 的订单
query.Where(x => x.OrderNo.Contains("ORD"));

// 查询描述包含关键词的订单
query.Where(x => x.Description.Contains("urgent"));
4.4.2 StartsWith - 以...开头
// 查询订单号以 "ORD" 开头的订单
query.Where(x => x.OrderNo.StartsWith("ORD"));
4.4.3 EndsWith - 以...结尾
// 查询订单号以 "001" 结尾的订单
query.Where(x => x.OrderNo.EndsWith("001"));

4.5 集合查询(In 查询)

使用集合的 Contains 方法实现 In 查询。

// 查询状态在指定列表中的订单
var statusList = new[] { 1, 2, 3 };
query.Where(x => statusList.Contains(x.Status));

// 查询销售组在指定列表中的订单
var salesGroups = new[] { "GROUP_A", "GROUP_B", "GROUP_C" };
query.Where(x => salesGroups.Contains(x.SalesGroup));

4.6 排序

4.6.1 升序排序
// 按创建日期升序排序
query.OrderBy(x => x.CreatedDate);

// 按金额升序排序
query.OrderBy(x => x.Amount);
4.6.2 降序排序
// 按创建日期降序排序
query.OrderByDesc(x => x.CreatedDate);

// 按金额降序排序
query.OrderByDesc(x => x.Amount);
4.6.3 多字段排序
// 先按创建日期降序,再按金额升序
query
    .OrderByDesc(x => x.CreatedDate)
    .OrderBy(x => x.Amount);

4.7 分页

4.7.1 Skip 和 Take
// 跳过前 10 条,获取 20 条
query
    .Skip(10)
    .Take(20);

// 分页计算示例
int pageIndex = 1;
int pageSize = 20;
query
    .Skip((pageIndex - 1) * pageSize)
    .Take(pageSize);
4.7.2 TrackTotalHits

TrackTotalHits 方法用于启用跟踪总命中数,这对于分页查询非常重要。

query
    .Skip(0)
    .Take(20)
    .TrackTotalHits();  // 启用跟踪总命中数,可以获取总记录数

注意: 如果不调用此方法,Elasticsearch 默认只返回前 10,000 条记录的总数。

4.8 执行查询

使用 ToListAsync() 方法执行查询并返回文档列表。

// 执行查询并返回文档列表(内部使用 SearchAfter 滚动查询)
var documents = await query.ToListAsync(pageSize: 1000);

// 完整示例:直接获取文档列表
var orders = await _elasticsearchClient.Search<OrderDto>("orders*")
    .Where(x => x.Status == 1)
    .Where(x => x.CreatedDate >= DateTime.Now.AddDays(-7))
    .OrderByDesc(x => x.CreatedDate)
    .Skip(0)
    .Take(20)
    .ToListAsync();

// 如果需要总数或原始响应信息
var response = await _elasticsearchClient.Search<OrderDto>("orders*")
    .Where(x => x.Status == 1)
    .Where(x => x.CreatedDate >= DateTime.Now.AddDays(-7))
    .OrderByDesc(x => x.CreatedDate)
    .Skip(0)
    .Take(20)
    .TrackTotalHits()
    .ToSearchResponseAsync();

var totalCount = response.Total;

4.9 分页查询

使用 ToPageAsync 方法进行分页查询:

var response = await _elasticsearchClient.Search<OrderDto>("orders*")
    .Where(x => x.Status == 1)
    .TrackTotalHits()
    .ToPageAsync(pageIndex: 1, pageSize: 20);

var orders = response.Documents.ToList();
var totalCount = response.Total;

五、索引名称生成

5.1 自动生成索引名称

索引名称会根据文档类型的 EsIndexAttribute 特性和文档的 EsDateTime 字段自动生成:

[EsIndex(IndexPrefix = "orders", Format = IndexFormat.YearMonth)]
public class OrderDto : BaseEsModel
{
    public DateTime EsDateTime { get; set; }
}

var order = new OrderDto { EsDateTime = new DateTime(2024, 1, 15) };
// 索引名称:orders-2024-01

5.2 索引格式

支持两种索引格式:

  • YearMonth(年月格式):{prefix}-{yyyy-MM},例如 orders-2024-01
  • Year(年格式):{prefix}-{yyyy},例如 orders-2024
// 年月格式(默认)
[EsIndex(IndexPrefix = "orders", Format = IndexFormat.YearMonth)]
// 索引名称:orders-2024-01

// 年格式
[EsIndex(IndexPrefix = "orders", Format = IndexFormat.Year)]
// 索引名称:orders-2024

5.3 自定义索引名称生成器

实现 IIndexNameGenerator<T> 接口可以完全自定义索引名称生成逻辑:

public class CustomOrderIndexNameGenerator : IIndexNameGenerator<OrderDto>
{
    public string GenerateIndexName(OrderDto document)
    {
        // 自定义生成逻辑
        return $"orders-{document.EsDateTime:yyyy-MM-dd}";
    }

    public string GenerateIndexName(DateTime dateTime)
    {
        return $"orders-{dateTime:yyyy-MM-dd}";
    }

    public string GenerateIndexPattern()
    {
        return "orders-*";
    }
}

使用自定义生成器:

// 方式1:在特性中指定
[EsIndex(CustomGeneratorType = typeof(CustomOrderIndexNameGenerator))]
public class OrderDto : BaseEsModel { }

// 方式2:运行时注册
IndexNameGenerator.RegisterGenerator<OrderDto>(new CustomOrderIndexNameGenerator());

5.4 手动获取索引名称

var order = new OrderDto { EsDateTime = DateTime.Now };

// 从文档实例获取索引名称
string indexName = order.GetIndexNameFromAttribute();

// 从类型获取索引名称(基于当前时间)
string indexName = IndexNameGenerator.GenerateIndexNameFromAttribute<OrderDto>();

// 从类型获取索引名称(基于指定时间)
string indexName = IndexNameGenerator.GenerateIndexNameFromAttribute<OrderDto>(DateTime.Now);

// 生成索引通配符模式
string pattern = IndexNameGenerator.GenerateIndexPatternFromAttribute<OrderDto>();
// 返回:orders-*

六、完整示例

以下是一个完整的实际使用示例,展示了如何组合使用各种功能:

using Adi.ElasticSugar.Core;
using Adi.ElasticSugar.Core.Models;

// 1. 定义文档模型
[EsIndex(IndexPrefix = "orders", Format = IndexFormat.YearMonth)]
public class OrderDto : BaseEsModel
{
    public string OrderNo { get; set; }
    public decimal Amount { get; set; }
    public DateTime CreatedDate { get; set; }
    public int Status { get; set; }
    public string SalesGroup { get; set; }
}

// 2. 推送文档
public async Task PushOrderAsync(OrderDto order)
{
    // 单个推送
    await _elasticsearchClient.PushDocumentAsync(order);
    
    // 批量推送
    var orders = new List<OrderDto> { /* ... */ };
    await _elasticsearchClient.PushDocumentsAsync(orders, batchSize: 1000);
}

// 3. 查询文档
public async Task<List<OrderDto>> SearchOrdersAsync(SearchRequest req)
{
    var response = await _elasticsearchClient.Search<OrderDto>("orders*")
        // 条件判断:SalesGroup 不为空时添加等于条件
        .WhereIf(!string.IsNullOrEmpty(req.SalesGroup), 
            x => x.SalesGroup == req.SalesGroup)
        
        // OR 逻辑:Status 为 2 时,Status=2 或 SalesGroup="DEFAULT"
        .WhereIf(req.Status == 2,
            x => x.Status == 2 || x.SalesGroup == "DEFAULT")
        
        // 条件判断:Status 有值且不等于 2 时添加等于条件
        .WhereIf(req.Status.HasValue && req.Status != 2,
            x => x.Status == req.Status.Value)
        
        // 范围查询:创建日期大于等于某个值
        .WhereIf(req.StartDate.HasValue, 
            x => x.CreatedDate >= req.StartDate.Value)
        
        // 范围查询:创建日期小于等于某个值
        .WhereIf(req.EndDate.HasValue, 
            x => x.CreatedDate <= req.EndDate.Value)
        
        // 字符串查询:订单号包含关键词
        .WhereIf(!string.IsNullOrEmpty(req.OrderNoKeyword),
            x => x.OrderNo.Contains(req.OrderNoKeyword))
        
        // In 查询:状态在指定列表中
        .WhereIf(req.StatusList != null && req.StatusList.Any(),
            x => req.StatusList.Contains(x.Status))
        
        // 排序:按创建日期降序
        .OrderByDesc(x => x.CreatedDate)
        
        // 分页
        .Skip((req.PageIndex - 1) * req.PageSize)
        .Take(req.PageSize)
        
        // 跟踪总命中数
        .TrackTotalHits()
        
        // 执行查询
        .ToListAsync();
    
    return response.ToList();
}

七、支持的表达式和操作符

比较操作符

操作符 说明 示例
== 等于 x.Status == 1
!= 不等于 x.Status != 0
> 大于 x.Amount > 100
< 小于 x.Amount < 1000
>= 大于等于 x.CreatedDate >= DateTime.Now
<= 小于等于 x.CreatedDate <= DateTime.Now

逻辑操作符

操作符 说明 使用场景
&& AND 逻辑 在同一个 Where 中使用,连接多个条件
|| OR 逻辑 在同一个 Where 中使用,表示或的关系
多个 Where AND 逻辑 链式调用多个 Where 方法,它们之间是 AND 关系

字符串方法

方法 说明 示例
Contains(value) 包含指定字符串 x.OrderNo.Contains("ORD")
StartsWith(value) 以指定字符串开头 x.OrderNo.StartsWith("ORD")
EndsWith(value) 以指定字符串结尾 x.OrderNo.EndsWith("001")

集合方法

方法 说明 示例
collection.Contains(field) In 查询,字段值在集合中 statusList.Contains(x.Status)

八、类型支持

支持以下数据类型的查询:

  • 数字类型int, long, double, decimal, float, short, byte
  • 字符串类型string
  • 日期时间类型DateTime, DateTimeOffset
  • 布尔类型bool
  • GUID 类型Guid
  • 可空类型int?, DateTime?, bool? 等所有可空值类型

九、重要说明

9.1 字段路径自动构建

通过 Lambda 表达式自动提取字段路径,无需手动指定字符串,避免拼写错误。

// 自动将 x.Order.PaymentStatus 转换为 "Order.PaymentStatus"
query.Where(x => x.Order.PaymentStatus == 2);

// 自动将 x.CreatedDate 转换为 "CreatedDate"
query.Where(x => x.CreatedDate > DateTime.Now);

9.2 嵌套字段查询

如果字段路径包含多个部分(如 Order.PaymentStatus),系统会自动处理嵌套路径。第一个部分(如 Order)会被识别为可能的嵌套对象。

// 嵌套字段查询
query.Where(x => x.Order.PaymentStatus == 2);
query.Where(x => x.Customer.Address.City == "Beijing");

9.3 自动索引创建

推送文档时会自动检查索引是否存在,不存在则自动创建。索引的映射配置会根据文档类型的特性自动生成。

9.4 性能考虑

  • 表达式树解析会有一定的性能开销,但对于大多数场景来说是可以接受的
  • 查询构建器会缓存解析结果,提高重复查询的性能
  • 批量推送时使用 Bulk API,并支持自动分批处理
  • 索引管理器内置缓存机制,减少重复检查

9.5 错误处理

  • 所有查询方法都支持链式调用,如果某个步骤出错,会在执行 ToListAsync() 时抛出异常
  • 建议使用 try-catch 包裹查询执行代码
try
{
    var result = await query.ToListAsync();
    // 处理结果
}
catch (Exception ex)
{
    // 处理异常
    Console.WriteLine($"查询失败: {ex.Message}");
}

十、最佳实践

10.1 使用 WhereIf 处理可选参数

使用 WhereIf 方法可以优雅地处理可选查询参数,避免构建复杂的条件判断逻辑。

// 推荐:使用 WhereIf
query
    .WhereIf(!string.IsNullOrEmpty(keyword), x => x.OrderNo.Contains(keyword))
    .WhereIf(startDate.HasValue, x => x.CreatedDate >= startDate.Value)
    .WhereIf(endDate.HasValue, x => x.CreatedDate <= endDate.Value);

// 不推荐:使用 if 语句
if (!string.IsNullOrEmpty(keyword))
{
    query = query.Where(x => x.OrderNo.Contains(keyword));
}

10.2 合理使用 OR 逻辑

在同一个 Where 中使用 || 操作符实现 OR 逻辑,多个 Where 之间是 AND 关系。

// 推荐:在同一个 Where 中使用 || 实现 OR 逻辑
query.Where(x => x.Status == 1 || x.Status == 2);

// 不推荐:使用多个 Where(这样是 AND 关系,不是 OR)
query.Where(x => x.Status == 1)
     .Where(x => x.Status == 2);  // 错误!这样永远查不到结果

10.3 使用 TrackTotalHits 获取准确总数

对于需要分页的查询,务必调用 TrackTotalHits() 方法以获取准确的总记录数。

// 推荐:分页查询时使用 TrackTotalHits
var response = await query
    .Skip((pageIndex - 1) * pageSize)
    .Take(pageSize)
    .TrackTotalHits()
    .ToListAsync();

10.4 合理使用索引名称

使用索引别名或通配符可以简化索引管理。

// 使用通配符查询多个索引
var query = _elasticsearchClient.Search<OrderDto>("orders-2024-*");

// 使用索引别名
var query = _elasticsearchClient.Search<OrderDto>("orders-current");

10.5 批量推送优化

对于大量文档的推送,建议:

  • 使用 PushDocumentsAsync 进行批量推送
  • 根据实际情况调整 batchSize 参数(默认 1000)
  • 对于超大数据量,考虑分批调用
// 推荐:批量推送,自动分批处理
await _elasticsearchClient.PushDocumentsAsync(orders, batchSize: 2000);

// 不推荐:循环单个推送
foreach (var order in orders)
{
    await _elasticsearchClient.PushDocumentAsync(order);  // 性能差
}

十一、API 参考

ElasticsearchClientDocumentExtensions

PushDocumentAsync<T>

推送单个文档到 Elasticsearch,推送前会自动检查索引是否存在,不存在则自动创建。

参数:

  • document (T): 要推送的文档,必须继承 BaseEsModel
  • numberOfShards (int): 分片数量,默认 3
  • numberOfReplicas (int): 副本数量,默认 1

返回:

  • Task<IndexResponse>: 推送结果

注意: 字段映射配置只能通过 EsFieldAttribute 特性完成,不支持手动配置。

PushDocumentsAsync<T>

批量推送文档到 Elasticsearch,推送前会自动检查索引是否存在,不存在则自动创建。

参数:

  • documents (IEnumerable<T>): 要推送的文档列表
  • numberOfShards (int): 分片数量,默认 3
  • numberOfReplicas (int): 副本数量,默认 1
  • batchSize (int): 批量操作的大小,默认 1000

返回:

  • Task<BulkResponse>: 批量推送结果

注意: 字段映射配置只能通过 EsFieldAttribute 特性完成,不支持手动配置。

ElasticsearchClientIndexExtensions

CreateIndexForDocumentAsync<T>

根据文档对象创建索引,如果索引已存在则不创建。

参数:

  • document (T): 文档实例
  • numberOfShards (int): 分片数量,默认 3
  • numberOfReplicas (int): 副本数量,默认 1

返回:

  • Task<string>: 创建的索引名称

注意: 字段映射配置只能通过 EsFieldAttribute 特性完成,不支持手动配置。

CreateIndexesForDocumentsAsync<T>

批量创建索引,会自动去重,相同的索引名称只会创建一次。

参数:

  • documents (IEnumerable<T>): 文档实例列表
  • numberOfShards (int): 分片数量,默认 3
  • numberOfReplicas (int): 副本数量,默认 1

返回:

  • Task<Dictionary<string, List<T>>>: 索引名称和对应文档列表的字典

注意: 字段映射配置只能通过 EsFieldAttribute 特性完成,不支持手动配置。

IndexManager()

创建索引管理器实例。

返回:

  • ElasticsearchIndexManager: 索引管理器实例

ElasticsearchIndexManager

CreateIndexIfNotExistsAsync<T>

创建索引,如果存在则不创建。

参数:

  • indexName (string): 索引名称
  • numberOfShards (int): 分片数量,默认 3
  • numberOfReplicas (int): 副本数量,默认 1

返回:

  • Task<bool>: 如果索引已存在或创建成功返回 true

注意: 字段映射配置只能通过 EsFieldAttribute 特性完成,不支持手动配置。

IndexExistsAsync

检查索引是否存在。

参数:

  • indexName (string): 索引名称

返回:

  • Task<bool>: 如果索引存在返回 true
DeleteIndexAsync

删除索引。

参数:

  • indexName (string): 索引名称

返回:

  • Task<bool>: 删除成功返回 true

ElasticsearchClientExtensions

Search<T>(string index)

创建搜索查询构建器,返回 EsSearchQueryable<T> 实例。

参数:

  • index (string): 索引名称,支持通配符(如 "orders*")和多个索引(如 "orders-2024-01,orders-2024-02"

返回:

  • EsSearchQueryable<T>: 查询构建器实例

EsSearchQueryable<T>

查询构建器类,提供链式调用的查询方法。

Where(Expression<Func<T, bool>> predicate)

添加 Where 条件,多个 Where 方法之间是 AND 关系。

参数:

  • predicate (Expression<Func<T, bool>>): Lambda 表达式,定义查询条件

返回:

  • EsSearchQueryable<T>: 查询构建器实例,支持链式调用
WhereIf(bool condition, Expression<Func<T, bool>> predicate)

根据条件判断是否添加 Where 条件。

参数:

  • condition (bool): 判断条件
  • predicate (Expression<Func<T, bool>>): Lambda 表达式,定义查询条件

返回:

  • EsSearchQueryable<T>: 查询构建器实例,支持链式调用
OrderBy(Expression<Func<T, object>> field)

按指定字段升序排序。

参数:

  • field (Expression<Func<T, object>>): Lambda 表达式,指定排序字段

返回:

  • EsSearchQueryable<T>: 查询构建器实例,支持链式调用
OrderByDesc(Expression<Func<T, object>> field)

按指定字段降序排序。

参数:

  • field (Expression<Func<T, object>>): Lambda 表达式,指定排序字段

返回:

  • EsSearchQueryable<T>: 查询构建器实例,支持链式调用
Skip(int count)

跳过指定数量的文档,用于分页。

参数:

  • count (int): 跳过的文档数量

返回:

  • EsSearchQueryable<T>: 查询构建器实例,支持链式调用
Take(int count)

获取指定数量的文档,用于分页。

参数:

  • count (int): 获取的文档数量

返回:

  • EsSearchQueryable<T>: 查询构建器实例,支持链式调用
TrackTotalHits()

启用跟踪总命中数。调用此方法后,查询结果会包含总记录数信息。

返回:

  • EsSearchQueryable<T>: 查询构建器实例,支持链式调用
ToListAsync()

执行查询并返回文档列表(内部使用 SearchAfter 分页滚动)。

返回:

  • Task<IReadOnlyList<T>>: 异步返回文档列表

参数:

  • pageSize (int?): 单次查询数量,默认 1000
ToSearchResponseAsync()

执行查询并返回原始响应信息。

返回:

  • Task<SearchResponse<T>>: 异步返回查询结果
ToPageAsync(int pageIndex, int pageSize)

执行分页查询并返回结果。

参数:

  • pageIndex (int): 页码(从1开始)
  • pageSize (int): 每页数量

返回:

  • Task<SearchResponse<T>>: 异步返回查询结果

十二、常见问题

1. 如何获取查询的总记录数?

调用 TrackTotalHits() 方法后,查询结果会包含总记录数信息。

var response = await query
    .TrackTotalHits()
    .ToSearchResponseAsync();

var total = response.Total;  // 总记录数

2. 如何实现模糊查询?

使用 Contains 方法可以实现模糊查询。

query.Where(x => x.OrderNo.Contains("keyword"));

3. 如何实现范围查询?

使用比较操作符实现范围查询。

// 日期范围
query.Where(x => x.CreatedDate >= startDate && x.CreatedDate <= endDate);

// 数值范围
query.Where(x => x.Amount >= minAmount && x.Amount <= maxAmount);

4. 如何实现多字段排序?

链式调用多个排序方法,先调用的优先级更高。

query
    .OrderByDesc(x => x.CreatedDate)  // 先按创建日期降序
    .OrderBy(x => x.Amount);           // 再按金额升序

5. 如何处理可空类型?

直接使用可空类型的比较操作即可。

// 判断可空类型是否有值
query.WhereIf(status.HasValue, x => x.Status == status.Value);

// 判断可空类型是否为 null
query.Where(x => x.OrderNo == null);

6. 如何查询嵌套对象?

直接使用点号访问嵌套对象的属性即可,系统会自动处理嵌套路径。

query.Where(x => x.Order.PaymentStatus == 2);
query.Where(x => x.Customer.Address.City == "Beijing");

7. 如何自定义索引名称生成逻辑?

实现 IIndexNameGenerator<T> 接口,并在特性中指定或运行时注册。

[EsIndex(CustomGeneratorType = typeof(CustomOrderIndexNameGenerator))]
public class OrderDto : BaseEsModel { }

8. 批量推送时如何控制批次大小?

通过 batchSize 参数控制,默认 1000。

await _elasticsearchClient.PushDocumentsAsync(orders, batchSize: 2000);

目标框架

  • .NET 8.0
  • .NET 9.0
  • .NET 10.0

依赖项

  • Elastic.Clients.Elasticsearch (>= 8.12.0)

许可证

[在此添加许可证信息]

贡献

欢迎提交 Issue 和 Pull Request!

更新日志

Version 1.0.0

  • 初始版本
  • 支持文档推送(单个和批量)
  • 支持自动索引创建和管理
  • 支持索引名称自动生成(年、年月格式)
  • 支持自定义索引名称生成器
  • 支持基本的 Where 查询
  • 支持逻辑操作符(&&, ||)
  • 支持字符串扩展方法(Contains, StartsWith, EndsWith)
  • 支持排序和分页
  • 支持条件判断(WhereIf)
  • 支持自动字段映射
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.3 109 1/26/2026
1.1.2 95 1/23/2026
1.1.1 100 1/23/2026
1.1.0 106 1/6/2026
1.0.3 104 1/6/2026
1.0.2 99 1/5/2026
1.0.1 107 1/5/2026
1.0.0 108 1/4/2026