mysql搜索面临的问题
- 性能低下
- 没有相关性排名 - 刚需
- 无法全文搜索
- 搜索不准确 - 没有分词
什么是全文搜索
生活中的数据总体分为两种:结构化数据和非结构化数据。
- 结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
- 非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等。
非结构化数据又一种叫法叫全文数据。
按照数据的分类,搜索也分为两种:
- 对结构化数据的搜索:如对数据库的搜索,用SQL语句。再如对元数据的搜索,如利用windows搜索对文件名,类型,修改时间进行搜索等。
- 对非结构化数据的搜索:如利用windows的搜索也可以搜索文件内容,Linux下的grep命令,再如用Google和百度可以搜索大量内容数据。
对非结构化数据也即对全文数据的搜索主要有两种方法:
顺序扫描法(Serial Scanning):
比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为要找的文件,接着看下一个文件,直到扫描完所有的文件。如利用windows的搜索也可以搜索文件内容,只是相当的慢。假如有一个80G硬盘,如果想在上面找到一个内容包含某字符串的文件,可能需要几个小时的时间。Linux下的grep命令也是这一种方式。这是一种比较原始的方法,但对于小数据量的文件,这种方法还是最直接,最方便的。但是对于大量的文件,这种方法的速度就很慢。
全文检索(Full-text Search):
即先建立索引,再对索引进行搜索。索引是从非结构化数据中提取出之后重新组织的信息。
Elasticsearch介绍
Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene™ 基础上的搜索引擎.当然 Elasticsearch 并不仅仅是 Lucene 那么简单,它不仅包括了全文搜索功能,还可以进行以下工作:
- 分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索。
- 实时分析的分布式搜索引擎。
- 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。
ES的适用场景
- 维基百科
- The Guardian、新闻
- Stack Overflow
- Github
- 电商网站、检索商品
- 日志数据分析、logstash采集日志、ES进行复杂的数据分析(ELK)
- 商品价格监控网站、用户设定价格阈值
- BI系统、商业智能、ES执行数据分析和挖掘
ES特点
- 可以作为一个大型的分布式集群(数百台服务器)技术,处理PB级数据,服务大公司,可以运行在单机上,服务小公司。
- ES不是什么新技术,主要是将全文检索、数据分析以及分布式技术合并在一起,才形成了独一无二的ES.lucene(全文检索)、商用的数据分析软件、分布式数据库 (mycat)
- 对用户而言,是开箱即用,非常简单,作为中小型的应用,直接3分钟部署ES,就可以作为生产环境的系统使用,数据量不大,操作不是很复杂。
- 数据库的功能面对很多领域是不够用的(事务,还有各种联机事务的操作):特殊的功能,比如全文检索、同义词处理、相关度排名、复杂数据分析、海量数据近实时处理;ES作为传统数据库的一个补充,提供了数据库所不能提供的很多功能。
Elasticsearch安装
关闭并禁用防火墙
systemctl stop firewalld.service
systemctl disable firewalld.service
systemctl status firewalld.service
通过docker安装elasticsearch
#新建es的config配置文件夹
mkdir -p /data/elasticsearch/config
#新建es的data目录
mkdir -p /data/elasticsearch/data
#新建es的plugins目录
mkdir -p /data/elasticsearch/plugins
#给目录设置权限
chmod 777 -R /data/elasticsearch
#写入配置到elasticsearch.yml中, 下面的 > 表示覆盖的方式写入, >>表示追加的方式写入,但是要确保http.host: 0.0.0.0不能被写入多次
echo "http.host: 0.0.0.0" >> /data/elasticsearch/config/elasticsearch.yml
#安装es
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms128m -Xmx256m" \
-v /data/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /data/elasticsearch/data:/usr/share/elasticsearch/data \
-v /data/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.10.1
通过docker安装 kibana
docker run -d --name kibana -e ELASTICSEARCH_HOSTS="http://es的IP:9200" -p 5601:5601 kibana:7.10.1
es中的type、index、mapping
mysql | ** |
---|---|
database | |
table | index(7.x开始type为固定值_doc) |
row | document |
column | field |
schema | mapping |
sql | DSL(Descriptor Structure Laguage) |
1.索引 **
有两个含义: 动词(insert) , 名词(表)
Elasticsearch将它的数据存储到一个或者多个索引(index)中,索引就像数据库**,可以向索引写入文档或者从索引中读取文档。
PUT /movies/movie/1
{
"title": "The Godfather",
"director": "Francis Ford Coppola",
"year": 1972,
"genres": ["Crime", "Drama"]
}
2.文档
文档(document)是Elasticsearch中的主要实体。所以,所有使用Elasticsearch最终都会归结到文档的搜索上
从客户端看,文档就是一个JSON对象,文档由字段构成,每个字段包含字段名以及一个或多个字段值。文档之间可能有各自不同的字段集合,文档没有固定的模式或强制的结构。
3.类型(7.x开始取消)
Elasticsearch中每个文档都有与之对应的类型(type)定义,允许在一个索引存储多种文档类型,并提供不同映射。
类型就像一个数据库表
4.映射
映射做的就是,存储分析链所需的信息。
主要就是设置一些参数,根据这些参数来做过滤还是分割词条。
新建数据
查看索引
GET _cat/indices
通过put+id新建数据
在customer下保存id为1的数据, 这里id是必须的
PUT /account/_doc/1
{
"name":"xjs",
"age":18,
"company":{
"name":"001",
"address":"beijing"
}
}
同一个请求发送多次,下面的信息会产生变化
"_version" : 11,
"result" : "updated", #这里第一次是created,后续都是updated
"_seq_no" : 10, #版本号
关于 _version和_seq_no的区别和作用参考官方文档
发送post不带id新建数据
POST user/_doc/
{
"name":"aa"
}
POST user/_doc/2
{
"name":"bb"
}
如果post带id就和put一样的操作了, put是不允许不带id的
post + _create
没有就创建,有就报错
POST user/_create/1
{
"name":"bobby"
}
查看index
GET _cat/indices //查看所有索引
GET /account //查看index的基本信息
获取数据
GET user/_doc/1
{
"_index" : "user", //所属
"_type" : "_doc",
"_id" : "1",
"_version" : 11, #版本号
"_seq_no" : 10, #并发控制,用于乐观锁
"_primary_term" : 1, #同_seq_no, 主分片重新分配, 如重启,就会变化
"found" : true,
"_source" : {
"name" : "aa"
}
}
只返回source的值
GET user/_source/1
搜索数据
Elasticsearch有两种查询方式
1. URI带有查询条件(轻量查询)
查询能力有限,不是所有的查询都可以使用此方式
2. 请求体中带有查询条件(复杂查询)
查询条件以JSON格式表现,作为查询请求的请求体,适合复杂的查询
通过url查询数据
请求参数位于_search端点之后,参数之间使用&分割,例如:
GET /_search?pretty&q=title:azure&explain=true&from=1&size=10&sort=title:asc&fields:user,title,content
搜索API的最基础的形式是没有指定任何查询的空搜索,它简单地返回集群中所有索引下的所有文档。
通过request body查询数据
GET account/_search
{
"query": {
"match_all": {}
}
}
更新和删除数据
给已有的数据新增字段
POST users/_doc/1
{
"age":18
}
此时会发现,已有的数据的name字段没有了,只有age字段,PUT也是同样的问题。
此时需要使用
POST user/_update/1
{
"doc": {
"age":18
}
}
删除数据和索引
//删除某条数据
DELETE users/_doc/1
//删除整个表index
DELETE users
批量插入和批量查询
批量操作(bulk)
用法
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
Postman批量插入最后一行一定要空行才行
批量获取(mget)
GET /_mget
{
"docs": [
{
"_index": "my-index-000001",
"_id": "1"
},
{
"_index": "my-index-000001",
"_id": "2"
}
]
}
query dsl查询
一个完整的查询语句应该如何写
from-limit
size-offset
对于es来说,from和size分页在数据量比较小的情况下可行,反之使用scroll
{
"query": {
"match_all": {}
},
"from":1,
"size":10
}
全文查询 - 分词
1. match查询(匹配查询)
文档
match
:模糊匹配,需要指定字段名,但是输入会进行分词,比如"hello world"会进行拆分为hello和world,然后匹配,如果字段中包含hello或者world,或者都包含的结果都会被查询出来,也就是说match是一个部分匹配的模糊查询。查询条件相对来说比较宽松。大小写不敏感,分词中满足其中一个词
GET user/_search
{
"query": {
"match": {
"address": "street"
}
}
}
2. match_phrase查询 短语查询
官方文档
match_phase
:会对输入做分词,但是需要结果中也包含所有的分词,而且顺序要求一样。以"hello world"为例,要求结果中必须包含hello和world,而且还要求他们是连着的,顺序也是固定的,hello that word不满足,world hello也不满足条件。大小写不敏感,分词必须满足短句条件
GET user/_search
{
"query": {
"match_phrase": {
"address": "Madison street"
}
}
}
3. multi_match查询
官方文档
multi_match
查询提供了一个简便的方法用来对多个字段执行相同的查询,即对指定的多个字段进行match查询
POST resume/_doc/12
{
"title": "后端工程师",
"desc": "多年go语言开发经验, 熟悉go的基本语法, 熟悉常用的go语言库",
"want_learn":"python语言"
}
POST resume/_doc/13
{
"title": "go工程师",
"desc": "多年开发经验",
"want_learn":"java语言"
}
POST resume/_doc/14
{
"title": "后端工程师",
"desc": "多年开发经验",
"want_learn":"rust语言"
}
GET resume/_search
{
"query": {
"multi_match": {
"query": "go",
"fields": ["title", "desc"]
}
}
}
//权重设置指明 ^
"fields": ["title^2", "desc"]
4、query_string查询
官方文档
query_string
:和match的方式类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛,很灵活。
GET user/_search
{
"query": {
"query_string": {
"default_field": "address",
"query": "Madison AND street"
}
}
}
match all查询
查询所有的数据
GET user/_search
{
"query": {
"match_all": {}
}
}
term 级别查询
1、term查询
term
: 这种查询和match在有些时候是等价的,比如查询单个的词hello,那么会和match查询结果一样,但是如果查询"hello world",结果就相差很大,因为这个输入不会进行分词,就是说查询的时候,是查询字段分词结果中是否有"hello world"的字样,而不是查询字段中包含"hello world"的字样,elasticsearch会对字段内容进行分词,“hello world"会被分成hello和world,不存在"hello world”,因此这里的查询结果会为空。这也是term查询和match的区别。
term是原子查询,不会做分词操作
"boost": 1.0 //设置权重
{
"query": {
"term": {
"address": "street"
}
}
}
2、range查询 - 范围查询
GET user/_search
{
"query":{
"range": {
"age": {
"gte": 20,
"lte": 30
}
}
}
}
3、exists查询
查询某个字段是否存在的数据
GET user/_search
{
"query": {
"exists": {
"field": "school"
}
}
}
4、fuzzy模糊查询
GET user/_search
{
"query": {
"match": {
"address": {
"query": "Midison streat",
"fuzziness": 1
}
}
}
}
GET /_search
{
"query": {
"fuzzy": {
"user.id": {
"value": "ki"
}
}
}
}
5、复合查询
官方文档
Elasticsearch bool查询对应Lucene BooleanQuery, 格式如下
{
"query":{
"bool":{
"must":[
],
"should":[
],
"must_not":[
],
"filter":[
]
}
}
}
must: 必须匹配,查询上下文,加分
should: 应该匹配,查询上下文,加分
must_not: 必须不匹配,过滤上下文,过滤
filter: 必须匹配,过滤上下文,过滤
bool查询采用了一种匹配越多越好的方法,因此每个匹配的must或should子句的分数将被加在一起,以提供每个文档的最终得分
GET user/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"state": "tn"
}
},
{
"range": {
"age": {
"gte": 20,
"lte": 30
}
}
}
],
"must_not": [
{
"term": {
"gender": "m"
}
}
],
"should": [
{
"match": {
"firstname": "Decker"
}
}
],
"filter": [
{
"range": {
"age": {
"gte": 25,
"lte": 30
}
}
}
]
}
}
}
Mapping
什么是 Mapping?
Mapping 类似于数据库中的表结构定义 schema,它有以下几个作用:
定义索引中的字段的名称定义字段的数据类型,比如字符串、数字、布尔字段,倒排索引的相关配置,比如设置某个字段为不被索引、记录 position 等在 ES 早期版本,一个索引下是可以有多个 Type ,从 7.0 开始,一个索引只有一个 Type,也可以说一个 Type 有一个 Mapping 定义。
Mapping 设置
在创建一个索引的时候,可以对 dynamic 进行设置,可以设成 false、true 或者 strict。
比如一个新的文档,这个文档包含一个字段,当 Dynamic 设置为 true 时,这个文档可以被索引进 ES,这个字段也可以被索引,也就是这个字段可以被搜索,Mapping 也同时被更新;当 dynamic 被设置为 false 时候,存在新增字段的数据写入,该数据可以被索引,但是新增字段被丢弃;当设置成 strict 模式时候,数据写入直接出错。
另外还有 index 参数,用来控制当前字段是否被索引,默认为 true,如果设为 false,则该字段不可被搜索。
参数 index_options 用于控制倒排索引记录的内容,有如下 4 种配置:
doc:只记录 doc id
freqs:记录 doc id 和 term frequencies
positions:记录 doc id、term frequencies 和 term position
offsets:记录 doc id、term frequencies、term position 和 character offects
另外,text 类型默认配置为 positions,其他类型默认为 doc,记录内容越多,占用存储空间越大。
null_value 主要是当字段遇到 null 值时的处理策略,默认为 NULL,即空值,此时 ES 会默认忽略该值,可以通过设定该值设定字段的默认值,另外只有 KeyWord 类型支持设定 null_value。
copy_to 作用是将该字段的值复制到目标字段,实现类似 _all 的作用,它不会出现在 _source 中,只用来搜索。
除了上述介绍的参数,还有许多参数,大家感兴趣的可以在官方文档中进行查看。
字段数据类型
ES 字段类型类似于 MySQL 中的字段类型,ES 字段类型主要有:核心类型、复杂类型、地理类型以及特殊类型,具体的数据类型如下图所示:
核心类型
从图中可以看出核心类型可以划分为字符串类型、数字类型、日期类型、布尔类型、基于 BASE64 的二进制类型、范围类型。
字符串类型
其中,在 ES 7.x 有两种字符串类型:text 和 keyword,在 ES 5.x 之后 string 类型已经不再支持了。
text 类型适用于需要被全文检索的字段,例如新闻正文、邮件内容等比较长的文字,text 类型会被 Lucene 分词器(Analyzer)处理为一个个词项,并使用 Lucene 倒排索引存储,text 字段不能被用于排序,如果需要使用该类型的字段只需要在定义映射时指定 JSON 中对应字段的 type 为 text。
keyword 适合简短、结构化字符串,例如主机名、姓名、商品名称等,可以用于过滤、排序、聚合检索,也可以用于精确查询。
数字类型
数字类型分为 long、integer、short、byte、double、float、half_float、scaled_float。
数字类型的字段在满足需求的前提下应当尽量选择范围较小的数据类型,字段长度越短,搜索效率越高,对于浮点数,可以优先考虑使用 scaled_float 类型,该类型可以通过缩放因子来精确浮点数,例如 12.34 可以转换为 1234 来存储。
日期类型
在 ES 中日期可以为以下形式:
格式化的日期字符串,例如 2020-03-17 00:00、2020/03/17
时间戳(和 1970-01-01 00:00:00 UTC 的差值),单位毫秒或者秒即使是格式化的日期字符串,ES 底层依然采用的是时间戳的形式存储。
布尔类型
JSON 文档中同样存在布尔类型,不过 JSON 字符串类型也可以被 ES 转换为布尔类型存储,前提是字符串的取值为 true 或者 false,布尔类型常用于检索中的过滤条件。
二进制类型
二进制类型 binary 接受 BASE64 编码的字符串,默认 store 属性为 false,并且不可以被搜索。
范围类型
范围类型可以用来表达一个数据的区间,可以分为5种:
integer_range、float_range、long_range、double_range 以及 date_range。
复杂类型
复合类型主要有对象类型(object)和嵌套类型(nested):
对象类型
JSON 字符串允许嵌套对象,一个文档可以嵌套多个、多层对象。可以通过对象类型来存储二级文档,不过由于 Lucene 并没有内部对象的概念,ES 会将原 JSON 文档扁平化,例如文档:
实际上 ES 会将其转换为以下格式,并通过 Lucene 存储,即使 name 是 object 类型:
嵌套类型
嵌套类型可以看成是一个特殊的对象类型,可以让对象数组独立检索,例如文档:
username 字段是一个 JSON 数组,并且每个数组对象都是一个 JSON 对象。如果将 username 设置为对象类型,那么 ES 会将其转换为:
可以看出转换后的 JSON 文档中 first 和 last 的关联丢失了,如果尝试搜索 first 为 wu,last 为 xy 的文档,那么成功会检索出上述文档,但是 wu 和 xy 在原 JSON 文档中并不属于同一个 JSON 对象,应当是不匹配的,即检索不出任何结果。
嵌套类型就是为了解决这种问题的,嵌套类型将数组中的每个 JSON 对象作为独立的隐藏文档来存储,每个嵌套的对象都能够独立地被搜索,所以上述案例中虽然表面上只有 1 个文档,但实际上是存储了 4 个文档。
地理类型
地理类型字段分为两种:经纬度类型和地理区域类型:
经纬度类型
经纬度类型字段(geo_point)可以存储经纬度相关信息,通过地理类型的字段,可以用来实现诸如查找在指定地理区域内相关的文档、根据距离排序、根据地理位置修改评分规则等需求。
地理区域类型
经纬度类型可以表达一个点,而 geo_shape 类型可以表达一块地理区域,区域的形状可以是任意多边形,也可以是点、线、面、多点、多线、多面等几何类型。
特殊类型
特殊类型包括 IP 类型、过滤器类型、Join 类型、别名类型等,在这里简单介绍下 IP 类型和 Join 类型,其他特殊类型可以查看官方文档。
IP 类型
IP 类型的字段可以用来存储 IPv4 或者 IPv6 地址,如果需要存储 IP 类型的字段,需要手动定义映射:
Join 类型
Join 类型是 ES 6.x 引入的类型,以取代淘汰的 _parent 元字段,用来实现文档的一对一、一对多的关系,主要用来做父子查询。
Join 类型的 Mapping 如下:
其中,my_join_field 为 Join 类型字段的名称;relations 指定关系:question 是 answer 的父类。
例如定义一个 ID 为 1 的父文档:
接下来定义一个子文档,该文档指定了父文档 ID 为 1:
什么是 Dynamic Mapping?
Dynamic Mapping 机制不需要手动定义 Mapping,ES 会自动根据文档信息来判断字段合适的类型,但是有时候也会推算的不对,比如地理位置信息有可能会判断为 Text,当类型如果设置不对时,会导致一些功能无法正常工作,比如 Range 查询。
类型自动识别
ES 类型的自动识别是基于 JSON 的格式,如果输入的是 JSON 是字符串且格式为日期格式,ES 会自动设置成 Date 类型;当输入的字符串是数字的时候,ES 默认会当成字符串来处理,可以通过设置来转换成合适的类型;如果输入的是 Text 字段的时候,ES 会自动增加 keyword 子字段,还有一些自动识别如下图所示:
下面例子是看看是怎么类型自动识别的,输入如下请求,创建索引:
然后使用 GET /mapping_test/_mapping 查看,结果如下图所示:
可以从结果中看出,ES 会根据文档信息自动推算出合适的类型。
哦豁,万一想修改 Mapping 的字段类型,能否更改呢?分以下两种情况来探究下:
修改 Mapping 字段类型?
如果是新增加的字段,根据 Dynamic 的设置分为以下三种状况:
当 Dynamic 设置为 true 时,一旦有新增字段的文档写入,Mapping 也同时被更新。
当 Dynamic 设置为 false 时,索引的 Mapping 是不会被更新的,新增字段的数据无法被索引,也就是无法被搜索,但是信息会出现在 _source 中。
当 Dynamic 设置为 strict 时,文档写入会失败。
另外一种是字段已经存在,这种情况下,ES 是不允许修改字段的类型的,因为 ES 是根据 Lucene 实现的倒排索引,一旦生成后就不允许修改,如果希望改变字段类型,必须使用 Reindex API 重建索引。
不能修改的原因是如果修改了字段的数据类型,会导致已被索引的无法被搜索,但是如果是增加新的字段,就不会有这样的影响。
Elasticsearch Analyze
Elasticsearch 中文本分析Analysis是把全文本转换成一系列的单词(term/token)的过程,也叫分词。文本分析是使用分析器 Analyzer 来实现的,Elasticsearch内置了分析器,用户也可以按照自己的需求自定义分析器。
为了提高搜索准确性,除了在数据写入时转换词条,匹配 Query 语句时候也需要用相同的分析器对查询语句进行分析。
Analyzer 的组成
Analyzer 由三部分组成:Character Filters、Tokenizer、Token Filters
Character Filters
Character Filters字符过滤器接收原始文本text的字符流,可以对原始文本增加、删除字段或者对字符做转换。一个Analyzer 分析器可以有 0-n 个按顺序执行的字符过滤器。
Tokenizer
Tokenizer 分词器接收Character Filters输出的字符流,将字符流分解成的那个的单词,并且输出单词流。例如空格分词器会将文本按照空格分解,将 “Quick brown fox!” 转换成 [Quick, brown, fox!]。分词器也负责记录每个单词的顺序和该单词在原始文本中的起始和结束偏移 offsets 。
一个Analyzer 分析器有且只有 1个分词器。
Token Filters
Token Filter单词过滤器接收分词器 Tokenizer 输出的单词流,可以对单词流中的单词做添加、移除或者转换操作,例如 lowercase token filter会将单词全部转换成小写,stop token filter会移除 the、and 这种通用单词, synonym token filter会往单词流中添加单词的同义词。<br /> Token filters不允许改变单词在原文档的位置以及起始、结束偏移量。
一个Analyzer 分析器可以有 0-n 个按顺序执行的单词过滤器。
Elasticsearch内置的分词器
Standard Analyzer - 默认分词器,按词切分,小写处理
Simple Analyzer - 按照非字母切分(符号被过滤),小写处理
Stop Analyzer - 小写处理,停用词过滤(the ,a,is)
Whitespace Analyzer - 按照空格切分,不转小写
Keyword Analyzer - 不分词,直接将输入当做输出
Patter Analyzer - 正则表达式,默认 \W+
Language - 提供了 30 多种常见语言的分词器
例子:The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.
Standard Analyzer
- 默认分词器
- 按词分类
- 小写处理
#standard
GET _analyze
{
"analyzer": "standard",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
输出:
[the,2,quick,brown,foxes,a,jumped,over,the,lazy,dog’s,bone]
Simple Analyzer
- 按照非字母切分,非字母则会被去除
- 小写处理
#simpe
GET _analyze
{
"analyzer": "simple",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
输出:
[the,quick,brown,foxes,jumped,over,the,lazy,dog,s,bone]
Stop Analyzer
- 小写处理
- 停用词过滤(the,a, is)
GET _analyze
{
"analyzer": "stop",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
输出:
[quick,brown,foxes,jumped,over,lazy,dog,s,bone]
Whitespace Analyzer
- 按空格切分
#stop
GET _analyze
{
"analyzer": "whitespace",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
输出:
[The,2,QUICK,Brown-Foxes,jumped,over,the,lazy,dog’s,bone.]
Keyword Analyzer
- 不分词,当成一整个 term 输出
#keyword
GET _analyze
{
"analyzer": "keyword",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
输出:
[The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.]
Patter Analyzer
- 通过正则表达式进行分词
- 默认是 \W+(非字母进行分隔)
GET _analyze
{
"analyzer": "pattern",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
输出:
[the,2,quick,brown,foxes,jumped,over,the,lazy,dog,s,bone]
Language Analyzer
支持语言:arabic, armenian, basque, bengali, bulgarian, catalan, czech, dutch, english, finnish, french, galician, german, hindi, hungarian, indonesian, irish, italian, latvian, lithuanian, norwegian, portuguese, romanian, russian, sorani, spanish, swedish, turkish.
#english
GET _analyze
{
"analyzer": "english",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
输出:
[2,quick,brown,fox,jump,over,the,lazy,dog,bone]
中文分词要比英文分词难,英文都以空格分隔,中文理解通常需要上下文理解才能有正确的理解,比如 [苹果,不大好吃]和
[苹果,不大,好吃],这两句意思就不一样。
常用的插件分词器
IK Analyzer - 对中文分词友好,支持远程词典热更新,有ik_smart 、ik_max_word 两种分析器
pinyin Analyzer - 可以对中文进行拼音分析,搜索时使用拼音即可搜索出来对应中文
ICU Analyzer - 提供了 Unicode 的支持,更好的支持亚洲语言
hanLP Analyzer - 基于NLP的中文分析器
分词的重要性
文本分词
单词是语言中重要的基本元素。一个单词可以代表一个信息单元,有着指代名称、功能、动作、性质等作用。在语言的进化史中,不断有新的单词涌现,也有许多单词随着时代的变迁而边缘化直至消失。根据统计,《汉语词典》中包含的汉语单词数目在37万左右,《牛津英语词典》中的词汇约有17万。
理解单词对于分析语言结构和语义具有重要的作用。因此,在机器阅读理解算法中,模型通常需要首先对语句和文本进行单词分拆和解析。
分词(tokenization)的任务是将文本以单词为基本单元进行划分。由于许多词语存在词型的重叠,以及组合词的运用,解决歧义性是分词任务中的一个挑战。不同的分拆方式可能表示完全不同的语义。如在以下例子中,两种分拆方式代表的语义都有可能:
南京市|长江|大桥
南京|市长|江大桥
分词的意义 - nlp
1.将复杂问题转化为数学问题
在 机器学习的文章 中讲过,机器学习之所以看上去可以解决很多复杂的问题,是因为它把这些问题都转化为了数学问题。
而 NLP 也是相同的思路,文本都是一些「非结构化数据」,需要先将这些数据转化为「结构化数据」,结构化数据就可以转化为数学问题了,而分词就是转化的第一步。
2.词是一个比较合适的粒度
词是表达完整含义的最小单位。
字的粒度太小,无法表达完整含义,比如”鼠“可以是”老鼠“,也可以是”鼠标“。
而句子的粒度太大,承载的信息量多,很难复用。比如”传统方法要分词,一个重要原因是传统方法对远距离依赖的建模能力较弱。”
中英文分词的3个典型区别
区别1:分词方式不同,中文更难
英文有天然的空格作为分隔符,但是中文没有。所以如何切分是一个难点,再加上中文里一词多意的情况非常多,导致很容易出现歧义。下文中难点部分会详细说明。
区别2:英文单词有多种形态
英文单词存在丰富的变形变换。为了应对这些复杂的变换,英文NLP相比中文存在一些独特的处理步骤,称为词形还原(Lemmatization)和词干提取(Stemming)。中文则不需要
词性还原:does,done,doing,did 需要通过词性还原恢复成 do。
词干提取:cities,children,teeth 这些词,需要转换为 city,child,tooth”这些基本形态
区别3:中文分词需要考虑粒度问题
例如「中国科学技术大学」就有很多种分法:
- 中国科学技术大学
- 中国 \ 科学技术 \ 大学
- 中国 \ 科学 \ 技术 \ 大学
粒度越大,表达的意思就越准确,但是也会导致召回比较少。所以中文需要不同的场景和要求选择不同的粒度。这个在英文中是没有的。
中文分词的3大难点
难点 1:没有统一的标准
目前中文分词没有统一的标准,也没有公认的规范。不同的公司和组织各有各的方法和规则。
难点 2:歧义词如何切分
例如「兵乓球拍卖完了」就有2种分词方式表达了2种不同的含义:
- 乒乓球 \ 拍卖 \ 完了
- 乒乓 \ 球拍 \ 卖 \ 完了
难点 3:新词的识别
信息爆炸的时代,三天两头就会冒出来一堆新词,如何快速的识别出这些新词是一大难点。比如当年「蓝瘦香菇」大火,就需要快速识别。
3种典型的分词方法
分词的方法大致分为 3 类:
- 基于词典匹配
- 基于统计
- 基于深度学习
给予词典匹配的分词方式
优点:速度快、成本低
缺点:适应性不强,不同领域效果差异大
基本思想是基于词典匹配,将待分词的中文文本根据一定规则切分和调整,然后跟词典中的词语进行匹配,匹配成功则按照词典的词分词,匹配失败通过调整或者重新选择,如此反复循环即可。代表方法有基于正向最大匹配和基于逆向最大匹配及双向匹配法。
基于统计的分词方法
优点:适应性较强
缺点:成本较高,速度较慢
这类目前常用的是算法是 **HMM、CRF、SVM、深度学习 **等算法,比如stanford、Hanlp分词工具是基于CRF算法。以CRF为例,基本思路是对汉字进行标注训练,不仅考虑了词语出现的频率,还考虑上下文,具备较好的学习能力,因此其对歧义词和未登录词的识别都具有良好的效果。
基于深度学习
优点:准确率高、适应性强
缺点:成本高,速度慢
例如有人员尝试使用双向LSTM+CRF实现分词器,其本质上是序列标注,所以有通用性,命名实体识别等都可以使用该模型,据报道其分词器字符准确率可高达97.5%。
常见的分词器都是使用机器学习算法和词典相结合,一方面能够提高分词准确率,另一方面能够改善领域适应性。
中文分词工具
下面排名根据 GitHub 上的 star 数排名:
- jieba
- Hanlp
- IK
- Stanford 分词
- ansj 分词器
- 哈工大 LTP
- KCWS分词器
- 清华大学THULAC
- ICTCLAS
英文分词工具
- Keras
- Spacy
- Gensim
- NLTK
总结
分词就是将句子、段落、文章这种长文本,分解为以字词为单位的数据结构,方便后续的处理分析工作。
分词的原因:
- 将复杂问题转化为数学问题
- 词是一个比较合适的粒度
- 深度学习时代,部分任务中也可以「分字」
中英文分词的3个典型区别:
- 分词方式不同,中文更难
- 英文单词有多种形态,需要词性还原和词干提取
- 中文分词需要考虑粒度问题
中文分词的3大难点
- 没有统一的标准
- 歧义词如何切分
- 新词的识别
3个典型的分词方式:
- 基于词典匹配
- 基于统计
- 基于深度学习
ik分词器安装和配置
1. 下载
下载地址
下载的版本一定要和es的版本保持一致
2. 解压拷贝到plugins目录下
将目录文件夹改名为ik
3. 设置权限
cd /data/elasticsearch/plugins
chmod 777 -R ik
4. docker start 重启容器
docker restart xxxx
5. 分词器使用
ik_smart 和 ik_max_word
GET _analyze
{
"text":"中国科学技术大学",
"analyzer": "ik_smart"
}
6、自定义分词器词库
cd /data/elasticsearch/plugins/ik/config/
mkdir custom
vim mydic.dic //添加自定义的词
>>>
中国牙膏
vim extra_stopword.dic //自定义取消不必要的词
>>>
的
是
呦
//返回上级
cd..
//修改配置文件,添加自定义的配置文件,填写相对路径
vim IKAnalyzer.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">custom/mydic.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">custom/extra_stopword.dic</entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
重启docker
倒排索引算法
Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene™ 基础上的搜索引擎.当然 Elasticsearch 并不仅仅是 Lucene 那么简单,它不仅包括了全文搜索功能,还可以进行以下工作:
- 分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索。
- 实时分析的分布式搜索引擎。
- 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。
基本概念
先说Elasticsearch的文件存储,Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条用户数据:
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"birthDate": "1990/05/01",
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
用Mysql这样的数据库存储就会容易想到建立一张User表,有balabala的字段等,在Elasticsearch里这就是一个_文档_,当然这个文档会属于一个User的_类型_,各种各样的类型存在于一个_索引_当中。这里有一份简易的将Elasticsearch和关系型数据术语对照表:
关系数据库 ⇒ 数据库 ⇒ 表 ⇒ 行 ⇒ 列(Columns)
Elasticsearch ⇒ 索引(Index) ⇒ 类型(type) ⇒ 文档(Docments) ⇒ 字段(Fields)
代码块12
一个 Elasticsearch 集群可以包含多个索引(数据库),也就是说其中包含了很多类型(表)。这些类型中包含了很多的文档(行),然后每个文档中又包含了很多的字段(列)。Elasticsearch的交互,可以使用Java API,也可以直接使用HTTP的Restful API方式,比如打算插入一条记录,可以简单发送一个HTTP的请求:
PUT /megacorp/employee/1
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
更新,查询也是类似这样的操作,具体操作手册可以参见Elasticsearch权威指南
索引
Elasticsearch最关键的就是提供强大的索引能力了,其实InfoQ的这篇时间序列数据库的秘密(2)——索引写的非常好,我这里也是围绕这篇结合自己的理解进一步梳理下,也希望可以帮助大家更好的理解这篇文章。
Elasticsearch索引的精髓:
一切设计都是为了提高搜索的性能
另一层意思:为了提高搜索的性能,难免会牺牲某些其他方面,比如插入/更新,否则其他数据库不用混了。前面看到往Elasticsearch里插入一条记录,其实就是直接PUT一个json的对象,这个对象有多个fields,比如上面例子中的_name, sex, age, about, interests_,那么在插入这些数据到Elasticsearch的同时,Elasticsearch还默默1的为这些字段建立索引–倒排索引,因为Elasticsearch最核心功能是搜索。
Elasticsearch是如何做到快速索引的
InfoQ那篇文章里说Elasticsearch使用的倒排索引比关系型数据库的B-Tree索引快,为什么呢?
什么是B-Tree索引?
二叉树查找效率是logN,同时插入新的节点不必移动全部节点,所以用树型结构存储索引,能同时兼顾插入和查询的性能。因此在这个基础上,再结合磁盘的读取特性(顺序读/随机读),传统关系型数据库采用了B-Tree/B+Tree这样的数据结构:
为了提高查询的效率,减少磁盘寻道次数,将多个值作为一个数组通过连续区间存放,一次寻道读取多个数据,同时也降低树的高度。
什么是倒排索引?
继续上面的例子,假设有这么几条数据(为了简单,去掉about, interests这两个field):
| ID | Name | Age | Sex |
| -- |:------------:| -----:| -----:|
| 1 | Kate | 24 | Female
| 2 | John | 24 | Male
| 3 | Bill | 29 | Male
ID是Elasticsearch自建的文档id,那么Elasticsearch建立的索引如下:
Name:
| Term | Posting List |
| -- |:----:|
| Kate | 1 |
| John | 2 |
| Bill | 3 |
Age:
| Term | Posting List |
| -- |:----:|
| 24 | [1,2] |
| 29 | 3 |
Sex:
| Term | Posting List |
| -- |:----:|
| Female | 1 |
| Male | [2,3] |
Posting List
Elasticsearch分别为每个field都建立了一个倒排索引,Kate, John, 24, Female这些叫term,而[1,2]就是Posting List。Posting list就是一个int的数组,存储了所有符合某个term的文档id。
看到这里,不要认为就结束了,精彩的部分才刚开始…
通过posting list这种索引方式似乎可以很快进行查找,比如要找age=24的同学,爱回答问题的小明马上就举手回答:我知道,id是1,2的同学。但是,如果这里有上千万的记录呢?如果是想通过name来查找呢?
Term Dictionary
Elasticsearch为了能快速找到某个term,将所有的term排个序,二分法查找term,logN的查找效率,就像通过字典查找一样,这就是Term Dictionary。现在再看起来,似乎和传统数据库通过B-Tree的方式类似啊,为什么说比B-Tree的查询快呢?
Term Index
B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,直接通过内存查找term,不读磁盘,但是如果term太多,term dictionary也会很大,放内存不现实,于是有了Term Index,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,可以理解term index是一颗树:
这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。
所以term index不需要存下所有的term,而仅仅是他们的一些前缀与Term Dictionary的block之间的映射关系,再结合FST(Finite State Transducers)的压缩技术,可以使term index缓存到内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘随机读的次数。
这时候爱提问的小明又举手了:"那个FST是神马东东啊?"
一看就知道小明是一个上大学读书的时候跟我一样不认真听课的孩子,数据结构老师一定讲过什么是FST。但没办法,我也忘了,这里再补下课:
FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output.
假设现在要将mop, moth, pop, star, stop and top(term index里的term前缀)映射到序号:0,1,2,3,4,5(term dictionary的block位置)。最简单的做法就是定义个Map,找到自己的位置对应入座就好了,但从内存占用少的角度想想,有没有更优的办法呢?答案就是:FST理论(可自行百度)
⭕️表示一种状态
–>表示状态的变化过程,上面的字母/数字表示状态变化和权重
将单词分成单个字母通过⭕️和–>表示出来,0权重不显示。如果⭕️后面出现分支,就标记权重,最后整条路径上的权重加起来就是这个单词对应的序号。
FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output.
FST以字节的方式存储所有的term,这种压缩方式可以有效的缩减存储空间,使得term index足以放进内存,但这种方式也会导致查找时需要更多的CPU资源。
压缩技巧
Elasticsearch里除了上面说到用FST压缩term index外,对posting list也有压缩技巧。
小明喝完咖啡又举手了:"posting list不是已经只存储文档id了吗?还需要压缩?"
嗯,再看最开始的例子,如果Elasticsearch需要对同学的性别进行索引(这时传统关系型数据库已经哭晕在厕所……),会怎样?如果有上千万个同学,而世界上只有男/女这样两个性别,每个posting list都会有至少百万个文档id。Elasticsearch是如何有效的对这些文档id压缩的呢?
Frame Of Reference
增量编码压缩,将大数变小数,按字节存储
首先,Elasticsearch要求posting list是有序的(为了提高搜索的性能,再任性的要求也得满足),这样做的一个好处是方便压缩,看下面这个图例:
如果数学不是体育老师教的话,还是比较容易看出来这种压缩技巧的。
未压缩之前,6个整数,每个整数用4个字节压缩,一共需要24个字节;通过将数据增量编码,将73,300,302…,变成73, 227, 2,没有丢掉数据信息的情况下,将数据减小。标识位8,本身需要一个字节空间,表示73,227,2每个数都用8bit = 1byte来存储,需要1byte * 3 = 3bytes内存空间来存储,标识位5,本身需要1个字节空间存储,表示30,11,29都用5bit来存储,5bit * 3 = 15bit,但是需要16bit = 2bytes来存储,那么8,73,227,2,5,30,11,29,一共需要1 + 3 + 1 + 2 = 7bytes空间即可。
原理就是通过增量,将原来的大数变成小数仅存储增量值,再精打细算按bit排好队,最后通过字节存储,而不是大大咧咧的尽管是2也是用int(4个字节)来存储。
Roaring bitmaps
说到Roaring bitmaps,就必须先从bitmap说起。Bitmap是一种数据结构,假设有某个posting list:
[1,3,4,7,10]
对应的bitmap就是:
[1,0,1,1,0,0,1,0,0,1]
非常直观,用0/1表示某个值是否存在,比如10这个值就对应第10位,对应的bit值是1,这样用一个字节就可以代表8个文档id,旧版本(5.0之前)的Lucene就是用这样的方式来压缩的,但这样的压缩方式仍然不够高效,如果有1亿个文档,那么需要12.5MB的存储空间,这仅仅是对应一个索引字段(往往会有很多个索引字段)。于是有人想出了Roaring bitmaps这样更高效的数据结构。
Bitmap的缺点是存储空间随着文档个数线性增长,Roaring bitmaps需要打破这个魔咒就一定要用到某些指数特性:
将posting list按照65535为界限分块,比如第一块所包含的文档id范围在065535之间,第二块的id范围是65536131071,以此类推。再用<商,余数>的组合表示每一组id,这样每组里的id范围都在0~65535内了,剩下的就好办了,既然每组id不会变得无限大,那么就可以通过最有效的方式对这里的id存储。
"为什么是以65535为界限?"
程序员的世界里除了1024外,65535也是一个经典值,因为它=2^16-1,正好是用2个字节能表示的最大数,一个short的存储单位,注意到上图里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大块,用节省点用bitset存,小块就豪爽点,2个字节我也不计较了,用一个short[]存着方便。
那为什么用4096来区分大块还是小块呢?
个人理解:都说程序员的世界是二进制的,4096*2bytes = 8192bytes < 1KB, 磁盘一次寻道可以顺序把一个小块的内容都读出来,再大一位就超过1KB了,需要两次读。
联合索引
上面说了半天都是单field索引,如果多个field索引的联合查询,倒排索引如何满足快速查询的要求呢?
- 利用跳表(Skip list)的数据结构快速做“与”运算,或者
- 利用上面提到的bitset按位“与”
先看看跳表的数据结构:
将一个有序链表level0,挑出其中几个元素到level1及level2,每个level越往上,选出来的指针元素越少,查找时依次从高level往低查找,比如55,先找到level2的31,再找到level1的47,最后找到55,一共3次查找,查找效率和2叉树的效率相当,但也是用了一定的空间冗余来换取的。
假设有下面三个posting list需要联合索引:
如果使用跳表,对最短的posting list中的每个id,逐个在另外两个posting list中查找看是否存在,最后得到交集的结果。
如果使用bitset,就很直观了,直接按位与,得到的结果就是最后的交集。
总结和思考
Elasticsearch的索引思路:
将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数(同时也利用磁盘顺序读特性),结合各种奇技淫巧的压缩算法,用及其苛刻的态度使用内存。
所以,对于使用Elasticsearch进行索引时需要注意:
- 不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的
- 同样的道理,对于String类型的字段,不需要analysis的也需要明确定义出来,因为默认也是会analysis的
- 选择有规律的ID很重要,随机性太大的ID(比如java的UUID)不利于查询
关于最后一点,个人认为有多个因素:
其中一个(也许不是最重要的)因素: 上面看到的压缩算法,都是对Posting list里的大量ID进行压缩的,那如果ID是顺序的,或者是有公共前缀等具有一定规律性的ID,压缩比会比较高;
全文检索
前面是针对单个字段,name,age说明的快速索引解决方案,elastic通常是全文检索,文档类型的,通常需要将文档分词。比如有这样2个文档:
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,首先将每个文档的 content 域拆分成单独的 词(称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:
Term Doc_1 Doc_2-------------------------Quick | | XThe | X |brown | X | Xdog | X |dogs | | Xfox | X |foxes | | Xin | | Xjumped | X |lazy | X | Xleap | | Xover | X | Xquick | X |summer | | Xthe | X |
那么,对于brown这个词来说,Posting list就是[1, 2],quick的Posting list就是[2],当然了,对于大小写,同义词都是要考虑的,这样就变成了前面的单个词那样,同样利用倒排索引,增量编码,大数变小数,使用字节存储。