(内部分享转外销)
上次的分享介绍了用于构造相关度的TF-IDF及其改进算法BM25 ,可以看到的这些算法都以单词为单元,计算对应的相关度,这次的分享主要介绍Elasticsearch是如何从一个大段文本中产生这样的单元的
(知乎似乎不支持目录插入?)
本文基本是ES的官方文档的翻译,掺杂了少量在使用这些组件的感悟
<!-- TOC -->
- [主要组件和处理流程](#主要组件和处理流程)
- [Tokenizer 标记器](#tokenizer-标记器)
- [Filters 过滤器](#filters-过滤器)
- [Analyzers 分析器](#analyzers-分析器)
- [Normalizers 规范器](#normalizers-规范器)
- [调试](#调试)
- [处理流](#处理流)
- [详细列表(ES 6.3)](#详细列表es-63)
- [Tokenizer](#tokenizer)
- [Standard Tokenizer](#standard-tokenizer)
- [Letter Tokenizer](#letter-tokenizer)
- [Lowercase Tokenizer](#lowercase-tokenizer)
- [Whitespace Tokenizer](#whitespace-tokenizer)
- [UAX URL Email Tokenizer](#uax-url-email-tokenizer)
- [Classic Tokenizer](#classic-tokenizer)
- [NGram Tokenizer](#ngram-tokenizer)
- [Edge NGram Tokenizer](#edge-ngram-tokenizer)
- [Keyword Tokenizer](#keyword-tokenizer)
- [Pattern Tokenizer](#pattern-tokenizer)
- [Char Group Tokenizer](#char-group-tokenizer)
- [Path Tokenizer](#path-tokenizer)
- [Filters 过滤/转换器](#filters-过滤转换器)
- [和Tokenizer的差不多的](#和tokenizer的差不多的)
- [词干转换器Stemmer](#词干转换器stemmer)
- [Keyword 和Stemmer的搭配](#keyword-和stemmer的搭配)
- [Shingle](#shingle)
- [Stop Token Filter](#stop-token-filter)
- [Phonetic 同读音过滤器](#phonetic-同读音过滤器)
- [Synonym 同义词](#synonym-同义词)
- [对齐、去重](#对齐去重)
- [正则:捕获和替换](#正则捕获和替换)
- [Normalization Filter](#normalization-filter)
<!-- /TOC -->
主要组件和处理流程
Tokenizer 标记器
接收字符串流,把其打碎成tokens (通常是单个单词),输出是tokens的流
一个简单的whitespace
tokenizer,会按照空格打碎,比如把"Quick brown fox!"
转换成["Quick", "brown", "fox!"]
更多的Tokenizer后面我会介绍
Filters 过滤器
分为 字符(Character)层的过滤器和Token层的过滤器,可以过滤或转换一些字符或者Token 例如可以过滤掉HTML标签<b>
或者解码HTML
&
->&
-> (non-breaking space)<
-> <
Analyzers 分析器
相当于是Tokenizer+Filter,内置了若干常用的分析器
例如上文说的Whitespace
Normalizers 规范器
类似于分析器,但只能产生出单个Token,而分析器是可以一对多的 另外,Normalizers会使得 文本进入引擎后,在后面的流程中得到的都是变换后的结果 举个栗子:简单的lowercase 功能是小写化
在Analyzer中
title = "Quick brown fox!"
会转换成["quick", "brown", "fox!"]
并计算对应的TFIDF分数,但搜索出来的时候,显示的title依旧是 "Quick brown fox!"
如果在Title这个字段上应用的是Normalizers,那么除了索引时的转换,从索引中拿出来的对应title将会变成"quick brown fox!"
这种情况会在ES 的keyword
类型(可以认为是unique或hash)的使用上产生差异
调试
可以在{host}/_analyze
上调试词法分析,结合Tokenizer/Filter 以获得预期的结果
POST _analyzer
{
"analyzer":"arabic",
"text": " "
}
响应:
{
"tokens": [
{
"token": " ",
"start_offset": 4,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": " ",
"start_offset": 11,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 3
}
//略了一部分
]
}
处理流
过滤字符 -> 生成词块-> 过滤词块->进入索引Character Filter
->Tokenizer
->Token Filter
同样的,在搜索阶段,如果对text
类型的字段进行match
查询,也会由search_analyzer
控制对于query的分析,并对其进行搜索
Tokenizer + Filter... 可以等效于另一个 Tokenizer。Analyzer 也可以包含 Tokenizer + Filter
详细列表(ES 6.3)
Tokenizer
Standard Tokenizer
一个标准的标记器,使用Unicode Text Segmentation算法,从词的边界断开,过滤掉了绝大部分符号字符
POST _analyze
{
"tokenizer": "standard",
"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 ]
可以调节的参数:max_token_length 控制token的大长度,如果超过则从大长度处断开
Letter Tokenizer
只要不是字母,就打断
POST _analyze
{
"tokenizer": "letter",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
[ The, QUICK, Brown, Foxes, jumped, over, the, lazy, dog, s, bone ]
Lowercase Tokenizer
和上面那个差不多,但是还会顺便小写掉
Lowercase Tokenizer = Letter tokenizer + Lowercase filter
POST _analyze
{
"tokenizer": "lowercase",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
[ the, quick, brown, foxes, jumped, over, the, lazy, dog, s, bone ]
Whitespace Tokenizer
用空白字符(包括空格和制表符)断开
POST _analyze
{
"tokenizer": "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. ]
可以调节的参数:max_token_length 控制token的大长度,如果超过则从大长度处断开
UAX URL Email Tokenizer
辨识URL和Email地址,打断成对应的Token
POST _analyze
{
"tokenizer": "uax_url_email",
"text": "Email me at john.smith@global-international.com"
}
[ Email, me, at, john.smith@global-international.com ]
作为对比,这个文本在标准tokenizer上返回的结果是
[ Email, me, at, john.smith, global, international.com ]
这里又要注意到一点(.),上面我们看到的bone.
的点是在词尾,所以被过滤掉了,但它不会过滤掉在单词中间的点
可以调节的参数:max_token_length
Classic Tokenizer
主要基于英语语法,启发式的对待缩略词、公司名、email地址、主机名 在非英语语言上表现也不一定好
主要策略
- 在绝大多数符号字符上分隔单词,移除符号。和standard一样,点
.
在词的边缘的时候会被移除
dog's bone.. join.smith
{
"token": "dog's",
"start_offset": 45,
"end_offset": 50,
"type": "<APOSTROPHE>",
"position": 9
},
{
"token": "bone",
"start_offset": 51,
"end_offset": 55,
"type": "<ALPHANUM>",
"position": 10
},
{
"token": "join.smith",
"start_offset": 58,
"end_offset": 68,
"type": "<HOST>",
"position": 11
}
- 在连字符
-
上做切分,除非两边是数字
{
"token": "asd86-10086",
"start_offset": 69,
"end_offset": 80,
"type": "<NUM>",
"position": 12
}
- 可以识别出来email地址、主机名
{
"tokenizer": "classic",
"text": "I@robot.com http://www.baidu.com/123"
}
[
{
"token": "I@robot.com",
"start_offset": 0,
"end_offset": 11,
"type": "<EMAIL>",
"position": 0
},
{
"token": "http",
"start_offset": 12,
"end_offset": 16,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "www.baidu.com",
"start_offset": 19,
"end_offset": 32,
"type": "<HOST>",
"position": 2
},
{
"token": "123",
"start_offset": 33,
"end_offset": 36,
"type": "<ALPHANUM>",
"position": 3
}
]
NGram Tokenizer
在字符级别做ngram 可调参数: min_gram 小长度, 默认1 max_gram 大长度, 默认2 token_chars 可以被包含在token中的字符集,默认是全部包含,可以选择
- letter — for example a, b, ï or 京
- digit — for example 3 or 7
- whitespace — for example " " or "\n"
- punctuation — for example ! or "
- symbol — for example $ or √
POST _analyze
{
"tokenizer": "ngram",
"text": "Quick Fox"
}
[ Q, Qu, u, ui, i, ic, c, ck, k, "k ", " ", " F", F, Fo, o, ox, x ]
如果token_chars=letter 空格那些就没了
{
"tokenizer": {
"type":"ngram",
"token_chars":"letter"
},
"text": "Quick Fox"
}
[ Q, Qu, u, ui, i, ic, c, ck, k, F, Fo, o, ox, x ]
Edge NGram Tokenizer
和上面的参数是一样的,区别是先把文本打碎成单词,然后只从单词开始的位置算ngram
{
"tokenizer": {"type":"edge_ngram",
"token_chars": [
"letter",
"digit"
]
},
"text": "Quick Fox"
}
{
"tokens": [
{
"token": "Q",
"start_offset": 0,
"end_offset": 1,
"type": "word",
"position": 0
},
{
"token": "Qu",
"start_offset": 0,
"end_offset": 2,
"type": "word",
"position": 1
},
{
"token": "F",
"start_offset": 6,
"end_offset": 7,
"type": "word",
"position": 2
},
{
"token": "Fo",
"start_offset": 6,
"end_offset": 8,
"type": "word",
"position": 3
}
]
}
可以用来做search-as-you-type 功能,不过使用自动补全会更有效率一些。我们App中的type-hint是用自动补全做的
Keyword Tokenizer
等于什么都不做,输出是什么,输出就是什么
Pattern Tokenizer
根据给定的正则去切分simple_pattern
和pattern
,前者比后者的正则表达式约束更多,但前者也更快simple_pattern_split
的区别是用正则表达式匹配到的字符切分,上面两个则是挑选出匹配到的字符
像是re.match和re.sub的区别
Char Group Tokenizer
根据给定的字符组去切分,比正则标记器的效率更高一些
Path Tokenizer
像文件系统一样进行切分,生成每一个路径
例如:/foo/bar/baz → [/foo, /foo/bar, /foo/bar/baz ]
Filters 过滤/转换器
非常多,ES 6.3给出了43个filter,这里只列出比较有用的和之前在搜索中已经应用过的
和Tokenizer的差不多的
基本顾名思义
- Standard Token Filter
- Length Token Filter
- Lowercase Token Filter
- Uppercase Token Filter
- NGram Token Filter
- Edge NGram Token Filter
- Trim
- Truncate
- Limit Token Count Token Filter 限制索引文档中的Token数量
- Classic Token Filter
- Apostrophe Token Filter 撇号
- Remove Duplicates Token Filter
词干转换器Stemmer
porter_stem:使用Porter stemming算法
栗子: is this shorts possibly plastered
-> [is,thi,short,possibl,plaster]
stemmer: 一些基于语言的词干转换器。English等同于porter_stem
词干分析器基本只在动词上用的比较合理,在我们的电商场景里,short和shorts提取词干后都会变成short. 这个可以在商品评分和相关度评分得到一个比较好的均衡之后再次进行测试
stemmer_override: 可以手动指定映射规则
kstem: 一个用于英语的高性能过滤器。结合了算法和内置词典
"t shirts shorts jackets"-> "t shirt shorts jacket",
snowball: 基于Snowball-generated的词干过滤器,也支持多种语言。看了下snowball的文档,也是基于词法规则的
综合起来,词干转换器以前的AB测试效果不是很好,后期改善了相关度和商品评分之后可以再次进行试验。
Keyword 和Stemmer的搭配
Repeat(keyword_repeat)和Marker(keyword_marker)通常会和上面的词干转换器搭配
keyword_marker 可以保护某些单词不被词干转换器处理掉 { "type": "keyword_marker", "keywords": ["cats"] } 这个规则将会使得后面的词干处理器不处理cats
keyword_repeat 会使得被处理掉的单词再次被重复
"I like cats" -> [I, like, cats, cat]
这个在我们的搜索场景中也是比较有用,但目前还没有使用到
Shingle
是Token级别的ngramplease divide this sentence into shingles
-> ["please divide", "divide this", "this sentence", "sentence into", and "into shingles"]
在我们的场景里比较有用,一个例子是tshirt/t shirt/t-shirt,ear ring/earring这样的标题,shingle会使得产生的token可以被模糊匹配上,少量改善了召回质量(目前基本被ring的商品评分过高覆盖了这个效果)
如果没有shingle,用户输入的 ear ring 就只能和ring 进行模糊匹配,匹配不上earring.
另外,由于shingle字段的查询的复杂度较高,并且ES对于这样的复杂度缺乏提前终止的保护,timeout参数也无法保护ES。通常在query的单词数量过大的时候就不再使用shingle字段了,否则会造成ES压力增大
common_grams :Common Grams Token Filtery的效果差不多,只不过是用下划线连接起来了bigram
cjk_bigram: 对CJK字符集(中日韩)的bigram
Stop Token Filter
停止词过滤器,将会过滤掉停止词
t shirts is this the-> t shirts
在ES一层使用这个停止词的优点是它会过滤掉词语,因此会使得标题、query的tf-idf分数不被停止词影响
以前也上过,这个可以在商品评分和相关度评分得到一个比较好的均衡之后再次进行测试
目前线上用的是外置的停止词过滤,缺点是搜索时依旧会被影响tf-idf分数
Phonetic 同读音过滤器
基于一些规则,在索引时对相同的读音构造出一致的索引 涉及算法:Soundex, Metaphone, and a variety of other algorithms
"t shirts shorts fulness"->"[T,XRTS,XRTS,FLNS]
Synonym 同义词
先进行同义词的对照,可以根据配置,把同义词设置到1个或多个映射上
synonym_graph和synonym,主要的困难是AWS的ES托管,不能访问ES本地路径,所以我们无法加载同义词表。另外同义词表每次的更改,需要重建整个索引,代价也较大。目前我们没有使用
对齐、去重
truncate 对齐,大于长度的token将会被裁剪到对应的尺寸
unique 去重,去掉重复的token,会优化tf-idf的表现,参数only_on_same_position 控制是否控制只对相邻的token去重
正则:捕获和替换
pattern_capture 捕获正则表达式
pattern_replace 替换
主要注意事项:要小心正则表达式中的灾难回溯
Normalization Filter
一些语言相关的normalization 主要作用是移除特殊的字符