elasticsearch中文搜索引擎实践

Elasticsearch搜索引擎简介

搜索引擎基本是目前各互联网公司必备的组件。比如淘宝网的商品搜索,微信的公众号文章搜索,去哪儿的酒店机票搜索等,搜索是几乎各个app必备功能。

而elasticsearch作为最主流的搜索引擎之一,以其易用性和极强的可扩展性备受青睐。使用es可以满足我们产品的大多数搜索需求,是一个可以快速上手并应用于生产环境的搜索引擎。es不仅可以作为搜索引擎,更由于其优秀的可扩展性大量应用于产业界的日志分析系统,注明的elk组合也是人尽皆知的日志分析解决方案。es甚至支持机器学习

本文主要结合实际案例介绍如何通过elasticsearch实现搜索引擎功能,不涉及日志分析等功能。包括es搭建,索引设计,如何设计查询语句等,由于涉及的案例比较简单,且还未深入研究,有其他不完备之处,欢迎指正。

一个完整的搜索引擎,一般需要包括 数据采集,索引,过滤,排序,详情等阶段。es提供了倒排索引,filter,rank,详情(source)四个阶段的功能。数据收集器根据场景不同可以采用不同的方案。比如百度这种全品类搜索引擎,采用爬虫做数据采集器。而一般的垂直品类,通过db数据同步到es即可。垂直app一般是后者。


简易搜索引擎架构

其他详细资料请参考以下文档:

  1. Elasticsearch官网文档
    文档很详细,也都可看到最新的跟新进度
  2. Elasticsearch: 权威指南
    中文文档,依据2.0版本编写。思路清晰,适合入门学习。也深入了elasticsearch的原理讲解。缺点是不是最新的信息,有些语法或者实现已经过时。

项目需求

简化的项目需求如下:

  1. 搜索的目标是feed,可以参考今日头条等app的feed流
  2. 需要支持模糊搜索,暂时只考虑中文/英文 搜索,主要是中文搜索,不考虑拼音
  3. 搜索的字段包括: 标题,发布者用户名
  4. feed有多种类型,某种类型的feed在符合搜索条件的基础上需排到靠前的位置,比如影视剧类型feed排名靠前

技术方案

接下来介绍整体技术方案实施细节。

  1. 线上业务,要考虑搜索引擎中间件的可靠性,索引选择集群部署方案
  2. 考虑中文搜索,为了提高准确度就要使用中文分词器。默认的分词器对中文是按子分词,准确度很差。
  3. 我们需求中需将某些固定类型的feed排名靠前,并不是只展示这几种类型。这里涉及到如何计算搜索项的相关度,根据特定条件调整相关度分数。

搭建elasticsearch集群

es集群搭建步骤网上比较多,这里不详细介绍,需要的同学可从官网查找方法,或者在互联网查看详细步骤,这里简单说几点

  1. 可以通过docker安装elasticsearch,docker安装起来比较简单,可参考docker安装es
  2. 建议大家线上服务选择最新的稳定版本安装,比如7.X版本对集群的协调子系统做了升级,稳定性和创建集群的效率有所提升
  3. es6.8之前安全模块x-pack是收费的,6.8以后免费使用了,所以线上使用时务必做安全配置,起码有用户名密码
    4 es集群配置,集群配置的问题官网有详细介绍,网上也有很多资料,大家参考文档 elasticsearch配置

分词器

目前主流的来源中文分词器有ikhanlp,前者用起来简单但是词库较少,需要自己维护词库。后者支持自然需要处理,有丰富的词库,支持训练词库,使用起来稍微复杂些。

因为是demo,我们先选择ik做中文分词器。大家可以进一步学习,选择适合自己的分词器
ik分词器可以通过插件的形式安装进es,具体安装方法见 elasticsearch-analysis-ik。ik分为两种分词器,ik_smart和ik_max_words。两种分词器的分词效果如官网介绍

ik_max_word: 会将文本做最细粒度的拆分,比如会将“中华人民共和国国歌”拆分为
“中华人民共和国,中华人民,中华,华人,人民共和国,人民,人,民,共和国,共和,和,国国,国歌”,
会穷尽各种可能的组合,适合 Term Query;

ik_smart: 会做最粗粒度的拆分,比如会将“中华人民共和国国歌”拆分为“中华人民共和国,国歌”,
适合 Phrase 查询。

实践过程中发现如果使用ik_max_word做query和索引时使用的ik_max_word做分词时,由于切分的词较多,很多单独的字被切出来做搜索和倒排索引条件,导致精度较低。
我们变通一下,查询时使用ik_smart作为分词器,索引时使用ik_max_word作为分词器,可以提高查询精度。当然我们也可以通过es本身的查询策略,对精度做优化,需要细致研究下es api。

字段定义

{
    "feed": {
        "mappings": {
            "properties": {
                "title": {
                    "type": "text",
                    "analyzer": "ik_max_word"
                },
                "user_name": {
                    "type": "text",
                    "analyzer": "ik_max_word"
                },
                "fid": {
                    "type": "long"
                },
                "ftype": {
                    "type": "integer"
                }
            }
        }
    }
}

字段包括标题,用户名,feed id,feed 类型。我们只在es存储索引字段,其他详情可以在搜索到id后通过业务系统获取详细数据。减轻es存储和计算压力。

标题和用户名称做了分词

查询语句

根据标题和用户名做query,query时使用ik_smart分词器

{
    "from": 0,
    "size": 20,
    "query": {
        "bool": {
            "should": [{
                    "match": {
                        "title": {
                            "query": "18岁",
                            "operator": "OR",
                            "analyzer": "ik_smart"
                        }
                    }
                },
                {
                    "match": {
                        "user_name": {
                            "query": "18岁",
                            "operator": "OR",
                            "analyzer": "ik_smart"
                        }
                    }
                }
            ]
        }
    }
}

function score

现在还有一个问题,我们要让固定类型的feed排名靠前,可以加一个must或者sould的条件吗?答案是不合理。因为如果把类型作为筛选条件,那必然会导致搜索结果的召回率或者准确度有影响。比如放在must条件中,那搜索出来的结果只有固定条件的feed,导致结果集不完整。如果放在should条件中,那会导致搜索出来的结果有不满足标题和用户名条件,只符合类型条件的数据,属于错误数据。

es提供了function score函数,可以影响搜索结果评分,我们只需修改结果分数,不需影响结果集

{
    "from": 0,
    "size": 20,
    "query": {
        "function_score": {
            "query": {

                "bool": {
                    "should": [{
                            "match": {
                                "title": {
                                    "query": "18岁",
                                    "operator": "OR",
                                    "analyzer": "ik_smart"
                                }
                            }
                        },
                        {
                            "match": {
                                "user_name": {
                                    "query": "18岁",
                                    "operator": "OR",
                                    "analyzer": "ik_smart"
                                }
                            }
                        }
                    ]
                }
            },
            "functions": [{
                "filter": {
                    "match": {
                        "ftype": {
                            "query": 6
                        }
                    }
                },
                "weight": 4.0
            }]
        }
    }
}

好了,至此一个具备基本功能的中文搜索引擎构建完成了。仅供参考,想把搜索引擎做好,还需要进一步学习es的原理和api做具体调优工作。

推荐阅读更多精彩内容