针对 ElasticSearch .Net 客户端的一些封装
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
ElasticSearch 为.net提供了两个客户端,分别是 Elasticsearch.Net 和 NEST
Elasticsearch.net为什么会有两个客户端?
Elasticsearch.Net是一个非常底层且灵活的客户端,它不在意你如何的构建自己的请求和响应。它非常抽象,因此所有的Elasticsearch API被表示为方法,没有太多关于你想如何构建json/request/response对象的东东,并且它还内置了可配置、可重写的集群故障转移机制。
Elasticsearch.Net有非常大的弹性,如果你想更好的提升你的搜索服务,你完全可以使用它来做为你的客户端。
NEST是一个高层的客户端,可以映射所有请求和响应对象,拥有一个强类型查询DSL(领域特定语言),并且可以使用.net的特性比如协变、Auto Mapping Of POCOs,NEST内部使用的依然是Elasticsearch.Net客户端。
具体客户端的用法可参考官方的文档说明,本文主要针对 NEST 的查询做扩展。
起因:之前在学习Dapper的时候看过一个 DapperExtensions 的封装 其实Es的查询基本就是类似Sql的查询 。因此参考DapperExtensions 进行了Es版本的迁移
通过官网说明可以看到 NEST 的对象初始化的方式进行查询 都是已下面的方式开头:
var searchRequest = new SearchRequest<XXT>(XXIndex)
我们可以通过查看源码

我们可以看到所有的查询基本都是在SearchRequest上面做的扩展 这样我们也可以开始我们的第一步操作:
1.关于分页,我们定义如下分页对象:
View Code2.定义ElasticsearchPage 分页对象
View Code至此我们的SearchRequest的初始化操作已经完成了我们可以通过如下方式进行调用
View Code通过SearchRequest的源码我们可以得知,所有的查询都是基于内部属性进行(扩展的思路来自DapperExtensions):
3.QueryContainer的扩展 ,类似Where 语句:
我们定义一个 比较操作符 类似 Sql中的 like != in 等等
View Code接着我们定义一个 如下接口,主要包括:
1. 提供返回一个 QueryContainer GetQuery方法
2. 属性名称 PropertyName
3. 操作符 ExpressOperator
4. 谓词值 Value
1 /// <summary>
2 /// 谓词接口
3 /// </summary>
4 public interface IPredicate
5 {
6 QueryContainer GetQuery(QueryContainer query);
7 }
8
9 /// <summary>
10 /// 基础谓词接口
11 /// </summary>
12 public interface IBasePredicate : IPredicate
13 {
14 /// <summary>
15 /// 属性名称
16 /// </summary>
17 string PropertyName { get; set; }
18 }
19
20 public abstract class BasePredicate : IBasePredicate
21 {
22 public string PropertyName { get; set; }
23 public abstract QueryContainer GetQuery(QueryContainer query);
24 }
25
26 /// <summary>
27 /// 比较谓词
28 /// </summary>
29 public interface IComparePredicate : IBasePredicate
30 {
31 /// <summary>
32 /// 操作符
33 /// </summary>
34 ExpressOperator ExpressOperator { get; set; }
35 }
36
37 public abstract class ComparePredicate : BasePredicate
38 {
39 public ExpressOperator ExpressOperator { get; set; }
40 }
41
42 /// <summary>
43 /// 字段谓词
44 /// </summary>
45 public interface IFieldPredicate : IComparePredicate
46 {
47 /// <summary>
48 /// 谓词的值
49 /// </summary>
50 object Value { get; set; }
51 }
具体实现定义 FieldPredicate 并且继承如上接口,通过操作符映射为 Nest具体查询对象
View Code4.定义好这些后我们就可以拼接我们的条件了,我们定义了 PropertyName 但是我们更倾向于一种类似EF的查询方式 可以通过 Expression<Func<T, object>> 的方式所以我们这边提供一个泛型方式
,因为在创建 Elasticsearch 文档的时候我们已经建立了Map 文件 我们通过反射读取 PropertySearchName属性 就可以读取到我们的 PropertyName 这边 PropertySearchName 是自己定义的属性
为什么不反解Nest 的属性 针对不同类型需要反解的属性也是不相同的 所以避免麻烦 直接重新定义了新的属性 。代码如下:
View Code然后我们就可以来定义的们初始化IFieldPredicate 的方法了
首先我们解析我们的需求:
1.我们需要一个Expression<Func<T, object>>
2.我们需要一个操作符
3.我们需要比较什么值
针对需求我们可以得到这样一个方法:
注:所依赖的反射方法详解文末
1 /// <summary>
2 /// 工厂方法创建一个新的 IFieldPredicate 谓语: [FieldName] [Operator] [Value].
3 /// </summary>
4 /// <typeparam name="T">实例类型</typeparam>
5 /// <param name="expression">返回左操作数的表达式 [FieldName].</param>
6 /// <param name="op">比较运算符</param>
7 /// <param name="value">谓语的值.</param>
8 /// <returns>An instance of IFieldPredicate.</returns>
9 public static IFieldPredicate Field<T>(Expression<Func<T, object>> expression, ExpressOperator op, object value) where T : class
10 {
11 var propertySearchName = (PropertySearchNameAttribute)
12 LoadAttributeHelper.LoadAttributeByType<T, PropertySearchNameAttribute>(expression);
13
14 return new FieldPredicate<T>
15 {
16 PropertyName = propertySearchName.Name,
17 ExpressOperator = op,
18 Value = value
19 };
20 }
然后 我们就可以像之前拼接sql的方式来进行拼接条件了
就以我们项目中的业务需求做个演示
1 var predicateList = new List<IPredicate>();
2 //最大价格
3 if (requestContentDto.MaxPrice != null)
4 predicateList.Add(Predicates.Field<Content>(x => x.UnitPrice, ExpressOperator.Le,
5 requestContentDto.MaxPrice));
6 //最小价格
7 if (requestContentDto.MinPrice != null)
8 predicateList.Add(Predicates.Field<Content>(x => x.UnitPrice, ExpressOperator.Ge,
9 requestContentDto.MinPrice));
然后针对实际业务我们在写sql的时候就回有 (xx1 and xx2) or xx3 这样的业务需求了
针对这种业务需求 我们需要在提供一个 IPredicateGroup 进行分组查询谓词
首先我们定义一个PredicateGroup 加入谓词时使用的操作符 GroupOperator
1 /// <summary>
2 /// PredicateGroup 加入谓词时使用的操作符
3 /// </summary>
4 public enum GroupOperator
5 {
6 And,
7 Or
8 }
然后我们定义 IPredicateGroup 及实现
1 /// <summary>
2 /// 分组查询谓词
3 /// </summary>
4 public interface IPredicateGroup : IPredicate
5 {
6 /// <summary>
7 /// </summary>
8 GroupOperator Operator { get; set; }
9
10 IList<IPredicate> Predicates { get; set; }
11 }
12
13 /// <summary>
14 /// 分组查询谓词
15 /// </summary>
16 public class PredicateGroup : IPredicateGroup
17 {
18 public GroupOperator Operator { get; set; }
19 public IList<IPredicate> Predicates { get; set; }
20
21 /// <summary>
22 /// GetQuery
23 /// </summary>
24 /// <param name="query"></param>
25 /// <returns></returns>
26 public QueryContainer GetQuery(QueryContainer query)
27 {
28 switch (Operator)
29 {
30 case GroupOperator.And:
31 return Predicates.Aggregate(query, (q, p) => q && p.GetQuery(query));
32 case GroupOperator.Or:
33 return Predicates.Aggregate(query, (q, p) => q || p.GetQuery(query));
34 default:
35 throw new ElasticsearchException("构建Elasticsearch查询谓词异常");
36 }
37 }
38 }
现在我们可以用 PredicateGroup来组装我们的 谓词
同样解析我们的需求:
1.我们需要一个GroupOperator
2.我们需要谓词列表 IPredicate[]
针对需求我们可以得到这样一个方法:
1 /// <summary>
2 /// 工厂方法创建一个新的 IPredicateGroup 谓语.
3 /// 谓词组与其他谓词可以连接在一起.
4 /// </summary>
5 /// <param name="op">分组操作时使用的连接谓词 (AND / OR).</param>
6 /// <param name="predicate">一组谓词列表.</param>
7 /// <returns>An instance of IPredicateGroup.</returns>
8 public static IPredicateGroup Group(GroupOperator op, params IPredicate[] predicate)
9 {
10 return new PredicateGroup
11 {
12 Operator = op,
13 Predicates = predicate
14 };
15 }
这样我们就可以进行组装了
用法:
1 //构建或查询
2
3 var predicateList= new List<IPredicate>();
4
5 //关键词
6 if (!string.IsNullOrWhiteSpace(requestContentDto.SearchKey))
7 predicateList.Add(Predicates.Field<Content>(x => x.Title, ExpressOperator.Like,
8 requestContentDto.SearchKey));
9
10 var predicate = Predicates.Group(GroupOperator.And, predicateList.ToArray());
11 //构建或查询
12 var predicateListOr = new List<IPredicate>();
13 if (!string.IsNullOrWhiteSpace(requestContentDto.Brand))
14 {
15 var array = requestContentDto.Brand.Split(',').ToList();
16 predicateListOr
17 .AddRange(array.Select
18 (item => Predicates.Field<Content>(x => x.Brand, ExpressOperator.Like, item)));
19 }
20
21 var predicateOr = Predicates.Group(GroupOperator.Or, predicateListOr.ToArray());
22
23 var predicatecCombination = new List<IPredicate> {predicate, predicateOr};
24 var pgCombination = Predicates.Group(GroupOperator.And, predicatecCombination.ToArray());
然后我们的 IPredicateGroup 优雅的和 ISearchRequest 使用呢 我们提供一个链式的操作方法
1 /// <summary>
2 /// 初始化query
3 /// </summary>
4 /// <param name="searchRequest"></param>
5 /// <param name="predicate"></param>
6 public static ISearchRequest InitQueryContainer(this ISearchRequest searchRequest, IPredicate predicate)
7 {
8 if (predicate != null)
9 {
10 searchRequest.Query = predicate.GetQuery(searchRequest.Query);
11 }
12 return searchRequest;
13
14 }
至此我们的基础查询方法已经封装完成
然后通过 Nest 的进行查询即可
var response = ElasticClient.Search<T>(searchRequest);
具体演示代码(以项目的业务)
1 var elasticsearchPage = new ElasticsearchPage<Content>("content")
2 {
3 PageIndex = pageIndex,
4 PageSize = pageSize
5 };
6
7 #region terms 分组
8
9 var terms = new List<IFieldTerms>();
10 var classificationGroupBy = "searchKey_classification";
11 var brandGroupBy = "searchKey_brand";
12
13 #endregion
14
15 var searchRequest = elasticsearchPage.InitSearchRequest();
16 var predicateList = new List<IPredicate>();
17 //分类ID
18 if (requestContentDto.CategoryId != null)
19 predicateList.Add(Predicates.Field<Content>(x => x.ClassificationCode, ExpressOperator.Like,
20 requestContentDto.CategoryId));
21 else
22 terms.Add(Predicates.FieldTerms<Content>(x => x.ClassificationGroupBy, classificationGroupBy, 200));
23
24 //品牌
25 if (string.IsNullOrWhiteSpace(requestContentDto.Brand))
26 terms.Add(Predicates.FieldTerms<Content>(x => x.BrandGroupBy, brandGroupBy, 200));
27 //供应商名称
28 if (!string.IsNullOrWhiteSpace(requestContentDto.BaseType))
29 predicateList.Add(Predicates.Field<Content>(x => x.BaseType, ExpressOperator.Like,
30 requestContentDto.BaseType));
31 //是否自营
32 if (requestContentDto.IsSelfSupport == 1)
33 predicateList.Add(Predicates.Field<Content>(x => x.IsSelfSupport, ExpressOperator.Eq,
34 requestContentDto.IsSelfSupport));
35 //最大价格
36 if (requestContentDto.MaxPrice != null)
37 predicateList.Add(Predicates.Field<Content>(x => x.UnitPrice, ExpressOperator.Le,
38 requestContentDto.MaxPrice));
39 //最小价格
40 if (requestContentDto.MinPrice != null)
41 predicateList.Add(Predicates.Field<Content>(x => x.UnitPrice, ExpressOperator.Ge,
42 requestContentDto.MinPrice));
43 //关键词
44 if (!string.IsNullOrWhiteSpace(requestContentDto.SearchKey))
45 predicateList.Add(Predicates.Field<Content>(x => x.Title, ExpressOperator.Like,
46 requestContentDto.SearchKey));
47
48 //规整排序
49 var sortConfig = SortOrderRule(requestContentDto.SortKey);
50 var sorts = new List<ISort>
51 {
52 Predicates.Sort<Content>(sortConfig.Key, sortConfig.SortOrder)
53 };
54
55 var predicate = Predicates.Group(GroupOperator.And, predicateList.ToArray());
56 //构建或查询
57 var predicateListOr = new List<IPredicate>();
58 if (!string.IsNullOrWhiteSpace(requestContentDto.Brand))
59 {
60 var array = requestContentDto.Brand.Split(',').ToList();
61 predicateListOr
62 .AddRange(array.Select
63 (item => Predicates.Field<Content>(x => x.Brand, ExpressOperator.Like, item)));
64 }
65
66 var predicateOr = Predicates.Group(GroupOperator.Or, predicateListOr.ToArray());
67
68 var predicatecCombination = new List<IPredicate> {predicate, predicateOr};
69 var pgCombination = Predicates.Group(GroupOperator.And, predicatecCombination.ToArray());
70
71 searchRequest.InitQueryContainer(pgCombination)
72 .InitSort(sorts)
73 .InitHighlight(requestContentDto.HighlightConfigEntity)
74 .InitGroupBy(terms);
75
76 var data = _searchProvider.SearchPage(searchRequest);
77
78 #region terms 分组赋值
79
80 var classificationResponses = requestContentDto.CategoryId != null
81 ? null
82 : data.Aggregations.Terms(classificationGroupBy).Buckets
83 .Select(x => new ClassificationResponse
84 {
85 Key = x.Key.ToString(),
86 DocCount = x.DocCount
87 }).ToList();
88
89 var brandResponses = !string.IsNullOrWhiteSpace(requestContentDto.Brand)
90 ? null
91 : data.Aggregations.Terms(brandGroupBy).Buckets
92 .Select(x => new BrandResponse
93 {
94 Key = x.Key.ToString(),
95 DocCount = x.DocCount
96 }).ToList();
97
