针对 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     }
View Code
  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
            };
        }
    }
View Code
  至此我们的SearchRequest的初始化操作已经完成了我们可以通过如下方式进行调用
1  var elasticsearchPage = new ElasticsearchPage<Content>("content")
2             {
3                 PageIndex = pageIndex,
4                 PageSize = pageSize
5             };
6 
7 var searchRequest = elasticsearchPage.InitSearchRequest();
View Code

      通过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     }
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     }
View Code

  具体实现定义 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     }
View Code

  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     }
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         }
View Code

然后 我们就可以像之前拼接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));
View Code

然后针对实际业务我们在写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     }
View Code

然后我们定义 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     }
View Code

现在我们可以用 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         }
View Code

这样我们就可以进行组装了

用法:

 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());
View Code

然后我们的  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         }
View Code

至此我们的基础查询方法已经封装完成

然后通过 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;
View Code

关于排序、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及端口即可

 

  

posted @ 2018-07-17 13:55  飘渺丶散人  阅读(5465)  评论(14编辑  收藏  举报