본문 바로가기
Databricks/RAG

한국어 RAG 검색 (Kiwi) ① 한국어 BM25·토크나이저 선택과 Kiwi 기본 설정

by 여행을 떠나자! 2026. 6. 28.

📚 시리즈 — 한국어 RAG 검색 (Kiwi)

 

 

1. 개요 및 목적

 

1.1 본 가이드의 목적

본 가이드는 A항공 매뉴얼 RAG 의 한국어 검색 품질을 일관되게 운영하기 위한 토크나이저·동의어 처리 표준을 정의합니다. 한국어는 어근에 조사·어미·접사가 결합하는 교착어이고 매뉴얼에는 IATA·기재 코드·복합명사가 빈출하므로, 단순한 공백 토큰화로는 BM25 검색의 recall 이 크게 떨어집니다. 본 가이드는 다음 네 가지를 Single Source of Truth (SSOT) 로 다룹니다:

  • 한국어 형태소 분석기 Kiwi 가 가진 사용자 사전·검색 품질 개선 기능을 어떻게 활용할지
  • 라이브러리가 지원하지 않는 동의어 확장을 어디서 어떻게 구현할지
  • 사용자·동의어 사전을 UC Volume 에서 어떻게 관리할지
  • LangGraph Retriever Node 와 Databricks Vector Search HYBRID 검색에 어떻게 연결할지

 

1.2 적용 범위

본 가이드의 Kiwi 호출은 문서 전처리(배치) · 검색(실시간) 두 단계입니다. 그 사이에 끼는 VSI sync 단계는 Vector Search 가 Delta 컬럼을 source 로 BM25·HNSW 색인을 구축하는 단계로, Kiwi 호출이 없습니다.

단계 동작 흐름 컴퓨트 환경 비고
문서 전처리 (배치) 원본 chunk → Kiwi 토큰화 + 임베딩 → Delta 컬럼(chunk_tokenized*, chunk_embedding) 적재 Databricks Jobs (A항공 채택 = Classic Job Cluster, §10.1) + Spark Pandas UDF §10.1 Iterator Pandas UDF 패턴
VSI sync (자동) Delta 컬럼 → create_delta_sync_index(columns_to_sync=[...]) → VSI (BM25 + HNSW) Vector Search Delta Sync (관리형) Kiwi 미호출. self-managed embedding 이므로 임베딩 모델도 미호출
검색 (실시간) 사용자 쿼리 → Kiwi 토큰화 (+ 동의어 OR 확장) → similarity_search(query_text, query_vector) → top-K Model Serving 에 배포된 RAG 에이전트 (LangGraph) §10.2 module-global lazy init + rolling update 무중단 (§9.4 hot reload)

 

2. 한국어 토큰화와 BM25

 

2.1 한국어 특성

한국어는 어근에 조사·어미·접사가 차례로 결합하여 한 어절(공백 단위 단어) 안에 여러 형태소가 압축됩니다.

  • 위탁수하물의 = 위탁(NNG) + 수하물(NNG) + 의(JKG)
  • 예약하셨습니다 = 예약(NNG) + 하(XSV) + 시(EP) + 었(EP) + 습니다(EF)
  • 킬로그램까지는 = 킬로그램(NNB) + 까지(JX) + 는(JX)

세 문제가 동시에 발생합니다.

  1. 조사·어미 변형 — 같은 명사 수하물수하물, 수하물을, 수하물의, 수하물로 등 수십 가지 표면형으로 등장합니다. 단순 공백 토큰화로는 모두 다른 토큰으로 처리됩니다.
  2. 띄어쓰기 임의성 — 정형 매뉴얼에서도 위탁수하물위탁 수하물 이 혼재합니다. 질의 위탁수하물 무게 가 본문 위탁 수하물의 무게 제한 과 매칭되지 않을 수 있습니다.
  3. 복합명사 빈도수하물허용량, 초과수하물요금, 리튬배터리 같은 복합명사가 본문 키워드로 그대로 사용됩니다. 분해 없이는 부분 매칭이 어렵습니다.

 

2.2 BM25 점수 구조

BM25 는 질의 토큰별로 세 가지 항 — TF(Term Frequency, 문서 내 빈도) · IDF(Inverse Document Frequency, 토큰 희소성) · 문서 길이 가중치 — 의 곱을 합산해 점수를 매깁니다.

score(D, Q) = Σᵢ  TF̃(qᵢ, D)  ×  IDF(qᵢ)  ×  LengthNorm(D)
직관 본 가이드 정책 연계
TF̃(qᵢ, D) (Term Frequency) 문서 안 등장 빈도. k₁ 으로 포화 보정되어 반복해도 점수가 무한히 오르지 않음 정책 B (혼합 색인) 가 통째+분해 두 패스 union 으로 의도적 TF 가중 (§7.4)
IDF(qᵢ) (Inverse Document Frequency) 토큰의 희소성. 흔한 토큰은 0 에 가깝고 희소할수록 큼 (코퍼스 전체 통계 N·df 로 결정) 동의어를 인덱스 시 확장하면 df 가 부풀어 IDF 가 평탄해짐 — Case A 금지 근거 (부록 A.2)
LengthNorm(D) 문서 길이 가중치. avgdl 보다 길면 페널티, b 가 강도 (0 = 페널티 없음, 1 = 완전 정규화) 정책 B 도입 후 평균 문서 길이 증가 → b 재튜닝 근거 (정책 B 도입 시)

Databricks Vector Search 기본값은 k₁ = 1.2, b = 0.75 이며 컬렉션 통계 N(전체 문서 수)·avgdl(평균 길이) 은 인덱스가 자동 계산합니다. 토큰화 정책이 바뀌면 두 통계가 따라 변하므로 정책 B 같이 토큰 수가 늘면 b 재튜닝이 필요합니다.

여기서 핵심은 토큰 정규화가 곧 recall 이라는 점입니다 — 인덱싱 단계에서 수하물의수하물 로 정규화되지 않으면, 질의 수하물 이 들어왔을 때 BM25 는 두 토큰을 무관한 단어로 처리하여 점수가 0 에 가까워집니다.

전체 정밀 공식·로그 형태 IDF·확률론적 유도는 Robertson & Zaragoza (2009) 참조 (Robertson, S. & Zaragoza, H. (2009) — The Probabilistic Relevance Framework: BM25 and Beyond (PDF)).

 

2.3 토큰화 파이프라인 시각화

킬로그램 은 의존명사(NNB) 이므로 기본 화이트리스트에서 제외되지만, 사용자 사전으로 NNG 승격하면 색인에 포함됩니다 (§5 참조).


 

3. 토크나이저 선택

§1.1 에서 결과로만 명시한 "토크나이저로 채택한 Kiwi" 의 비교·결정 과정을 짧게 정리합니다. 외부 BM25 벤치마크와 kiwipiepy 공식 매뉴얼을 근거로 활용했습니다.

 

3.1 한국어 형태소 분석기 후보

분석기 코어 Python 바인딩 사용자 사전 API 라이선스
Kiwi C++ kiwipiepy (v0.23+) 풍부 (런타임 5 API) LGPL
Mecab-ko C++ (mecab fork) python-mecab-ko / konlpy mecab-ko-dic 외부 빌드 BSD/LGPL
Khaiii C++ (CNN) khaiii (Kakao) YAML 사용자 사전 Apache 2.0
Komoran Java PyKomoran / konlpy TXT 외부 사전 Apache 2.0
Kkma Java konlpy 외부 (수동 빌드) GPLv2
Okt (구 Twitter) Scala/Java konlpy 없음 Apache 2.0
Hannanum Java konlpy 없음 GPL

라이선스·활성 유지보수 시기는 각 repo 의 일반 알려진 정보 기반 추정입니다. 최종 채택 전 각 GitHub LICENSE·마지막 커밋 시점 재확인을 권장합니다.

 

3.2 Kiwi 채택 사유

본 가이드는 Databricks 환경 + LangGraph + UC Volume 운영 컨텍스트에서 다음 네 가지 사유로 Kiwi 를 채택합니다.

  1. 풍부한 사용자 사전 APIadd_user_word·load_user_dictionary·add_pre_analyzed_word·add_rule·add_re_rule 5 종 런타임 등록 API. 외부 사전 빌드 없이 즉시 반영되어 hot reload 와 시너지가 큼. JVM 후보·Mecab-ko 의 정적 사전 빌드 사이클과 대비 (kiwipiepy 매뉴얼).
  2. 복합명사 분해 + LM 옵션split_complex위탁수하물허용량 같은 즉석 합성어를 LM 기반으로 분해. KNLM/SBG/CoNg + cong-global 실험판으로 정확도·속도 트레이드오프 운영 측 조정 가능 — 후보 중 명시적 LM 옵션 노출은 Kiwi 만.
  3. Databricks 환경 적합 — pure Python 휠 + C++ 코어. Pandas UDF·Model Serving 모두 pip install kiwipiepy 한 줄 설치. JVM 후보(Komoran·Kkma·Okt) 대비 컨테이너 부팅·메모리 부담 적음.
  4. 한국어 BM25 벤치마크 — AutoRAG 한국어 BM25 토크나이저 벤치마크에서 Mecab·Kiwi 가 상위 성능 보고. 본 가이드는 사용자 사전 API·LM 옵션 격차 로 Kiwi 채택 Making benchmark of different tokenizer in BM25... (footnote 출처: 부록 A.6).

LGPL 영향 — Kiwi 의 LGPL 은 재배포 시 라이브러리 소스 공개 의무가 따릅니다. Databricks 컨테이너 내부에서 import 호출은 재배포가 아니며, 운영팀이 작성한 사용자 사전 TSV·동의어 CSV·LangGraph 노드 코드는 LGPL 영향을 받지 않습니다. 사내 사용·외부 미배포 조건에서 위험은 낮으나, 채택 전 사내 법무 검토를 권장합니다.

본 가이드는 Kiwi (kiwipiepy ≥ 0.23.1) 을 표준으로 채택합니다.


 

4. Kiwi 기능과 권장 설정

본 절은 Kiwi 토크나이저의 기본 사용 (§4.1) 과 검색 품질 개선 옵션 (§8 — 오타 교정·언어모델·정규화) 을 다룹니다. 사용자 사전 (§5), 동의어 사전 (§6), 복합명사 처리 (§7) 는 별도 chapter 정본. 두 사전의 적용 시점 결정 은 §5.6 (사용자 사전 양쪽 동일) / §6.4 (동의어 검색 시점만). 운영 호출 패턴·공유 모듈 코드는 §10 정본.

 

4.1 Kiwi 기본 사용

Kiwi (KOrean Intelligent Word Identifier) 는 bab2min 이 개발한 C++ 형태소 분석기이며, Python 바인딩 kiwipiepy 로 제공됩니다. LM(언어모델) 기반 N-best 분석으로 사전 후보를 점수화하기 때문에 사용자 사전·LM 옵션 이 풍부합니다 (§3.2 채택 사유 참조).

from kiwipiepy import Kiwi

kiwi = Kiwi(load_default_dict=True, integrate_allomorph=True)
tokens = kiwi.tokenize("위탁수하물 무게 제한이 23킬로그램입니다.")
for tok in tokens:
    print(tok.form, tok.tag, tok.start, tok.len)
# 위탁    NNG  0  2
# 수하물  NNG  2  3
# 무게    NNG  6  2
# ...
API 반환 용도
kiwi.tokenize(text, ...) list[Token] (top-1) 가장 흔한 단일 호출 — 인덱싱·검색 양쪽에서 사용
kiwi.analyze(text, top_n=N) list[(분석결과, score)] (top-N) 후보 비교가 필요한 디버깅·평가
kiwi.split_into_sents(text) list[Sentence] 문장 분할

Token 객체는 form(표면형)·tag(세종 품사 태그)·start(원문 offset)·len(길이) 을 포함합니다. 품사 태그 정의는 부록 D 참조.

기본 옵션 두 가지가 형태소 분해 결과에 영향을 줍니다.

  • load_default_dict=True (기본) — Kiwi 내장 사전 로드. 운영에서는 항상 True.
  • integrate_allomorph=True (기본) — 이형태(예: /, 면서/으면서) 통합. 인덱싱·검색 측 표준화에 유리.

 

4.2 전성·파생 명사 토큰 보존 (어간+ETN · 명사+XSN 후처리 병합)

Kiwi 는 각 형태소를 form·tag·start·len 으로 돌려주는데, 일부 명사는 어근 형태소가 별도 형태소로 분해되어 BM25 색인에서 누락되거나 문맥에 따라 들쭉날쭉 쪼개집니다. 두 유형이 대표적입니다.

유형 1 — 어간 + 명사형 전성어미(ETN: //) — §4.1 예제의 두 번째 출력(운항 지연 알림을 받았습니다.)에서 보듯, 알림알리/VV + ㅁ/ETN 으로 쪼개지고 은 온전한 음절이 아닌 자모 한 글자 가 됩니다. 참고로 OpenSearch Nori(mecab-ko-dic 기반) 표준 분석기도 알림 을 통째 명사로 보유하지 않고 알리(VV 어간)로 분해합니다 — 차이는 POS 필터 정책뿐으로, Nori 기본 stoptags 는 동사 어간을 색인에 남겨 알리 가 들어가는 반면, 본 가이드 Kiwi 화이트리스트는 명사만 남겨 알림0 토큰 이 됩니다. 즉 두 분석기 모두 표면형 알림 을 보존하지 않으므로, 후처리 병합은 양쪽 대비 고유한 가치를 가집니다 (실측 비교: 부록 E.3).

유형 2 — 명사 + 파생접미사(XSN)할인권 은 문맥에 따라 할인권/NNG(통째) 또는 할인/NNG + 권/XSN(분해)로 갈립니다. 후자는 권/XSN 이 화이트리스트에 없어 탈락하고 할인 으로만 색인됩니다. 선생님(선생/NNG + 님/XSN가능성(가능/NNG + 성/XSN) 등도 같은 구조입니다.

BM25 영향 — §2.3 화이트리스트(NNG·NNP·SL·SN·SH) 에 VV·ETN·XSN 이 없으므로 분해된 어간·어미·접미사는 색인에서 탈락합니다. 더 큰 문제는 문맥 의존적 비일관성입니다 — 문서 할인권 사용할인권 으로 색인되는데 질의 할인권할인 으로 토큰화되거나(또는 그 반대), 인덱스·질의 토큰이 어긋나면 BM25 매칭이 0 이 됩니다.

두 가지 대응

  • 사용자 사전 per-word NNG 승격 (§5.2) — 정확하지만 전성·파생 명사가 많아 사전 관리 부담이 큽니다.
  • 후처리 병합 (사전 0 건, 본 절 권장)어간 + ETN·명사 + XSN 패턴을 start/len 으로 원문 표면형을 떠서 단일 NNG 토큰으로 되붙입니다. Match.JOIN_* 옵션은 용언 파생접미사(XSV/XSA)만 합치고 ETN·XSN 은 미대상이므로, 토큰 리스트 후처리가 가장 단순합니다.
KEEP_TAGS = {"NNG", "NNP", "SL", "SN", "SH"}        # §2.3 색인 화이트리스트
STEM_TAGS = {"VV", "VA", "XSV", "XSA", "VX"}        # 어간류(용언·파생접미사)
NOUN_TAGS = {"NNG", "NNP"}                          # 명사

def merge_derived_nouns(text, tokens):
    """파생·전성 명사를 원문 표면형 1토큰(NNG)으로 병합.

    - 어간(STEM) + ㅁ/음/기(ETN)  → 알림, 찾기   (전성명사)
    - 명사(NN)  + 파생접미사(XSN) → 할인권, 선생님 (파생명사)
    명사 핵 뒤에 이어지는 XSN 은 연속으로 흡수합니다.
    """
    out, i, n = [], 0, len(tokens)
    while i < n:
        t = tokens[i]
        j = None
        if i + 1 < n and t.tag in STEM_TAGS and tokens[i + 1].tag == "ETN":
            j = i + 1                                  # 어간 + ETN 핵
        elif t.tag in NOUN_TAGS:
            j = i                                      # 명사 핵
        if j is not None:
            k = j + 1
            while k < n and tokens[k].tag == "XSN":    # 뒤따르는 파생접미사 흡수
                k += 1
            if k > j + 1 or j > i:                     # 실제 병합 대상이 있을 때만
                last = tokens[k - 1]
                out.append((text[t.start:last.start + last.len], "NNG"))  # start/len = 전 버전 안전
                i = k
                continue
        out.append((t.form, t.tag)); i += 1
    return out

Token.span(= (start, end) 튜플) 으로 text[t.span[0]:last.span[1]] 도 동일 결과지만, span 은 비교적 최근 추가된 편의 속성이라 start/len 조합이 전 버전에서 안전합니다.

이 병합을 거치면 알림·찾기(ETN), 할인권·선생님·가능성(XSN) 같은 파생·전성 명사가 표면형으로 색인되며, 이미 NNG 로 잡히는 모임·문제점·장난감 등은 영향을 받지 않습니다.

트레이드오프 — (1) 어휘화 안 된 명사형(그가 옴을)까지 병합하지만, BM25 에선 IDF 가 낮아 노이즈가 거의 없고 인덱스·질의가 같은 표면형으로 수렴해 오히려 유리합니다. (2) XSN 병합은 할인권 을 통째 1토큰으로 색인하므로 질의 할인(단독)은 BM25 정확매칭에서 할인권 문서를 놓칠 수 있으나, 권/XSN 은 단독 색인 가치가 없고 부분매칭은 Hybrid 의 dense 채널이 보완합니다 (§7.4 정책 A 와 동일 논리). (3) 들것(= 들/VV + ㄹ/ETM + 것/NNB, ETM+의존명사) 은 본 규칙 밖이며, 문맥이 있으면 Kiwi 가 이미 들것/NNG 로 잡습니다.

운영 코드에서는 이 병합을 §10.3 공유 모듈에 두어 인덱싱·검색 양쪽이 동일하게 거치도록 강제합니다.

 

📚 시리즈 — 한국어 RAG 검색 (Kiwi)

댓글