Elasticsearch:如何搜索含有复合词的语言

server/2025/2/1 18:44:08/
aidu_pl">

作者:来自 Elastic Peter Straßer

复合词在文本分析和标记过程中给搜索引擎带来挑战,因为它们会掩盖词语成分之间的有意义的联系。连字分解器标记过滤器等工具可以通过解构复合词来帮助解决这些问题。

德语以其长复合词而闻名:Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz 是德语词典中最长的单词 —— 对于没有准备好处理复合词的搜索引擎来说是一场噩梦。许多其他语言如荷兰语、瑞典语等也都有这个概念。甚至英语中也有一些这样的词语,尽管程度较轻。想想 “sunflower” 或 “basketball”。

让我们讨论一下这些词语所带来的问题和挑战,为什么这是一个问题以及如何解决它。

问题

在进行全文搜索时,Elasticsearch 等搜索引擎会在查询和索引时分析文本并将文本转换为标记(tokens)。我们想要提取单词的含义,而不是完全匹配字符串。对于我们的搜索,我们不必担心兔子是在 “running” 还是在 “runs” —— 我们只是将单词简化为其词根形式:“run”。

当我们处理复合词时,如果我们不以某种方式解决它,这个阶段就会失败。假设我们有一个包含文档的索引:“Basketballs”。如果我们使用标准英语分析器来分析这一点,我们会得到:

GET _analyze
{"text": "Basketballs", "analyzer": "english"
}

响应:

{"tokens": [{"token": "basketbal","start_offset": 0,"end_offset": 11,"type": "<ALPHANUM>","position": 0}]
}

在这个例子中,复合词 “basketballs” 被标记化为 “basketbal”。虽然我们能够将其转换为小写并去除复数形式,但我们无法捕捉到 “basketball” 也是一种 “ball” 的含义。现在,如果我们在索引中搜索 “ball”,我们希望能够找到 “basketball”,但 “bal”(经过分析)并不匹配 “basketbal”,因此我们没有得到任何结果!

那么,我们该如何解决这个问题呢?

也许同义词有用?

我们首先想到的可能是尝试使用同义词将不同的子词与复合词关联起来。由于复合词的使用相当有限,这对于英语来说已经足够好了:

basketball => basketball, ball

现在我们来看看德语。语法的工作方式是将任意数量的单词组合起来形成一个更精确的单词。

Rind (cow - 牛) 和 Fleisch (meat - 肉) 变成 Rindfleisch (牛肉)。

Rind(cow - 牛)、Fleisch(meat - 肉)和 Etikett(label - 标签)变成 Rindfleischetikett(牛肉标签)。这个过程可以任意长,直到我们得到诸如 Rindfleischetikettierungsüberwachungsaufgabenübertragungsgesetz 之类的可爱的词语。

为了用我们的同义词文件解决这个问题,我们必须对无限数量的单词排列进行建模:

# cowmeat, cowmeatlabel, meatlabel
rindfleisch => rindfleisch, rind, fleisch
rindfleischetikett => rindfleischetikett, rind, fleisch, etikett
fleischetikett => fleischetikett, fleisch, etikett
…

在德语等语言中,这很快变得不切实际。所以我们必须从相反的角度来看待这个问题。我们不是从复合词开始寻找其复合词,而是查看可用的复合部分并根据这些知识解构单词。

连字分解器 - Hyphenation Decompounder

连字分解器标记过滤器(Hyphenation Decompounder Token Filter)是一个 Lucene 标记过滤器,它依赖连字规则来检测潜在的单词拆分。规则文件(Rule files)以对象格式化对象 (Objects For Formatting - OFFO) 格式指定,我们也可以在其中找到一些示例文件。我们还需要一个单词列表,用于将复合词分解为其子部分。单词列表可以内联提供,但对于生产工作负载,我们通常还会将文件上传到磁盘,因为这些文件可能非常大,并且通常包含整个词典。

可以根据其许可证提供的德语示例文件可在此存储库中找到。

那么它有什么作用呢?

# word list: coffee, sugar, cup
# text: coffee cup
GET _analyze
{"tokenizer": "standard","filter": ["lowercase",{"type": "hyphenation_decompounder","hyphenation_patterns_path": "analysis/hyphenation_patterns.xml","word_list": ["kaffee", "zucker", "tasse"]}],"text": "Kaffeetasse"
}Response:# coffee cup, coffee, cup
[ "kaffeetasse", "kaffee", "tasse"]

这有助于确保搜索 “Tasse”(杯子)的用户能够找到包含较大复合词 “Kaffeetasse”(咖啡杯)的文档。

注意

  • 查看此文章,了解如何上传包以便能够在 Elastic Cloud Hosted 部署中访问这些文件。
  • 还有 Dictionary Decompounder,它可以在没有连字规则的情况下执行相同的操作,而是强制执行单词检测。对于大多数用例,我们推荐使用连字分解器。

避免部分匹配

由于我们通常使用包含数千个单词的整本词典的单词列表,因此分解器可能会使用默认设置以非预期的方式拆分单词,从而导致不相关的匹配。

# word list: coffee, fairy, cup
# text: coffee cup
GET _analyze
{"tokenizer": "standard","filter": ["lowercase",{"type": "hyphenation_decompounder","hyphenation_patterns_path": "analysis/hyphenation_patterns.xml","word_list": ["kaffee", "fee", "tasse"]}],"text": "Kaffeetasse"
}Response:# coffee cup, coffee, fairy, cup
["kaffeetasse", "kaffee", "fee", "tasse"]

此示例在 “Kaffee”(coffee - 咖啡)中检测到 “fee”(fairy - 仙女)。这当然是意外的,并非有意为之。另一个示例可能是 “Streifenbluse”(striped blouse- 条纹衬衫),其中会找到 “Reifen”(tires - 轮胎)。“Streifen”(stripe - 条纹)、“Reifen”(轮胎)和 “Bluse”(blouse - 衬衫)都是我们通常想要拆分的常用词。

我们的用户搜索 “fee”(fairies - 仙女)和 “reifen”(tires - 轮胎)时,现在会找到 coffee 和 blouses!这可不妙。

在 8.17 中,hyphenation_decompounder 中添加了一个新的参数 no_sub_matches 来解决此问题。

# word list: coffee, fairy, cup
# text: coffee cup
GET _analyze
{"tokenizer": "standard","filter": ["lowercase",{"type": "hyphenation_decompounder","hyphenation_patterns_path": "analysis/hyphenation_patterns.xml","word_list": ["kaffee", "fee", "tasse"],"no_sub_matches": true}],"text": "Kaffeetasse"
}Response:# coffee cup, coffee, cup
["kaffeetasse", "kaffee", "tasse"]

这可以防止创建 “fee”(fairy)标记并且我们的搜索按预期工作!

匹配所有查询 terms

根据我们目前所见,搜索德语文本的索引映射可能类似于以下索引定义:

PUT products
{"mappings": {"properties": {"full_text": {"type": "text","analyzer": "german_analyzer_with_decompounding"}}},"settings": {"analysis": {"analyzer": {"german_analyzer_with_decompounding": {"type": "custom","tokenizer": "standard","filter": ["lowercase","german_stop_words_filter","german_decompounder","german_normalization","german_stemmer"]},"german_analyzer_without_decompounding": { "type": "custom","tokenizer": "standard","filter": ["lowercase","german_stop_words_filter","german_normalization","german_stemmer"]}},"filter": {"german_stop_words_filter": {"type": "stop","stopwords": "_german_"},"german_decompounder": {"only_longest_match": "true","word_list_path": "dictionary/dictionary.txt","type": "hyphenation_decompounder","hyphenation_patterns_path": "dictionary/hyphenation_patterns.xml"},"german_stemmer": {"type": "stemmer","language": "light_german"}}}}
}

注意:在实际生产环境中,其中很可能会有围绕 asciifolding、表情符号过滤器(emoji filters)或同义词(synonyms)的过滤器,但这已经是一个很好的起点,应该会为德语文本获得良好的结果。

当搜索多个术语时,我们通常会期望(不考虑高级查询放松策略)我们指定的所有搜索词都包含在我们的结果中。因此,当在电子商务商店中搜索 Lederjacke(leather jacket - 皮夹克)时,我们希望我们的产品是皮革制成的夹克,而不是皮革制品和夹克的随机组合。

实现此目的的方法是将搜索查询中的运算符设置为 AND。所以我们这样做并在我们的产品中搜索 “Lederjacke”(皮夹克):

GET products/_search
{"query": {"match": {"full_text": { "query": "lederjacke",  "operator": "and" }}}
}
# returns all leather products and all jackets

令人惊讶的是,这并不像我们预期的那样。我们找到了所有含有皮革或夹克的产品。这是因为运算符在标记化之前进行评估,并且使用 OR 评估标记过滤器生成的标记。

为了解决这个问题,我们需要在应用程序中分解我们的术语。我们可以先调用 _analyze API,然后将分解后的术语传递给我们的搜索查询。因为我们已经分解了,所以我们在过滤器链中使用了没有分解器过滤器的搜索分析器(search analyzer)。

GET _analyze
{"tokenizer": "standard","filter": ["lowercase",{"type": "hyphenation_decompounder","hyphenation_patterns_path": "analysis/hyphenation_patterns.xml","word_list_path": "analysis/word_list.xml","no_sub_matches": true}],"text": "Lederjacke" 
}Response: ["leder", "jacke"]GET products/_search
{"query": {"match": {"full_text": { "query": "leder jacke","operator": "and","analyzer": "german_analyzer_without_decompounding"}}}
}
# returns only leather jackets 

搜索分解词的替代方法

虽然 Elasticsearch Serverless 具有根据负载动态扩展的能力,为搜索应用程序带来了许多巨大优势,但在撰写本文时,目前无法上传文件并在这些项目中使用连字分解器。

替代工具

可以在 Elastic 堆栈之外使用的替代方案是适用于 Java 的 JWordSplitter 和 CharSplit 模型或 CompoundPiece 模型,它们采用机器学习方法分解单词,而无需配置文件。

以下是如何将 CompoundPiece 与 Hugging Face 转换器库一起使用:

from transformers import pipeline
pipe = pipeline("text2text-generation", model="benjamin/compoundpiece")
result = pipe("Lederjacke", max_length=100)
print(result[0]['generated_text'].split('-'))STDOUT: ['Leder', 'Jacke']

它支持 56 种语言,并且无需配置文件即可工作,这是实现多语言应用程序分解的好方法。

语义搜索

我们在这里的许多文章中都涵盖了使用文本嵌入模型的语义搜索。这些模型能够解释文本的含义,可以通过将文档和查询转换为向量并找到与查询最接近的文档来进行搜索。

将此处讨论的词汇搜索与向量搜索相结合称为混合搜索。这也有助于大大提高结果的质量并解释文本中复合词背后的含义。

结论

分解是构建有效的多语言搜索应用程序的重要工具 - 尤其是涉及德语等语言时。通过使用连字符分解器等工具,我们可以确保我们的搜索能够理解复合词背后的含义,为用户提供更好、更相关的结果。

与对搜索算法的任何调整一样,评估其对搜索性能的整体影响非常重要。通过浏览我们关于 _eval API 的文章,了解有关如何客观衡量搜索结果质量的更多信息。

Elasticsearch 包含许多新功能,可帮助你为你的用例构建最佳搜索解决方案。深入了解我们的示例笔记本以了解更多信息,开始免费云试用,或立即在本地机器上试用 Elastic。

原文:How to search languages with compound words - Elasticsearch Labs


http://www.ppmy.cn/server/164131.html

相关文章

玩转大语言模型——使用langchain和Ollama本地部署大语言模型

系列文章目录 玩转大语言模型——使用langchain和Ollama本地部署大语言模型 玩转大语言模型——ollama导入huggingface下载的模型 玩转大语言模型——langchain调用ollama视觉多模态语言模型 玩转大语言模型——使用GraphRAGOllama构建知识图谱 玩转大语言模型——完美解决Gra…

STM32外设应用

1. 什么是STM32外设&#xff1f; STM32微控制器集成了多种外设&#xff0c;这些外设可以帮助我们实现各种功能&#xff0c;比如控制LED灯、读取传感器数据、与其他设备通信等。常见的外设有GPIO&#xff08;通用输入输出&#xff09;、ADC&#xff08;模数转换器&#xff09;、…

软件工程概论试题一

一、单选 1.下面关于“发布测试”的观点错误的是( )。 A.系统开发团队不应该负责发布测试 B.发布测试是一个确认检查的过程 C.发布测试的目的是让系统供应商确信系统足够好可以使用了 D.发布测试通常采用白盒测试 正答&#xff1a;D 2.下图最有可能是()体系结构风格。 A. C…

Pyecharts之图表组合与布局优化

在数据可视化中&#xff0c;我们经常需要将多个图表组合在一起&#xff0c;以展示不同维度的数据或者进行对比分析。同时&#xff0c;合理的布局能够提升图表的可读性和用户体验。Pyecharts 提供了强大的组件和方法&#xff0c;让我们可以轻松实现图表的组合和布局优化。本篇将…

leetcode——搜索二维矩阵II(java)

编写一个高效的算法来搜索 *m* x *n* 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,1…

代码随想录_栈与队列

栈与队列 232.用栈实现队列 232. 用栈实现队列 使用栈实现队列的下列操作&#xff1a; push(x) – 将一个元素放入队列的尾部。 pop() – 从队列首部移除元素。 peek() – 返回队列首部的元素。 empty() – 返回队列是否为空。 思路: 定义两个栈: 入队栈, 出队栈, 控制出入…

395. 至少有K个重复字符的最长子串

参考题解&#xff1a;https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/solutions/623991/jie-ben-ti-bang-zhu-da-jia-li-jie-di-gui-obla 递归含义&#xff1a;返回字符串s中至少有k个重复字符的最长子串递归终止条件&#xff1a;当s的…

洛谷P3372 【模板】线段树 1以及分块

【模板】线段树 1 题目描述 如题&#xff0c;已知一个数列&#xff0c;你需要进行下面两种操作&#xff1a; 将某区间每一个数加上 k k k。求出某区间每一个数的和。 输入格式 第一行包含两个整数 n , m n, m n,m&#xff0c;分别表示该数列数字的个数和操作的总个数。 …