搜索引擎 ElasticSearch.NET 客户端封装

2018 年 7 月 19 日 DotNet

(点击上方蓝字,可快速关注我们)

来源:飘渺丶散人

cnblogs.com/wulaiwei/p/9319821.html


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


/// <summary>

/// 分页类型

/// </summary>

public class PageEntity

{

    /// <summary>

    ///     每页行数

    /// </summary>

    public int PageSize { get; set; }


    /// <summary>

    ///     当前页

    /// </summary>

    public int PageIndex { get; set; }


    /// <summary>

    ///     总记录数

    /// </summary>

    public int Records { get; set; }


    /// <summary>

    ///     总页数

    /// </summary>

    public int Total

    {

        get

        {

            if (Records > 0)

                return Records % PageSize == 0 ? Records / PageSize : Records / PageSize + 1;


            return 0;

        }

    }

    /// <summary>

    ///  排序列

    /// </summary>

    public string Sidx { get; set; }


    /// <summary>

    ///     排序类型

    /// </summary>

    public string Sord { get; set; }

}


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的初始化操作已经完成了我们可以通过如下方式进行调用


var elasticsearchPage = new ElasticsearchPage<Content>("content")

{

    PageIndex = pageIndex,

    PageSize = pageSize

};

var searchRequest = elasticsearchPage.InitSearchRequest();


通过SearchRequest的源码我们可以得知,所有的查询都是基于内部属性进行(扩展的思路来自DapperExtensions):


3、QueryContainer的扩展 ,类似Where 语句:


我们定义一个 比较操作符 类似 Sql中的  like  !=  in  等等  


/// <summary>

///     比较操作符

/// </summary>

public enum ExpressOperator

{

    /// <summary>

    ///     精准匹配 term(主要用于精确匹配哪些值,比如数字,日期,布尔值或 not_analyzed 的字符串(未经分析的文本数据类型): )

    /// </summary>

    Eq,


    /// <summary>

    ///     大于

    /// </summary>

    Gt,


    /// <summary>

    ///     大于等于

    /// </summary>

    Ge,


    /// <summary>

    ///     小于

    /// </summary>

    Lt,


    /// <summary>

    ///     小于等于

    /// </summary>

    Le,


    /// <summary>

    ///     模糊查询 (You can use % in the value to do wilcard searching)

    /// </summary>

    Like,


    /// <summary>

    /// in 查询

    /// </summary>

    In

}


接着我们定义一个 如下接口,主要包括:


  • 提供返回一个 QueryContainer GetQuery方法 

  • 属性名称 PropertyName

  • 操作符 ExpressOperator

  • 谓词值 Value


/// <summary>

///     谓词接口

/// </summary>

public interface IPredicate

{

    QueryContainer GetQuery(QueryContainer query);

}


/// <summary>

///     基础谓词接口

/// </summary>

public interface IBasePredicate : IPredicate

{

    /// <summary>

    ///     属性名称

    /// </summary>

    string PropertyName { get; set; }

}


public abstract class BasePredicate : IBasePredicate

{

    public string PropertyName { get; set; }

    public abstract QueryContainer GetQuery(QueryContainer query);

}


/// <summary>

///     比较谓词

/// </summary>

public interface IComparePredicate : IBasePredicate

{

    /// <summary>

    ///     操作符

    /// </summary>

    ExpressOperator ExpressOperator { get; set; }

}


public abstract class ComparePredicate : BasePredicate

{

    public ExpressOperator ExpressOperator { get; set; }

}


/// <summary>

///     字段谓词

/// </summary>

public interface IFieldPredicate : IComparePredicate

{

    /// <summary>

    ///     谓词的值

    /// </summary>

    object Value { get; set; }

}


具体实现定义 FieldPredicate  并且继承如上接口,通过操作符映射为 Nest具体查询对象


public class FieldPredicate<T> : ComparePredicate, IFieldPredicate

    where T : class

{

    public object Value { get; set; }


    public override QueryContainer GetQuery(QueryContainer query)

    {

        switch (ExpressOperator)

        {

            case ExpressOperator.Eq:

                query = new TermQuery

                {

                    Field = PropertyName,

                    Value = Value

                };

                break;

            case ExpressOperator.Gt:

                query = new TermRangeQuery

                {

                    Field = PropertyName,

                    GreaterThan = Value.ToString()

                };

                break;

            case ExpressOperator.Ge:

                query = new TermRangeQuery

                {

                    Field = PropertyName,

                    GreaterThanOrEqualTo = Value.ToString()

                };

                break;

            case ExpressOperator.Lt:

                query = new TermRangeQuery

                {

                    Field = PropertyName,

                    LessThan = Value.ToString()

                };

                break;

            case ExpressOperator.Le:

                query = new TermRangeQuery

                {

                    Field = PropertyName,

                    LessThanOrEqualTo = Value.ToString()

                };

                break;

            case ExpressOperator.Like:

                query = new MatchPhraseQuery

                {

                    Field = PropertyName,

                    Query = Value.ToString()

                };

                break;

            case ExpressOperator.In:

                query = new TermsQuery

                {

                    Field = PropertyName,

                    Terms=(List<object>)Value

                };

                break;

            default:

                throw new ElasticsearchException("构建Elasticsearch查询谓词异常");

        }

        return query;

    }

}


4、定义好这些后我们就可以拼接我们的条件了,我们定义了 PropertyName  但是我们更倾向于一种类似EF的查询方式  可以通过 Expression<Func<T, object>> 的方式所以我们这边提供一个泛型方式,因为在创建 Elasticsearch  文档的时候我们已经建立了Map 文件 我们通过反射读取 PropertySearchName属性  就可以读取到我们的 PropertyName  这边 PropertySearchName 是自己定义的属性


为什么不反解Nest 的属性   针对不同类型需要反解的属性也是不相同的  所以避免麻烦 直接重新定义了新的属性 。代码如下:


public class PropertySearchNameAttribute: Attribute

{

    public PropertySearchNameAttribute(string name)

    {

        Name = name;

    }

    public string Name { get; set; }

}


然后我们就可以来定义的们初始化IFieldPredicate 的方法了


首先我们解析我们的需求:


1、我们需要一个Expression<Func<T, object>>


2、我们需要一个操作符


3、我们需要比较什么值


针对需求我们可以得到这样一个方法:


注:所依赖的反射方法详解文末


/// <summary>

///     工厂方法创建一个新的  IFieldPredicate 谓语: [FieldName] [Operator] [Value].

/// </summary>

/// <typeparam name="T">实例类型</typeparam>

/// <param name="expression">返回左操作数的表达式  [FieldName].</param>

/// <param name="op">比较运算符</param>

/// <param name="value">谓语的值.</param>

/// <returns>An instance of IFieldPredicate.</returns>

public static IFieldPredicate Field<T>(Expression<Func<T, object>> expression, ExpressOperator op, object value) where T : class

{

    var propertySearchName = (PropertySearchNameAttribute)

        LoadAttributeHelper.LoadAttributeByType<T, PropertySearchNameAttribute>(expression);


    return new FieldPredicate<T>

    {

        PropertyName = propertySearchName.Name,

        ExpressOperator = op,

        Value = value

    };

}


然后 我们就可以像之前拼接sql的方式来进行拼接条件了


就以我们项目中的业务需求做个演示


var predicateList = new List<IPredicate>();

//最大价格

if (requestContentDto.MaxPrice != null)

                predicateList.Add(Predicates.Field<Content>(x => x.UnitPrice, ExpressOperator.Le,

                    requestContentDto.MaxPrice));

//最小价格

if (requestContentDto.MinPrice != null)

                predicateList.Add(Predicates.Field<Content>(x => x.UnitPrice, ExpressOperator.Ge,

                    requestContentDto.MinPrice));


然后针对实际业务我们在写sql的时候就回有  (xx1  and  xx2) or  xx3 这样的业务需求了  


针对这种业务需求  我们需要在提供一个 IPredicateGroup 进行分组查询谓词


首先我们定义一个PredicateGroup 加入谓词时使用的操作符 GroupOperator


/// <summary>

///     PredicateGroup 加入谓词时使用的操作符

/// </summary>

public enum GroupOperator

{

    And,

    Or

}


然后我们定义 IPredicateGroup 及实现


/// <summary>

///     分组查询谓词

/// </summary>

public interface IPredicateGroup : IPredicate

{

    /// <summary>

    /// </summary>

    GroupOperator Operator { get; set; }


    IList<IPredicate> Predicates { get; set; }

}


/// <summary>

///     分组查询谓词

/// </summary>

public class PredicateGroup : IPredicateGroup

{

    public GroupOperator Operator { get; set; }

    public IList<IPredicate> Predicates { get; set; }


    /// <summary>

    ///     GetQuery

    /// </summary>

    /// <param name="query"></param>

    /// <returns></returns>

    public QueryContainer GetQuery(QueryContainer query)

    {

        switch (Operator)

        {

            case GroupOperator.And:

                return Predicates.Aggregate(query, (q, p) => q && p.GetQuery(query));

            case GroupOperator.Or:

                return Predicates.Aggregate(query, (q, p) => q || p.GetQuery(query));

            default:

                throw new ElasticsearchException("构建Elasticsearch查询谓词异常");

        }

    }

}


现在我们可以用 PredicateGroup来组装我们的 谓词


同样解析我们的需求:


1、我们需要一个GroupOperator


2、我们需要谓词列表 IPredicate[]


针对需求我们可以得到这样一个方法:


/// <summary>

///     工厂方法创建一个新的 IPredicateGroup 谓语.

///     谓词组与其他谓词可以连接在一起.

/// </summary>

/// <param name="op">分组操作时使用的连接谓词 (AND / OR).</param>

/// <param name="predicate">一组谓词列表.</param>

/// <returns>An instance of IPredicateGroup.</returns>

public static IPredicateGroup Group(GroupOperator op, params IPredicate[] predicate)

{

    return new PredicateGroup

    {

        Operator = op,

        Predicates = predicate

    };

}


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


用法:


//构建或查询

var predicateList= new List<IPredicate>();

//关键词

if (!string.IsNullOrWhiteSpace(requestContentDto.SearchKey))

predicateList.Add(Predicates.Field<Content>(x => x.Title, ExpressOperator.Like,

requestContentDto.SearchKey));

var predicate = Predicates.Group(GroupOperator.And, predicateList.ToArray());

//构建或查询

var predicateListOr = new List<IPredicate>();

if (!string.IsNullOrWhiteSpace(requestContentDto.Brand))

{

var array = requestContentDto.Brand.Split(',').ToList();

predicateListOr

.AddRange(array.Select

(item => Predicates.Field<Content>(x => x.Brand, ExpressOperator.Like, item)));

}


var predicateOr = Predicates.Group(GroupOperator.Or, predicateListOr.ToArray());


var predicatecCombination = new List<IPredicate> {predicate, predicateOr};

var pgCombination = Predicates.Group(GroupOperator.And, predicatecCombination.ToArray());


然后我们的  IPredicateGroup  优雅的和  ISearchRequest 使用呢  我们提供一个链式的操作方法 


/// <summary>

/// 初始化query

/// </summary>

/// <param name="searchRequest"></param>

/// <param name="predicate"></param>

public static ISearchRequest InitQueryContainer(this ISearchRequest searchRequest, IPredicate predicate)

{

    if (predicate != null)

    {

        searchRequest.Query = predicate.GetQuery(searchRequest.Query);

    }

    return searchRequest;

}


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


然后通过 Nest 的进行查询即可


var response = ElasticClient.Search<T>(searchRequest);


具体演示代码(以项目的业务)


var elasticsearchPage = new ElasticsearchPage<Content>("content")

{

    PageIndex = pageIndex,

    PageSize = pageSize

};


#region terms 分组


var terms = new List<IFieldTerms>();

var classificationGroupBy = "searchKey_classification";

var brandGroupBy = "searchKey_brand";


#endregion


var searchRequest = elasticsearchPage.InitSearchRequest();

var predicateList = new List<IPredicate>();

//分类ID

if (requestContentDto.CategoryId != null)

    predicateList.Add(Predicates.Field<Content>(x => x.ClassificationCode, ExpressOperator.Like,

        requestContentDto.CategoryId));

else

    terms.Add(Predicates.FieldTerms<Content>(x => x.ClassificationGroupBy, classificationGroupBy, 200));


//品牌

if (string.IsNullOrWhiteSpace(requestContentDto.Brand))

    terms.Add(Predicates.FieldTerms<Content>(x => x.BrandGroupBy, brandGroupBy, 200));

//供应商名称

if (!string.IsNullOrWhiteSpace(requestContentDto.BaseType))

    predicateList.Add(Predicates.Field<Content>(x => x.BaseType, ExpressOperator.Like,

        requestContentDto.BaseType));

//是否自营

if (requestContentDto.IsSelfSupport == 1)

    predicateList.Add(Predicates.Field<Content>(x => x.IsSelfSupport, ExpressOperator.Eq,

        requestContentDto.IsSelfSupport));

//最大价格

if (requestContentDto.MaxPrice != null)

    predicateList.Add(Predicates.Field<Content>(x => x.UnitPrice, ExpressOperator.Le,

        requestContentDto.MaxPrice));

//最小价格

if (requestContentDto.MinPrice != null)

    predicateList.Add(Predicates.Field<Content>(x => x.UnitPrice, ExpressOperator.Ge,

        requestContentDto.MinPrice));

//关键词

if (!string.IsNullOrWhiteSpace(requestContentDto.SearchKey))

    predicateList.Add(Predicates.Field<Content>(x => x.Title, ExpressOperator.Like,

        requestContentDto.SearchKey));


//规整排序

var sortConfig = SortOrderRule(requestContentDto.SortKey);

var sorts = new List<ISort>

{

    Predicates.Sort<Content>(sortConfig.Key, sortConfig.SortOrder)

};


var predicate = Predicates.Group(GroupOperator.And, predicateList.ToArray());

//构建或查询

var predicateListOr = new List<IPredicate>();

if (!string.IsNullOrWhiteSpace(requestContentDto.Brand))

{

    var array = requestContentDto.Brand.Split(',').ToList();

    predicateListOr

        .AddRange(array.Select

            (item => Predicates.Field<Content>(x => x.Brand, ExpressOperator.Like, item)));

}


var predicateOr = Predicates.Group(GroupOperator.Or, predicateListOr.ToArray());


var predicatecCombination = new List<IPredicate> {predicate, predicateOr};

var pgCombination = Predicates.Group(GroupOperator.And, predicatecCombination.ToArray());


searchRequest.InitQueryContainer(pgCombination)

    .InitSort(sorts)

    .InitHighlight(requestContentDto.HighlightConfigEntity)

    .InitGroupBy(terms);


var data = _searchProvider.SearchPage(searchRequest);


#region terms 分组赋值


var classificationResponses = requestContentDto.CategoryId != null

    ? null

    : data.Aggregations.Terms(classificationGroupBy).Buckets

        .Select(x => new ClassificationResponse

        {

            Key = x.Key.ToString(),

            DocCount = x.DocCount

        }).ToList();


var brandResponses = !string.IsNullOrWhiteSpace(requestContentDto.Brand)

    ? null

    : data.Aggregations.Terms(brandGroupBy).Buckets

        .Select(x => new BrandResponse

        {

            Key = x.Key.ToString(),

            DocCount = x.DocCount

        }).ToList();


#endregion


//初始化


#region 高亮


var titlePropertySearchName = (PropertySearchNameAttribute)

    LoadAttributeHelper.LoadAttributeByType<Content, PropertySearchNameAttribute>(x => x.Title);


var list = data.Hits.Select(c => new Content

{

    Key = c.Source.Key,

    Title = (string) c.Highlights.Highlight(c.Source.Title, titlePropertySearchName.Name),

    ImgUrl = c.Source.ImgUrl,

    BaseType = c.Source.BaseType,

    BelongMemberName = c.Source.BelongMemberName,

    Brand = c.Source.Brand,

    Code = c.Source.Code,

    BrandFirstLetters = c.Source.BrandFirstLetters,

    ClassificationName = c.Source.ClassificationName,

    ResourceStatus = c.Source.ResourceStatus,

    BrandGroupBy = c.Source.BrandGroupBy,

    ClassificationGroupBy = c.Source.ClassificationGroupBy,

    ClassificationCode = c.Source.ClassificationCode,

    IsSelfSupport = c.Source.IsSelfSupport,

    UnitPrice = c.Source.UnitPrice

}).ToList();


#endregion


var contentResponse = new ContentResponse

{

    Records = (int) data.Total,

    PageIndex = elasticsearchPage.PageIndex,

    PageSize = elasticsearchPage.PageSize,

    Contents = list,

    BrandResponses = brandResponses,

    ClassificationResponses = classificationResponses

};

return contentResponse;


关于排序、group by 、 高亮 的具体实现不做说明  思路基本一致  可以参考git上面的代码。


源码 


https://github.com/wulaiwei/WorkData.Core/tree/master/WorkData/WorkData.ElasticSearch


为什么要对 Nest 进行封装:


1、项目组不可能每个人都来熟悉一道 Nest的 api ,缩小上手难度


2、规范查询方式  


看完本文有收获?请转发分享给更多人

关注「DotNet」,提升.Net技能 

登录查看更多
1

相关内容

ElasticSearch是一个基于Lucene的分布式实时搜索引擎解决方案。属于Elastic Stack的一部分,同时另有 logstash, kibana, beats等开源项目。
【2020新书】实战R语言4,323页pdf
专知会员服务
102+阅读 · 2020年7月1日
【实用书】学习用Python编写代码进行数据分析,103页pdf
专知会员服务
198+阅读 · 2020年6月29日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
238+阅读 · 2020年5月21日
Python导论,476页pdf,现代Python计算
专知会员服务
262+阅读 · 2020年5月17日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
121+阅读 · 2020年5月10日
【SIGMOD2020-腾讯】Web规模本体可扩展构建
专知会员服务
30+阅读 · 2020年4月12日
【书籍推荐】简洁的Python编程(Clean Python),附274页pdf
专知会员服务
182+阅读 · 2020年1月1日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
96+阅读 · 2019年12月4日
Keras作者François Chollet推荐的开源图像搜索引擎项目Sis
专知会员服务
30+阅读 · 2019年10月17日
浅谈 Kubernetes 在生产环境中的架构
DevOps时代
11+阅读 · 2019年5月8日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
使用 Canal 实现数据异构
性能与架构
20+阅读 · 2019年3月4日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
Neo4j 和图数据库起步
Linux中国
8+阅读 · 2017年12月20日
干货|全文检索Solr集成HanLP中文分词
全球人工智能
4+阅读 · 2017年8月27日
33款可用来抓数据的开源爬虫软件工具 (推荐收藏)
数据科学浅谈
7+阅读 · 2017年7月29日
A survey on deep hashing for image retrieval
Arxiv
15+阅读 · 2020年6月10日
Learning to See Through Obstructions
Arxiv
7+阅读 · 2020年4月2日
Arxiv
35+阅读 · 2019年11月7日
Arxiv
5+阅读 · 2018年5月1日
Arxiv
5+阅读 · 2018年3月28日
VIP会员
相关VIP内容
【2020新书】实战R语言4,323页pdf
专知会员服务
102+阅读 · 2020年7月1日
【实用书】学习用Python编写代码进行数据分析,103页pdf
专知会员服务
198+阅读 · 2020年6月29日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
238+阅读 · 2020年5月21日
Python导论,476页pdf,现代Python计算
专知会员服务
262+阅读 · 2020年5月17日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
121+阅读 · 2020年5月10日
【SIGMOD2020-腾讯】Web规模本体可扩展构建
专知会员服务
30+阅读 · 2020年4月12日
【书籍推荐】简洁的Python编程(Clean Python),附274页pdf
专知会员服务
182+阅读 · 2020年1月1日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
96+阅读 · 2019年12月4日
Keras作者François Chollet推荐的开源图像搜索引擎项目Sis
专知会员服务
30+阅读 · 2019年10月17日
相关资讯
浅谈 Kubernetes 在生产环境中的架构
DevOps时代
11+阅读 · 2019年5月8日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
使用 Canal 实现数据异构
性能与架构
20+阅读 · 2019年3月4日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
Neo4j 和图数据库起步
Linux中国
8+阅读 · 2017年12月20日
干货|全文检索Solr集成HanLP中文分词
全球人工智能
4+阅读 · 2017年8月27日
33款可用来抓数据的开源爬虫软件工具 (推荐收藏)
数据科学浅谈
7+阅读 · 2017年7月29日
相关论文
A survey on deep hashing for image retrieval
Arxiv
15+阅读 · 2020年6月10日
Learning to See Through Obstructions
Arxiv
7+阅读 · 2020年4月2日
Arxiv
35+阅读 · 2019年11月7日
Arxiv
5+阅读 · 2018年5月1日
Arxiv
5+阅读 · 2018年3月28日
Top
微信扫码咨询专知VIP会员