针对 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.关于分页,我们定义如下分页对象:
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及端口即可