文本分析是将非结构化文本 (如电子邮件的正文或产品描述) 转换为结构化格式,以便于搜索的过程。
如果索引中包含文本字段,或者文本搜索没有返回预期的结果,就可以通过配置文本 Analysis 来微调搜索。
1. Text Analysis 概述
文本分析让 Elasticsearch 能够执行全文搜索,返回所有相关结果,而不仅仅是精确匹配。
- 精确匹配
- 2017-01-01,exact value,搜索的时候,必须输入 2017-01-01,才能搜索出来。如果你输入一个 01,是搜索不出来的;
- 全文搜索
- 就不是说单纯的只是匹配完整的一个值,而是可以对值进行拆分词语后(分词)进行匹配,也可以通过缩写、时态、大小写、同义词等进行匹配;
- 你在文本里搜索 Quick fox jumps,可能不仅希望返回包含 quick foxs 的文档,也希望返回 fox leap 的文档,因为 jump 和 leap 是同义词,相同意思的也满足需求可以作为结果返回;
1.1 Tokenization
- 文本分析通过 tokenization 分词操作,让全文搜索变成了可能。
- Tokenization 分词操作,将文本分解成更小的块,称为 token,大多情况下,这些 token 是单独的单词。
- 将一段句子拆分成一个一个的单个的单词,并分别将每个单词编入索引,只要匹配到一个搜索关键词就可以匹配到结果。
1.2 Normalization
- 切分词语后,文本分析就可以对单个 token 进行匹配,但是每个 token 的匹配也只是进行字面意义上的直接比较;
- 我们希望可以在匹配关键词时,可以忽略大小写,时态,同时只要与搜索词相近意思的也符合条件,将结果返回;
- 为了解决这个问题,文本分析将切分词语后的 token 转化为一个标准化的格式,这允许匹配到与搜索词不完全相同但足够相似的相关 token,例如:
- Quick 小写化:quick;
- foxes 简化它的词根:fox;
- jump、leap 这样的同义词,可以作为一个单词进行索引:jump;
- 为了确保搜索词与这些 token 匹配,可以对搜索短语使用相同的分词和规范化规则,例如,搜索短语 Foxes leap 会被规范化为搜索关键词 fox jump;
1.3 自定义文本分析
- 文本分析是由分词器执行的,分词器包含一组规则来控制文本分析的过程;
- ES 默认分词器是 standard analyzer;
- 如果想自定义文本分析,可以使用其他的内置分词器,或者使用一个自定义的分词器;
- 一个自定义的分词器可以控制文本分析的每一步,包括:
- 在 tokenization 之前对文本进行修改;
- 控制文本的切词规则;
- 在索引 token 前,对 token 进行规范化;
2. Text Analysis 基本概念
2.1 Analyzer 组成
无论是内置的,还是自定义的 Analyzer 都是由底层的三个模块组成:character filters, tokenizers, and token filters;
我们可以自己组合这些模块来定义新的自定义 Analyzer;
1)Character filters
- 针对原始文本进行处理,例如去除 HTML 标记;
- 一个 Analyzer 可以由 0 个或多个 character filter,按顺序使用;
-
2)Tokenizer
分词器,将接收的文本按照一定的规则进行切分;
- Tokenizer 还负责记录每个 token 在文本中的顺序和位置,以及 token 所代表的原始单词的开始字符与结束字符的偏移量;
- 一个 Analyzer 文本分析器只能有一个 Tokenizer 分词器;
-
3)Token filters
针对切分的单词进行加工,比如转为小写、删除 stopwords、增加同义词;
- 规范化操作 Normalization;
- Token filters 不允许改变每个 token 记录在 Tokenizer 中的位置或字符偏移量;
- 一个 Analyzer 可以有 0 个或多个 Token filters,它们按顺序使用;
- 《内置的 Token filter 详解》更新ing
2.2 Stemming 词干分析
- Stemming,就是将一个单词还原成它的词根形式的过程,这是 Normalization 规范化中的一种方式。
- 例如:walking 和 walked 有相同的词根 walk,一旦进行了词根分析,任何一个词的出现都将在搜索中与另一个词匹配;
- Stemming 词干分析虽然依赖于语言,但是通常需要移除单词的前缀和后缀;
- 某些情况下,一些词的被词干分析后得到的词根不是一个正确的单词,但它在搜索中并不重要,如果一个单词的所有变体都简化为相同的词根形式,那么它们也将正确匹配;
- 例如:jumping 和 jumpiness 经过词干分析后都是 jumpi,但 jumpi 不是一个正确的单词,但是不影响搜索;
Stemmer token filters,token 词干过滤器,es 中的词干分析过程是由它来处理的,有以下几类:
算法词干分析器对每个单词应用一系列规则,将其简化为根形式。
- 例如,用于英语的 Algorithmic stemmers 可能会从复数单词的末尾去掉 -s 和 -es 后缀;
- 优势:
- 很少的配置,而且可以在大多数情况下满足需求;
- 占用的内存很少;
- 通常比 Dictionary stemmers 要快;
- 缺点:
- 大多数的 Algorithmic stemmers 只能对包含词根的单词进行简化,如果是不包含词根的不规则单词,那么就不能进行转化,例如:
- be, are, and am;
- mouse and mice;
- foot and feet;
- 大多数的 Algorithmic stemmers 只能对包含词根的单词进行简化,如果是不包含词根的不规则单词,那么就不能进行转化,例如:
以下 token filters 使用了 Algorithmic stemmers:
字典词干分析器在提供的字典中查找单词,用字典中的词根单词替换未词根化的单词变体;
- 理论上,Dictionary stemmers 很适合以下情况;
- 对不规则的单词进行词干分析;
- 区分拼写很像,但是概念上不相关的词,例如:
- organ、organization;
- broker、broken;
- 实际上,Algorithmic stemmers 的表现通常优于 Dictionary stemmers。这是因为 Dictionary stemmers 有以下的缺点:
- 字典的质量
- Dictionary stemmers 好坏取决于它的字典。为了发挥作用,这些词典必须包含大量的单词,定期更新,并随语言趋势而变化。通常情况下,当一本词典问世时,它已经是不完整的,它的一些条目已经过时了。
- 大小和性能
- Dictionary stemmers 必须将字典中的所有单词、前缀和后缀加载到内存中。这可能会使用大量的RAM。低质量的字典可能在移除前缀和后缀的时候效率也很低,这可能会大大降低词干分析的速度。
- 字典的质量
- hunspell,就是使用的字典词干分析器;
- 官方建议:在使用 hunspell 前,先试试 Algorithmic stemmers;
:::tips
有时,词干分析会产生拼写类似但在概念上不相关的共享根词。例如,stemmer 会将 skies 和 skiing 简化为同一个词根:ski。
为了防止这种情况并更好地控制词根分析,可以使用以下的 token filters:
- stemmer_override,它允许您定义针对特定 token 进行词干分析的规则;
- keyword_marker,它将特定的 tokens 定义为 keyword,keyword 类型的 tokens 将不会被后续的 stemmer token filters 处理;
conditional,与 keyword_marker 类似,将 tokens 定义为 keyword 类型;
对于内置的词干分析器,可以使用 stem_exclusion 参数来指定一个不会被词根化的单词列表; :::
2.3 Token graphs 分词图
- 当 tokenizer 分词器将文本切分成 token 流时,它还会记录以下信息:
- position:每个 token 在 token 流中的位置;
- positionLength,token 在 token 流占的位置个数;
- 使用 position 和 positionLength,就可以为一个 token 流生成一个 token graph,每一个 position 代表一个节点,每个 token 代表一个指向下一个 position 的边;
1)Synonyms 同义词
- 一些 token filter 会向 token stream 中添加一些新的 token(例如,同义词)。这些新增的 token 的 position 通常与它们的同义词分布在相同的位置。
2)Multi-position tokens 横跨多个position的token
- 一些 token filter 会向 token stream 中添加一些横跨多个 position 的 token。这类 token 通常时一些单词组成的词组的缩写,例如:atm 是 automatic teller machine 的同义词。
- 但是,只有某些 token filter 才能被称为 graph token filter,会准确的记录这类 multi_position token 的 positionLength:
- 举例,domain name system 和它的同义词 dns,它们的 position 都是0,然而,dns 的 positionLength 是3,其他的 token 都是默认 positionLength 为1。
3)搜索中使用 token graph
- 索引时,是会忽略 positionLength 信息,而且不支持包含横跨多个 position 的 token 的 token graph。
- 但是,查询时,例如 match、match_phrase,会从想要查询的字符串中生成多个 token graph (包括 multi_position tokens 的 graph),借此创建多个子查询。
- 举例,使用 match_phrase 查询:domain name system is fragile;
- 在对搜索字符串进行文本分析时,domain name system 的同义词 dns 会被添加到查询文本的 token stream 中,dns 的 positionLength 是3;
- match_phrase 查询会使用这个 graph 生成多个子查询,这意味着匹配到的文档更加的全面。
dns is fragile
domain name system is fragile
4)Invalid token graphs
- 下面的 token filter 可以添加跨越多个位置的 token (如 dns),但只记录默认的 positionLength 为1:
- 这意味着如果 token stream 包含 domain name system 这样的词组时,也会向 token stream 中添加同义词 token(dns),但是 dns 的 positionLength 是默认值 1,导致 token graph 是错误的;
- 无效的 token graph 可能导致意外的搜索结果,要避免使用。
3. 配置 Text Analysis
- 一般情况下,ES 提供的默认分析器 standard analyzer 可以满足大部分的使用需求;
- 如果一些内置的分析器也无法满足需求,可以使用 ES 提供的一些 option 选项功能来对 analyzer 进行微调
- 例如,给 standard analyzer 配置 stop words 的自定义移除列表;
- 最后就是,可以通过组合 analyzer 的底层模块来定制自己的分析器;
3.1 使用 Analyzer API 测试
1)测试内置的 Analyzer:
POST _analyze
{
"analyzer": "whitespace",
"text": "The quick brown fox."
}
2)测试自定义组合模块
也可以组合 tokenizer、character filter、token filter 来进行测试:
- 一个 tokenizer 分词器;
- 0 或多个 token filter;
- 0 或多个 character filter;
:::infoPOST _analyze
{
"tokenizer": "standard",
"filter": [ "lowercase", "asciifolding" ],
"text": "Is this déja vu?"
}
######返回结果
{
"tokens": [
{
"token": "is",
"start_offset": 0,
"end_offset": 2,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "this",
"start_offset": 3,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "deja",
"start_offset": 8,
"end_offset": 12,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "vu",
"start_offset": 13,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 3
}
]
}
Analyzer 不仅将文本切分成了 tokens,还记录一些额外的信息:
在一个索引中自定义一个 Analyzer,然后可以针对这个索引使用 Analyzer API 对 Custom Analyzer 进行测试:
# settings 中定义一个analyzer叫做std_folded
# mappings 中 my_text 字段使用这个自定义的 analyzer
PUT my_index
{
"settings": {
"analysis": {
"analyzer": {
"std_folded": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"asciifolding"
]
}
}
}
},
"mappings": {
"properties": {
"my_text": {
"type": "text",
"analyzer": "std_folded"
}
}
}
}
# my_index索引中使用 _analyzer 测试方式1:通过analyzer名称
GET my_index/_analyze
{
"analyzer": "std_folded",
"text": "Is this déjà vu?"
}
# my_index索引中使用 _analyzer 测试方式2:通过字段名,这个字段要在mappings中配置好使用自定义anaylzer
GET my_index/_analyze
{
"field": "my_text",
"text": "Is this déjà vu?"
}
3.2 配置内置的 Analyzer
- 内置 Analyzer 详解;
- 内置的 Analyzer 可以通过配置 options 选项参数来对分析器进行微调:
```json
PUT my_index
{
“settings”: {
“analysis”: {
} }, “mappings”: { “properties”: {"analyzer": { "std_english": { "type": "standard", "stopwords": "_english_" } }
} } }"my_text": { "type": "text", "analyzer": "standard", "fields": { "english": { "type": "text", "analyzer": "std_english" } } }
POST my_index/_analyze { “field”: “my_text”, “text”: “The old brown cow” }
POST my_index/_analyze { “field”: “my_text.english”, “text”: “The old brown cow” }
:::info
- 在 standard analyzer 基础上定义了一个名为 std_english Analyzer,并为它配置上 ES 预定义的 english stopwords 移除列表;
- my_text 字段使用 standard Analyzer 分词,不会移除 stopwords,结果:[ the, old, brown, cow ];
- my_text.english 子字段使用自定义的 std_english Analyzer,所以 English stopwords 会被移除,结果:[ old, brown, cow ]
:::
<a name="KwawW"></a>
## 3.3 配置自定义的 Analyzer
<a name="IeG9t"></a>
### 1)配置项
- **tokenizer**(必选项):一个内置或者自定义的 tokenizer;
- **char_filter**(非必选):0个或多个内置、自定义的 character filter 组成的数组;
- **filter**(非必选):0个或多个内置、自定义的 token filter 组成的数据;
- **position_increment_gap**:默认值100。如果一个字段是由多个 text 组成的数组,那么在索引时,ES 会给 text元素之间插入一个假空隙,使得两个元素之间的 position 保持一个距离,以防止搜索词组时跨越多个元素进行匹配。可以在 Mappings 中为 text 字段修改 position_increment_gap 的值。例如:
- 有字段值:[ "John Abraham", "Lincoln Smith"];
- match_phrase 搜索:"Abraham Lincoln";
- 因为 ES 在元素间插入了间隙,所以不会跨元素搜索,没有匹配的结果;
- 如果 match_phrase 添加参数 "slop": 101,那么就会把整个数组的所有元素当作一个 text 去匹配,元素间的 position 没有间隙。会匹配到结果。
<a name="O3Lu4"></a>
### 2)一个比较复杂的例子
- **Character Filter**
- 自定义一个名称为 emoticons 的 Character Filter,其类型是 Mapping Character Filter,将 ":)" 表情符转换为 "_happy_", ":(" 表情符转换为 "_sad_";
- **Tokenizer**
- 自定义一个名称为 punctuation 的 Tokenizer,其类型是 Pattern Tokenizer,根据数组中的空格及标点符号 [ .,!?] 进行分词;
- **Token Filters**
- 内置的 Lowercase Token Filter
- 自定义一个名为 english_stop 的 Token Filter,其类型是 Stop Token Filter, 使用 ES 预定义的 English stop words 移除列表;
:::info
字段值:"I'm a :) person, and you?"<br />分词结果:[ i'm, _happy_, person, you ]
:::
```json
UT my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"char_filter": [
"emoticons"
],
"tokenizer": "punctuation",
"filter": [
"lowercase",
"english_stop"
]
}
},
"tokenizer": {
"punctuation": {
"type": "pattern",
"pattern": "[ .,!?]"
}
},
"char_filter": {
"emoticons": {
"type": "mapping",
"mappings": [
":) => _happy_",
":( => _sad_"
]
}
},
"filter": {
"english_stop": {
"type": "stop",
"stopwords": "_english_"
}
}
}
}
}
POST my_index/_analyze
{
"analyzer": "my_custom_analyzer",
"text": "I'm a :) person, and you?"
}
3.4 指定 Analyzer
ES 提供了不同 level、不同场景下指定内置,或者自定义的 Analyzer:
- 字段、索引、查询 level;
- 索引时,查询时; :::tips
尽量保持简单
ES 通过按顺序检查下面的参数,来确定索引时使用什么 Analyzer:
- mappings 中字段的 analyzer 参数;
- settings 中 analysis.analyzer.default 参数;
- 如果这些都没有设定,那么就使用默认的 standard analyzer;
(1) 为字段指定 Analyzer
PUT my_index { "mappings": { "properties": { "title": { "type": "text", "analyzer": "whitespace" } } } }
(2) 为 Index 指定默认 Analyzer
UT my_index { "settings": { "analysis": { "analyzer": { "default": { "type": "simple" } } } } }
2)ES 如何确定 Search 的 Analyzer?
:::warning
通常没必要在 Search 时指定一个与 Index 不同的 Analyzer
- 这样做,会对相关性造成负面的影响,导致预期外的搜索结果;
- 如果为 Search 单独指定了一个 Analyzer,官方建议在上线前对文本分析多进行测试。 :::
ES 通过按顺序检查下面的参数,来确定搜索时使用什么 Analyzer:
- search query 中的 analyzer 参数;
- mappings 中字段的 search_analyzer 参数;
- settings 中 analysis.analyzer.default_search 参数;
- mappings 中字段的 analyzer 参数;
- 如果这些都没有设定,那么就使用默认的 standard analyzer;
(1) 为 query 指定 Search Analyzer
GET my_index/_search { "query": { "match": { "message": { "query": "Quick foxes", "analyzer": "stop" } } } }
(2) 为字段指定 Search Analyzer
:::tips
如果在 mappings 中为字段配置了 search_analyzer,那么该字段的 analyzer 也必须配置; :::
PUT my_index { "mappings": { "properties": { "title": { "type": "text", "analyzer": "whitespace", "search_analyzer": "simple" } } } }
(3) 为 Index 指定 default Search Analyzer
:::tips
如果在 settings 中指定了索引的默认 Search Analyzer,那么 settings 中的 analysis.analyzer.default 也必须设置; :::
PUT my_index { "settings": { "analysis": { "analyzer": { "default": { "type": "simple" }, "default_search": { "type": "whitespace" } } } } }