文档分析器
文档分析主要包括下面几个过程:
- 将一块文本分成适合于倒排索引的独立的 词条
- 将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 recall
分析器执行上面的工作。
传统分析器的结构
分析器实际上是将三个功能封装到了一个包里:
- 字符过滤器:
首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉 HTML,或者将 & 转化成 and。 - 分词器
其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。 - Token 过滤器
最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。
内置分析器
Elasticsearch 还附带了可以直接使用的预包装的分析器。接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条:
- 标准分析器
标准分析器是 Elasticsearch 默认使用的分析器。它是分析各种语言文本最常用的选择。 它根据 Unicode 联盟 定义的单词边界
划分文本。删除绝大部分标点。最后,将词条小写。
它会产生:set, the, shape, to, semi, transparent, by, calling, set_trans, 5 - 简单分析器
简单分析器在任何不是字母的地方分隔文本,将词条小写。它会产生:set, the, shape, to, semi, transparent, by, calling, set, trans - 空格分析器
空格分析器在空格的地方划分文本。
它会产生:Set, the, shape, to, semi-transparent, by, calling, set_trans(5) - 语言分析器
特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),
它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。
英语 分词器会产生下面的词条:set, shape, semi, transpar, call, set_tran, 5;注意看 transparent、 calling 和 set_trans 已经变为词根格式
分析器测试
- 当你查询一个 全文域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。
- 当你查询一个 精确值 域时,不会分析查询字符串,而是搜索你指定的精确值。
为了理解发生了什么,你可以使用 analyze API
来看文本是如何被分析的。
- 在消息体里,指定分析器和要分析的文本
//GET http://localhost:9200/_analyze { "analyzer": "standard", "text": "Text to analyze" }
- 结果中每个元素代表一个单独的词条:
{ "tokens": [ { "token": "text", "start_offset": 0, "end_offset": 4, "type": "<ALPHANUM>", "position": 1 }, { "token": "to", "start_offset": 5, "end_offset": 7, "type": "<ALPHANUM>", "position": 2 }, { "token": "analyze", "start_offset": 8, "end_offset": 15, "type": "<ALPHANUM>", "position": 3 } ] }
- token 是实际存储到索引中的词条。
- position 指明词条在原始文本中出现的位置。
- start_offset 和 end_offset 指明字符在原始字符串中的位置。
IK 分词器
首先我们通过 Postman 发送 GET 请求查询分词效果
//GET http://localhost:9200/_analyze
{
"text":"测试单词"
}
结果如下,可以看到ES 的默认分词器无法识别中文中测试、单词这样的词汇,而是简单的将每个字拆完分为一个词
{
"tokens": [
{
"token": "测",
"start_offset": 0,
"end_offset": 1,
"type": "<IDEOGRAPHIC>",
"position": 0
},
{
"token": "试",
"start_offset": 1,
"end_offset": 2,
"type": "<IDEOGRAPHIC>",
"position": 1
},
{
"token": "单",
"start_offset": 2,
"end_offset": 3,
"type": "<IDEOGRAPHIC>",
"position": 2
},
{
"token": "词",
"start_offset": 3,
"end_offset": 4,
"type": "<IDEOGRAPHIC>",
"position": 3
}
]
}
这样的结果显然不符合我们的使用要求,所以我们需要下载 ES 对应版本的中文分词器。
IK 中文分词器的安装
-
我们这里采用
IK 中文分词器
,下载地址为:
https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.8.0
-
将解压后的后的文件夹放入 ES 根目录下的
plugins
目录下,重启 ES 即可使用。
-
我们这次加入新的查询参数"analyzer":
"ik_max_word"
//GET http://localhost:9200/_analyze { "text":"测试单词", "analyzer":"ik_max_word" }
ik_max_word
:会将文本做最细粒度的拆分ik_smart
:会将文本做最粗粒度的拆分
-
结果如下:
{ "tokens": [ { "token": "测试", "start_offset": 0, "end_offset": 2, "type": "CN_WORD", "position": 0 }, { "token": "单词", "start_offset": 2, "end_offset": 4, "type": "CN_WORD", "position": 1 } ] }
IK 的词汇扩展
对于一些人名或者专有名词我们希望ES会将其识别为一个词,而不进行分词,这是我们就需要进行词汇扩展。
- 比如当我们对 “弗雷尔卓德” 进行查询时:
//GET http://localhost:9200/_analyze { "text":"弗雷尔卓德", "analyzer":"ik_max_word" }
- 结果如下:
{ "tokens": [ { "token": "弗", "start_offset": 0, "end_offset": 1, "type": "CN_CHAR", "position": 0 }, { "token": "雷", "start_offset": 1, "end_offset": 2, "type": "CN_CHAR", "position": 1 }, { "token": "尔", "start_offset": 2, "end_offset": 3, "type": "CN_CHAR", "position": 2 }, { "token": "卓", "start_offset": 3, "end_offset": 4, "type": "CN_CHAR", "position": 3 }, { "token": "德", "start_offset": 4, "end_offset": 5, "type": "CN_CHAR", "position": 4 } ] }
仅仅可以得到每个字的分词结果,我们需要做的就是使分词器识别到弗雷尔卓德也是一个词语。
- 首先进入 ES 根目录中的 plugins 文件夹下的 ik 文件夹,进入 config 目录,创建
custom.dic
文件,写入弗雷尔卓德
。 - 同时打开
IKAnalyzer.cfg.xml
文件,将新建的 custom.dic 配置其中,重启 ES 服务器。
自定义分析器
虽然 Elasticsearch
带有一些现成的分析器,然而在分析器 Elasticsearch
真正的强大之处在于,你可以通过在一个适合你的特定数据的设置之中组合字符过滤器、分词器、词汇单元过滤器来创建自定义的分析器。
在 分析器结构中 我们说过,一个 分析器 就是在一个包里面组合了三种函数的一个包装器, 三种函数按照顺序被执行:
-
字符过滤器
字符过滤器 用来 整理 一个尚未被分词的字符串。例如,如果我们的文本是 HTML 格式的,它会包含像<p>
或者<div>
这样的 HTML 标签,这些标签是我们不想索引的。
我们可以使用 html 清除 字符过滤器 来移除掉所有的 HTML 标签,并且像把Á
转换为相对应的Unicode 字符
Á 这样,转换 HTML 实体。一个分析器可能有 0 个或者多个字符过滤器。 -
分词器
一个分析器 必须 有一个唯一的分词器。 分词器把字符串分解成单个词条或者词汇单元。 标准分析器里使用的 标准 分词器 把一个字符串根据单词边界分解成单个词条,并且移除掉大部分的标点符号,然而还有其他不同行为的分词器存在。例如,- 关键词分词器: 完整地输出 接收到的同样的字符串,并不做任何分词。
- 空格分词器: 只根据空格分割文本 。
- 正则分词器: 根据匹配正则表达式来分割文本 。
-
词单元过滤器
经过分词,作为结果的词单元流会按照指定的顺序通过指定的词单元过滤器 。词单元过滤器可以修改、添加或者移除词单元。我们已经提到过lowercase 和 stop 词过滤器
,- 但是在 Elasticsearch 里面还有很多可供选择的词单元过滤器。比如,
词干过滤器
把单词遏制 为词干。 ascii_folding
过滤器移除变音符,把一个像"très"
这样的词转换为"tres"
ngram 和 edge_ngram 词单元过滤器:
可以产生 适合用于部分匹配或者自动补全的词单元。
- 但是在 Elasticsearch 里面还有很多可供选择的词单元过滤器。比如,
-
接下来,我们看看如何创建自定义的分析器,代码如下:
// PUT http://localhost:9200/my_index { "settings": { "analysis": { "char_filter": { "&_to_and": { "type": "mapping", "mappings": [ "&=> and "] }}, "filter": { "my_stopwords": { "type": "stop", "stopwords": [ "the", "a" ] }}, "analyzer": { "my_analyzer": { "type": "custom", "char_filter": [ "html_strip", "&_to_and" ], "tokenizer": "standard", "filter": [ "lowercase", "my_stopwords" ] }} }}}
-
索引被创建以后,使用
analyze API
来 测试这个新的分析器:// GET http://127.0.0.1:9200/my_index/_analyze { "text":"The quick & brown fox", "analyzer": "my_analyzer" }
-
结果展示如下:
{ "tokens": [ { "token": "quick", "start_offset": 4, "end_offset": 9, "type": "<ALPHANUM>", "position": 1 }, { "token": "and", "start_offset": 10, "end_offset": 11, "type": "<ALPHANUM>", "position": 2 }, { "token": "brown", "start_offset": 12, "end_offset": 17, "type": "<ALPHANUM>", "position": 3 }, { "token": "fox", "start_offset": 18, "end_offset": 21, "type": "<ALPHANUM>", "position": 4 } ] }
文档冲突(并发控制)
场景叙述
- 当我们使用
index API
更新文档 ,可以一次性读取原始文档,做我们的修改,然后重新索引 整个文档 。 最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在 Elasticsearch 中。 - 试想我们使用 Elasticsearch 存储我们网上商城商品库存的数量, 每次我们卖一个商品的时候,我们在 Elasticsearch 中将库存数量减少。有一天,管理层决定做一次促销。突然地,我们一秒要卖好几个商品。 假设有两个 web 程序并行运行,每一个都同时处理所有商品的销售. 并发问题
- 如上图所示,在高并发的情况下,count 有可能减了两次才是99
上面情况在传统数据库中的解决方案一般有两种:
- 悲观并发控制: 操作时就将其锁住
- 乐观并发控制:设置版本号,循环等待
ES 中的乐观并发控制
Elasticsearch 是分布式的。当文档创建、更新或删除时, 新版本的文档必须复制到集群中的其他节点。Elasticsearch 也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许 顺序是乱的 。
Elasticsearch 需要一种方法确保文档的旧版本不会覆盖新的版本。
- 当我们之前讨论
index
,GET
和delete
请求时,我们指出每个文档都有一个_version
(版本)号,当文档被修改时版本号递增。 - Elasticsearch 使用这个 version 号来确保变更以正确顺序得到执行。
- 如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
我们可以利用 version 号
来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version 号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。
{
"error": {
"root_cause": [
{
"type": "action_request_validation_exception",
"reason": "Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use `if_seq_no` and `if_primary_term` instead;"
}
],
"type": "action_request_validation_exception",
"reason": "Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use `if_seq_no` and `if_primary_term` instead;"
},
"status": 400
}
老的版本 es 使用 version,但是新版本不支持了,会报下面的错误,提示我们用
if_seq_no
和if_primary_term
外部版本控制
- 一个常见的设置是使用其它数据库作为主要的数据存储,使用
Elasticsearch
做数据检索, 这意味着主数据库的所有更改发生时都需要被复制到 Elasticsearch ,如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并发问题。 - 如果你的主数据库已经有了版本号 或一个能作为版本号的字段值比如
timestamp
那么你就可以在 Elasticsearch 中通过增加version_type=external
到查询字符串的方式重用 - 这些相同的版本号, 版本号必须是大于零的整数, 且小于
9.2E+18
, 一个 Java 中 long 类型的正值。
外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同,Elasticsearch 不是检查当前 _version
和请求中指定的版本号是否相同, 而是检查当前 _version
是否 小于 指定的版本号。 如果请求成功,外部的版本号作为文档的新 _version
进行存储。
Kibana
Kibana 是一个免费且开放的用户界面,能够让你对 Elasticsearch 数据进行可视化,并让你在 Elastic Stack
中进行导航。
-
你可以进行各种操作,从跟踪查询负载,到理解请求如何流经你的整个应用,都能轻松完成。
-
下载地址:
https://artifacts.elastic.co/downloads/kibana/kibana-7.8.0-windows-x86_64.zip
-
解压缩下载的 zip 文件
-
修改
config/kibana.yml
文件# 默认端口 server.port: 5601 # ES 服务器的地址 elasticsearch.hosts: ["http://localhost:9200"] # 索引名 kibana.index: ".kibana" # 支持中文 i18n.locale: "zh-CN"
-
Windows 环境下执行
bin/kibana.bat
文件
-
通过浏览器访问 :
http://localhost:5601