转载

用插件来提升Elasticsearch过滤效率

Elasticsearch的查询方式

es是个基于lucence的全文搜索引擎,查询主要由两种组成:查询(query)和过滤(filter) 。

在查询方面,query和filter语句可以实现数据搜索功能,而query上独有的权重设计则让返回的数据在排列上更具灵活性,这是相对与sql语句的order来说的。Filter主要应用于对query返回的数据记录进行两次查询,由于它不考虑基于权重的打分,所以速度较快。

query查询会根据查询条件匹配符合条件的记录,并且根据记录中字段的权重(weight)计算分值(scroe), 分值越高就代表越匹配,在返回的结果集中就会排在更前面。

filter过滤同样会根据查询条件匹配符合条件的记录,但它不会去计算任何分值,也不会关心返回的排序问题,因此效率会高一点。而且在默认情况下,过滤不会保存缓存结果,如果常用,需要指定 "_cache" : true来打开缓存,这样下次相同条件下的查询可以加快速度。(注:实际使用时相同查询条件前提下,不指定也可以加快速度)

假设存在多个查询条件(比如:同时存在条件A和条件B) 的情况下,这时原则上使用查询和过滤是等价的,但在某些场合下使用过滤可以优化查询。如果匹配条件A的结果集是一个很小的集合,这是在集合上做对条件B的过滤的话,性能肯定优于对整个数据集做同时匹配条件A和条件B的查询。原理图如下:

用插件来提升Elasticsearch过滤效率

可以想象如果匹配条件A的数据集几乎等同于原数据集的场合下,那么先查询条件A后过滤条件B的方式等同于对整个数据集做同时满足条件A和条件B的查询。

适用场景

两个表的关联查询在sql中一句话就可以实现,但在es中就比较麻烦了。

虽然es官方文档支持两个表的关联查询(parent-child关系),但必须在插入数据时实现两个表在key上的关联,实现非常麻烦,而且不是很灵活。使用自定义filter的话,可以通过对某表查询返回的数据上(query语句实现)进行第二次过滤,达到对多表关联查询的目的。我的这种方法是解决多表关联查询的方案之一。

两种查询方式的DSL语句

1. 首先在es中建立索引和文档,然后加入测试数据。

在es的head插件中输入建立索引和文档的语句:

http://localhost:9200/testindex  {  "settings" : {         "number_of_replicas": "0",         "number_of_shards": "4"              },     "mappings" : {         "article" : {             "dynamic" : true,             "properties" : {                 "title" : {                     "type" : "string"                 },                 "author" : {                     "type" : "string"                 }             }         }     } }

建立索引名为testindex的索引.

文档名为article. 包含两个字段:title,author,类型都是string

如图所示:

(点击放大图像)

用插件来提升Elasticsearch过滤效率

2. 添加数据入文档。

用插件来提升Elasticsearch过滤效率

如图所示:

(点击放大图像)

用插件来提升Elasticsearch过滤效率

以此类推,再加入若干条数据,结果如下:

索引:testindex 文档:article  字段:id   title           author       1   good article     cy       2   good article2    cy2       3   good article3    cy3       4   bad article      pt

最后如下图所示。

(点击放大图像)

用插件来提升Elasticsearch过滤效率

2. 同时满足条件A和条件B的查询DSL

http://localhost:9200/testindex/_search/  {   "query": {      "bool": {        "must": [         { "term": { "title":   "good" }},         { "term": { "author":   "cy" }}                             ]     }   } }

Es中使用分词进行查询匹配,作者在建立文档的字段时没有特别指定使用哪种分词,默认是单词完全匹配,结果如下所示:

显示id=1的那条记录完全匹配

(点击放大图像)

用插件来提升Elasticsearch过滤效率

3. 先查询条件A后过滤条件B的DSL

http://localhost:9200/_search/  {   "query": {      "bool": {        "must": [         { "term": { "title":   "good"         }}         ],       "filter": [          { "term":  { "author": "cy" }}       ]     }   } }

结果如下所示:

同样显示id=1的那条记录完全匹配

(点击放大图像)

用插件来提升Elasticsearch过滤效率

使用定制过滤(filter)

如果需要使用复杂的查询(无需考虑权重),或者需要查询外接数据源(如需查询sql server),可以通过使用定制的过滤(filter)实现。

语句如下:

http://localhost:9200/testindex/_search {   "query": {      "bool": {        "must": [         { "term": { "title":   "good"     }}         ],       "filter": [          {"demofilter":{"author":"cy"}}       ]     }   } }

注:前两个语句没有在url中指定index名,由于使用的是默用的查询和过滤,执行速度依然快过在url中指定index名(testindex)的定制过滤(demofilter)查询DSL

结果如图所示:

(点击放大图像)

用插件来提升Elasticsearch过滤效率

三个语句在效果上是完全等价的,在执行速度上如下所示

A:同时满足条件A和条件B的查询DSL,花费7毫秒

B:先查询条件A后过滤条件B的DSL,花费3毫秒

C: 使用定制过滤的DSL,花费16毫秒

结论:使用定制过滤最花费时间,但同时也更灵活,适应更复杂的场景。

A,B,C的查询结果是一样的,但是得分(score)不一样。其中A查询中的两个

字段都会被es计算权重(默认为1),因此得分(1.507513)相对比B和C都高,如果调整字段的权重,那最后相应的得分也会改变。在B,C中过滤的字段是不被计算

权重的,得分相对较低(都是0.625).

定制过滤(filter)的程序实现

在编写es插件时,参考了ik分词插件,这里表示感谢。

代码已放入guthub, url: https://github.com/chensed/my-first-github

注:es版本变化较大,es1.x版本的插件和2.x的插件实现还是有较大差别。

es(2.11版本) 的过滤插件主要继承了3个类。

Plugin类:所有插件的入口,当 es在插件描述文件(plugin-descriptor.properties)中扫描到XXXcustomizedplugin(继承自Plugin类),es会装载这个类,用户可以在过载函数中登记定制的parser。代码如下:

public void onModule(IndicesModule module) {         module.registerQueryParser(FilterDemoParser.class); }

QueryParser类:过滤分析类,用户可以继承此类实现以下两个主要功能。

1. 标识过滤的关键词

public static final String NAME = "demofilter";       @Override     public String[] names() {         return new String[]{ NAME };     }

2. 分析并得到参数,启动过滤类

Filter@Override     public Filter parse(QueryParseContext parseContext) throws IOException, QueryParsingException {         XContentParser.Token token;        String name=null;         String value=null;               HashMap mp=new HashMap<String,String>(10);                 while ((token = parseContext.parser().nextToken()) != XContentParser.Token.END_OBJECT) {                       if (token==XContentParser.Token.FIELD_NAME)           continue;           name=parseContext.parser().currentName();          value=parseContext.parser().text();             mp.put(name, value);           //将参数保存到hashmap,本例name=author ,key=cy         }         return new DemoFilter(mp); }

Filter类:过滤类,用户继承此类,实现的主要功能就是从reader接口中得到数据,然后在数据记录上设置比特位过滤数据:”1”代表需要这条记录,“0“代表不需要这条记录。

代码如下所示:

@Override     public  DocIdSet getDocIdSet(LeafReaderContext context,  Bits acceptDocs) throws IOException {                 long startTime = System.currentTimeMillis();         int max = context.reader().maxDoc();   //得到记录条数                  OpenBitSet  bits = new OpenBitSet(max); //根据记录条数初始化bit集合                              DocsEnum termDocs =null;                  String name=null,val=null;                      if (paramMap.size()==1){   //从参数map中得到参数,从reader接口中得到数据进行过滤                        Iterator iter = paramMap.entrySet().iterator();           if (iter.hasNext()) {                Map.Entry entry = (Map.Entry) iter.next();                name = (String)entry.getKey();                val = (String)entry.getValue();           }                termDocs =context.reader().termDocsEnum(new Term(name,val));         }                    if(termDocs == null){              return null;   //不符合过滤条件的返回空,相当于比特位为0         }                      while(termDocs.nextDoc() != DocsEnum.NO_MORE_DOCS){            bits.set(termDocs.docID());    //得到符合过滤条件的docid(记录key), 在设置相应比特位为1          }           return bits;   //返回bit集合             }

作者介绍

陈渊 ,上海酷宝信息技术有限公司担任搜索引擎工程师, 关注搜索引擎,大数据挖掘。

感谢杜小芳对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号: InfoQChina )关注我们。

原文  http://www.infoq.com/cn/articles/use-plugins-to-enhance-elasticsearch-filtering-efficiency
正文到此结束
Loading...