拼写错误

我们期望在类似时间和价格的结构化数据上执行一个查询来返回精确匹配的文档。 然而,好的全文检索不应该是完全相同的限定逻辑。 相反,我们可以扩大范围以包括 可能 的匹配,而根据相关性得分将更好的匹配推到结果集的顶部。

事实上,只能完全匹配的全文搜索可能会困扰你的用户。 难道不希望在搜索 quick brown fox 时匹配一个包含 fast brown foxes 的文档, 搜索 Johnny Walker 同时匹配 Johnnie Walker ,搜索 Arnold Shcwarzenneger 同时匹配 Arnold Schwarzenegger ?

如果存在完全符合用户查询的文档,他们应该出现在结果集的顶部,而较弱的匹配可以被包含在列表的后面。 如果没有精确匹配的文档,至少我们可以显示有可能匹配用户要求的文档,它们甚至可能是用户最初想要的!

我们已经在 归一化词元 看过自由变音匹配, 将单词还原为词根 中的词干, 同义词 中的同义词, 但所有这些方法假定单词拼写正确,或者每个单词拼写只有唯一的方法。

Fuzzy matching 允许查询时匹配错误拼写的单词,而语音语汇单元过滤器可以在索引时用来进行 近似读音 匹配。

模糊性

模糊匹配 对待 “模糊” 相似的两个词似乎是同一个词。首先,我们需要对我们所说的 模糊性 进行定义。

在1965年,Vladimir Levenshtein 开发出了 Levenshtein distance, 用来度量从一个单词转换到另一个单词需要多少次单字符编辑。他提出了三种类型的单字符编辑:

  • 一个字符 替换 另一个字符: _f_ox -> _b_ox

  • 插入 一个新的字符:sic -> sic_k_

  • 删除 一个字符:b_l_ack -> back

Frederick Damerau 后来在这些操作基础上做了一个扩展:

  • 相邻两个字符的 换位 : _st_ar -> _ts_ar

举个例子,将单词 bieber 转换成 beaver 需要下面几个步骤:

  1. b 替换成 v :bie_b_er → bie_v_er

  2. i 替换成 a :b_i_ever → b_a_ ever

  3. ea 进行换位:b_ae_ver → b_ea_ver

这三个步骤表示 Damerau-Levenshtein edit distance 编辑距离为 3 。

显然,从 beaver 转换成 bieber 是一个很长的过程—他们相距甚远而不能视为一个简单的拼写错误。 Damerau 发现 80% 的拼写错误编辑距离为 1 。换句话说, 80% 的拼写错误可以对原始字符串用 单次编辑 进行修正。

Elasticsearch 指定了 fuzziness 参数支持对最大编辑距离的配置,默认为 2 。

当然,单次编辑对字符串的影响取决于字符串的长度。对单词 hat 两次编辑能够产生 mad , 所以对一个只有 3 个字符长度的字符串允许两次编辑显然太多了。 fuzziness 参数可以被设置为 AUTO ,这将导致以下的最大编辑距离:

  • 字符串只有 1 到 2 个字符时是 0

  • 字符串有 3 、 4 或者 5 个字符时是 1

  • 字符串大于 5 个字符时是 2

当然,你可能会发现编辑距离 2 仍然是太多了,返回的结果似乎并不相关。 把最大 fuzziness 设置为 1 ,你可以得到更好的结果和更好的性能。

模糊查询

fuzzy 查询term 查询的模糊等价。 也许你很少直接使用它,但是理解它是如何工作的,可以帮助你在更高级别的 match 查询中使用模糊性。

为了解它是如何运作的,我们首先索引一些文档:

POST /my_index/my_type/_bulk
{ "index": { "_id": 1 }}
{ "text": "Surprise me!"}
{ "index": { "_id": 2 }}
{ "text": "That was surprising."}
{ "index": { "_id": 3 }}
{ "text": "I wasn't surprised."}

现在我们可以为词 surprize 运行一个 fuzzy 查询:

GET /my_index/my_type/_search
{
  "query": {
    "fuzzy": {
      "text": "surprize"
    }
  }
}

fuzzy 查询是一个词项级别的查询,所以它不做任何分析。它通过某个词项以及指定的 fuzziness 查找到词典中所有的词项。 fuzziness 默认设置为 AUTO

在我们的例子中, surprise 比较 surprisesurprised 都在编辑距离 2 以内, 所以文档 1 和 3 匹配。通过以下查询,我们可以减少匹配度到仅匹配 surprise

GET /my_index/my_type/_search
{
  "query": {
    "fuzzy": {
      "text": {
        "value": "surprize",
        "fuzziness": 1
      }
    }
  }
}

提高性能

fuzzy 查询的工作原理是给定原始词项及构造一个 编辑自动机— 像表示所有原始字符串指定编辑距离的字符串的一个大图表。

然后模糊查询使用这个自动机依次高效遍历词典中的所有词项以确定是否匹配。 一旦收集了词典中存在的所有匹配项,就可以计算匹配文档列表。

当然,根据存储在索引中的数据类型,一个编辑距离 2 的模糊查询能够匹配一个非常大数量的词项同时执行效率会非常糟糕。 下面两个参数可以用来限制对性能的影响:

prefix_length

不能被 `模糊化'' 的初始字符数。 大部分的拼写错误发生在词的结尾,而不是词的开始。 例如通过将 `prefix_length 设置为 3 ,你可能够显著降低匹配的词项数量。

max_expansions

如果一个模糊查询扩展了三个或四个模糊选项, 这些新的模糊选项也许是有意义的。如 果它产生 1000 个模糊选项,那么就基本没有意义了。 设置 max_expansions 用来限制将产生的模糊选项的总数量。模糊查询将收集匹配词项直到达到 max_expansions 的限制。

模糊匹配查询

match 查询支持开箱即用的模糊匹配:

GET /my_index/my_type/_search
{
  "query": {
    "match": {
      "text": {
        "query":     "SURPRIZE ME!",
        "fuzziness": "AUTO",
        "operator":  "and"
      }
    }
  }
}

查询字符串首先进行分析,会产生词项 [surprize, me] ,并且每个词项根据指定的 fuzziness 进行模糊化。

同样, multi_match 查询也支持 fuzziness ,但只有当执行查询时类型是 best_fields 或者 most_fields

GET /my_index/my_type/_search
{
  "query": {
    "multi_match": {
      "fields":  [ "text", "title" ],
      "query":     "SURPRIZE ME!",
      "fuzziness": "AUTO"
    }
  }
}

matchmulti_match 查询都支持 prefix_lengthmax_expansions 参数。

Tip
模糊性(Fuzziness)只能在 match and multi_match 查询中使用。不能使用在短语匹配、常用词项或 cross_fields 匹配。

模糊性评分

用户喜欢模糊查询。他们认为这种查询会魔法般的找到正确拼写组合。 很遗憾,实际效果平平。

假设我们有1000个文档包含 Schwarzenegger ,只是一个文档的出现拼写错误 Schwarzeneger 。 根据 term frequency/inverse document frequency 理论,这个拼写错误文档比拼写正确的相关度更高,因为错误拼写出现在更少的文档中!

换句话说,如果我们对待模糊匹配类似其他匹配方法,我们将偏爱错误的拼写超过了正确的拼写,这会让用户抓狂。

Tip
模糊匹配不应用于参与评分—​只能在有拼写错误时扩大匹配项的范围。

默认情况下, match 查询给定所有的模糊匹配的恒定评分为1。这可以满足在结果列表的末尾添加潜在的匹配记录,并且没有干扰非模糊查询的相关性评分。

Tip

在模糊查询最初出现时很少能单独使用。他们更好的作为一个 bigger 场景的部分功能特性,如 search-as-you-type 完成 建议did-you-mean 短语 建议

语音匹配

最后,在尝试任何其他匹配方法都无效后,我们可以求助于搜索发音相似的词,即使他们的拼写不同。

有一些用于将词转换成语音标识的算法。 Soundex 算法是这些算法的鼻祖, 而且大多数语音算法是 Soundex 的改进或者专业版本,例如 MetaphoneDouble Metaphone (扩展了除英语以外的其他语言的语音匹配), Caverphone 算法匹配了新西兰的名称, Beider-Morse 算法吸收了 Soundex 算法为了更好的匹配德语和依地语名称, Kölner Phonetik 为了更好的处理德语词汇。

值得一提的是,语音算法是相当简陋的,他们设计初衷针对的语言通常是英语或德语。这限制了他们的实用性。 不过,为了某些明确的目标,并与其他技术相结合,语音匹配能够作为一个有用的工具。

首先,你需要从 https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-phonetic.html 获取语音分析插件并在集群的每个节点安装, 然后重启每个节点。

然后,您可以创建一个使用语音语汇单元过滤器的自定义分析器,并尝试下面的方法:

PUT /my_index
{
  "settings": {
    "analysis": {
      "filter": {
        "dbl_metaphone": { (1)
          "type":    "phonetic",
          "encoder": "double_metaphone"
        }
      },
      "analyzer": {
        "dbl_metaphone": {
          "tokenizer": "standard",
          "filter":    "dbl_metaphone" (2)
        }
      }
    }
  }
}
  1. 首先,配置一个自定义 phonetic 语汇单元过滤器并使用 double_metaphone 编码器。

  2. 然后在自定义分析器中使用自定义语汇单元过滤器。

现在我们可以通过 analyze API 来进行测试:

GET /my_index/_analyze?analyzer=dbl_metaphone
Smith Smythe

每个 SmithSmythe 在同一位置产生两个语汇单元: SM0XMT 。 通过分析器播放 JohnJonJohnnie 将产生两个语汇单元 JNAN ,而 Jonathon 产生语汇单元 JN0NANTN

语音分析器可以像任何其他分析器一样使用。 首先映射一个字段来使用它,然后索引一些数据:

PUT /my_index/_mapping/my_type
{
  "properties": {
    "name": {
      "type": "string",
      "fields": {
        "phonetic": { (1)
          "type":     "string",
          "analyzer": "dbl_metaphone"
        }
      }
    }
  }
}

PUT /my_index/my_type/1
{
  "name": "John Smith"
}

PUT /my_index/my_type/2
{
  "name": "Jonnie Smythe"
}
  1. name.phonetic 字段使用自定义 dbl_metaphone 分析器。

可以使用 match 查询来进行搜索:

GET /my_index/my_type/_search
{
  "query": {
    "match": {
      "name.phonetic": {
        "query": "Jahnnie Smeeth",
        "operator": "and"
      }
    }
  }
}

这个查询返回全部两个文档,演示了如何进行简陋的语音匹配。 用语音算法计算评分是没有价值的。 语音匹配的目的不是为了提高精度,而是要提高召回率—​以扩展足够的范围来捕获可能匹配的文档。

通常更有意义的使用语音算法是在检索到结果后,由另一台计算机进行消费和后续处理,而不是由人类用户直接使用。


书籍推荐