1、问题
在使用搜索引擎(Elasticsearch或Solr)作为应用的后台搜索平台的时候,会遇到停用词(stopwords)的问题。
在信息检索中,停用词是为节省存储空间和提高搜索效率,处理文本时自动过滤掉某些字或词,这些字或词即被称为Stop Words(停用词)。停用词大致分为两类。一类是语言中的功能词,这些词极其普遍而无实际含义,比如“the”、“is“、“which“、“on”等。另一类是词汇词,比如'want'等,这些词应用广泛,但搜索引擎无法保证能够给出真正相关的搜索结果,难以缩小搜索范围,还会降低搜索效率。实践中,通常把这些词从问题中过滤,从而节省索引的存储空间、提高搜索性能。
但是在实际语言环境中,停用词有时也有用的。比如,莎士比亚的名句:“To be or not to be.”所有的词都是停用词。特别当停用词和通配符(*)同时使用的时候,问题就来了:“the”、“is“、“on”还是停用词码?
2、解决方案
实际运用中,没有一个解决方案是100%完美的。很多时候需要我们根据实际用例作相应的调整和折中,来达到期望的结果。在这个时候,需要用80/20原则,把目标专著在提高用户体验上。
2.1、对不同的搜索对象区别对待
过滤停用词是为节省存储空间和提高搜索效率。实践中,不同的应用场景和对象对存储空间和搜索效率的需求不一样。比如,文章的标题,一般都很短,而且有大量的限定词区别词的定义,它对节省存储空间和效率的要求不高,但是常常需要停用词来限定名词的意义。我们可以考虑保留停用词。而对于文章体的全文本,存储空间和效率的要求很高,使用停用词过滤可以大大减少存储空间,提高搜索效率。
对Elasticsearch,下面是我们用到的索引定义:消息标题是text类型,没有使用停用词,而消息文本是standard_text类型,这个类型在设置里定义了使用英语标准的停用词过滤。
{
"demo": {
"settings": {
"index": {
"number_of_shards": "5",
"number_of_replicas": "1",
"analysis": {
"analyzer": {
"standard_text": {
"type": "standard",
"stopwords": "_english_"
}
}
}
},
"mappings": {
"msg": {
"_routing": { "required": true },
"properties": {
"title": {
"type": "text"
},
"body": {
"type": "text",
"analyzer": "standard_text"
}
}
}
}
}
}
2.2、match查询
考虑一个例子:“and”。作为停用词,在“and”会在索引创建的时候被过滤掉:POST store/_analyze { "field": "body", "text": ["and"] }
得到的分析结果是:{ "tokens": [] }
但是,如果我们用title字段来分析的时候,结果会得到保存:POST store/_analyze { "field": "title", "text": ["and"] }
{ "tokens": [{
"token": "and",
"start_offset": 0,
"end_offset": 3,
"type": "<ALPHANUM>",
"position": 0
}
]
}
但是当我们需要搜索文本的时候,会出现很多不如意的地方。比如,如果我们需要查消息体内chris && and && john这三个词的时候,因为and被过滤了,而查询条件又是与操作,导致没有任何信息符合。有人说,能不能把and从查询条件中去除啊?可以,虽然有点麻烦,总是可以做。但是,有几个新问题需要解决:
- 你需要拿到所有语言的停用词才能做这个预处理。
- 万一这些语言的停用词变了呢?我们还需要及时更正。
幸运的是Elasticsearch的match查询提供了一个功能解决这个问题,同时我们不需要在应用程序中预处理停用词:zero_terms_query和cutoff_frequency。
- zero_terms_query
如果使用的分析器删除查询中的所有标记(如停用词),默认行为完全不匹配任何文档(none)。 可以使用zero_terms_query选项改变默认,none(默认),或all对应于match_all查询。
当查询使用"operator" : "and"的时候,需要把zero_terms_query设置为all。如果"operator" : "or",默认选项是我们需要的:
GET demo/msg/_search
{
"query": {
"match" : {
"body" : {
"query" : "chris and john",
"operator" : "and",
"zero_terms_query": "all"
}
}
}
}
- cutoff_frequency
match查询支持cutoff_frequency,允许指定绝对或相对的文档频率:
- OR:高频单词被放入“或许有”的类别,仅在至少有一个低频(低于截断)单词满足条件时才积分;
- AND:高频单词被放入“或许有”的类别,仅在所有低频(低于截断)单词满足条件时才积分。
该查询允许在运行时动态地处理停用词,相对领域独立,并且不需要停用词文件。它防止评分/迭代高频词,只在更重要(更低频率)的词与文档匹配时才考虑。但是,如果所有查询条件都高于给定的cutoff_frequency,查询会自动转换为纯联合(和)查询以确保快速执行。
cutoff_frequency可以是相对于文档的总数的小数[0..1),也可以是绝对值[1, +∞)。
GET demo/msg/_search
{
"query": {
"match" : {
"body" : {
"query" : "chris and john",
"cutoff_frequency" : 0.001
}
}
}
}
2.3、common 查询
大致说,common查询会分析查询文本,确定哪些单词“重要”,并使用这些单词进行搜索。 只有在文件与重要文字相匹配后才考虑“不重要”的字眼。“common查询”背后的动机是充分利用停用词清除的功能(更快的搜索),而不会完全消除停用词(因为它们有时可能有助于得分)。
执行此查询时会分几步:
- 查询会被发送到索引的每个shard;
- 在每个shard,Elasticsearch都会查看每个术语的文档频率
- 如果一个词的文档频率低于0.1%(0.001),那么它被认为是“低频”。 否则,它将被移到次要的“高频”列表中
- “低频”列表被重写为(逻辑AND)。 在这个例子中,它会包含“bonsai”,“cool”
- 然后将任何高频的文档分到剩余的高频列表中(“this”,“is”)
看看下面例子:
{
"common": {
"body": {
"query": "this is bonsai cool",
"cutoff_frequency": 0.001
}
}
}
在系统内,它被重写为:
{
"bool": {
"must": [
{ "term": { "body": "bonsai"}},
{ "term": { "body": "cool"}}
],
"should": [
{ "term": { "body": "this"}}
{ "term": { "body": "is"}}
]
}
}
3、通配符
我们还有一个问题,match查询不支持通配符。Elasticsearch对通配符支持包括两个情况:
- keyword:wildcard,prefix
- text:wildcard,prefix,match_phrase_prefix
第一种情况下,keyword的字段不会对索引和查询时文本做预处理。因此,在对该字段索引和查询的时候,应用程序必须做简单的一致性处理 ,比如把单词字母都变成小写。
第二种情况,text在索引时是通过分析器处理和过滤的,比如每个词都会正规化。而查询时,wildcard,match_prefix,match_phrase_prefix每个方法对查询文本的处理都不一样,需要分开对待:
- wildcard:查询时Elasticsearch不会通过分析器处理,因此,应用程序必须对查询文本做简单的一致性处理。
- prefix,match_phrase_prefix:查询时Elasticsearch会通过分析器处理。这时候停用词会被过滤掉。