Elasticsearch全文搜索的使用和原理

之前使用了下MongoDB的中文全文搜索,结果惨不忍睹。很多文中明明存在的词就是搜索不到,查文档才发现MongoDB免费版并没有提供针对中文的分词器,所以全文搜索的结果就可想而知了。查了一圈觉得免费的中文全文搜索解决方案里,最好的应该是elasticsearch了吧。所以最近学习了下,并把它用到了项目里,效果还不错。

Elasticsearch是一个基于Apache Lucene(TM)的开源搜索引擎。无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。

Elasticsearch可以简单的理解为是为Lucene套上了一层RESTful的接口,和一层分布式的扩展的包装层,使它不受语言限制地通过HTTP请求操作,也不受硬件性能限制地随意横向扩展。

安装与调错

安装教程其实到处都是,但是我在几台机器上安装都没能一次就启动起来,如果不是在配置不错的服务器上安装,多半也会踩些坑,这里记录一下。用的是Elasticsearch-5.5.1版本。

安装

$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.5.1.zip
$ unzip elasticsearch-5.5.1.zip
$ cd elasticsearch-5.5.1/

不能用root用户启动:

sudo chown -R noroot:noroot elasticsearch-5.5.1/   # 这里的noroot为一个非root用户

如果没有java8环境:

sudo apt-get install default-jdk  #安装java8
sudo apt-get install oracle-java8-installer #或者更新到java8

安装中文分词器ik

./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.1/elasticsearch-analysis-ik-5.5.1.zip
如果下载缓慢,可以先用其他vps下载,再本地安装
sudo ./bin/elasticsearch-plugin install file:///tmp/elasticsearch-analysis-i5.5.1.zip 

启动:

$ ./bin/elasticsearch

如果一起正常,访问localhost:9200就可以看到Elasticsearch的基本信息了:

curl localhost:9200
{
  "name" : "admgvq_",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "JfseDbLfTZe8U0nJDUtmxA",
  "version" : {
    "number" : "5.5.1",
    "build_hash" : "19c13d0",
    "build_date" : "2017-07-18T20:44:24.823Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.0"
  },
  "tagline" : "You Know, for Search"
}

调错

遇到问题时google了一堆别人博客里的方法,照着弄了一遍反而把自己弄晕了,大多数文章都只给了一个不知道哪里看来的解决办法,但并没有说是啥原因。后来发现英文文档其实说的挺清楚的,要是开始耐心看看反而会节约不少时间。相关的配置文档在这里。我遇到的问题是用户可用的文件描述符不够和虚拟内存不够。

  • 用户可用文件描述符不够:
max file descriptors [4096] for elasticsearch process likely too low, increase to at least [65536]

相关文档的解决办法是:

sudo su  
ulimit -n 65536 
su elasticsearch # Elasticsearch 可以改为其他非root用户

但是ulimit -n的修改只在当前shell的session有效,登出用户就失效了。更多关于ulimit`和文件描述符的信息可以看看这篇文章
还可以编辑文件 /etc/security/limits.conf:

sudo nano  /etc/security/limits.conf
添加
* hard nofile 65536
* soft nofile 65536

不过这只在下次登录时有效,因为init.d会忽略上面的修改。所以终极办法是编辑/etc/pam.d/su:

session    required   pam_limits.so

相当于是每次登陆时都去读取limits.conf中的配置。
然而,当我想把es的启动写到supervisor里,想让它随supervisor开机启动的时候,问题又回来了。因为supervisor开机启动并没有用户登录的过程,所以可用文件描述符并没有被修改到。暂时没有找到如何永久修改可用文件描述符,让es开机启动的方法,如果你刚好看到这篇文章,并找到了方法,恳请留言告诉我 :)

  • 虚拟内存不够
# 暂时
切换root用户:
sysctl -w vm.max_map_count=262144  
# 永久
nano /etc/sysctl.conf
添加
vm.max_map_count=655360
重启后验证:
sysctl vm.max_map_count

搜索原理

Elasticsearch中的存储结构和关系型数据库有些区别:

Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices   -> Types  -> Documents -> Fields

Elasticsearch集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型(types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段(Fields)(列)。

倒排索引

Elasticsearch会为每一个字段创建一个倒排索引(Inverted index),所谓倒排索引,就是将文档->词的对应关系变为词->文档的对应关系,比如:

 Docs  | brown | fox | quick | the |
 ------------------------------------
 Doc 1 |   X   |  X  |   X   |  X  |
 Doc 2 |       |  X  |   X   |     |
 Doc 3 |   X   |  X  |       |  X  |
  ...  |  ..   | ..  |  ..   | ..  |

倒排索引就是将这个对应关系矩阵作转置:

 Term  | Doc 1 | Doc 2 | Doc 3 | ...
 ------------------------------------
 brown |   X   |       |  X    | ...
 fox   |   X   |   X   |  X    | ...
 quick |   X   |   X   |       | ...
 the   |   X   |       |  X    | ...

所以倒排索引(Inverted index)叫反向索引或者转置索引可能还更容易理解些。有了词->文档的对应关系,当我们拿到搜索词时就可以很容易的找到包含他的文档了。搜索词可能也不止一个,这时候就把搜索词先分词,再逐个匹配,根据匹配程度打分,最后依据打分值返回搜索结果。

分析器

看到这里你就会发现,创建词->文档的对应关系是搜索的关键一步,一份文档中可能不是所有的内容都需要被搜索,比如标点、HTML标签、停用词等。而且每个词可能还有时态、单复数等形态的变化。针对中文还需要专门的分词器。这些工作都需要在分析器中完成。
一个分析器需要包含字符过滤器、分词器、标记过滤器三个部分:

  • 字符过滤器:过滤HTML标签等字符。
  • 分词器:根据标点或空格分割单词,当然中文需要运用其他的分词技术。
  • 标记过滤器:过滤停用词,替换大小写、时态、单复数等。

映射

Elasticsearch会在为索引创建映射(mapping)的时候指定分析器,一个映射定义了字段类型,每个字段的数据类型,以及字段被Elasticsearch处理的方式。映射还用于设置关联到类型上的元数据。

  • type规定了字段的数据类型,常用的数据类型:

    数据类型 type
    字符串 string
    整型 byte, short, integer, long
    浮点型 float, double
    Bool boolean
    时间 date
  • index表示数据以什么方式被索引:analyzed(分析此字段,默认)not_analyzed(不分析此字段), no(不能被搜索)

  • analyzer指定了用什么分析器。

  • search_analyzer表示搜索内容用什么分析器。

curl -X PUT 'localhost:9200/blog' -d '
{
  "mappings": {
    "post": {
      "properties": {
        "url": {
          "type": "string",
          "index": "not_analyzed"
        },
        "created_at": {
          "type": "date"
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        },
        "title": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_max_word"
        }
      }
    }
  }
}'

上面的例子中,url不需要经过分析器分析,所以设置它的indexnot_analyzed,而contenttitle字段需要全文搜索,并且是中文内容,所以analyzer使用中文分析器ik_max_word

使用

Elasticsearch的操作包括请求体查询和结构化查询两种,都是通过HTTP请求进行操作。不同的是前者更像是调用api,把请求内容都放在url里。而后者是把请求内容放到body中,更像是mongo的查询方式。结构化查询的功能较前者要强大很多,而且其他语言封装的es库也大多使用这种查询方式。
使用结构化查询添加索引:

PUT /blog/post/
{
    "title" : "...",
    "content" :  "...",
    "url" :        "http://...",
    "created_at" :      "2017-11-1"
}

调用python的Elasticsearch包:

from elasticsearch import Elasticsearch
es = Elasticsearch([{'host': '127.0.0.1', 'port': 9200}])
es.index(index='blog', doc_type='post', body={'title': title, 'content': content, 'url': url, 'created_at': created_at})

查询用得最多的是match全文搜索,当然还有很多其他类型的查询,不过如果不是全文搜索直接在关系型数据库中就完成了。

GET /_search
{
    "query": {"match": {'content': '...'}},
    "sort": [
               {"_score": {"order": "desc"}},
               {"created_at": {"order": "desc"}}
           ]
}

python:

res = es.search(index="blog",
                         body={"query": {"match": {'content': keyword.encode('utf-8')}}, "sort": [
                                    {"_score": {"order": "desc"}},
                                    {"created_at": {"order": "desc"}}
                                     ]})

数据同步

由于Elasticsearch只负责全文搜索功能,数据主要还是存储在SQL或者NoSQL里的,这时就需要将数据库里的数据实时同步到es里去,最简单的方法是在向数据库插入数据时,也同时向es插入一条索引,前提是数据库不会对这些数据经常做修改。
当然更好的办法是直接同步数据库操作。这个操作基本是利用数据库的操作日志完成的。比如mongo的mongo-connector.
使用mongo-connector需要先开启mongo的复制集:

mongod --replSet myDevReplSet

然后初始化复制集:

rs.initiate()

最后启动mongo-connector:

mongo-connector -m 127.0.0.1:27017 -t 127.0.0.1:9200 -d elastic_doc_manager

这时mongo的所有操作就会同步到es。但这样会把整个数据库的操作同步到es,而且es中的映射都是用的默认设置。所以还需要按照配置文档写一份配置文件,决定需要同步的库和配置映射。
我这里用到的只是Elasticsearch的全文搜索功能,当然es最厉害的分布式实时搜索由于自己的数据量没达到那个量级,也就没有尝试了。

Comments
Write a Comment
  • 请问博客有rss吗?想订阅一下,以便查看更新~

    • Melw00d reply

      @nearg1e https://jiayi.space/feed ^_^