pile·
백엔드·네이버페이네이버페이·

OpenSearch 검색엔진으로 네이버페이 통합 검색 만들기

네이버페이 유저인프라 백엔드 팀이 사내 서비스·이벤트·기능을 찾아주는 통합 검색을 OpenSearch로 구축한 과정을 정리한다. 하드코딩돼 있던 서비스 정보를 DB·어드민으로 정비하고, 한글 자모 분리와 형태소 분석을 위한 커스텀 플러그인을 직접 개발했다. 색인 교체와 플러그인 패치를 무중단으로 처리하는 운영 설계까지 다룬다.

핵심 포인트
  • 원본 DB는 그대로 두고 OpenSearch에 검색 전용 역색인을 별도로 두어, 로직 변경·롤백 시 원본을 건드리지 않게 설계했다.
  • 자동완성처럼 미완성 입력에 반응하려고 한글을 초성·중성·종성으로 쪼개는 자모 분리 토큰 필터를 Kotlin 플러그인으로 구현했다.
  • 사내 형태소 분석기 linguist2로 조사·접속사를 걸러 정제된 토큰을 만들고, 형태소→ngram→자모→edge ngram 순으로 분석한다.
  • index alias의 atomic 스위칭으로 다운타임 없이 색인을 교체한다.
  • 커스텀 플러그인은 코드 변경 시 클러스터 재시작이 필요해, 노드 단위 순차 패치로 무중단 운영을 설계했다.
상세 정리
  • 배경: 네이버페이엔 수백 개 서비스가 있어 일일이 찾아 쓰는 UX가 나빴다. 서비스·이벤트·기능을 손쉽게 찾는 검색이 목표였다.
  • 데이터 정비: FE에 하드코딩돼 있던 서비스 정보를 DB화하고, 관리자가 등록·수정하는 어드민을 개발했다. 아이콘·뱃지·노출 우선순위도 함께 관리한다.
  • 검색엔진 선택: 사내 클라우드에서 온디맨드로 제공되는 OpenSearch를 사용했다. Elasticsearch 기반 분산 검색엔진으로 역색인 구조라 대용량에도 빠르다.
  • 색인 운영: 하루 한 번 자동 색인에 어드민의 수동 즉시 색인을 더했고, 잦은 재색인을 위해 index alias를 두어 사용자 요청은 alias만 바라보게 했다.
  • 자모 분리 원리: 한글은 유니코드 구조가 규칙적이라 '가' 시작점을 기준으로 초성·중성·종성을 나눌 수 있다. "한글"은 "ㅎㅏㄴㄱㅡㄹ"로 분해되며, Kotlin TokenFilter가 나눗셈·나머지로 계산한다.
  • 토큰 분석 흐름: "해외 QR결제 안내"는 형태소 분석(해외/QR/결제/안내) → ngram·소문자화 → 자모 분리 → edge ngram을 거친다.
  • 검색 매칭: "해외결제" 검색은 ㅎㅐㅇㅚ·ㄱㅕㄹㅈㅔ로 분석돼 AND 검색으로 해당 문서를 찾는다. 텍스트 일치뿐 아니라 태그·URL·로그인 여부·이벤트 종료 여부를 조합하고 boosting으로 우선순위를 조절한다.
  • 무중단 패치 설계: 패치 대상 노드의 샤드를 다른 노드로 리로케이션해 green을 확인한 뒤, 프로세스 중지·플러그인 재설치·재기동을 거쳐 샤드를 다시 재배치하고 green을 확인한다. 이 과정을 모든 노드에 반복한다.
  • 검증: 개발 클러스터에서 실시간 요청·순차 패치·샤드 재배치 지연·노드 join 실패 시나리오를 시뮬레이션해 눈에 띄는 순단이 없음을 확인했고, 현재 내부 스크립트로 자동화 중이다.
  • 최근 검색어: 사용자별 최대 10개를 MongoDB 단일 문서 atomic 연산으로 저장한다. concat·sort·slice를 조합한 aggregation update를 한 번의 findAndModify로 처리한다.
  • 장애 사례: 오픈 직후 $로 시작하는 검색어가 MongoDB 쿼리 생성 시 path 표현식으로 오인돼 null이 들어갔다. 해당 키를 literal로 인식하도록 보완해 해결했다.
왜 읽나OpenSearch로 한글 검색을 만들며 자모 분리 플러그인과 무중단 클러스터 패치를 고민하는 검색·백엔드 개발자에게 실전 레퍼런스다.
네이버페이
네이버페이 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2