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

Posted 程序员共读

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了搜索引擎 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、关于分页,我们定义如下分页对象:  


/// <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、规范查询方式  


  • 来源:飘渺丶散人

  • cnblogs.com/wulaiwei/p/9319821.html

  • 程序员共读整理发布,转载请联系作者获得授权

以上是关于搜索引擎 ElasticSearch.NET 客户端封装的主要内容,如果未能解决你的问题,请参考以下文章

Elasticsearch.Net.UnexpectedElasticsearchClientException:在从 ES 获取数据期间

如何从 elasticsearch.net / NEST 获取 geo_point 字段的距离

NEST/Elasticsearch.Net 发送原始JSON请求(Post数据)

通过elasticsearch.net中的字符串数组查询字符串数组

elk日志使用

go操作ElasticSearch7.3.2