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.关于分页,我们定义如下分页对象:


1 /// <summary>
2 /// 分页类型
3 /// </summary>
4 public class PageEntity
5 {
6 /// <summary>
7 /// 每页行数
8 /// </summary>
9 public int PageSize { get; set; }
10
11 /// <summary>
12 /// 当前页
13 /// </summary>
14 public int PageIndex { get; set; }
15
16 /// <summary>
17 /// 总记录数
18 /// </summary>
19 public int Records { get; set; }
20
21 /// <summary>
22 /// 总页数
23 /// </summary>
24 public int Total
25 {
26 get
27 {
28 if (Records > 0)
29 return Records % PageSize == 0 ? Records / PageSize : Records / PageSize + 1;
30
31 return 0;
32 }
33 }
34
35
36 /// <summary>
37 /// 排序列
38 /// </summary>
39 public string Sidx { get; set; }
40
41 /// <summary>
42 /// 排序类型
43 /// </summary>
44 public string Sord { get; set; }
45 }
2.定义ElasticsearchPage 分页对象


/// <summary>
/// ElasticsearchPage
/// </summary>
public class ElasticsearchPage<T> : PageEntity
{
public string Index { get; set; }
public ElasticsearchPage(string index)
{
Index = index;
}
/// <summary>
/// InitSearchRequest
/// </summary>
/// <returns></returns>
public SearchRequest<T> InitSearchRequest()
{
return new SearchRequest<T>(Index)
{
From = (PageIndex - 1) * PageSize,
Size = PageSize
};
}
}
至此我们的SearchRequest的初始化操作已经完成了我们可以通过如下方式进行调用


1 var elasticsearchPage = new ElasticsearchPage<Content>("content")
2 {
3 PageIndex = pageIndex,
4 PageSize = pageSize
5 };
6
7 var searchRequest = elasticsearchPage.InitSearchRequest();
通过SearchRequest的源码我们可以得知,所有的查询都是基于内部属性进行(扩展的思路来自DapperExtensions):
3.QueryContainer的扩展 ,类似Where 语句:
我们定义一个 比较操作符 类似 Sql中的 like != in 等等


1 /// <summary>
2 /// 比较操作符
3 /// </summary>
4 public enum ExpressOperator
5 {
6 /// <summary>
7 /// 精准匹配 term(主要用于精确匹配哪些值,比如数字,日期,布尔值或 not_analyzed 的字符串(未经分析的文本数据类型): )
8 /// </summary>
9 Eq,
10
11 /// <summary>
12 /// 大于
13 /// </summary>
14 Gt,
15
16 /// <summary>
17 /// 大于等于
18 /// </summary>
19 Ge,
20
21 /// <summary>
22 /// 小于
23 /// </summary>
24 Lt,
25
26 /// <summary>
27 /// 小于等于
28 /// </summary>
29 Le,
30
31 /// <summary>
32 /// 模糊查询 (You can use % in the value to do wilcard searching)
33 /// </summary>
34 Like,
35
36 /// <summary>
37 /// in 查询
38 /// </summary>
39 In
40 }
接着我们定义一个 如下接口,主要包括:
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具体查询对象


1 public class FieldPredicate<T> : ComparePredicate, IFieldPredicate
2 where T : class
3 {
4 public object Value { get; set; }
5
6 public override QueryContainer GetQuery(QueryContainer query)
7 {
8 switch (ExpressOperator)
9 {
10 case ExpressOperator.Eq:
11 query = new TermQuery
12 {
13 Field = PropertyName,
14 Value = Value
15 };
16 break;
17 case ExpressOperator.Gt:
18 query = new TermRangeQuery
19 {
20 Field = PropertyName,
21 GreaterThan = Value.ToString()
22 };
23 break;
24 case ExpressOperator.Ge:
25 query = new TermRangeQuery
26 {
27 Field = PropertyName,
28 GreaterThanOrEqualTo = Value.ToString()
29 };
30 break;
31 case ExpressOperator.Lt:
32 query = new TermRangeQuery
33 {
34 Field = PropertyName,
35 LessThan = Value.ToString()
36 };
37 break;
38 case ExpressOperator.Le:
39 query = new TermRangeQuery
40 {
41 Field = PropertyName,
42 LessThanOrEqualTo = Value.ToString()
43 };
44 break;
45 case ExpressOperator.Like:
46 query = new MatchPhraseQuery
47 {
48 Field = PropertyName,
49 Query = Value.ToString()
50 };
51 break;
52 case ExpressOperator.In:
53 query = new TermsQuery
54 {
55 Field = PropertyName,
56 Terms=(List<object>)Value
57 };
58 break;
59 default:
60 throw new ElasticsearchException("构建Elasticsearch查询谓词异常");
61 }
62 return query;
63 }
64 }
4.定义好这些后我们就可以拼接我们的条件了,我们定义了 PropertyName 但是我们更倾向于一种类似EF的查询方式 可以通过 Expression<Func<T, object>> 的方式所以我们这边提供一个泛型方式
,因为在创建 Elasticsearch 文档的时候我们已经建立了Map 文件 我们通过反射读取 PropertySearchName属性 就可以读取到我们的 PropertyName 这边 PropertySearchName 是自己定义的属性
为什么不反解Nest 的属性 针对不同类型需要反解的属性也是不相同的 所以避免麻烦 直接重新定义了新的属性 。代码如下:


1 public class PropertySearchNameAttribute: Attribute
2 {
3 public PropertySearchNameAttribute(string name)
4 {
5 Name = name;
6 }
7 public string Name { get; set; }
8 }
然后我们就可以来定义的们初始化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
98 #endregion
99
100 //初始化
101
102 #region 高亮
103
104 var titlePropertySearchName = (PropertySearchNameAttribute)
105 LoadAttributeHelper.LoadAttributeByType<Content, PropertySearchNameAttribute>(x => x.Title);
106
107 var list = data.Hits.Select(c => new Content
108 {
109 Key = c.Source.Key,
110 Title = (string) c.Highlights.Highlight(c.Source.Title, titlePropertySearchName.Name),
111 ImgUrl = c.Source.ImgUrl,
112 BaseType = c.Source.BaseType,
113 BelongMemberName = c.Source.BelongMemberName,
114 Brand = c.Source.Brand,
115 Code = c.Source.Code,
116 BrandFirstLetters = c.Source.BrandFirstLetters,
117 ClassificationName = c.Source.ClassificationName,
118 ResourceStatus = c.Source.ResourceStatus,
119 BrandGroupBy = c.Source.BrandGroupBy,
120 ClassificationGroupBy = c.Source.ClassificationGroupBy,
121 ClassificationCode = c.Source.ClassificationCode,
122 IsSelfSupport = c.Source.IsSelfSupport,
123 UnitPrice = c.Source.UnitPrice
124 }).ToList();
125
126 #endregion
127
128 var contentResponse = new ContentResponse
129 {
130 Records = (int) data.Total,
131 PageIndex = elasticsearchPage.PageIndex,
132 PageSize = elasticsearchPage.PageSize,
133 Contents = list,
134 BrandResponses = brandResponses,
135 ClassificationResponses = classificationResponses
136 };
137 return contentResponse;
关于排序、group by 、 高亮 的具体实现不做说明 思路基本一致 可以参考git上面的代码
源码详见 Git
https://github.com/wulaiwei/WorkData.Core/tree/master/WorkData/WorkData.ElasticSearch
为什么要对 Nest 进行封装:
1.项目组不可能每个人都来熟悉一道 Nest的 api ,缩小上手难度
2.规范查询方式
新增使用范例
https://github.com/wulaiwei/WorkData.Core/tree/master/WorkData/WorkDataEs
当前只内置了5条测试数据 你可以根据自己的需求添加自己想要的测试数据
你需要更改配置文件
https://github.com/wulaiwei/WorkData.Core/blob/master/WorkData/WorkDataEs/Config/commonConfig.json
参数"Uri": "http://:",
为你的Es服务端 ip及端口即可
来源:oschina
链接:https://my.oschina.net/u/4400196/blog/3283415