Elasticsearch在全文检索领域几乎是绕不开的选择,而.NET生态下官方推荐的高级客户端就是NEST。相比直接拼JSON的Lowlevel客户端Elasticsearch.Net,NEST把所有查询DSL都映射成了强类型的C# API,写查询就像写Lambda表达式一样,编译期就能发现问题。这篇文章就来系统地讲一讲,在C#里如何用NEST完成文档的索引、搜索以及各种常见查询的写法。

一、NEST客户端的安装与连接配置
首先通过NuGet安装NEST包,可以在包管理器控制台执行安装命令,也可以在项目里直接引用。安装完成后,最核心的一步就是创建ElasticClient实例,它是所有操作的入口。
Install-Package NEST
连接单节点Elasticsearch的写法非常直接,指定节点地址后构造ConnectionSettings,再传入ElasticClient即可。如果集群开启了认证,或者需要设置默认索引、超时时间,都可以在ConnectionSettings上以链式调用的方式配置。
var settings = new ConnectionSettings(new Uri("http://127.0.0.1:9200"))
.DefaultIndex("products") // 设置默认索引,后续操作不用重复指定
.RequestTimeout(TimeSpan.FromSeconds(30))
.BasicAuthentication("elastic", "your_password"); // 集群开启认证时配置
var client = new ElasticClient(settings);这里有个容易踩的坑:如果你的Elasticsearch是8.x版本且开启了HTTPS自签证书,直接连接会报SSL验证错误,可以在配置中关闭证书校验(仅限开发环境),生产环境务必正确配置证书。另外,NEST对字段命名默认采用驼峰转换,C#属性ProductName会被序列化成productName,如果索引里的字段是下划线风格,需要通过.DefaultFieldNameInferrer()自定义映射规则,否则查询时会因为字段名对不上而查不到数据。
二、索引文档与基础搜索
查询之前得先有数据。假设我们有一个商品实体类,先用NEST把它索引到Elasticsearch里。实体类建议通过特性标注字段类型,比如分词字段和精确匹配字段要区分开。
public class Product
{
public int Id { get; set; }
[Text(Name = "productName")] // 分词字段,用于全文搜索
public string ProductName { get; set; }
[Keyword(Name = "category")] // 不分词,用于精确匹配和聚合
public string Category { get; set; }
public decimal Price { get; set; }
[Date(Name = "createTime")]
public DateTime CreateTime { get; set; }
}
// 索引一条文档,指定ID
var response = await client.IndexAsync(new Product
{
Id = 1,
ProductName = "无线蓝牙降噪耳机",
Category = "数码",
Price = 599.00m,
CreateTime = DateTime.Now
}, i => i.Id(1));
if (response.Result == Result.Created)
{
Console.WriteLine("索引成功");
}数据就位后就可以搜索了。最简单的用法是MatchAll查询,配合From和Size做分页,取出文档后通过response.Documents就能拿到已经反序列化好的强类型集合,这一点比手动解析JSON省事太多了。
var searchResponse = await client.SearchAsync<Product>(s => s
.From(0) // 起始位置,分页用
.Size(10) // 每页数量
.Query(q => q.MatchAll())
);
var total = searchResponse.Total; // 命中的总文档数
var docs = searchResponse.Documents; // List<Product>
Console.WriteLine($"共命中 {total} 条记录");三、常用查询方式详解
1. term精确查询与match全文查询
term查询不做分词处理,适合查Keyword类型的字段,比如按分类筛选;而match查询会先对输入内容分词,再去倒排索引里匹配,适合Text类型的全文搜索。这两个查询的使用场景经常被初学者搞混,导致term查Text字段怎么都查不到数据,原因就是term拿的是原始输入去匹配已经分词后的词条,自然对不上。
// term精确匹配分类字段
var termResult = await client.SearchAsync<Product>(s => s
.Query(q => q
.Term(t => t.Field(f => f.Category).Value("数码"))
)
);
// match全文搜索,支持分词
var matchResult = await client.SearchAsync<Product>(s => s
.Query(q => q
.Match(m => m
.Field(f => f.ProductName)
.Query("蓝牙耳机")
.Operator(Operator.And) // 分词后所有词条都要命中,默认是Or
)
)
);2. range范围查询
价格区间、时间范围这类筛选用Range查询,写法同样直观。注意数值和日期都是通过各自的重载方法来指定条件的。
var rangeResult = await client.SearchAsync<Product>(s => s
.Query(q => q
.Range(r => r
.Field(f => f.Price)
.GreaterThanOrEquals(100)
.LessThan(1000)
)
)
);3. bool组合查询
实际业务里很少只用单一条件,Bool查询是把多个条件组合起来的核心。它有四个子句:must表示必须匹配且参与算分,should表示满足即可加分,must_not表示必须不匹配,filter表示必须匹配但不参与算分。一个典型的电商搜索例子如下:
var boolResult = await client.SearchAsync<Product>(s => s
.Query(q => q
.Bool(b => b
.Must(m => m
.Match(mm => mm.Field(f => f.ProductName).Query("耳机")))
.Filter(
f => f.Term(t => t.Field(x => x.Category).Value("数码")),
f => f.Range(r => r.Field(x => x.Price).GreaterThanOrEquals(100))
)
.MustNot(mn => mn
.Term(t => t.Field(x => x.Category).Value("下架")))
)
)
.Sort(so => so.Descending(f => f.Price)) // 按价格降序
);关于must和filter的选择有个实用原则:需要相关性排序的条件放must,纯筛选类的条件放filter。因为filter不计算评分,Elasticsearch还可以对它做缓存,性能明显更好。把所有条件一股脑塞进must是新手常见的性能误区。
四、分页、高亮与字段过滤
搜索结果要直接展示给用户,通常还缺三样东西:深分页、关键词高亮、只返回需要的字段。NEST对这些都有完整支持。高亮部分要注意,返回的高亮片段在response.Hits里,而不在Documents里,需要自己取出来替换或拼接。
var result = await client.SearchAsync<Product>(s => s
.From((page - 1) * pageSize)
.Size(pageSize)
.Query(q => q.Match(m => m.Field(f => f.ProductName).Query("耳机")))
.Source(src => src
.Includes(i => i
.Fields(
f => f.ProductName,
f => f.Price)))
.Highlight(h => h
.Fields(f => f
.Field(x => x.ProductName)
.PreTags("<em class='hl'>")
.PostTags("</em>")
.FragmentSize(100)))
);
foreach (var hit in result.Hits)
{
var doc = hit.Source;
// 高亮片段存在时优先使用
var displayName = hit.Highlights.ContainsKey("productName")
? string.Join("", hit.Highlights["productName"].Highlights)
: doc.ProductName;
Console.WriteLine($"{displayName} - {doc.Price}");
}另外提醒一点,From + Size分页默认上限是一万条,超过后会报错。深分页场景应该改用SearchAfter,或者用滚动接口Scroll处理全量导出类需求,NEST对这两种方式都提供了对应API,写法上只是把From换掉而已,改造成本很低。掌握了这些查询方法,C#项目里绝大部分Elasticsearch搜索需求都能覆盖到位了。
C# ElasticsearchNEST客户端文档搜索修改时间:2026-09-03 20:55:09