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

Discourse 팀에게,

우리는 아랍어와 페르시아어 콘텐츠가 상당한 비중을 차지하는 다국어 포럼을 운영 중이며, 아랍어 정서법 정규화(orthographic normalization)와 관련하여 검색 기능에서 치명적인 한계를 경험하고 있습니다.

:magnifying_glass_tilted_left: 문제 설명

아랍어 문자에는 의미적으로 동일한 문자에 대해 여러 가지 유니코드 표현이 존재합니다. 안타깝게도 Discourse의 현재 검색 엔진은 이러한 변형들을 서로 다른 것으로 취급하는 것으로 보이며, 이로 인해 불완전하거나 오해의 소지가 있는 검색 결과가 반환되고 있습니다.

예시:

  • إطلاق مقامي를 검색하면 정확한 일치 항목만 반환되며, اطلاق مقامي, أطلاق مقامي, 또는 إطلاق‌مقامي가 포함된 게시물은 제외됩니다.
  • 마찬가지로, ي (U+064A)로 검색하면 ی (U+06CC)와 일치하지 않으며, ك (U+0643)도 ک (U+06A9)와 일치하지 않습니다. 아랍어/페르시아어 맥락에서 기능적으로 동등함에도 불구하고요.

이 문제는 해즈바(hamza) 변형(أ, إ, ء, ؤ, ئ)뿐만 아니라 다음과 같은 일반적인 치환에도 영향을 미칩니다:

문자 유니코드 제안된 정규화
أ, إ, ء, آ 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 ک로 정규화

사용자가 음소표기(diacritics)를 생략하거나 다른 키보드 레이아웃을 사용하는 경우 이 문제는 더욱 복잡해져, 파편화된 검색 동작을 초래합니다.


:gear: 제안된 해결책

인덱싱 및 쿼리 파싱 시 유니코드 인식 정규화 레이어를 구현하는 것을 권장합니다. 이는 다음과 같이 달성할 수 있습니다:

  1. 인덱싱된 콘텐츠와 사용자 쿼리 모두를 전처리하여 문자 변형을 통합합니다.
  2. 아랍어 NLP 라이브러리나 검색 엔진(예: Farasa, Hazm 또는 사용자 정의 정규식 기반 매퍼)에서 사용되는 것과 유사한 정규화 규칙을 적용합니다.
  3. 선택적으로, 근사 일치(near-exact matches)를 위해 퍼지 매칭(fuzzy matching) 또는 레벤슈타인 거리를 지원합니다.

다음은 정규화 함수의 단순화된 예시(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.ymllocale_default 참조) 다른 모든 로케일에서는 기본적으로 비활성화되어야 합니다. 코어에는 이미 발음 기호가 있는 문자를 위한 유사한 정규화 로직이 있으므로(참조: lib/search.rb), 이 기능을 구현할 때 유용한 참고 자료가 될 것입니다.

4개의 좋아요

오사마, 정말 감사해요! 이 제안이 잘 받아들여진 걸 보니 정말 기쁩니다.

2개의 좋아요

이 문제의 해당 부분에 대해, Unicode 표준 NFKC(그 중 하나를 선택하는 경우) 정규화에 대해 이야기하는 건가요?

(우리가 정확히 무엇을 하는지조차 확신이 서지 않습니다… 쿠킹 파이프라인에서 게시물 텍스트를 정규화하는 것으로 추정하고 있습니다?)

1개의 좋아요

저는 기술 전문가가 아니지만, 페르시아어-아랍어 이중 언어 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)를 구축합니다