📚 시리즈 — 한국어 RAG 검색 (Kiwi)
- ① 한국어 BM25·토크나이저 선택과 Kiwi 기본 설정
- ② 사용자·동의어 사전과 복합명사 처리 — 현재 글
- ③ 검색 품질 개선·사전 관리·Vector Search 통합
- ④ 부록 — 동의어 적용 위치·IATA 사전 생성/검증·품사·Nori 비교
5. 사용자 사전
왜 필요한가 — Kiwi 의 기본 LM 분석기는 일반 한국어 코퍼스로 학습되어 도메인 특화 어휘 (IATA·항공 약어·복합명사·단위명사 등) 를 잘못 분해하거나 누락할 수 있습니다. 사용자 사전은 도메인 어휘를 런타임에 등록해 형태소 분석의 태그·우선순위·분해 방식 을 제어합니다.
언제 쓰는가 — 4가지 시나리오:
- 신규 약어·고유명사 인식 (
DGR,ULD,B777) - 의존명사(
NNB) → 일반명사(NNG) 재태깅 — BM25 화이트리스트 통과 (§2.3) - 변이형·오탈자 정규화 (
수화물→수하물) - 복합명사·브랜드명 사전 분해 (
스카이패스→스카이+패스)
5.1 사용자 사전 API 5종 (개요)
| API | 시그니처 | 용도 | 본 가이드 절 |
|---|---|---|---|
add_user_word |
(form, tag, score, orig_word) |
개별 단어 런타임 등록 — 가장 흔함 | §5.2 |
add_pre_analyzed_word |
(form, analyzed, score) |
사전 분해 결과 강제 — 불규칙 합성어 | §5.3 |
add_rule |
(tag, replacer, score) |
callable 변형 규칙 | §5.4 |
add_re_rule |
(tag, pattern, repl, score) |
정규식 변형 규칙 | §5.4 |
load_user_dictionary |
(path) |
TSV 파일 일괄 로드 — 운영 권장 | §5.5 |
kiwipiepy ≥ 0.17 부터 공백 포함 복합명사 등록 가능.
5.2 add_user_word — 가장 흔한 패턴
인자 의미:
form— 등록할 표면형 (예:"DGR","킬로그램")tag— 강제할 품사 태그 (예:"NNP","NNG")score— 우선순위 가중치 (동일 표면형이 여러 형태소로 분석될 때)orig_word(옵션) — 원형 매핑 (변이형 정규화 시)
score 의미 — Kiwi 는 입력 텍스트의 가능한 형태소 분해 후보들에 점수를 매겨 가장 높은 점수를 선택합니다. score 는 사용자 사전에 등록된 form 이 매칭됐을 때 그 후보의 LM 점수에 가산되는 가중치입니다. 양수면 LM 기본 분해보다 우선 채택, 음수면 LM 기본 후보보다 후순위로 정렬됩니다.
동작 예시 — 킬로그램 NNG 승격: 1. LM 기본 분석: 킬로그램 → NNB (의존명사) 2. 사용자 등록 (+3 가산): 킬로그램 → NNG (일반명사) 후보 점수 +3 3. 두 후보 경쟁 → 가산된 NNG 후보가 top-1 으로 선택 4. 선택된 토큰 태그가 NNG → KEEP_TAGS 통과 → BM25 색인에 포함
위 코드의 의존명사 → NNG 승격 (
킬로그램·시간) 이 필요한 이유는 §2.3 도식 직후 노트 참조 —KEEP_TAGS화이트리스트에서NNB제외이기 때문입니다.
score 가이드: - 신규 약어·고유명사 통째 등록 — +3 ~ +5 권장 (LM 분해 후보보다 우선) - 의존명사 → NNG 승격 — +3 권장 (BM25 화이트리스트 통과) - 변이형·표기 통일 — -3 ~ -5 권장 (원형 우선)
# 신규 약어·고유명사 — NNP 고정, +5
for term in ["DGR", "AVI", "ULD", "PMC", "AKE", "RKSI", "KE", "B777", "B787"]:
kiwi.add_user_word(term, "NNP", score=5.0)
# 의존명사 → NNG 승격 — +3
kiwi.add_user_word("킬로그램", "NNG", score=3.0)
kiwi.add_user_word("시간", "NNG", score=3.0)
# 변이형·표기 통일 — -3 (원형 "수하물" 우선)
kiwi.add_user_word("수화물", "NNG", score=-3.0, orig_word="수하물")
5.3 add_pre_analyzed_word — 불규칙 합성어 분해 강제
불규칙 합성어·브랜드명에 대해 사전 분해 결과를 강제합니다. score=-3 권장 (원본 LM 분석을 침범하지 않도록 후순위).
kiwi.add_pre_analyzed_word(
"스카이패스",
[("스카이", "NNP"), ("패스", "NNG")],
score=-3.0,
)
5.4 add_rule / add_re_rule — 변형 규칙
사전 파일(YAML/TSV) 에 담기 어려운 패턴 변이 (표기 오류·구어·음절 늘림 등) 를 코드 룰로 일괄 정규화할 때 사용합니다. 입력 텍스트에서 pattern 에 매칭되는 부분을 찾으면 그 부분의 표면형을 repl 로 바꾸고 tag 태그를 부여한 토큰으로 인식 — 즉 텍스트 정규화 + 형태소 등록 이 결합된 룰입니다.
인자 의미 (add_re_rule):
tag— 매칭 결과에 부여할 품사 태그 (예:"NNG")pattern— 입력 텍스트에서 찾을 정규식 (예:r"수화물")repl— 매칭된 부분을 치환할 문자열 (예:"수하물")score— 동일 표면형에 다른 분석 후보가 있을 때의 우선순위. 변이형은-3이하 권장 (원형 보존 후순위, §5.2 score 가이드)
add_rule 은 pattern 자리에 정규식 대신 callable 을 받는 형태로, 정규식으로 표현하기 어려운 복잡한 변환 로직이 필요할 때만 사용합니다.
# 정규식 — 모든 "수화물" 을 "수하물" 로 정규화
kiwi.add_re_rule("NNG", r"수화물", "수하물", score=-3.0)
위 룰이 등록되면 텍스트 "수화물 분실" 의 토큰화 결과는 [(수하물/NNG), (분실/NNG)] 가 됩니다 — 표면형은 수화물 → 수하물 로 정규화되고 태그는 NNG 부여. 인덱싱·검색 양쪽에 동일 룰이 적용되면 표기 변이에 무관하게 단일 정형(수하물) 으로 매칭 일관성을 확보합니다.
운영 권장:
- 규모 관리 — 룰 폭증 시 유지보수 비용 증가. 50건 이내 권장
- 대안 — 규칙성이 약한 변이는
add_pre_analyzed_word(§5.3) 로 정확한 분해 결과 등록이 더 안전
5.5 load_user_dictionary — TSV 파일 일괄 로드
코드 호출(add_user_word 등) 대신 TSV 파일 에 사용자 사전을 모아 일괄 로드합니다. 운영팀이 PR 로 사전을 관리하고 인덱싱·검색이 동일 파일을 읽도록 강제하는 가장 흔한 운영 패턴입니다 (§5.6 양쪽 동일 강제).
최소 형식 (Kiwi 사용자 사전 TSV 스펙):
# 컬럼: form\ttag\tscore (3컬럼 기본)
# 컬럼: form\torig/tag\tscore (이형태 — orig 매핑)
# 컬럼: form\tm1/t1 + m2/t2 + ... (기분석)
# UTF-8, Tab 구분, `#` 주석, score 는 실수, tag 는 세종 표준
DGR NNP 5.0
킬로그램 NNG 3.0
수화물 수하물/NNG -3.0
스카이패스 스카이/NNP + 패스/NNG -3.0
함정 — 4 컬럼 형식 금지: 변이형 매핑을 4번째 컬럼으로
수화물\tNNG\t-3.0\t수하물처럼 적으면load_user_dictionary가ValueError: Wrong dictionary format으로 실패합니다. 이형태 매핑은 2번째 컬럼orig/tag슬래시 표기로 표현하세요.add_user_word(form, tag, score, orig_word=...)의 4-인자 시그니처와 TSV 컬럼 수는 다릅니다.
로드:
from glob import glob
for path in sorted(glob("/Volumes/airag_prd/dict/user_dict/current/airag_*.tsv")):
kiwi.load_user_dictionary(path)
카테고리별 파일 정책 — A항공 운영은 사전을 카테고리별 파일로 분리(
airag_iata.tsv·airag_compound.tsv등)하고airag_*.tsvglob 패턴으로 일괄 로드합니다. Kiwi API 는 단일 파일만 받으므로 애플리케이션 코드가sorted(glob(...))으로 N 파일을 순차 로드합니다. 카테고리 개수는 고정 아님 — 디렉토리에 파일을 추가하면 자동 인식. 환경(dev/staging/prd) 은 catalog 레벨(airag_dev·airag_staging·airag_prd)에서 분리됩니다.
UC Volume 권한 분리·운영 PR·실제 샘플 행 상세는 §9 (UC Volume 사전 관리) 정본 참조.
5.6 적용 시점 — 양쪽 동일 강제
본 가이드 권장: 사용자 사전은 인덱싱·검색 양쪽에 동일하게 적용 합니다 (동의어 사전의 검색 시점만 (§6.4) 과 정반대).
근거 — 사용자 사전은 토큰의 form·tag·boundary 자체를 재정의하므로 (§5.2 score 의미), 한쪽만 적용하면 인덱스·검색 토큰 불일치 → hit 0 발생 (정책 D 와 동치, §7.4 참조).
운영 강제 (§10.3 bootstrap_dictionary() 가 양쪽에서 import 되도록 강제):
- fail-fast —
load_user_dictionary()가 IOError 면 silent 진행 금지 (사전 한쪽만 로드된 비대칭 차단)
6. 동의어 사전
본 절은 동의어 사전의 형태 선택 (라이브러리 내장 vs 애플리케이션 — §6.1~6.3), 적용 시점 (§6.4), 다중 토큰 매칭 구현 (§6.5) 을 다룹니다. 동의어를 언제 적용하느냐 (인덱싱 / 검색 / 양쪽) 의 외부 권위 자료 기반 비교·결정은 부록 A 입니다.
먼저 검색 엔진의 동의어 사전이 운영되는 2 가지 형태 부터 살펴봅니다.
6.1 형태 1 — 분석기·검색엔진 라이브러리 내장형
OpenSearch/Elasticsearch 의 synonym 또는 synonym_graph 토큰 필터, Apache Solr 의 SynonymFilterFactory 같이 분석기 파이프라인 안에 동의어 필터를 끼워 넣는 방식입니다. 인덱스 settings 의 분석기 정의에 동의어 사전을 등록하면, 토큰화 직후 같은 분석기 안에서 자동으로 동의어가 확장됩니다.
// OpenSearch 예시 — 분석기 내장형
"analysis": {
"filter": {
"ke_synonyms": {
"type": "synonym_graph",
"synonyms": ["대한항공, 코리안에어, KE, KAL", "제주항공, 제주에어, 7C, JJA", "아시아나항공, 아시아나, OZ, AAR", "위탁수하물, 체크인수하물"]
}
},
"analyzer": {
"ke_analyzer": {
"tokenizer": "nori_tokenizer",
"filter": ["nori_part_of_speech", "ke_synonyms"]
}
}
}
- 장점: 토큰화·동의어가 같은 분석기 안에 묶여 일관성 보장.
- 단점: 사전 변경 시 인덱스 close → settings 갱신 → open → 기존 문서 reindex 사이클 필요 (close-open 만으로는 settings 만 갱신되고 기존 색인된 토큰은 옛 동의어 매핑 그대로).
- Kiwi 지원 여부: 미지원
6.2 형태 2 — 애플리케이션 레벨 외부 구현
토크나이저는 토큰화만 담당하고, 애플리케이션 코드가 토큰 시퀀스를 받아 외부 동의어 사전을 보고 OR 확장 하는 방식입니다. Kiwi 에는 사용자 사전과 달리 동의어 사전 등록 API (예: add_synonym()) 가 없으므로, 동의어 확장은 애플리케이션 코드로 직접 구현해야 합니다.
양방향은 로드 시점에 빌드: 운영자는 사전 파일(CSV)에 동의어 그룹 한 줄 만 작성합니다. §10.3
load_synonyms가 로드 시점에 그룹의 모든 어휘를 매칭 구조로 펼쳐, 어느 어휘로 검색해도 그룹 전체가 매칭됩니다(양방향). 검색 시점expand_query_for_bm25는 이미 빌드된 구조를 조회만 합니다. 한 방향으로만 확장해야 하는 관계는=>단방향 모드로 작성합니다(§6.6·§9.1).
개념 예시 — 복합명사 동의어 그룹 (CSV 한 줄, 공백 포함 다중 토큰):
기내 반입, 휴대 수하물
검색 질의가 어느 표현이든, BM25 입력 질의로 그룹 전체가 OR 확장됩니다:
검색 "기내 반입 규정" → 확장 "기내 반입 휴대 수하물 규정"
검색 "휴대수하물 무게" → 확장 "기내 반입 휴대 수하물 무게" ← 붙여 써도 매칭
→ "기내 반입" 으로 검색해도 휴대 수하물 이라고만 적힌 문서까지 매칭됩니다(양방향). 붙여 쓴 입력(휴대수하물)도 재토큰화로 같은 그룹에 수렴합니다. 매칭 알고리즘·정본 코드는 §6.5·§10.3 을 참조하세요.
- 장점: 동의어를 바꿔도 문서 재색인 0 (검색 질의에만 동의어를 더함).
기내 반입같은 여러 단어 동의어(복합명사)도 안전. 확장한 검색어를 공백으로 이어 보내면 BM25 가 OR 매칭하므로 특별한 검색 문법도 불필요. - 단점: Kiwi 가 동의어를 대신 처리하지 않아 확장 코드를 직접 구현 해야 함. 동의어 사전은 검색 때만 쓰지만, 확장 검색어가 색인 토큰과 맞물리려면 토큰화(사용자 사전·옵션)가 색인·검색 동일 해야 함 (§10.3). 갱신은 재배포로 반영 (hot reload 미지원, §9.4).
6.3 본 가이드의 형태 선택
Kiwi 가 형태 1 을 미지원할 뿐 아니라, Databricks Vector Search 가 BM25 채널에 사용자 정의 분석기를 끼울 지점을 미지원합니다. 따라서 본 가이드는 형태 2 (애플리케이션 레벨 외부 구현) 를 채택합니다. 적용 시점 결정(부록 A) 과는 독립된 형태 선택입니다.
6.4 적용 시점 결정 — 검색 시점 권장
본 가이드 권장: 동의어는 검색 시점 OR 확장 (Case B) 만 적용. 인덱싱 시 확장은 IDF 인플레이션·token graph 평탄화로 BM25 코퍼스 통계가 왜곡됩니다.
근거·외부 자료 인용 상세는 부록 A 참조.
6.5 다중 토큰 동의어
본 가이드는 동의어 어휘에 공백 포함 다중 토큰 (한 어휘 최대 5) 을 허용합니다. 위탁 수하물·기내 반입 같은 복합명사 표현이 항공 도메인에 흔하고, Kiwi 의 split_complex=True 가 붙여 쓴 복합명사를 개별 토큰으로 분해하므로 (위탁수하물 → ['위탁','수하물']), 단일 토큰 키만으로는 이런 표현을 안정적으로 매칭할 수 없기 때문입니다. 본 절은 그 매칭 설계를 다룹니다.
6.5.1 데이터 흐름

동의어 확장은 LangGraph 앙상블의 FTS(BM25) 브랜치에만 적용됩니다. Semantic(벡터) 브랜치는 원문 임베딩을 그대로 쓰므로 동의어 영향이 없습니다.
query_type: FTS 브랜치는 self-managed 인덱스에
query_type="FULL_TEXT"(순수 BM25) 로 호출합니다. 단일 호출 Managed Hybrid (query_type="HYBRID", 벡터+키워드 내부 결합) 와 달리, 앙상블은 FTS·Semantic 을 개별 호출 후 결합 합니다.
6.5.2 매칭 알고리즘 (3단계)
expand_query_for_bm25 의 입력은 이미 Kiwi 토큰화 + 화이트리스트(SEARCH_TAGS) 필터를 거친 토큰 리스트입니다. 확장은 역할이 다른 세 단계 로 이뤄집니다 (정본 코드 §10.3):
| 단계 | 역할 | 한 줄 요약 |
|---|---|---|
| ① Aho-Corasick 매칭 | 찾기 | 등록된 동의어 키와 일치하는 모든 구간을 한 번 스캔으로 찾음 (겹치는 것도 모두) |
| ② greedy 최장 비중첩 선택 | 고르기 | 찾은 매칭 중 각 위치에서 가장 긴 것을 서로 겹치지 않게 골라 확장 |
| ③ 토큰 단위 중복 제거 | 정리 | 확장 결과를 개별 토큰으로 펼친 뒤 중복 토큰 제거 |
①은 "무엇이 매칭되는가"(엔진), ②는 "그중 무엇을 쓸까"(선택), ③은 "출력을 어떻게 정리할까"(중복 제거) — 서로 역할이 다릅니다.
① Aho-Corasick 매칭 — 찾기
여러 패턴(동의어 구)을 하나의 상태 기계로 합쳐, 질의를 한 번 통과시키면 걸리는 패턴을 모두 찾는 고전 다중패턴 검색 알고리즘이 Aho-Corasick 입니다. pyahocorasick 은 이를 C 로 구현한 파이썬 라이브러리입니다 (다중패턴 검색에 널리 쓰이는 표준). AC 는 겹치는 매칭까지 모두 돌려주며, 그중 무엇을 쓸지는 ②가 정합니다.
KEY_SEQUENCE모드 — pyahocorasick 은 키를 글자 문자열(KEY_STRING) 또는 정수 시퀀스(KEY_SEQUENCE) 로 받습니다. 글자 단위면수하물이수하물함안에서도 매칭되는 경계 오류가 생기므로, 토큰(형태소) 단위 매칭을 위해KEY_SEQUENCE를 씁니다.- 토큰 id 튜플 — 각 토큰에 정수 id 를 부여해(
tok2id) 동의어 구를 정수 튜플로 등록하고, 질의도 같은 방식으로 변환합니다. 단일 토큰 동의어는 길이-1 키, 다중 토큰은 2~5 토큰 키로 함께 등록됩니다.
사전 등록 : 위탁 수하물, 체크인 수하물
tok2id : 위탁→0, 수하물→1, 체크인→2
키(튜플) : (0,1)="위탁 수하물", (2,1)="체크인 수하물"
질의 "체크인 수하물 분실" → id (2,1,_UNK) → automaton.iter → (2,1) 매칭 = "체크인 수하물"
(_UNK = 사전에 없는 토큰의 sentinel id — 키에 절대 없는 값이라 매칭 안 됨)
② greedy 최장 비중첩 선택 — 고르기
①이 찾은 매칭은 서로 겹칠 수 있습니다. 그중 각 시작 위치에서 가장 긴 매칭을 택하고, 택한 토큰은 소비해 겹치지 않게, 왼→오로 진행합니다 (정식 용어로는 leftmost-longest matching). 사전에 그룹 둘이 있다고 가정:
그룹 1 (복합명사) : 위탁 수하물, 체크인 수하물
그룹 2 (단일어) : 수하물, 짐, 가방
질의 A — "위탁 수하물 분실":
토큰 : [위탁, 수하물, 분실]
i=0 : '위탁 수하물'(2토큰) 과 '수하물'(1토큰) 둘 다 매칭 가능
→ 더 긴 '위탁 수하물' 채택(최장) → 2토큰 소비(비중첩)
i=2 : '분실' 미매칭 → 원형 유지
확장 : 위탁 수하물 체크인 분실
→ 수하물 은 '위탁 수하물' 에 흡수되어 단독으로 짐·가방까지 확장되지 않습니다.
질의 B — "수하물 분실" (수하물 단독 등장 → 대조):
토큰 : [수하물, 분실]
i=0 : '수하물'(1토큰) 매칭 → 그룹 2 확장
확장 : 수하물 짐 가방 분실
→ 같은 수하물 이라도 앞 토큰 문맥 에 따라 결과가 달라집니다 (A 는 흡수, B 는 확장).
③ 토큰 단위 중복 제거 — 정리
②가 고른 매칭을 확장할 때, 결과를 개별 토큰 으로 펼친 뒤 중복 토큰을 제거합니다. 다중 토큰 동의어들이 공통 토큰을 공유하기 때문입니다 — 예: 위탁 수하물·체크인 수하물 은 수하물 을 공유 → 펼치면 수하물 이 두 번 → 한 번으로 정리.
② 확장 결과 : 위탁 수하물 + 체크인 수하물 → [위탁, 수하물, 체크인, 수하물]
③ 중복 제거 : 위탁 수하물 체크인 (수하물 1회)
왜 필수인가 — FULL_TEXT BM25 는 query_text 의 중복 term 을 점수에 선형 가산 합니다 (실측, self-managed Direct Access 인덱스):
| 질의 | 동일 문서 점수 |
|---|---|
수하물 |
1.0594 |
수하물 수하물 |
2.1188 (×2) |
수하물 수하물 수하물 |
3.1782 (×3) |
→ 중복을 두면 그 단어를 가진 문서만 부풀어 순위가 왜곡됩니다 (수하물 짐 가방 → 수하물 짐 수하물 가방 시 수하물-only 문서가 짐·가방 문서를 추월). 그래서 출력은 항상 개별 토큰 + 중복 제거 형태입니다.
6.5.3 키 빌드 (사전 → 매칭 자동자 구축)
적용 시점: 키 빌드는 사전 로드 1회 (
load_synonyms— 검색 워커·레플리카 부팅 시) 입니다. 질의마다가 아니며, 그 결과(Aho-Corasick 자동자)를 매 질의의 §6.5.2 ①②③ 가 조회만 합니다. (그래서 사전 갱신은 재배포로 반영 — §9.4)
한눈에 — 키 빌드는 사람이 쓴 CSV 를 기계가 매칭에 쓸 자동자 로 변환·구축하는 단계입니다:
입력 : 동의어 사전 CSV (사람이 쓴 그룹 — 예: "기내 반입, 휴대 수하물")
│
▼ 키 빌드 = load_synonyms (부팅 시 1회)
│ ├ 각 어휘를 검색용 Kiwi 로 재토큰화 → 정규 "키"
│ └ 키들을 Aho-Corasick 자동자로 구축
▼
출력 : 매칭 구조(자동자 + tok2id) → 매 질의의 §6.5.2 ①②③ 가 사용
같은 뜻이라도 사람마다 위탁수하물 처럼 붙여 쓰기도 하고 위탁 수하물 처럼 띄어 쓰기도 하며, Kiwi 가 단어를 쪼개는 방식도 사용자 사전 등록 여부에 따라 달라집니다. 그래서 CSV 에 적힌 글자를 그대로 키로 써 버리면, 정작 질의가 만든 토큰과 모양이 안 맞아 매칭을 놓칠 수 있습니다.
이를 피하려고 사전을 불러올 때(load_synonyms) 어휘를 검색·인덱싱에 쓰는 바로 그 경로(kiwi_search_tokens — §4.2 병합 + SEARCH_TAGS 필터) 로 한 번 돌려, 나온 토큰(소문자로 맞춰 공백으로 이은 것)을 키로 삼습니다. 질의·인덱스도 똑같은 경로를 거치니 키 = 질의 = 인덱스 가 같은 모양으로 정렬되어 매칭됩니다.
CSV "위탁수하물" → kiwi_search_tokens → ['위탁','수하물'] → 키 "위탁 수하물"
CSV "위탁 수하물" → kiwi_search_tokens → ['위탁','수하물'] → 키 "위탁 수하물" ← 붙임·띄움이 같은 키로 수렴
질의 "위탁수하물 분실" → kiwi_search_tokens → ['위탁','수하물','분실'] ← 키와 동일 granularity
키 빌드는 역할이 다른 3단 규칙 으로 동작합니다 — ①이 기본이고, ②③은 ①이 안 되는 어휘만 받치는 폴백입니다:
| 단계 | 사용 함수 | 동작 |
|---|---|---|
| ① 기본 | kiwi_search_tokens |
검색·인덱싱과 동일 경로로 키 생성 → 키=질의 정렬 (대부분 어휘) |
| ② 빈 결과 폴백 | kiwi_key_tokens |
단독 토큰화 시 명사 외 품사로만 잡혀 ①이 비는 어휘(예: 짐→MAG, 달러→NNB)는 구두점만 제외하고 표면형 보존 → 문맥 질의의 NNG form 과 일치 |
| ③ 충돌 폴백 | kiwi_key_tokens |
①의 축약 키가 이미 등록된 다른 그룹 키와 겹치면, 미정렬(더 긴) 키로 다시 만들어 그룹 손실 방지 |
또한 한 어휘의 키가 비거나 5토큰을 초과하면 그 어휘만 제외하고 나머지로 그룹을 구성합니다 (제외분은 WARNING 로그 + dropped_members 집계). 살아남은 키가 2개 미만이면 그 그룹은 등록하지 않습니다.
신조어·과축약 합성어는 사용자 사전에 등록합니다.
베시넷·칠러처럼 Kiwi 가 모르는 신조어는 단독 토큰화 시 자모·동사로 깨지고,출항편·출발편처럼 의존명사(편)가 붙은 합성어는 ①에서 머리단어(출항)로 축약돼 다른 그룹과 충돌할 수 있습니다. 이런 어휘는 전체 단어를 user_dict 에 NNG/NNP 로 등록 하면 단독·문맥 토큰화가 모두 단일 토큰으로 고정돼 키=질의가 보장됩니다 (§5). 즉 폴백(②③)은 안전장치이고, 이 부류는 user_dict 등록이 정공법 입니다.
여기서 구두점 은 괄호 ( )·마침표 .·하이픈 - 같은 문장부호를 말합니다 — 그룹 구분자 쉼표 , 는 그 전 CSV 파싱에서 이미 분리되어 키 빌드가 보지 않습니다.
"위험물(DG)" → 토큰화 → 위험물 · ( · DG · ) → 구두점 제거 → [위험물, DG]
"수하물." → 토큰화 → 수하물 · . → 구두점 제거 → [수하물]
6.6 단방향 확장 (=>)
§6.5 까지의 그룹(콤마)은 모든 어휘가 상호 등가인 양방향(equivalent) 입니다. 반면 한쪽 방향으로만 확장해야 자연스러운 관계도 있습니다 — 예를 들어 포괄어 질의를 세부 품목으로 넓히되, 세부 품목 검색이 포괄어 문서까지 끌어와 정밀도를 떨어뜨리는 것은 막고 싶은 경우입니다. 이를 위해 본 가이드는 단방향 매핑 => 를 표준으로 지원합니다 (OpenSearch synonym_graph 의 explicit mapping 과 동일 문법).
동작 — LHS => RHS 는 좌측(LHS) 어휘로 검색될 때만 우측(RHS)을 확장에 더합니다. load_synonyms 는 LHS 어휘만 매칭 키로 등록하고, 매칭 시 확장값을 [원어] + RHS 로 둡니다. RHS 는 키가 아니므로 역방향은 없습니다.
사전 : 비상장비 => 산소마스크, 소화기, 구명조끼
질의 "비상장비 위치" → 확장 "비상 장비 산소 마스크 소화기 구명조끼 위치" (원어 유지 + RHS 추가)
질의 "소화기" → 확장 "소화기" (역방향 없음)
왜 원어를 유지(RHS 추가)하는가 — 동의어는 검색 시점에만 확장하고 인덱싱 시에는 적용하지 않으므로(§6.4), 문서는 원어(비상장비) 그대로 색인됩니다. Solr => 처럼 원어를 RHS 로 치환 하면 검색 질의가 색인 원어와 어긋나 recall 을 잃습니다. 치환이 안전한 것은 인덱싱 시점에도 동의어를 적용해 색인이 canonical 로 정규화될 때뿐이며, 본 아키텍처는 검색 시점 전용이라 원어 유지가 맞습니다.
equivalent vs directional 선택 — 두 어휘가 서로 바꿔 써도 되면 equivalent(콤마), 한쪽으로만 넓혀야 하면 directional(=>). 대부분의 동의어는 equivalent 이고, => 는 위 비대칭 관계에서만 사용합니다. 작성 규칙·skip 조건은 §9.1, 정본 코드는 §10.3 을 참조하세요.
7. 복합명사 처리 전략
복합명사는 항공 매뉴얼 도메인의 핵심 어휘 유형이고 BM25 검색 품질이 가장 흔들리는 지점입니다. §8 의 split_complex 와 §5 의 add_user_word·add_pre_analyzed_word 를 복합명사 처리 정책 한 가지 주제로 묶어 다룹니다.
7.1 복합명사가 어려운 이유 (한국어·BM25 관점)
한국어 복합명사는 조사 없이 명사들을 자유롭게 이어붙여 새 단어를 만들 수 있는 교착어 특성 때문에 토크나이저·검색 엔진 양쪽에 동시에 부담을 줍니다. 위탁수하물·초과수하물요금·위험물포장구분코드 처럼 항공 매뉴얼에 빈출하는 표현은 사전에 없는 즉석 합성이 대부분입니다.
BM25 관점에서 세 가지가 동시에 작용합니다.
- TF 분포 왜곡 —
위탁수하물통째 색인 시 문서당 1회 카운트. 분해 시 두 토큰이 각각 1회씩 → 문서 길이(dl) 증가. - IDF 약화 —
수하물같은 분해 토큰은 코퍼스 전체에서 흔해 IDF 가 낮음.위탁수하물통째(희소·고 IDF) 보다 변별력 감소. - 쿼리-문서 비대칭 — 인덱싱은 통째인데 사용자가
수하물 위탁 절차로 풀어 쓰면 BM25 점수 0. 반대도 마찬가지.
복합명사 정책은 결국 어느 비대칭을 허용하고, 어느 쪽 토큰을 얼마나 풍부하게 색인할지 의 트레이드오프입니다.
7.2 Kiwi 의 복합명사 처리 메커니즘
Kiwi 의 합성명사 분할 옵션 — tokenize(..., split_complex=...) (kiwipiepy ≥ 0.18):
- 본 가이드 표준:
True(Kiwi 기본값은False) - 동작: LM 기반 N-best 분석에서 분할 가능 형태소를 최대한 분할
- 예시:
위탁수하물→위탁/NNG+수하물/NNG
사용자 사전과 휴리스틱의 우선순위 — Kiwi 내부적으로 합성명사 분해는 LM 점수 기반 N-best 탐색의 분기이며, 사용자 사전에 합성어가 등록되어 있으면 score 가 LM 분해 후보와 경쟁합니다. Score 권장값은 §5.2 정본 참조.
7.3 복합명사 등록 방법 비교
| 방법 | 동작 | 장점 | 단점 | 적용 시나리오 |
|---|---|---|---|---|
split_complex=True 만 |
LM 자동 분해 | 사용자 사전 불필요 | 도메인 특화 합성어 오분해 위험 | 일반 코퍼스, 도메인 사전 미정비 |
add_user_word("위탁수하물", "NNG", 3) 통째 |
단일 NNG | 키워드 명확, 고유명사 안정 | 부분 매칭 불가 | 단일 키워드로 검색되는 고유명사·제품명 |
add_pre_analyzed_word(...) 분해 강제 |
결정적 분해 | 부분 매칭 가능, 재현성 100% | 사전 작성 비용 | 도메인 핵심 합성어 + 부분 검색 요구 |
add_user_word 통째 + 검색 시 동의어 확장 |
단일 인식 + 확장 | 양쪽 효과 | 동의어 사전 운영 부담 | 약어·코드 매핑 |
7.4 인덱싱·검색 정책 비교
| # | 인덱싱 | 검색 | Nori 등가 | 효과 | 위험 |
|---|---|---|---|---|---|
| A | 분해 | 분해 | decompound_mode=discard |
권장 (본 가이드 표준) — Hybrid (BM25+dense) 의 dense 채널이 어휘 변형 보완. 인덱스 단순 | 분해 토큰 IDF↓ — dense 채널이 보완 |
| B | 분해 + 통째 혼합 |
분해 또는 통째 | decompound_mode=mixed |
recall 최대 | 인덱스 크기 1.3~1.8x, BM25 b 재튜닝 (향후 도입 시 별도 SOP) — 향후 옵션 |
| C | 통째 | 통째 | decompound_mode=none |
정확 매칭 단순 | 부분 매칭 0 |
| D | 통째 | 분해 | (없음) | 비추 — 비대칭으로 hit 사실상 불가 | — |
권장 — 정책 A (분해 기본, §10 정본): Hybrid 검색의 dense 채널이 어휘 변형을 보완해 정책 A 단독으로 충분합니다. Nori 기본값 discard 와 동일 정책.
정책 B 도입 시 (향후 옵션) — 사용자 사전 TSV 통째 등록 (§5.5) +
add_pre_analyzed_word강제 분해 (§5.3) 또는blocklist우회 (kiwi.tokenize(..., blocklist=...), kiwipiepy 0.21+) + 이중 패스 union (split_complex=False/True결과 결합) + BM25b재튜닝 (향후 도입 시 별도 SOP). 사용자 사전 score 가 LM 분해를 이겨 정책 C 로 퇴화 하는 함정 회피가 핵심.
7.5 항공 도메인 복합명사 카탈로그 (시작 예시)
운영팀 사전 빌드 시 참고 — 실제 사전은 도메인 PR 로 확장.
| 카테고리 | 예시 어휘 | 권장 처리 |
|---|---|---|
| 객실·수하물 | 위탁수하물·기내수하물·초과수하물·체크인카운터·환승게이트 | add_user_word NNG +3 |
| 화물·위험물 | 위험물·리튬배터리·드라이아이스·마스터빌(MAWB)·하우스빌(HAWB) | add_user_word NNG/NNP +3~+5 |
| 운항·스케줄 | 정기노선·임시노선·연결편·B777기재·A380기재 | add_user_word NNG/NNP +3~+5 |
| 마일리지·멤버십 | 스카이패스·모닝캄클럽·밀리언마일러·아시아나클럽·리프레시포인트·보너스항공권 | add_user_word NNP +5 |
| 항공 코드 | KE001·B777·UN3480 | add_user_word NNP +5 (편명은 패턴 등록 — 항공 코드 처리 방안 결정 후) |
📚 시리즈 — 한국어 RAG 검색 (Kiwi)
- ① 한국어 BM25·토크나이저 선택과 Kiwi 기본 설정
- ② 사용자·동의어 사전과 복합명사 처리 — 현재 글
- ③ 검색 품질 개선·사전 관리·Vector Search 통합
- ④ 부록 — 동의어 적용 위치·IATA 사전 생성/검증·품사·Nori 비교
'Databricks > RAG' 카테고리의 다른 글
| RAG 품질 ③ 아키텍처 — LangGraph 기반 Agentic RAG 검색 처리 흐름 (0) | 2026.07.04 |
|---|---|
| RAG 품질 ① 평가 — MLflow Evaluation 기반 검색·생성 품질 정량 측정 (0) | 2026.07.04 |
| 한국어 RAG 검색 (Kiwi) ④ 부록 — 동의어 적용 위치·IATA 사전 생성/검증·품사·Nori 비교 (0) | 2026.06.28 |
| 한국어 RAG 검색 (Kiwi) ③ 검색 품질 개선·사전 관리·Vector Search 통합 (0) | 2026.06.28 |
| 한국어 RAG 검색 (Kiwi) ① 한국어 BM25·토크나이저 선택과 Kiwi 기본 설정 (0) | 2026.06.28 |
댓글