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

我不是技术专家,但一直在研究这个问题,因为我想确保波斯-阿拉伯双语 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_replace 或 TRANSLATE 链来提高性能
  • 可选地在 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)在规范化列上