신상품 “에어팟프로”를 등록했는데 검색이 안 됐다. 로그를 보니 ES가 이걸 “에어 / 팟 / 프로”로 쪼개서 색인하고 있었다. 사용자가 “에어팟프로”를 검색하면 이것도 “에어 / 팟 / 프로”로 쪼개지니 매칭은 되는데, “에어팟”만 검색하면 안 잡혔다.
한국어 검색은 영어와 근본적으로 다르다. 단어를 어떻게 쪼개느냐가 검색 품질의 절반을 결정한다.
한국어는 왜 공백으로 못 쪼개나?
영어 “galaxy case”는 공백으로 끊으면 끝이다. 한국어는 교착어라 안 된다.
- 조사가 붙는다: “갤럭시를”, “갤럭시가”, “갤럭시는” → 전부 “갤럭시”로 정규화해야 한다.
- 복합어에 공백이 없다: “삼성전자갤럭시케이스” → 통째로 한 토큰이 돼버린다.
- 어미가 변한다: “예쁜”, “예뻐서”, “예쁘다” → “예쁘다”로 묶어야 한다.
공백 기준으로 쪼개면 “갤럭시를”과 “갤럭시가”가 다른 단어가 된다. 그래서 단어의 원형과 품사를 분석하는 형태소 분석기가 필요하다. 한국어 표준은 Nori다. Nori 말고도 은전한닢(Seunjeon)·아리랑 같은 형태소 분석기 플러그인이 있고, 보통 icu_normalizer로 유니코드 정규화(NFC/호환문자 통일)를 병행해 표기 흔들림을 잡는다.
Nori는 어떻게 쪼개나?
Nori는 토크나이저 하나와 토큰 필터 여러 개로 이뤄진 파이프라인이다. 핵심은 nori_tokenizer의 decompound_mode(복합어 분해 방식)다.
입력: "가곡집"
- none → 가곡집 (원형 유지)
- discard → 가곡, 집 (분해만, 원형 버림)
- mixed → 가곡집, 가곡, 집 (원형 + 분해 둘 다)
이커머스 검색은 보통 mixed를 쓴다. “갤럭시케이스”를 통째로 검색하는 사람도, “케이스”만 검색하는 사람도 다 잡아야 하기 때문이다.
{
"tokenizer": {
"ko_tokenizer": {
"type": "nori_tokenizer",
"decompound_mode": "mixed",
"user_dictionary": "userdict_ko.txt"
}
}
}
품사 필터가 왜 필요한가?
토큰화만 하면 “갤럭시를”이 “갤럭시 / 를”로 쪼개진다. “를”은 검색에 의미가 없는 조사다. 이런 걸 색인에 넣으면 노이즈만 늘고 용량만 먹는다.
nori_part_of_speech 필터가 조사(J), 어미(E), 접속사 같은 의미 없는 품사를 제거한다.
{
"filter": {
"ko_pos": {
"type": "nori_part_of_speech",
"stoptags": ["E", "J", "SC", "SE", "SF", "VCP", "VCN"]
}
}
}
여기에 nori_readingform(한자를 한글 발음으로, “中國” → “중국”), lowercase(영문 소문자화)를 더해 파이프라인을 완성한다.
신상품이 잘못 쪼개진다. 어떻게 하나?
Nori 기본 사전에는 신조어와 브랜드명이 없다. 그래서 “에어팟프로”를 “에어 / 팟 / 프로”로 잘못 쪼갠다.
해결은 사용자 사전(user_dictionary) 이다. 한 단어로 인식시킬 단어들을 파일에 등록한다.
# userdict_ko.txt
에어팟프로
갤럭시버즈프로
삼성전자
이커머스는 신상품과 브랜드가 매일 쏟아진다. 그래서 사용자 사전을 주기적으로 업데이트하고 재색인하는 프로세스 자체가 운영 업무가 된다. 신상품 검색 품질 이슈의 대부분이 여기서 나온다.
한 가지 주의: 사용자 사전은 토크나이저 단계에서 적용돼 decompound보다 먼저 동작하므로, 사용자 사전에 등록한 단어는 복합어 분해 대상에서 빠진다(한 토큰 유지). 반대로 동의어는 토큰화 이후 필터 단계라 순서가 뒤다. 즉 “사용자 사전 → decompound → 동의어” 우선순위가 어긋나면 의도와 다르게 쪼개지거나 매칭이 깨질 수 있다.
색인 analyzer와 검색 analyzer를 왜 다르게 두나?
ES는 색인할 때와 검색할 때 다른 analyzer를 쓸 수 있다(analyzer vs search_analyzer). 보통 이렇게 비대칭으로 둔다.
- 색인 시: 형태소 분석 + 복합어 분해 → 데이터를 넓게 펼쳐 저장
- 검색 시: 형태소 분석 + 동의어 확장 → 검색어를 넓게 펼쳐 질의
왜 동의어를 검색 시에만 두는지는 다음에서 설명한다.
동의어와 유의어는 어떻게 처리하나?
“노트북”과 “랩탑”을 같은 것으로 검색되게 하려면 synonym 토큰 필터를 쓴다. 사전 형식은 두 가지다.
# 확장형(equivalent) — 양방향, 모두 같은 것
노트북, 랩탑, laptop, 놋북
# 축약형(explicit) — 단방향, 왼쪽을 오른쪽으로
삼성, 삼성전자 => 삼성전자
다중 단어 동의어(“ny” = “new york”)를 정확히 처리하려면 구버전 synonym이 아니라 synonym_graph 를 써야 한다. 구버전은 다중 단어에서 토큰 위치가 꼬여 구문 검색이 깨진다.
동의어를 바꿨는데 재색인해야 하나?
여기가 운영의 핵심이다. 동의어는 색인 타임에 적용할 수도, 검색 타임에 적용할 수도 있다.
| 색인 타임 동의어 | 검색 타임 동의어 | |
|---|---|---|
| 동작 | 색인할 때 동의어를 펼쳐 저장 | 검색어를 동의어로 펼쳐 질의 |
| 사전 변경 시 | 전체 재색인 필요 | 사전만 리로드 |
| 인덱스 크기 | 커짐 | 그대로 |
| 권장 | 거의 안 씀 | 표준 |
동의어 사전은 자주 바뀐다. 그런데 색인 타임에 두면 사전을 한 번 고칠 때마다 수천만 문서를 재색인해야 한다. 불가능하다. 그래서 동의어는 검색 analyzer에만 둔다. 사전만 리로드하면 끝이다.
{
"search_analyzer": {
"ko_search": {
"tokenizer": "ko_tokenizer",
"filter": ["ko_pos", "synonym_graph_filter", "lowercase"]
}
}
}
형태소와 동의어가 충돌하는 함정
흔히 빠지는 함정이다. “삼성전자”를 동의어 사전에 등록했는데 안 잡히는 경우가 있다.
원인: 형태소 분석기가 “삼성전자”를 “삼성 / 전자”로 먼저 쪼개버리면, 동의어 필터가 보는 토큰은 “삼성”과 “전자”이지 “삼성전자”가 아니다. 동의어 사전의 “삼성전자”와 매칭될 토큰이 애초에 안 생긴다.
해결: 동의어로 잡고 싶은 단어는 사용자 사전에도 등록해서 한 토큰으로 유지시킨 뒤, 동의어 필터가 그걸 보게 한다. 사용자 사전과 동의어 사전은 함께 관리해야 한다. 또 필터 순서도 중요하다 — 토큰화 이후에 동의어 필터가 와야 한다.
운영 정리
- decompound_mode는 mixed: 복합어 통째 검색과 부분 검색을 모두 커버.
- 사용자 사전 운영 프로세스화: 신상품/브랜드 주기적 등록 + 재색인. 검색 품질 이슈의 단골 원인.
- 동의어는 검색 타임 synonym_graph: 사전 변경 시 재색인 없이 리로드. 색인 타임은 재색인 비용 때문에 지양.
- 사용자 사전 ↔ 동의어 사전 함께 관리: 동의어로 잡을 단어는 한 토큰으로 유지되도록 사용자 사전에도 등록.
- 색인/검색 analyzer 비대칭 설계: 색인은 복합어 분해, 검색은 동의어 확장.