アラビア語検索正規化:ハムザのバリエーション、ヤー/カーフの形式、および正書法上の等価性に対するサポートが不足

私は技術専門家ではありませんが、ペルシャ語とアラビア語のバイリンガルDiscourseフォーラムで検索クエリが漏れなく処理されるようにするために、この問題について調査してきました。DiscourseはPostgreSQLを使用しているため、正規化が不可欠です。ユーザーがペルシャ文字で検索しても、同じ単語がアラビア文字で保存されている場合やその逆の場合があります。適切な正規化なしでは、検索は失敗します。

私の調査と探求から学んだことに基づくと、Unicode NFKC正規化を使用することは確実な出発点です。これは、合字、表示形式、アラビア語/ペルシャ語の数字など、多くの互換性のあるケースを処理します。

しかし、ペルシャ語とアラビア語のテキストの場合、NFKCだけでは不十分です。これは、視覚的および意味的に同等でありながら、バイナリレベルで異なるいくつかの重要な文字バリアントを正規化しません。

以下に、私の調査と探求を通じて到達した手順と洞察を概説します。


:wrench: 全体的な設計戦略

  1. まずUnicode NFKC正規化を適用して、合字、表示形式、数字の統一を処理します。
  2. 次にカスタム文字マッピングを定義された順序で適用します(例:ハムザバリアントをアラビア語のヤより前に正規化)。
  3. ストレージと検索で正規化ポリシーを分離します。
    • 正規のストレージには保守的なプロファイルを使用します(ZWNJを保持し、意味的なシフトを回避します)。
    • 検索には寛容なプロファイルを使用します(ZWNJを無視し、ハムザバリアントを統一し、数字を正規化します)。
  4. すべてのマッピングは、データベースの中央マッピングテーブルまたはアプリケーションのRubyハッシュを介して設定可能である必要があります。

:one: 正規化プロファイル

:green_circle: 保守的(ストレージ用)

  • 最小限の変換
  • NFKCを適用
  • アラビア語のKaf/Yaをペルシャ語の同等物に正規化
  • 発音記号を削除
  • ZWNJを保持
  • original_text + normalized_conservativeとして保存

:blue_circle: 寛容(検索用)

  • 積極的なマッチング
  • すべての保守的なルールを適用
  • ZWNJを削除/無視
  • ハムザバリアントを基本文字に正規化
  • すべての数字を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 ハムザ形式 → Alef
ؤ و U+0624 → U+0648 ハムザWaw
ئ ی U+0626 → U+06CC ハムザ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 数字を正規化
発音記号 — U+064B–U+0652 すべてのharakatを削除
ZWJ — U+200D 可視化されない結合文字を削除
複数スペース 単一スペース — スペーシングを正規化

: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ハッシュの例(Discourse用)

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

:six: パフォーマンスと実用的な注意点

  1. アプリケーションレイヤーでNFKCを適用します(例:Ruby unicode_normalize(:nfkc))。
  2. 保守的プロファイルと寛容プロファイルで別々のインデックスを使用します。
  3. 特に設定されていない限り、意味的に重要な文字(例:ハムザ、Taa Marbuta)の強制的なマッピングは避けます。
  4. 実際のフォーラムデータでA/Bテストを実行して、ヒット率と偽陽性を測定します。
  5. 各マッピングを理由と例で文書化します。
  6. RubyとSQLの両方で各マッピングの単体テストを定義します。

:seven: 最終推奨事項

  • ベースとしてUnicode NFKCを使用します。
  • カスタムマッピングレイヤーで拡張します。
  • ストレージと検索でデュアルプロファイルを維持します。
  • アプリケーションとデータベースレイヤーの両方に正規化を実装します。
  • すべてのマッピングを文書化し、テストします。
  • 適切なインデックス(GIN + to_tsvector)を正規化された列に構築します。