目录
1.简介
大多数语言的单词都可以 词形变化,比如
- 单复数变化 : fox 、foxes
- 时态变化 : pay 、 paid 、 paying
- 性别变化 : waiter 、 waitress
- 动词人称变化 : hear 、 hears
- 代词变化 : I 、 me 、 my
- 不规则变化 : ate 、 eaten
- 情景变化 : so be it 、 were it so
词干提取 试图移除单词的变化形式之间的差别,从而达到将每个词都提取为它的词根形式。
词干提取是一种遭受两种困扰的模糊的技术:
- 词干弱提取 : 无法将同样意思的单词缩减为同一个词根。弱词干提取会导致搜索时无法返回相关文档。
- 词干过度提取 : 无法将不同含义的单词分开。词干过度提取会降低精准度:不相干的文档会在不需要返回的时候返回。
词形还原
原词是一组相关词的规范形式,或词典形式 — paying 、paid 和 pays 的原词是 pay 。
通常原词很像与其相关的词,但有时也不像 — is 、 was 、 am 和 being 的原词是 be 。
词形还原,很像词干提取,试图归类相关单词,但是它比词干提取先进一步的是它企图按单词的 词义 ,或意义归类。
同样的单词可能表现出两种意思—例如, wake 可以表现为 to wake up 或 a funeral 。
然而词形还原试图区分两个词的词义,词干提取却会将其混为一谈。
词形还原是一种更复杂和高资源消耗的过程,它需要理解单词出现的上下文来决定词的意思。
实践中,词干提取似乎比词形还原更高效,且代价更低。
1.1.词干提取算法(stemmer algorithm)
提供了一系列规则用于将一个词提取为它的词根形式。
提取单词词干时并不需要知道该词的任何信息。
[优点]
可以作为插件使用,速度快,占用内存少,有规律的单词处理效果好
[缺点]
没规律的单词效果不好
【举例】
[激进]
{
"settings": {
"analysis": {
"filter": {
"english_stop": {
"type": "stop",
"stopwords": "_english_"
},
"english_keywords": {
"type": "keyword_marker", // 分词过滤器列出那些不用被词干提取的单词。这个过滤器默认情况下是一个空的列表。
"keywords": []
},
"english_stemmer": {
"type": "stemmer",
"language": "english" // 词干提取器,激进
},
"english_possessive_stemmer": {
"type": "stemmer",
"language": "possessive_english" // 词干提取器
}
},
"analyzer": {
"english": {
"tokenizer": "standard",
"filter": [
"english_possessive_stemmer",
"lowercase",
"english_stop",
"english_keywords",
"english_stemmer"
]
}
}
}
}
}
[非激进]
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"english_stop": {
"type": "stop",
"stopwords": "_english_"
},
"light_english_stemmer": {
"type": "stemmer",
"language": "light_english" // 词干提取器,非激进
},
"english_possessive_stemmer": {
"type": "stemmer",
"language": "possessive_english"
}
},
"analyzer": {
"english": {
"tokenizer": "standard",
"filter": [
"english_possessive_stemmer",
"lowercase",
"english_stop",
"light_english_stemmer",
"asciifolding" // 分词过滤器
]
}
}
}
}
}
1.2.字典词干提取器
字典词干提取器只是简单地在字典里查找词。理论上可以给出比算法化词干提取器更好的结果。
一个字典词干提取器应当可以
- 返回不规则形式如
feet
和mice
的正确词干 - 区分出词形相似但词义不同的情形,比如
organ
andorganization
实践中一个好的算法化词干提取器一般优于一个字典词干提取器。应该有以下两大原因:
① 字典质量
一个字典词干提取器再好也就跟它的字典一样。 据牛津英语字典网站估计,英语包含大约75万个单词(包含变音变形词)。电脑上的大部分英语字典只包含其中的 10% 。
词的含义随时光变迁。mobility
提取词干 mobil
先前可能讲得通,但现在合并进了手机可移动性的含义。字典需要保持最新,这是一项很耗时的任务。通常等到一个字典变得好用后,其中的部分内容已经过时。
字典词干提取器对于字典中不存在的词无能为力。而一个基于算法的词干提取器,则会继续应用之前的相同规则,结果可能正确或错误。
② 大小与性能
字典词干提取器需要加载所有词汇、 所有前缀,以及所有后缀到内存中。这会显著地消耗内存。找到一个词的正确词干,一般比算法化词干提取器的相同过程更加复杂。
依赖于不同的字典质量,去除前后缀的过程可能会更加高效或低效。低效的情形可能会明显地拖慢整个词干提取过程。
另一方面,算法化词干提取器通常更简单、轻量和快速。
如果你所使用的语言有比较好的算法化词干提取器,这通常是比一个基于字典的词干提取器更好的选择。
对于算法化词干提取器效果比较差(或者压根没有)的语言,可以使用拼写检查(Hunspell)字典词干提取器
1.3.Hunspell 词干提取器
获取 Hunspell 词典:
- extensions.openoffice.org: 下载解压
.oxt
后缀的文件 - addons.mozilla.org: 下载解压
.xpi
扩展文件 - OpenOffice archive: 下载解压
.zip
文件
一个 Hunspell 词典由两个文件组成 — 具有相同的文件名和两个不同的后缀 — 如 en_US
—和下面的两个后缀的其中一个:
.dic
:包含所有词根,采用字母顺序,再加上一个代表所有可能前缀和后缀的代码表 【集体称之为词缀( affixes 】
.aff:
包含实际 .dic
文件每一行代码表对应的前缀和后缀转换
安装一个词典
Hunspell 语汇单元过滤器在特定的 Hunspell 目录里寻找词典, 默认目录是 ./config/hunspell/
。 .dic
文件和 .aff
文件应该要以子目录且按语言/区域的方式来命名。
config/
└ hunspell/
└ en_US/
├ en_US.dic
├ en_US.aff
└ settings.yml
- Hunspell 目录位置可以通过编辑
config/elasticsearch.yml
文件的:indices.analysis.hunspell.dictionary.location
设置来修改 en_US
是这个区域的名字,也是我们传给hunspell
语汇单元过滤器参数language
值- 一个语言一个设置文件
按语言设置
在语言的目录设置文件 settings.yml
包含适用于所有字典内的语言目录的设置选项。
ignore_case: true
strict_affix_parsing: true
【说明】
ignore_case
Hunspell 目录默认是区分大小写的,如,姓氏 Booker
和名词 booker
是不同的词,所以应该分别进行词干提取。 也许让 hunspell
提取器区分大小写是一个好主意,不过也可能让事情变得复杂:
-
一个句子的第一个词可能会被大写,因此感觉上会像是一个名词。
-
输入的文本可能全是大写,如果这样那几乎一个词都找不到。
-
用户也许会用小写来搜索名字,在这种情况下,大写开头的词将找不到。
一般来说,设置参数 ignore_case
为 true
是一个好主意。
strict_affix_parsing
词典的质量千差万别。 一些网上的词典的 .aff
文件有很多畸形的规则。 默认情况下,如果 Lucene 不能正常解析一个词缀(affix)规则, 它会抛出一个异常。 你可以通过设置 strict_affix_parsing
为 false
来告诉 Lucene 忽略错误的规则。
自定义词典
如果一个目录放置了多个词典 (.dic 文件), 他们会在加载时合并到一起。这可以让你以自定义的词典的方式对下载的词典进行定制:
config/
└ hunspell/
└ en_US/
├ en_US.dic
├ en_US.aff
├ custom.dic
└ settings.yml
custom 词典和 en_US 词典将合并到一起。
多个 .aff 文件是不允许的,因为会产生规则冲突。
.dic 文件和 .aff 文件的格式在这里讨论: 【Hunspell 词典格式】
创建一个 Hunspell 语汇单元过滤器
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"en_US": {
"type": "hunspell",
"language": "en_US" // 参数 language
和目录下对应的名称相同
}
},
"analyzer": {
"en_US": {
"tokenizer": "standard",
"filter": [ "lowercase", "en_US" ]
}
}
}
}
}
[测试]
GET /my_index/_analyze?analyzer=en_US
reorganizes
返回 organize
GET /_analyze?analyzer=english
reorganizes
返回 reorgan
[说明]
它不仅能移除前缀还能移除后缀。大多数算法词干提取仅能移除后缀。
Hunspell 词典会占用几兆的内存。幸运的是,Elasticsearch 每个节点只会创建一个词典的单例。
所有的分片都会使用这个相同的 Hunspell 分析器。
Hunspell 词典格式
【分析】在美式英语词典(US English dictionary),en_US.dic
文件包含了一个包含词 analyze
的实体
analyze/ADSG
en_US.aff
文件包含了一个针对标记 A
、 G
、D
和 S
的前后缀的规则。 其中应该只有一个能匹配,每一个规则的格式如下:
[type] [flag] [letters to remove] [letters to add] [condition]
例如,下面的后缀 (SFX
) 规则 D
。它是说,当一个词由一个辅音 (除了 a
、e
、i
、o
或 u
外的任意音节) 后接一个 y
,那么它可以移除 y
和添加 ied
结尾 (如,ready
→ readied
)。
SFX D y ied [^aeiou]y
前面提到的 A
、 G
、D
和 S
标记对应规则如下:
SFX D Y 4
SFX D 0 d e -》 analyze 以一个 e 结尾,所以它可以添加一个 d 变成 analyzed
SFX D y ied [^aeiou]y
SFX D 0 ed [^ey]
SFX D 0 ed [aeiou]y
SFX S Y 4
SFX S y ies [^aeiou]y
SFX S 0 s [aeiou]y
SFX S 0 es [sxzh]
SFX S 0 s [^sxzhy] -》analyze 不是由 s 、x 、z 、h 或 y 结尾,所以,它可以添加一个 s 变成 analyzes 。
SFX G Y 2
SFX G e ing e
SFX G 0 ing [^e]
PFX A Y 1
PFX A 0 re -》可以添加前缀 re 来形成 reanalyze 。这个规则可以组合后缀规则一起形成: reanalyzes 、reanalyzed 、 reanalyzing 。
1.4.选择一个词干提取器
english
:语汇单元过滤器(token filter)。light_english
:语汇单元过滤器(token filter)。minimal_english
:Lucene 里面的EnglishMinimalStemmer
,用来移除复数。lovins
:基于Lovins 提取器, 第一个词干提取器。porter
:基于 Porter 提取器。porter2
:基于 Porter2 提取器。possessive_english
:Lucene 里面的EnglishPossessiveFilter
,移除's
关于哪个是最好的词干提取器,不存在一个唯一的正确答案 — 它要看你具体的需求。
性能、质量、程度。
提取性能
如果一个好的算法词干提取器可用于你的语言,那明智的使用它而不是 Hunspell。它会更快并且消耗更少内存,并且会产生和通常一样好或者比 Hunspell 等价的结果.
如果精度和可定制性对你很重要,那么你需要(和有精力)来维护一个自定义的词典,那么 Hunspell 会给你比算法提取器更大的灵活性。
提取程度
不同的词干提取器会将词弱提取或过度提取到一定的程度。 light_
提取器提干力度不及标准的提取器。 minimal_
提取器同样也不那么积极。Hunspell 提取力度要激进一些。
是否想要积极提取还是轻量提取取决于你的场景。如果你的搜索结果是要用于聚类算法,你可能会希望匹配的更广泛一点(因此,提取力度要更大一点)。 如果你的搜索结果是面向最终用户,轻量的提取一般会产生更好的结果。对搜索来说,将名称和形容词提干比动词提干更重要,当然这也取决于语言。
另外一个要考虑的因素就是你的文档集的大小。 一个只有 10,000 个产品的小集合,你可能要更激进的提干来确保至少匹配到一些文档。 如果你的文档集很大,使用轻量的弱提取可能会得到更好的匹配结果。
选择
从推荐的一个词干提取器出发,如果它工作的很好,那没有什么需要调整的。如果不是,你将需要花点时间来调查和比较该语言可用的各种不同提取器, 来找到最适合你目的的那一个。
1.5.控制词干提取
开箱即用的词干提取方案永远也不可能完美。 尤其是算法提取器,他们可以愉快的将规则应用于任何他们遇到的词,包含那些你希望保持独立的词。 也许,在你的场景,保持独立的 skies
和 skiing
是重要的,你不希望把他们提取为 ski
(正如 english
分析器那样)。
阻止词干提取
语言分析器(参数 stem_exclusion 允许我们指定一个词语列表,让他们不被词干提取。
在内部,这些语言分析器使用[语汇单元过滤器] 来标记这些词语列表为 keywords ,用来阻止后续的词干提取过滤器来触碰这些词语。
【举例】
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"no_stem": {
"type": "keyword_marker",
"keywords": [ "skies" ]
}
},
"analyzer": {
"my_english": {
"tokenizer": "standard",
"filter": [
"lowercase",
"no_stem",
"porter_stem"
]
}
}
}
}
}
【测试】
GET /my_index/_analyze?analyzer=my_english
sky skies skiing skis
返回: sky, skies, ski, ski
虽然语言分析器只允许我们通过参数 stem_exclusion 指定一个词语列表来排除词干提取, 不过 keyword_marker 语汇单元过滤器同样
还接收一个 keywords_path 参数允许我们将所有的关键字存在一个文件。 这个文件应该是每行一个字,并且存在于集群的每个节点。
自定义提取
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"custom_stem": {
"type": "stemmer_override",
"rules": [
"skies=>sky",
"mice=>mouse",
"feet=>foot"
]
}
},
"analyzer": {
"my_english": {
"tokenizer": "standard",
"filter": [
"lowercase",
"custom_stem",
"porter_stem"
]
}
}
}
}
}
GET /my_index/_analyze?analyzer=my_english
The mice came down from the skies and ran over my feet
返回 the
, mouse
, came
, down
, from
, the
, sky
, and
, ran
, over
, my
, foot
原形词干提取
PUT /my_index
{
"settings": {
"analysis": {
"filter": {
"unique_stem": {
"type": "unique",
"only_on_same_position": true // 为了只有当重复语汇单元出现在相同位置时,移除它们
}
},
"analyzer": {
"in_situ": {
"tokenizer": "standard",
"filter": [
"lowercase",
"keyword_repeat", // 语汇单元过滤器必须出现在词干提取器之前
"porter_stem",
"unique_stem" // 过滤器是在词干提取器完成之后移除重复词项
]
}
}
}
}
}
不建议使用原形词干提取
原因如下
- 无法区分精准匹配和非精准匹配。
- 必须搞清楚 相关度分值是否如何计算的。