fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • claude-code 페이지로 이동
    • harness 페이지로 이동
    • llm 페이지로 이동
    • ops 페이지로 이동
    • practice 페이지로 이동
  • algorithm 페이지로 이동
    • Alias Method: 가중치 랜덤 선택을 전처리 O(n), 추출 O(1)로 바꾸기
    • Welford's Online Algorithm: 값을 저장하지 않고 평균과 분산 계산하기
  • architecture 페이지로 이동
    • distributed-systems 페이지로 이동
    • domain 페이지로 이동
    • evolution 페이지로 이동
    • patterns 페이지로 이동
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • postgresql 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • gaps and islands: SQL 창 함수로 이어진 구간 찾기
    • MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 정규화와 역정규화: 무결성과 조회 비용 사이의 선택
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
    • 종료 신호는 어디서 멈추는가
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS와 TLS: 핸드셰이크, 인증서, 종료 지점
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • Alpine 이미지의 Java 에서 링크 바꿔치기(TOCTOU)를 막지 못하는 이유
    • Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • 로그에 traceId 남기기 — MDC 부터 OpenTelemetry 까지
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 클러스터 아키텍처: Broker, KRaft Controller와 복제
    • Kafka 기본 개념: Topic, Partition, Offset과 Segment
    • Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • Kafka 파티션·리밸런스·컨슈머 지연 운영
    • Kafka를 로그로 이해하기: 복제, 재처리, 상태와 데이터 통합
    • Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • SSH forced command 로 배포 키가 실행할 수 있는 명령 제한하기
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • model-router 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • K8s 위 Spring Boot 앱의 메트릭 수집
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS가 줄지 않는 이유: CPython, gc.collect(), malloc_trim
  • rabbitmq 페이지로 이동
    • RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • 경험 및 경력기술서
    • 김병태 경력기술서
    • 김병태 포트폴리오
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • personal-projects 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • thinking 페이지로 이동
    • 자기주도 학습: 질문에서 시작해 글로 검증하기
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/database/OpenSearch 검색 품질을 좌우하는 a…
db

OpenSearch 검색 품질을 좌우하는 analyzer 구조: nori, ngram, tokenizer, token filter 제대로 이해하기

검색 기능은 백엔드 엔지니어가 피할 수 없는 과제 중 하나다. 상품 검색, 로그 검색, 자동완성, 오타 보정, 다국어 처리 같은 요구사항이 쌓이다 보면 결국 Elasticsearch 혹은 OpenSearch 같은 검색 엔진을 도입하게 되고, 그 순간부터 "왜 이 키워드로는 안 나오지?", "왜 부분 일치가 이상하게 동작하지?", "왜 한글은 조사까지 걸리지...

2026.05.07·10 min read·145 views

왜 이 주제를 공부해야 하는가

검색 기능은 백엔드 엔지니어가 피할 수 없는 과제 중 하나다. 상품 검색, 로그 검색, 자동완성, 오타 보정, 다국어 처리 같은 요구사항이 쌓이다 보면 결국 Elasticsearch 혹은 OpenSearch 같은 검색 엔진을 도입하게 되고, 그 순간부터 "왜 이 키워드로는 안 나오지?", "왜 부분 일치가 이상하게 동작하지?", "왜 한글은 조사까지 걸리지?" 같은 질문이 쏟아진다.

이 모든 질문의 답은 결국 한 곳으로 귀결된다. analyzer가 문자열을 어떻게 토큰으로 쪼개고 정규화하는가. analyzer를 이해하지 못한 채로 OpenSearch를 도입하면, 인덱스에 이상한 토큰이 들어가 있고 쿼리 시점에는 다른 토큰이 생성되어 매칭 자체가 실패하는 버그를 잡는 데 며칠을 쓰게 된다. 반대로 analyzer 구성 요소(character filter, tokenizer, token filter)와 주요 플러그인(nori, ngram)의 동작 방식을 제대로 잡아두면, 검색 품질 이슈의 80%는 색인/쿼리 설정 수준에서 해결된다.

시니어 백엔드 관점에서는 한 단계 더 들어간다. nori를 쓰면 정확도는 올라가지만 인덱스 크기와 색인 시간이 늘어나고, ngram을 쓰면 부분 일치가 가능해지지만 인덱스가 폭발한다. 이 trade-off를 이해하고, 요구사항에 맞춰 analyzer 파이프라인을 설계할 수 있어야 한다. 이 문서는 그 설계 감각을 잡기 위한 실전 가이드다.

OpenSearch analyzer의 기본 구조

OpenSearch의 analyzer는 세 단계 파이프라인이다.

핵심은 tokenizer는 반드시 하나, character filter와 token filter는 0개 이상이라는 점이다. 많은 사람이 "analyzer = nori"라고 착각하지만, 실제로는 "nori_tokenizer + 여러 token filter의 조합"이 가장 현실적인 analyzer다.

예를 들어 검색 쿼리 "스타벅스 매장 영업시간"이 들어왔을 때, standard analyzer는 공백으로만 자르기 때문에 [스타벅스, 매장, 영업시간] 같은 토큰이 만들어진다. 문제는 사용자가 "스타벅스매장"처럼 붙여 쓰면 하나의 토큰 스타벅스매장이 되어버려 색인된 스타벅스과 매칭되지 않는다. 한국어 검색에서 nori나 ngram이 필요한 이유가 여기에 있다.

nori 플러그인: 한국어 형태소 분석기

nori는 Elasticsearch/OpenSearch 공식에서 제공하는 한국어 형태소 분석 플러그인이다. 내부적으로는 Lucene의 KoreanAnalyzer를 래핑하고 있으며, 사전 기반(mecab-ko-dic 유래)으로 단어를 분석한다.

nori의 구성 요소

nori 플러그인은 설치 후 세 가지를 제공한다.

  • nori_tokenizer — 한국어 문장을 형태소 단위로 분리
  • nori_part_of_speech token filter — 특정 품사(조사, 어미 등)를 제거
  • nori_readingform token filter — 한자를 한글 독음으로 변환

nori_tokenizer에는 decompound_mode라는 중요한 옵션이 있다.

  • none — 복합명사를 분리하지 않는다. 백두산 → [백두산]
  • discard — 복합명사를 분리한 토큰만 남긴다. 백두산 → [백두, 산]
  • mixed — 원본 복합명사 + 분리된 구성 요소 모두 인덱싱. 백두산 → [백두산, 백두, 산]

검색 서비스 관점에서는 mixed가 흔히 선택된다. 사용자가 전체 단어로 검색하든 일부 단어로 검색하든 매칭되기 때문이다. 다만 인덱스 크기는 커진다.

nori 설정 예시

json
PUT /products
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "my_nori_tokenizer": {
          "type": "nori_tokenizer",
          "decompound_mode": "mixed",
          "user_dictionary_rules": [
            "스타벅스",
            "맥세이프",
            "멀티비타민민"
          ]
        }
      },
      "analyzer": {
        "korean_analyzer": {
          "type": "custom",
          "tokenizer": "my_nori_tokenizer",
          "filter": [
            "lowercase",
            "nori_part_of_speech_filter",
            "nori_readingform"
          ]
        }
      },
      "filter": {
        "nori_part_of_speech_filter": {
          "type": "nori_part_of_speech",
          "stoptags": [
            "E", "IC", "J", "MAG", "MAJ", "MM",
            "SP", "SSC", "SSO", "SC", "SE",
            "XPN", "XSA", "XSN", "XSV",
            "UNA", "NA", "VSV"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "korean_analyzer"
      }
    }
  }
}

여기서 주목할 점은 사용자 사전(user_dictionary_rules) 이다. 신조어, 브랜드명, 내부 도메인 용어는 기본 사전에 없기 때문에 nori가 엉뚱하게 쪼갠다. 예를 들어 "맥세이프"는 기본 사전에 없으면 [맥, 세이프]로 쪼개질 수 있다. 실제 서비스에서는 상품명, 브랜드, 카테고리를 사용자 사전으로 꾸준히 관리해야 한다.

stoptags는 제거할 품사 태그다. J(조사), E(어미), MAG(일반 부사) 같은 것들은 검색 매칭에 도움이 안 되기 때문에 제거한다. 이 리스트는 Lucene 문서에 전부 정의되어 있으니 프로젝트마다 조정하면 된다.

ngram tokenizer: 부분 일치와 자동완성

ngram은 문자열을 n글자 단위로 잘라 모든 부분 문자열을 토큰화한다.

  • min_gram: 2, max_gram: 3인 ngram으로 "검색"을 분석하면 [검, 검색, 색] 수준에서 토큰이 나온다(경계 설정에 따라 다르다).
  • "스타벅스"을 min_gram: 2, max_gram: 3으로 넣으면 [스타, 스타벅, 타벅, 타벅스, 벅스]이 된다.

이 덕분에 "타벅스"으로 검색해도 "스타벅스"이 매칭된다. 즉, 부분 일치(wildcard 없는 LIKE '%xxx%'와 유사한 UX)를 구현할 수 있다.

ngram vs edge_ngram

두 가지는 자주 혼동되지만 용도가 다르다.

  • ngram — 모든 위치의 부분 문자열. "olive" → [ol, oli, liv, ive, ...]
  • edge_ngram — 앞에서부터 자르는 부분 문자열. "olive" → [o, ol, oli, oliv, olive]

자동완성 기능에는 거의 항상 edge_ngram이 정답이다. 사용자가 "스"를 입력하면 "스타벅스", "스벅"이 떠야 하지만, "타벅"만 입력했는데 "스타벅스"이 떠서는 자동완성 UX가 어색해진다.

색인 시점 vs 검색 시점 analyzer

ngram을 설계할 때 가장 많이 하는 실수가 색인 analyzer와 검색 analyzer를 동일하게 쓰는 것이다.

  • 색인 시점: 문서의 "스타벅스"을 [스, 스타, 스타벅, 스타벅스]로 쪼개 저장
  • 검색 시점 사용자가 "스타"라고 입력했을 때 또 edge_ngram을 적용하면 [스, 스타]로 쪼개져 두 토큰 모두 매칭을 시도한다.

만약 검색 시점에도 edge_ngram을 쓰면, "스타"가 [스, 스타]로 확장되고, 이 중 "스"은 전혀 다른 상품("스벅세일"의 스 부분)과도 매칭되어 정확도가 급락한다. 그래서 일반적으로 다음 패턴을 쓴다.

json
"mappings": {
  "properties": {
    "name": {
      "type": "text",
      "analyzer": "autocomplete_index_analyzer",
      "search_analyzer": "standard"
    }
  }
}

색인은 edge_ngram으로 넓게 쪼개고, 검색은 standard나 keyword로 그대로 넣는다. 이 비대칭이 성능과 정확도 양쪽의 핵심이다.

bad vs improved 예제

Bad: 맹목적으로 nori만 적용하기

json
{
  "settings": {
    "analysis": {
      "analyzer": {
        "default": {
          "type": "nori"
        }
      }
    }
  }
}

이 설정의 문제는 세 가지다.

  1. decompound_mode가 기본값(discard)이라 원본 단어가 사라진다. "스타벅스"이 사전에 없거나 다르게 분해되면 "스타벅스"이라는 완전한 토큰이 인덱스에 아예 없다.
  2. 불용 품사 필터가 없어 조사·어미가 그대로 들어간다. "검색했다"가 [검색, 하, 였, 다] 같은 토큰으로 쪼개져 저장된다.
  3. 브랜드/상품 사전 관리가 없어 신조어마다 결과가 깨진다.

Improved: 역할별 analyzer 분리

json
{
  "settings": {
    "analysis": {
      "analyzer": {
        "korean_search": {
          "type": "custom",
          "tokenizer": "nori_tokenizer_mixed",
          "filter": ["lowercase", "ko_pos_filter"]
        },
        "autocomplete_index": {
          "type": "custom",
          "tokenizer": "edge_ngram_tokenizer",
          "filter": ["lowercase"]
        },
        "autocomplete_search": {
          "type": "custom",
          "tokenizer": "keyword",
          "filter": ["lowercase"]
        }
      },
      "tokenizer": {
        "nori_tokenizer_mixed": {
          "type": "nori_tokenizer",
          "decompound_mode": "mixed"
        },
        "edge_ngram_tokenizer": {
          "type": "edge_ngram",
          "min_gram": 1,
          "max_gram": 15,
          "token_chars": ["letter", "digit"]
        }
      },
      "filter": {
        "ko_pos_filter": {
          "type": "nori_part_of_speech",
          "stoptags": ["E", "J", "MAG", "SP"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "korean_search"
      },
      "name_autocomplete": {
        "type": "text",
        "analyzer": "autocomplete_index",
        "search_analyzer": "autocomplete_search"
      }
    }
  }
}

핵심은 한 필드에 여러 analyzer를 쓰지 않고, 목적별로 필드를 분리한다는 점이다. name은 정확도 중심, name_autocomplete은 부분 일치 중심으로 가고, 애플리케이션에서 용도에 맞게 쿼리한다.

로컬 연습 환경

도커로 OpenSearch를 띄우고 nori 플러그인을 설치한다.

bash
# docker-compose.yml
version: "3"
services:
  opensearch:
    image: opensearchproject/opensearch:2.13.0
    environment:
      - discovery.type=single-node
      - DISABLE_SECURITY_PLUGIN=true
      - OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g
    ports:
      - "9200:9200"
    volumes:
      - ./data:/usr/share/opensearch/data

컨테이너에 들어가 nori 플러그인을 설치한다.

bash
docker exec -it <container> bash
./bin/opensearch-plugin install analysis-nori
exit
docker restart <container>

실행 가능한 예제

_analyze API로 analyzer의 결과를 직접 확인할 수 있다. 이 API는 평소 디버깅에서 가장 많이 쓰는 도구다.

예제 1: nori decompound_mode 비교

bash
curl -X POST "localhost:9200/_analyze" -H 'Content-Type: application/json' -d'
{
  "tokenizer": {
    "type": "nori_tokenizer",
    "decompound_mode": "mixed"
  },
  "text": "대한민국헌법재판소"
}'

mixed에서는 [대한민국헌법재판소, 대한민국, 헌법, 재판소, ...]처럼 원본과 분해본이 모두 나온다. discard로 바꾸면 원본이 사라진다.

예제 2: edge_ngram 자동완성 확인

bash
curl -X POST "localhost:9200/_analyze" -H 'Content-Type: application/json' -d'
{
  "tokenizer": {
    "type": "edge_ngram",
    "min_gram": 1,
    "max_gram": 10,
    "token_chars": ["letter", "digit"]
  },
  "text": "스타벅스"
}'

결과로 [스, 스타, 스타벅, 스타벅스]이 나오면 색인이 제대로 되는지 쉽게 눈으로 확인할 수 있다.

예제 3: 사용자 사전 적용 후 비교

사전 없는 경우와 있는 경우의 토큰을 _analyze로 각각 실행해 비교한다. 사전에 "맥세이프"를 추가하기 전에는 [맥, 세이프]로 쪼개지지만, 추가 후에는 [맥세이프] 하나로 유지된다. 이 차이가 실제 검색 품질에서 눈에 보일 만큼 크다는 점을 직접 확인하는 것이 중요하다.

예제 4: 비대칭 analyzer로 자동완성 끝까지 검증

색인 후 실제 검색을 날려 본다.

bash
curl -X POST "localhost:9200/products/_search" -H 'Content-Type: application/json' -d'
{
  "query": {
    "match": {
      "name_autocomplete": {
        "query": "올리",
        "operator": "and"
      }
    }
  }
}'

검색어 "올리"가 standard로 분석되어 그대로 들어가고, 색인된 edge_ngram 토큰 중 "올리"와 정확히 일치하는 항목이 걸린다. 만약 검색 시에도 edge_ngram을 쓰고 있다면 정확도가 떨어진다는 점을 직접 비교해 본다.

흔한 오해와 실패 패턴

  • 오해: "nori를 쓰면 자동으로 검색 품질이 좋아진다." 사용자 사전 관리, 품사 필터 튜닝 없이 넣으면 오히려 엉망이 된다. 운영 중 검색 로그를 보면서 사전 관리는 꾸준히 해야 한다.
  • 오해: "ngram과 edge_ngram은 같은 것이다." 용도가 완전히 다르다. 자동완성에는 edge_ngram, 중간 부분 일치(한정적으로)에는 ngram이다.
  • 실패 패턴: 색인/검색 analyzer를 동일하게 두기. ngram류에서 가장 빈번한 버그다. search_analyzer를 명시하지 않아 검색 정확도가 무너지는 경우가 매우 많다.
  • 실패 패턴: min_gram을 1로 두기. 인덱스 크기가 수 배로 커지고 아무 단어나 매칭되기 시작한다. 자동완성이라도 보통 2부터 시작하는 것을 고려한다.
  • 실패 패턴: analyzer를 바꾸고 reindex를 안 하기. analyzer 변경은 새로 색인된 문서에만 적용된다. 기존 문서는 예전 토큰을 가지고 있기 때문에, analyzer 변경 후에는 새 인덱스를 만들고 alias 스왑으로 이관하는 패턴이 정석이다.

설계 관점에서의 trade-off

실무에서는 단일 analyzer로 모든 요구사항을 만족시킬 수 없다. 보통 다음처럼 간다.

  • 정확한 의미 검색: nori + 품사 필터 + 사용자 사전
  • 부분 일치/오타 관대 검색: ngram 또는 nori + synonym filter
  • 자동완성: edge_ngram (색인) + keyword/standard (검색)
  • 다국어 상품명: 필드 분리 후 각 언어별 analyzer

필드를 분리하는 설계가 핵심이다. multi_fields를 써서 name, name.autocomplete, name.raw 같이 한 소스를 여러 형태로 색인하면, 쿼리 레이어에서 가중치만 조정해도 깔끔하게 검색 품질을 튜닝할 수 있다.

인덱스 크기와 색인 속도도 반드시 같이 본다. edge_ngram min_gram: 1, max_gram: 20은 이론적으로는 가능하지만, 실제로는 인덱스가 몇 배로 불어나 색인 처리량이 눈에 띄게 떨어진다. 자동완성은 보통 min_gram: 2, max_gram: 10~15 범위에서 타협한다.

면접 답변 프레임

면접에서 "OpenSearch analyzer를 어떻게 설계하셨나요?"라는 질문이 나오면 다음 구조로 답하는 게 안정적이다.

  1. 문제 정의 먼저: "상품 검색에서 한글 부분 일치와 자동완성이 모두 필요했다. 단일 필드·단일 analyzer로는 정확도와 재현율을 동시에 만족시킬 수 없었다."
  2. 구성 요소로 분해: "analyzer는 character filter → tokenizer → token filter 파이프라인이고, 한국어는 nori_tokenizer, 자동완성은 edge_ngram tokenizer로 분리했다."
  3. 선택 근거: "정확도 중심 필드에는 nori + part_of_speech filter로 조사/어미를 제거했고, 자동완성 필드에는 edge_ngram을 색인 analyzer로, standard를 search analyzer로 둬 비대칭 구성을 했다."
  4. 운영 관점 언급: "브랜드명·신조어는 user_dictionary로 관리하고, analyzer 변경 시에는 새 인덱스 + alias 스왑으로 무중단 reindex했다."
  5. 숫자로 마무리: "그 결과 검색 누락 케이스를 약 X% 줄였고, 자동완성 latency도 Y ms 이내로 유지됐다."

즉답보다는 "왜 이런 구성이 필요했는지"를 문제 정의부터 풀어가는 게 시니어다운 답변이다. 인덱스 크기, 색인 속도, reindex 전략까지 자연스럽게 언급할 수 있으면 운영 경험이 있는 엔지니어라는 인상을 준다.

관련 예상 질문

  • nori의 decompound_mode 세 가지 차이를 설명하라. 어떤 상황에서 어떤 모드를 쓰는가.
  • edge_ngram과 ngram의 차이, 그리고 각각의 대표적인 유스케이스는.
  • 색인 analyzer와 검색 analyzer를 다르게 가져가는 이유는.
  • 사용자 사전은 왜 필요하고, 운영상 어떤 리스크가 있나.
  • analyzer를 변경할 때 기존 문서에는 왜 바로 반영되지 않는가. 어떻게 처리하는가.
  • min_gram을 너무 작게 잡으면 어떤 문제가 생기는가.
  • 특정 쿼리에서 기대한 결과가 나오지 않을 때 어떤 순서로 디버깅하는가. (힌트: _analyze, explain, profile API)

체크리스트

  • character filter, tokenizer, token filter의 역할과 순서를 그림 없이 설명할 수 있다.
  • nori_tokenizer의 decompound_mode 세 가지를 상황별로 고를 수 있다.
  • nori_part_of_speech의 stoptags 의미를 이해하고 필요한 품사를 고를 수 있다.
  • 사용자 사전을 추가하고 _analyze로 토큰 변화를 검증할 수 있다.
  • edge_ngram과 ngram을 용도에 맞춰 고르고, min_gram/max_gram의 영향을 설명할 수 있다.
  • 색인 analyzer와 검색 analyzer를 비대칭으로 설정하는 이유를 설명할 수 있다.
  • analyzer 변경 시 reindex + alias 스왑 흐름을 실제로 구성해 본 적이 있다.
  • _analyze API로 실제 토큰을 확인하고 검색 버그를 추적해 본 경험이 있다.
  • 인덱스 크기와 색인 속도 관점에서 ngram 설정을 튜닝할 수 있다.
  • 면접에서 "문제 정의 → 구성 요소 분해 → 선택 근거 → 운영 관점 → 결과" 순으로 답변 가능한 사례 하나가 준비되어 있다.
on this page
  • 01왜 이 주제를 공부해야 하는가
  • 02OpenSearch analyzer의 기본 구조
  • 03nori 플러그인: 한국어 형태소 분석기
  • nori의 구성 요소
  • nori 설정 예시
  • 04ngram tokenizer: 부분 일치와 자동완성
  • ngram vs edge_ngram
  • 색인 시점 vs 검색 시점 analyzer
  • 05bad vs improved 예제
  • Bad: 맹목적으로 nori만 적용하기
  • Improved: 역할별 analyzer 분리
  • 06로컬 연습 환경
  • 07실행 가능한 예제
  • 예제 1: nori decompound_mode 비교
  • 예제 2: edge_ngram 자동완성 확인
  • 예제 3: 사용자 사전 적용 후 비교
  • 예제 4: 비대칭 analyzer로 자동완성 끝까지 검증
  • 08흔한 오해와 실패 패턴
  • 09설계 관점에서의 trade-off
  • 10면접 답변 프레임
  • 11관련 예상 질문
  • 12체크리스트
tags
#study

이런 글도

  • db

    InnoDB 실행 계획이 바꾸는 잠금 순서: 같은 행을 고치는 두 UPDATE 가 가끔만 교착하는 이유

    2026.11.03
  • db

    gaps and islands: SQL 창 함수로 이어진 구간 찾기

    2026.10.30
  • db

    PostgreSQL 쿼리 패턴과 실행 계획: LATERAL 최신 한 건, unnest와 Inner Unique, gaps and islands

    2026.10.30
  • db

    PostgreSQL 실행 계획 읽기: Index Cond와 Filter, heap 접근, generic plan과 TimescaleDB 청크 제외

    2026.10.30

댓글 (0)