目录
1.3.停用词和标准分析器(Stopwords and the Standard Analyzer)
使用停用词过滤器(Using the stop Token Filter)
对常用词使用更多控制(More Control with Common Terms)
二元语法短语查询(Bigram Phrase Queries)
1.简介
词干提取的重要性不仅是因为它让搜索的内容更广泛、让检索的能力更深入,还因为它是压缩索引空间的工具。
一种最简单的减少索引大小的方法就是 索引更少的词。 有些词要比其他词更重要,只索引那些更重要的词来可以大大减少索引的空间。
词条简单分为两组
- 低频词(Low-frequency terms)在文档集合中相对出现较少的词,因为它们稀少,所以它们的权重值更高。
- 高频词(High-frequency terms)在索引下的文档集合中出现较多的常用词,例如
the
、and
、和is
。 这些词的权重小,对相关度评分影响不大。
词项到底是低频或是高频取决于它们所处的文档。单词 and
如果在所有都是中文的文档里可能是个低频词。在关于数据库的文档集合里,单词 database
可能是一个高频词项,它对搜索这个特定集合毫无帮助。
这些 停用词 通常在索引前就可以被过滤掉,同时对检索的负面影响不大。但是这样做真的是一个较好的解决方案?
1.1.停用词的优缺点
从索引里将这些词移除会使我们降低某种类型的搜索能力。难以完成以下事情
- 区分 happy 和 not happy。
- 搜索乐队名称 The The。
- 查找莎士比亚的名句 ``To be, or not to be'' (生存还是毁灭)。
- 使用挪威的国家代码:
no
。
移除停用词的最主要好处是性能
1.2.使用停用词
移除停用词的工作是由 stop
停用词过滤器完成的,可以通过创建自定义的分析器来使用它。但是,也有一些自带的分析器预置使用停用词过滤器
{ref}/analysis-lang-analyzer.html[语言分析器]
每个语言分析器默认使用与该语言相适的停用词列表,例如:english
英语分析器使用 english
停用词列表。
{ref}/analysis-standard-analyzer.html[standard
标准分析器]
默认使用空的停用词列表:none
,实际上是禁用了停用词。
{ref}/analysis-pattern-analyzer.html[pattern
模式分析器]
默认使用空的停用词列表:为 none
,与 standard
分析器类似。
1.3.停用词和标准分析器(Stopwords and the Standard Analyzer)
【举例】
PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"type": "standard",
"stopwords": [ "and", "the" ] // 过滤掉的停用词包括 and
和 the
}
}
}
}
}
任何语言分析器都可以使用相同的方式配置自定义停用词。
保持位置(Maintaining Positions)
GET /my_index/_analyze?analyzer=my_analyzer
The quick and the dead
{
"tokens": [
{
"token": "quick",
"start_offset": 4,
"end_offset": 9,
"type": "<ALPHANUM>",
"position": 1 // 标记每个词汇单元的位置
},
{
"token": "dead",
"start_offset": 18,
"end_offset": 22,
"type": "<ALPHANUM>",
"position": 4 // 标记每个词汇单元的位置
}
]
}
停用词如我们期望被过滤掉了,但有趣的是两个词项的位置 position
没有变化:quick
是原句子的第二个词,dead
是第五个。这对短语查询十分重要,因为如果每个词项的位置被调整了,一个短语查询 quick dead
会与以上示例中的文档错误匹配。
指定停用词(Specifying Stopwords)
"stopwords": [ "and", "the" ]
停用词可以通过指定一个特殊列表 none
来禁用。
PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_english": {
"type": "english", // 分析器是基于 english
分析器。
"stopwords": "_none_" // 禁用了停用词
}
}
}
}
}
停用词还可以使用一行一个单词的格式保存在文件中。通过 stopwords_path
参数设置路径。
PUT /my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_english": {
"type": "english",
"stopwords_path": "stopwords/english.txt"
}
}
}
}
}
使用停用词过滤器(Using the stop Token Filter)
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"spanish_stop": {
"type": "stop",
"stopwords": [ "si", "esta", "el", "la" ] // 停用词过滤器采用与 standard
分析器相同的参数 stopwords
和 stopwords_path
},
"light_spanish": {
"type": "stemmer",
"language": "light_spanish"
}
},
"analyzer": {
"my_spanish": {
"tokenizer": "spanish",
"filter": [ // 过滤器的顺序非常重要
"lowercase",
"asciifolding",
"spanish_stop",
"light_spanish"
]
}
}
}
}
}
更新停用词(Updating Stopwords)
想要更新分析器的停用词列表有多种方式, 分析器在创建索引时,当集群节点重启时候,或者关闭的索引重新打开的时候。
如果使用 stopwords
参数以内联方式指定停用词,那么你只能通过关闭索引,更新分析器的配置,然后在重新打开索引才能更新停用词。
如果使用 stopwords_path
参数指定停用词的文件路径 ,那么更新停用词就简单了。只需更新文件(在每一个集群节点上),然后通过两者之中的任何一个操作来强制重新创建分析器:
- 关闭和重新打开索引
- 一一重启集群下的每个节点
当然,更新的停用词不会改变任何已经存在的索引。这些停用词的只适用于新的搜索或更新文档。如果要改变现有的文档,则需要重新索引数据。
1.4.停用词与性能
保留停用词最大的缺点就影响搜索性能。使用 Elasticsearch 进行全文搜索,它需要为所有匹配的文档计算相关度评分 _score
从而返回最相关的前 10 个文档。
通常大多数的单词在所有文档中出现的频率低于0.1%,但是有少数词(例如 the
)几乎存在于所有的文档中。假设有一个索引含有100万个文档,查询 quick brown fox
词组,能够匹配上的可能少于1000个文档。但是如果查询 the quick brown fox
词组,几乎需要对索引中的100万个文档进行评分和排序,只是为了返回前 10 名最相关的文档。
问题的关键是 the quick brown fox
词组实际是查询 the
或 quick
或 brown
或 fox
— 任何文档即使它什么内容都没有而只包含 the
这个词也会被包括在结果集中。因此,我们需要找到一种降低待评分文档数量的方法。
and 操作符 (and Operator)
想要减少待评分文档的数量,最简单的方式就是在and
操作符 match
查询时使用 and
操作符
{
"match": {
"text": {
"query": "the quick brown fox",
"operator": "and"
}
}
}
等价于
{
"bool": {
"must": [
{ "term": { "text": "the" }},
{ "term": { "text": "quick" }},
{ "term": { "text": "brown" }},
{ "term": { "text": "fox" }}
]
}
}
bool
查询会智能的根据较优的顺序依次执行每个 term
查询:它会从最低频的词开始。因为所有词项都必须匹配,只要包含低频词的文档才有可能匹配。使用 and
操作符可以大大提升多词查询的速度。
最少匹配数(minimum_should_match)
使用 minimum_should_match
配置去掉结果中次相关的长尾。
{
"match": {
"text": {
"query": "the quick brown fox",
"minimum_should_match": "75%"
}
}
}
【说明】
四分之三的词都必须匹配,这意味着我们只需考虑那些包含最低频或次低频词的文档。 相比默认使用 or
操作符的简单查询,这为我们带来了巨大的性能提升。
1.5.词项的分别管理
在查询字符串中的词项可以分为更重要(低频词)和次重要(高频词)这两类。 只与次重要词项匹配的文档很有可能不太相关。实际上,我们想要文档能尽可能多的匹配那些更重要的词项。
match
查询接受一个参数 cutoff_frequency
,从而可以让它将查询字符串里的词项分为低频和高频两组。低频组(更重要的词项)组成 bulk
大量查询条件,而高频组(次重要的词项)只会用来评分,而不参与匹配过程。通过对这两组词的区分处理,我们可以在之前慢查询的基础上获得巨大的速度提升。
领域相关的停用词(Domain-Specific Stopwords)
cutoff_frequency 配置的好处是,在 特定领域 使用停用词不受约束。
例如,关于电影网站使用的词 movie 、color 、black 和 white ,这些词往往认为几乎没有任何意义。
使用 stop 词汇单元过滤器,这些特定领域的词必须手动添加到停用词列表中。然而 cutoff_frequency 会查看索引里词项的具体频率,
这些词会被自动归类为 高频词汇 。
【举例】
{
"match": {
"text": {
"query": "Quick and the dead",
"cutoff_frequency": 0.01
}
}
【说明】
任何词项出现在文档中超过1%,被认为是高频词。cutoff_frequency
配置可以指定为一个分数( 0.01
)或者一个正整数( 5
)。
【举例】此查询通过 cutoff_frequency
配置,将查询条件划分为低频组( quick
, dead
)和高频组( and
, the
)。
{
"bool": {
"must": { // 必须匹配至少一个低频/更重要的词项。
"bool": {
"should": [
{ "term": { "text": "quick" }},
{ "term": { "text": "dead" }}
]
}
},
"should": { // 高频/次重要性词项是非必须的
"bool": {
"should": [
{ "term": { "text": "and" }},
{ "term": { "text": "the" }}
]
}
}
}
}
【说明】
must 意味着至少有一个低频词— quick 或者 dead —必须出现在被匹配文档中。所有其他的文档被排除在外。
should 语句查找高频词 and 和 the ,但也只是在 must 语句查询的结果集文档中查询。
should 语句的唯一的工作就是在对如 Quick and the dead 和 The quick but dead 语句进行评分时,前者得分比后者高。
这种方式可以大大减少需要进行评分计算的文档数量。
将操作符参数设置成 and 会要求所有低频词都必须匹配,同时对包含所有高频词的文档给予更高评分。
但是,在匹配文档时,并不要求文档必须包含所有高频词。如果希望文档包含所有的低频和高频词,
我们应该使用一个 bool 来替代。
控制精度
minimum_should_match
参数可以与 cutoff_frequency
组合使用,但是此参数仅适用与低频词。
【举例】
{
"match": {
"text": {
"query": "Quick and the dead",
"cutoff_frequency": 0.01,
"minimum_should_match": "75%"
}
}
{
"bool": {
"must": {
"bool": {
"should": [
{ "term": { "text": "quick" }},
{ "term": { "text": "dead" }}
],
"minimum_should_match": 1
}
},
"should": {
"bool": {
"should": [
{ "term": { "text": "and" }},
{ "term": { "text": "the" }}
]
}
}
}
}
高频词
当使用 or
查询高频词条,如— To be, or not to be
—进行查询时性能最差。只是为了返回最匹配的前十个结果就对只是包含这些词的所有文档进行评分是盲目的。我们真正的意图是查询整个词条出现的文档,所以在这种情况下,不存低频所言,这个查询需要重写为所有高频词条都必须
{
"bool": {
"must": [
{ "term": { "text": "to" }},
{ "term": { "text": "be" }},
{ "term": { "text": "or" }},
{ "term": { "text": "not" }},
{ "term": { "text": "to" }},
{ "term": { "text": "be" }}
]
}
}
对常用词使用更多控制(More Control with Common Terms)
【举例】让所有低频词都必须匹配,而只对那些包括超过 75% 的高频词文档进行评分
{
"common": {
"text": {
"query": "Quick and the dead",
"cutoff_frequency": 0.01,
"low_freq_operator": "and",
"minimum_should_match": {
"high_freq": "75%"
}
}
}
}
1.6.停用词与短语查询
短语查询[phrase-matching]性能相对较差,特别是当短语中包括常用词的时候,如 “To be, or not to be”
短语全部由停用词组成,这是一种极端情况。原因在于几乎需要匹配全量的数据
在 停用词的两面 停用词的优缺点 中,提到移除停用词只能节省倒排索引中的一小部分空间。这句话只部分正确,一个典型的索引会可能包含部分或所有以下数据:
- 词项字典(Terms dictionary)
索引中所有文档内所有词项的有序列表,以及包含该词的文档数量。
- 倒排表(Postings list)
包含每个词项的文档(ID)列表。
- 词频(Term frequency)
每个词项在每个文档里出现的频率。
- 位置(Positions)
每个词项在每个文档里出现的位置,供短语查询或近似查询使用。
- 偏移(Offsets)
每个词项在每个文档里开始与结束字符的偏移,供词语高亮使用,默认是禁用的。
- 规范因子(Norms)
用来对字段长度进行规范化处理的因子,给较短字段予以更多权重。
将停用词从索引中移除会节省 词项字典 和 倒排表 里的少量空间,但 位置 和 偏移 是另一码事。位置和偏移数据很容易变成索引大小的两倍、三倍、甚至四倍。
位置信息
analyzed
字符串字段的位置信息默认是开启的, 所以短语查询能随时使用到它。
词项出现的越频繁,用来存储它位置信息的空间就越多。在一个大的文档集合中,对于那些非常常见的词,它们的位置信息可能占用成百上千兆的空间。
运行一个针对高频词 the
的短语查询可能会导致从磁盘读取好几G的数据。这些数据会被存储到内核文件系统的缓存中,以提高后续访问的速度,这看似是件好事,但这可能会导致其他数据从缓存中被剔除,进一步使后续查询变慢。
索引选项
是否真的需要使用短语查询或 近似查询?
不需要。
index_options
参数 允许控制索引里为每个字段存储的信息。 可选值如下:
docs
只存储文档及其包含词项的信息。这对 not_analyzed
字符串字段是默认的。
freqs
存储 docs
信息,以及每个词在每个文档里出现的频次。词频是完成TF/IDF 相关度计算的必要条件,但如果只想知道一个文档是否包含某个特定词项,则无需使用它。
positions
存储 docs
、 freqs
、 analyzed
,以及每个词项在每个文档里出现的位置。 这对 analyzed
字符串字段是默认的,但当不需使用短语或近似匹配时,可以将其禁用。
offsets
存储 docs
,freqs
,positions
, 以及每个词在原始字符串中开始与结束字符的偏移信息( {ref}/search-request-highlighting.html#postings-highlighter[postings
highlighter] )。这个信息被用以高亮搜索结果,但它默认是禁用的。
【举例】
PUT /my_index
{
"mappings": {
"my_type": {
"properties": {
"title": { // 使用默认的 positions
设置,所以它适于短语或近似查询。
"type": "string"
},
"content": { // 位置设置是禁用的,所以它无法用于短语或近似查询。
"type": "string",
"index_options": "freqs"
}
}
}
}
停用词
删除停用词是能显著降低位置信息所占空间的一种方式。
一个被删除停用词的索引仍然可以使用短语查询,因为剩下的词的原始位置仍然被保存着,这正如 保持位置(Maintaining Positions) 中看到的那样。 尽管如此,将词项从索引中排除终究会降低搜索能力,这使我们难以区分 Man in the moon 与 Man on the moon 这两个短语。可以用common_grams 过滤器
1.7.common_grams 过滤器
common_grams
过滤器是针对短语查询能更高效的使用停用词而设计的。它与 shingles 过滤器类似, 为每个相邻词对生成 ,用示例解释更为容易。
common_grams
过滤器根据 query_mode
设置的不同而生成不同输出结果:false
(为索引使用) 或 true
(为搜索使用)
【举例】
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"index_filter": { // index_filter
在索引时使用(此时 query_mode
的默认设置是 false
)
"type": "common_grams",
"common_words": "_english_"
},
"search_filter": { // search_filter
在查询时使用(此时 query_mode
的默认设置是 true
)
"type": "common_grams",
"common_words": "_english_", // common_words
参数可以接受与 stopwords
参数同样的选项,还可以接受参数 common_words_path
,使用存于文件里的常用词。
"query_mode": true
}
},
"analyzer": {
"index_grams": { // 创建一个索引时分析器
"tokenizer": "standard",
"filter": [ "lowercase", "index_filter" ]
},
"search_grams": { // 查询时分析器
"tokenizer": "standard",
"filter": [ "lowercase", "search_filter" ]
}
}
}
}
}
// text
字段索引时使用 index_grams
分析器,但是在搜索时默认使用 standard
分析器
PUT /my_index/_mapping/my_type
{
"properties": {
"text": {
"type": "string",
"analyzer": "index_grams",
"search_analyzer": "standard"
}
}
}
索引时(At Index Time)
如果对短语 The quick and brown fox 进行拆分,它生成如下词项:
Pos 1: the_quick
Pos 2: quick_and
Pos 3: and_brown
Pos 4: brown_fox
新的 index_grams
分析器生成以下词项:
Pos 1: the, the_quick
Pos 2: quick, quick_and
Pos 3: and, and_brown
Pos 4: brown
Pos 5: fox
所有的词项都是以 unigrams
形式输出的(the、quick 等等),但是如果一个词本身是常用词或者跟随着常用词,那么它同时还会在 unigram
同样的位置以 bigram
形式输出:the_quick
, quick_and
, and_brown
单字查询(Unigram Queries)
因为索引包含 unigrams
,可以使用与其他字段相同的技术进行查询
GET /my_index/_search
{
"query": {
"match": {
"text": {
"query": "the quick and brown fox",
"cutoff_frequency": 0.01
}
}
}
}
使用的是 standard
分析器,生成的词项为: the
, quick
, and
, brown
, fox
。
二元语法短语查询(Bigram Phrase Queries)
可以用专门的 search_grams
分析器让整个过程变得更高效
GET /my_index/_search
{
"query": {
"match_phrase": { // 短语查询
"text": {
"query": "The quick and brown fox",
"analyzer": "search_grams" // 对于短语查询,我们重写了默认的 search_analyzer
分析器,而使用 search_grams
分析器。
}
}
}
}
search_grams 分析器会生成以下词项:
Pos 1: the_quick
Pos 2: quick_and
Pos 3: and_brown
Pos 4: brown
Pos 5: fox
分析器排除了所有常用词的 unigrams
,只留下常用词的 bigrams
以及低频的 unigrams
。如 the_quick
这样的 bigrams
比单个词项 the
更为少见,这样有两个好处:
the_quick
的位置信息要比the
的小得多,所以它读取磁盘更快,对系统缓存的影响也更小。- 词项
the_quick
没有the
那么常见,所以它可以大量减少需要计算的文档。
两词短语(Two-Word Phrases)
优化可以更进一步,因为大多数的短语查询只由两个词组成,如果其中一个恰好又是常用词
GET /my_index/_search
{
"query": {
"match_phrase": {
"text": {
"query": "The quick",
"analyzer": "search_grams"
}
}
}
}
那么 search_grams
分析器会输出单个语汇单元:the_quick
。这将原来昂贵的查询(查询 the
和 quick
)转换成了对单个词项的高效查找。
1.8.停用词与相关性
在索引中保留停用词会降低相关度计算的准确性,特别是当我们的文档非常长时。
原因在于 位置 并没有强制对词频率的影响设置上限 。 基于逆文档频率的影响,非常常用的词可能只有很低的权重,但是在长文档中,单个文档出现的绝对数量很大的停用词会导致这些词被不自然的加权。
可以考虑对包含停用词的较长字段使用 Okapi BM25 相似度算法,而不是默认的 Lucene 相似度。