阿拉伯语搜索规范化:缺少对 Hamza 变体、Ya/Kaf 形式和拼写等效性的支持

您好 Discourse 团队:

我们正在运行一个多语言论坛,其中包含大量的阿拉伯语和波斯语内容,并且在与阿拉伯语正字法规范化相关的搜索功能方面遇到了一个关键限制。

:magnifying_glass_tilted_left: 问题描述

阿拉伯字母包含多个 Unicode 表示形式,但语义上是相同的字符。不幸的是,Discourse 当前的搜索引擎似乎将这些变体视为不同的字符,这会导致搜索结果不完整或具有误导性。

示例:

  • 搜索 إطلاق مقامي 只会返回精确匹配项,而包含 اطلاق مقاميأطلاق مقاميإطلاق‌مقامي 的帖子则被排除在外。
  • 同样,搜索 ي (U+064A) 不会匹配 ی (U+06CC),而 ك (U+0643) 也无法匹配 ک (U+06A9),尽管它们在阿拉伯语/波斯语环境中功能上是等效的。

这不仅影响 hamza 变体(أإءؤئ),还影响常见的替换,例如:

字符 Unicode 建议的规范化
أ, إ, ء, آ U+0623, U+0625, U+0621, U+0622 规范化为 ا
ؤ U+0624 规范化为 و
ئ U+0626 规范化为 ي
ى U+0649 规范化为 ي
ة U+0629 规范化为 ه
ي vs ی U+064A vs U+06CC 规范化为 ی
ك vs ک U+0643 vs U+06A9 规范化为 ک

当用户省略变音符号或使用不同的键盘布局时,此问题会加剧,导致搜索行为碎片化。


:gear: 建议的解决方案

我们建议在索引和查询解析期间实现一个支持 Unicode 的规范化层。这可以通过以下方式实现:

  1. 预处理索引内容和用户查询,以统一字符变体。
  2. 应用类似阿拉伯语 NLP 库或搜索引擎(例如 Farasa、Hazm 或自定义的基于正则表达式的映射器)使用的规范化规则
  3. 可选地,支持模糊匹配或 Levenshtein 距离以进行近乎精确的匹配。

下面是一个简化的规范化函数示例(Java 风格):

public static String normalizeArabic(String text) {
  return text.replace("أ", "ا")
             .replace("إ", "ا")
             .replace("آ", "ا")
             .replace("ؤ", "و")
             .replace("ئ", "ي")
             .replace("ى", "ي")
             .replace("ة", "ه")
             .replace("ي", "ی")
             .replace("ك", "ک");
}

:folded_hands: 请求

能否考虑将此规范化功能包含在核心搜索引擎或作为插件中?这将极大地改善使用 Discourse 的阿拉伯语和波斯语社区的可用性。

如果有现有的解决方法或插件可以解决此问题,我们将不胜感激。

感谢您的时间和构建如此强大的平台。

此致敬礼

1 个赞

search_ignore_accents 网站设置对这个问题有影响吗?

1 个赞

非常感谢您参与讨论。

回答您的问题:是的,我们的论坛已启用 search_ignore_accents 设置。
不幸的是,这并没有解决我们面临的问题。搜索结果仍然无法匹配拼写等效的阿拉伯语和波斯语字符,因此尽管有此设置,问题仍然存在。

1 个赞

我认为这是一个合理的要求,因为它将极大地改善阿拉伯语和波斯语站点的搜索体验。我们很乐意审查实现此功能的 PR,因此我将为其添加 pr-welcome 标签。

对于任何决定从事此功能的人:所有规范化逻辑都应通过站点设置进行限制,该设置默认启用阿拉伯语和波斯语站点的此功能(请参阅 site_settings.yml 中的 locale_default),而所有其他区域设置默认关闭此设置。Core 已为带重音字符实现了类似的规范化逻辑(请参阅 lib/search.rb),因此在实现此功能时,这将是一个有用的参考。

4 个赞

非常感谢你,奥萨马!我很高兴看到这个建议受到了好评。

2 个赞

对于这个问题的这部分,我们是在讨论 Unicode 标准 NFKC(举个例子)规范化吗?

(我甚至不确定我们做了什么……我假设我们在烹饪管道中规范化帖子文本?)

1 个赞

我不是技术专家,但一直在研究这个问题,因为我想确保波斯-阿拉伯双语 Discourse 论坛不会错过任何搜索查询。由于 Discourse 使用 PostgreSQL,因此规范化变得至关重要:用户可能使用波斯语字符进行搜索,而相同的单词以阿拉伯语字符存储——反之亦然。没有适当的规范化,搜索就会失败。
根据我的了解,使用 Unicode NFKC 规范化是一个不错的起点——它处理了许多兼容性问题,如连字、表示形式以及阿拉伯/波斯数字。

然而,对于波斯语和阿拉伯语文本,仅 NFKC 并不足够。它无法规范化几个关键的字符变体,这些变体在视觉和语义上是等效的,但在二进制级别上却不同。

下面,我将概述通过研究和探索得出的程序和见解。


:wrench: 总体设计策略

  1. 首先应用 Unicode NFKC 规范化,以处理连字、表示形式和数字统一。
  2. 然后按定义的顺序应用自定义字符映射(例如,在阿拉伯语 Ya 之前规范化 Hamza 变体)。
  3. 为存储与搜索设置不同的规范化策略
    • 对规范存储使用保守配置文件(保留 ZWNJ,避免语义偏移)。
    • 对搜索使用宽松配置文件(忽略 ZWNJ,统一 Hamza 变体,规范化数字)。
  4. 所有映射都应通过数据库中的中央映射表或应用程序中的 Ruby hash 来配置

:one: 规范化配置文件

:green_circle: 保守(用于存储)

  • 最小转换
  • 应用 NFKC
  • 将阿拉伯语 Kaf/Ya 规范化为波斯语等效项
  • 删除变音符号
  • 保留 ZWNJ
  • 存储为 original_text + normalized_conservative

:blue_circle: 宽松(用于搜索)

  • 积极匹配
  • 应用所有保守规则
  • 删除/忽略 ZWNJ
  • 将 Hamza 变体规范化为基本字母
  • 将所有数字转换为 ASCII
  • 可选地统一 Taa Marbuta → Heh
  • 用于查询预处理

:two: 全面的映射表

源字符 目标字符 Unicode 备注
ك ک U+0643 → U+06A9 阿拉伯语 Kaf → 波斯语 Kaf
ي ی U+064A → U+06CC 阿拉伯语 Ya → 波斯语 Ya
ى ی U+0649 → U+06CC 结尾 Ya 变体
أ, إ, ٱ ا Various → U+0627 Hamza 形式 → Alef
ؤ و U+0624 → U+0648 Hamza Waw
ئ ی U+0626 → U+06CC Hamza Ya
ء U+0621 删除或保留(可配置)
ة ه U+0629 → U+0647 Taa Marbuta → Heh(可选)
ۀ هٔ U+06C0 ↔ U+0647+U+0654 规范化组合形式
ڭ گ U+06AD → U+06AF 区域变体
U+200C ZWNJ:在保守模式下保留,在宽松模式下删除
٤, ۴ 4 U+0664, U+06F4 → ASCII 规范化数字
Diacritics U+064B–U+0652 删除所有 harakat
ZWJ U+200D 删除不可见的连接符
Multiple spaces Single space 规范化间距

:three: 快速映射片段(用于 SQL 或 Ruby)

ك → ک
ي → ی
ى → ی
أ → ا
إ → ا
ؤ → و
ئ → ی
ة → ه
ۀ → هٔ
ٱ → ا
٤, ۴ → 4
ZWNJ (U+200C) → (在宽松模式下删除)
Harakat (U+064B..U+0652) → 删除
ZWJ (U+200D) → 删除

:four: 在 PostgreSQL 中实现

  • 创建 text_normalization_map
  • 使用 regexp_replaceTRANSLATE 链来提高性能
  • 可选地在 PL/Python 或 PL/v8 中实现以支持 Unicode
  • 使用相同的逻辑规范化存储的内容和传入的查询

索引策略

  • 为规范索引存储 normalized_conservative
  • 使用 normalize_persian_arabic(query, 'permissive') 规范化查询
  • 如果使用宽松搜索,索引必须匹配相同的配置文件
  • 可选地存储两个版本以进行交叉比较

:five: Ruby Hash 示例(用于 Discourse)

NORMALIZATION_MAP = {
  "ك" => "ک",
  "ي" => "ی",
  "ى" => "ی",
  "أ" => "ا",
  "إ" => "ا",
  "ٱ" => "ا",
  "ؤ" => "و",
  "ئ" => "ی",
  "ة" => "ه",
  "ۀ" => "هٔ",
  "۴" => "4",
  "٤" => "4",
  "\u200C" => "", # ZWNJ
  "\u200D" => "", # ZWJ
}

:six: 性能和实际注意事项

  1. 在应用程序层应用NFKC(例如,Ruby unicode_normalize(:nfkc)
  2. 为保守和宽松配置文件使用单独的索引
  3. 除非明确配置,否则避免强制映射语义敏感字符(例如,Hamza、Taa Marbuta)
  4. 在实际论坛数据上运行A/B 测试,以衡量命中率和误报率
  5. 为每个映射提供理由和示例进行文档记录
  6. 为每个映射定义 Ruby 和 SQL 的单元测试

:seven: 最终建议

  • 使用 Unicode NFKC 作为基础
  • 通过自定义映射层进行扩展
  • 为存储和搜索维护双重配置文件
  • 在应用程序和数据库层实现规范化
  • 记录和测试每个映射
  • 构建适当的索引(GIN + to_tsvector)在规范化列上