之前使用了下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不需要经过分析器分析,所以设置它的index为not_analyzed,而content和title字段需要全文搜索,并且是中文内容,所以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最厉害的分布式实时搜索由于自己的数据量没达到那个量级,也就没有尝试了。