아랍어 검색 정규화: 함자 변형, 야/카프 형태 및 정서법적 동치성 지원 누락

저는 기술 전문가가 아니지만, 페르시아어-아랍어 이중 언어 Discourse 포럼에서 검색 쿼리가 누락되지 않도록 이 문제를 조사해 왔습니다. Discourse는 PostgreSQL을 사용하므로 정규화(normalization)가 필수적입니다. 사용자가 페르시아 문자로 검색을 시도할 수 있지만, 동일한 단어는 아랍 문자로 저장되어 있거나 그 반대일 수 있기 때문입니다. 적절한 정규화 없이는 검색이 실패하게 됩니다.

제가 배운 바에 따르면, Unicode NFKC 정규화를 사용하는 것은 좋은 출발점입니다. 이는 리가처(ligatures), 표시 형식(presentation forms), 아랍어/페르시아어 숫자 등 많은 호환성 사례를 처리합니다.

그러나 페르시아어와 아랍어 텍스트의 경우, NFKC만으로는 충분하지 않습니다. 이는 시각적 및 의미적으로 동등하지만 바이너리 수준에서 차이가 나는 몇 가지 중요한 문자 변형을 정규화하지 못합니다.

아래에서는 제 연구와 탐구를 통해 도출한 절차와 통찰력을 개요로 제시합니다.


:wrench: 전체 설계 전략

  1. 리가처, 표시 형식, 숫자 통합을 처리하기 위해 먼저 Unicode NFKC 정규화를 적용합니다.
  2. 정의된 순서로 사용자 지정 문자 매핑을 적용합니다(예: 아랍어 Ya보다 먼저 Hamza 변형을 정규화).
  3. 저장용과 검색용 정규화 정책을 분리합니다:
    • 정준 저장에는 보수적(Conservative) 프로필을 사용합니다(ZWNJ 보존, 의미 이동 방지).
    • 검색에는 관용적(Permissive) 프로필을 사용합니다(ZWNJ 무시, Hamza 변형 통합, 숫자 정규화).
  4. 모든 매핑은 데이터베이스의 중앙 집중식 매핑 테이블 또는 애플리케이션의 Ruby 해시를 통해 설정 가능해야 합니다.

:one: 정규화 프로필

:green_circle: 보수적 (저장용)

  • 최소한의 변환
  • NFKC 적용
  • 아랍어 Kaf/Ya를 페르시아어 동등 문자로 정규화
  • 부호(diacritics) 제거
  • ZWNJ 보존
  • original_text + normalized_conservative로 저장

:blue_circle: 관용적 (검색용)

  • 공격적인 매칭
  • 모든 보수적 규칙 적용
  • ZWNJ 제거/무시
  • Hamza 변형을 기본 문자로 정규화
  • 모든 숫자를 ASCII로 변환
  • 선택적으로 Taa Marbuta → Heh 통합
  • 쿼리 전처리(preprocessing)에 사용

: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 보이지 않는 조인자 제거
여러 공백 단일 공백 — 공백 정규화

:three: 빠른 매핑 스니펫 (SQL 또는 Ruby용)

ك → ک
ي → ی
ى → ی
أ → ا
إ → ا
ؤ → و
ئ → ی
ة → ه
ۀ → هٔ
ٱ → ا
٤, ۴ → 4
ZWNJ (U+200C) → (관용적 프로필에서 제거)
Harakat (U+064B..U+0652) → 제거
ZWJ (U+200D) → 제거

:four: PostgreSQL 구현

  • text_normalization_map 테이블 생성
  • 성능을 위해 regexp_replace 또는 TRANSLATE 체인 사용
  • Unicode 지원을 위해 PL/Python 또는 PL/v8으로 구현하는 것도 선택 사항
  • 저장된 콘텐츠와 들어오는 쿼리 모두 동일한 로직으로 정규화

인덱싱 전략

  • 정준 인덱싱을 위해 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. 명시적으로 설정되지 않는 한, 의미적으로 민감한 문자(예: Hamza, Taa Marbuta)의 강제 매핑을 피합니다
  4. 실제 포럼 데이터를 사용하여 A/B 테스트를 실행하여 적중률과 오양(False positives)을 측정합니다
  5. 각 매핑에 근거와 예시를 포함하여 문서화합니다
  6. 각 매핑에 대해 Ruby와 SQL 양쪽에서 단위 테스트를 정의합니다

:seven: 최종 권장 사항

  • 기본값으로 Unicode NFKC를 사용합니다
  • 사용자 지정 매핑 레이어로 확장합니다
  • 저장과 검색을 위한 이중 프로필을 유지합니다
  • 정규화를 애플리케이션 및 데이터베이스 양쪽 계층에서 구현합니다
  • 모든 매핑을 문서화하고 테스트합니다
  • 정규화된 컬럼에 적절한 인덱스(GIN + to_tsvector)를 구축합니다