저는 기술 전문가가 아니지만, 페르시아어-아랍어 이중 언어 Discourse 포럼에서 검색 쿼리가 누락되지 않도록 이 문제를 조사해 왔습니다. Discourse는 PostgreSQL을 사용하므로 정규화(normalization)가 필수적입니다. 사용자가 페르시아 문자로 검색을 시도할 수 있지만, 동일한 단어는 아랍 문자로 저장되어 있거나 그 반대일 수 있기 때문입니다. 적절한 정규화 없이는 검색이 실패하게 됩니다.
제가 배운 바에 따르면, Unicode NFKC 정규화를 사용하는 것은 좋은 출발점입니다. 이는 리가처(ligatures), 표시 형식(presentation forms), 아랍어/페르시아어 숫자 등 많은 호환성 사례를 처리합니다.
그러나 페르시아어와 아랍어 텍스트의 경우, NFKC만으로는 충분하지 않습니다. 이는 시각적 및 의미적으로 동등하지만 바이너리 수준에서 차이가 나는 몇 가지 중요한 문자 변형을 정규화하지 못합니다.
아래에서는 제 연구와 탐구를 통해 도출한 절차와 통찰력을 개요로 제시합니다.
전체 설계 전략
- 리가처, 표시 형식, 숫자 통합을 처리하기 위해 먼저 Unicode NFKC 정규화를 적용합니다.
- 정의된 순서로 사용자 지정 문자 매핑을 적용합니다(예: 아랍어 Ya보다 먼저 Hamza 변형을 정규화).
- 저장용과 검색용 정규화 정책을 분리합니다:
- 정준 저장에는 보수적(Conservative) 프로필을 사용합니다(ZWNJ 보존, 의미 이동 방지).
- 검색에는 관용적(Permissive) 프로필을 사용합니다(ZWNJ 무시, Hamza 변형 통합, 숫자 정규화).
- 모든 매핑은 데이터베이스의 중앙 집중식 매핑 테이블 또는 애플리케이션의 Ruby 해시를 통해 설정 가능해야 합니다.
정규화 프로필
보수적 (저장용)
- 최소한의 변환
- NFKC 적용
- 아랍어 Kaf/Ya를 페르시아어 동등 문자로 정규화
- 부호(diacritics) 제거
- ZWNJ 보존
original_text+normalized_conservative로 저장
관용적 (검색용)
- 공격적인 매칭
- 모든 보수적 규칙 적용
- ZWNJ 제거/무시
- Hamza 변형을 기본 문자로 정규화
- 모든 숫자를 ASCII로 변환
- 선택적으로 Taa Marbuta → Heh 통합
- 쿼리 전처리(preprocessing)에 사용
종합 매핑 테이블
| 원본 | 대상 | 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 | 보이지 않는 조인자 제거 |
| 여러 공백 | 단일 공백 | — | 공백 정규화 |
빠른 매핑 스니펫 (SQL 또는 Ruby용)
ك → ک
ي → ی
ى → ی
أ → ا
إ → ا
ؤ → و
ئ → ی
ة → ه
ۀ → هٔ
ٱ → ا
٤, ۴ → 4
ZWNJ (U+200C) → (관용적 프로필에서 제거)
Harakat (U+064B..U+0652) → 제거
ZWJ (U+200D) → 제거
PostgreSQL 구현
text_normalization_map테이블 생성- 성능을 위해
regexp_replace또는TRANSLATE체인 사용 - Unicode 지원을 위해 PL/Python 또는 PL/v8으로 구현하는 것도 선택 사항
- 저장된 콘텐츠와 들어오는 쿼리 모두 동일한 로직으로 정규화
인덱싱 전략
- 정준 인덱싱을 위해
normalized_conservative저장 normalize_persian_arabic(query, 'permissive')로 쿼리 정규화- 관용적 검색을 사용하는 경우, 인덱스는 동일한 프로필과 일치해야 함
- 교차 비교를 위해 두 버전 모두를 저장하는 것도 선택 사항
Ruby 해시 예시 (Discourse용)
NORMALIZATION_MAP = {
"ك" => "ک",
"ي" => "ی",
"ى" => "ی",
"أ" => "ا",
"إ" => "ا",
"ٱ" => "ا",
"ؤ" => "و",
"ئ" => "ی",
"ة" => "ه",
"ۀ" => "هٔ",
"۴" => "4",
"٤" => "4",
"\u200C" => "", # ZWNJ
"\u200D" => "", # ZWJ
}
성능 및 실용적 참고 사항
- 애플리케이션 계층에서 NFKC를 적용합니다(예: Ruby
unicode_normalize(:nfkc)) - 보수적 및 관용적 프로필을 위해 별도의 인덱스를 사용합니다
- 명시적으로 설정되지 않는 한, 의미적으로 민감한 문자(예: Hamza, Taa Marbuta)의 강제 매핑을 피합니다
- 실제 포럼 데이터를 사용하여 A/B 테스트를 실행하여 적중률과 오양(False positives)을 측정합니다
- 각 매핑에 근거와 예시를 포함하여 문서화합니다
- 각 매핑에 대해 Ruby와 SQL 양쪽에서 단위 테스트를 정의합니다
최종 권장 사항
- 기본값으로 Unicode NFKC를 사용합니다
- 사용자 지정 매핑 레이어로 확장합니다
- 저장과 검색을 위한 이중 프로필을 유지합니다
- 정규화를 애플리케이션 및 데이터베이스 양쪽 계층에서 구현합니다
- 모든 매핑을 문서화하고 테스트합니다
- 정규화된 컬럼에 적절한 인덱스(
GIN + to_tsvector)를 구축합니다