검색 기능 하나 만드는 데 인덱스를 5번 갈아엎은 이야기

2026. 8. 16. 02:26·강의&프로젝트

검색기능 구축: 함정을 피하는 4단계

"그냥 LIKE 쓰면 되는 거 아냐?"

데이터베이스에서 검색하는 방법은 다들 압니다.

 
sql
SELECT * FROM reports WHERE title LIKE '%저출산%';

문제는 이게 딱 저 글자만 찾는다는 겁니다.

  • "저출생 대응 방안" → 못 찾음
  • "출산율 하락 분석" → 못 찾음
  • 4,684건 정도면 괜찮지만, 수백만 건이면 느려짐
  • 어떤 결과가 더 관련 있는지 순서를 못 매김

우리가 네이버나 구글에서 기대하는 검색은 이런 게 아니죠. Elasticsearch(이하 ES) 는 바로 이 간극을 메우는 도구입니다.

이 글은 국회 발간물 4,684건을 수집해 검색 API를 만든 과정입니다. 코드보다는 "왜 이렇게 해야 하는가" 와 "어디서 넘어지는가" 에 초점을 맞췄습니다.


전체 그림: 4단계

 
[1. 크롤링] → [2. MySQL] → [3. ES 색인] → [4. 검색 API]
   데이터 수집    원본 보관     검색용 가공     FastAPI

여기서 입문자들이 가장 많이 하는 질문이 있습니다.

"ES에도 데이터를 넣는데, 왜 MySQL에 또 저장해요? 중복 아닌가요?"

역할이 완전히 다르기 때문입니다.

MySQLElasticsearch
역할 원본 보관소 검색 전용 사본
성격 절대 잃으면 안 됨 언제든 지우고 다시 만들어도 됨

이게 왜 중요하냐면 — 저는 이 프로젝트에서 ES 인덱스를 다섯 번 만들었다 지웠습니다. 설정을 바꿀 때마다요.

MySQL에 원본이 있었기 때문에 python bulk_index.py 한 줄이면 복구됐습니다. 만약 ES에만 데이터가 있었다면? 매번 크롤링을 다시 돌려야 했겠죠. 몇 시간짜리 작업입니다.

💡 원칙: 실험할 수 있는 구조를 먼저 만들어라. 되돌릴 수 있으면 겁 없이 시도하게 됩니다.


1단계: 데이터 모으기

첫 번째 함정 — 브라우저부터 켜지 마세요

처음엔 Selenium이나 Playwright(브라우저를 자동으로 조작하는 도구)를 쓰려고 했습니다. 4,684건이면 2~4시간짜리 작업입니다.

그런데 코드를 짜기 전에 사이트를 먼저 뜯어봤더니:

 
./list.do?page=2&categoryId=&pageSize=100

주소창에 페이지 번호가 그대로 있었습니다. 브라우저 없이 그냥 요청만 보내면 되는 구조였죠. 게다가 pageSize를 100으로 올릴 수 있어서 469페이지가 47페이지로 줄었습니다.

결과: 2~4시간 → 10분.

💡 교훈: 추측하지 말고 관찰하라. 크롬 개발자도구(F12) → Network 탭을 열고 페이지를 넘겨보세요. 10분 투자로 몇 시간을 아낍니다.

두 번째 함정 — 수집과 파싱을 분리하세요

초보자가 흔히 하는 실수가 이겁니다.

 
python
for page in range(1, 470):
    html = 요청(page)
    데이터 = 파싱(html)      # ← 여기가 문제
    DB에_저장(데이터)

400페이지쯤 긁었는데 파싱 규칙 하나가 틀렸다는 걸 발견하면? 처음부터 다시 긁어야 합니다.

이렇게 나누세요.

 
python
# 1단계: 원본 HTML을 그냥 파일로 저장
for page in range(1, 48):
    html = 요청(page)
    파일로_저장(f"raw/page_{page}.html")

# 2단계: 저장된 파일에서 파싱 (인터넷 불필요, 몇 번이든 재시도)
for 파일 in raw폴더:
    데이터 = 파싱(파일 읽기)

HTML 47개는 다 합쳐도 몇십 MB입니다. 저장 비용은 사실상 0인데, 파서를 20번 고쳐도 재수집이 필요 없습니다.

세 번째 함정 — "고유 번호"라고 다 같은 게 아닙니다

목록에 이런 값들이 있었습니다.

값정체
4684 화면 표시용 번호 — 새 글 올라오면 전부 밀림 ❌
1P-iU0ZDsFz 문서 ID — PDF 없는 글엔 아예 없음 ❌
49518 게시물 번호 — 모든 글에 존재 ✅

1P-iU0ZDsFz를 키로 쓸 뻔했는데, 검증하다 보니 PDF가 없는 문서 3건에는 그 값이 아예 없더군요. 그대로 진행했으면 3건이 조용히 사라졌을 겁니다.

💡 키를 고를 때 물어볼 것: "모든 데이터에 이 값이 반드시 있는가?"


2단계: 검증이 8할입니다

수집이 끝나면 반드시 숫자를 맞춰봐야 합니다.

 
전체 목록:        4,684건
주제별 합계:      4,375건
주제 없는 문서:     309건
─────────────────────────
4,375 + 309 = 4,684  ✅

이게 안 맞으면 어딘가 빠졌거나 중복된 겁니다. 실제로 저는 이 검증에서 카테고리 하나가 통째로 누락된 것을 발견했습니다.

이런 것들을 습관적으로 확인하세요.

  • 총 건수가 사이트 표기와 같은가?
  • 중복은 없는가?
  • 필수 항목(제목, 날짜)이 비어 있는 게 몇 건인가?

수집 직후 30분 투자하면, 나중에 며칠을 아낍니다.


3단계: 한국어 검색의 진짜 장벽

여기가 이 글의 핵심입니다. 영어 튜토리얼만 보고 따라 하면 반드시 막히는 지점이거든요.

형태소 분석기가 필요한 이유

영어는 띄어쓰기만 해도 단어가 나뉩니다. "search engine" → search, engine. 끝.

한국어는 다릅니다.

 
"저출산 대응을 위한 정책"
→ 저출산 / 대응 / 을 / 위한 / 정책

을, 위한 같은 건 검색에 방해만 됩니다. 그래서 ES에 nori라는 한국어 분석기를 붙입니다. 조사와 어미를 떼어내고 의미 있는 단어만 남기는 도구예요.

함정 ① — 동의어가 조용히 사라졌습니다

"저출산"으로 검색해도 "저출생"이나 "출산율" 문서가 나오게 하려고 동의어를 등록했습니다.

 
저출산, 저출생, 출산율

그런데 결과가 이상했습니다.

 
"저출산" 검색 → 저출산 ✅, 저출생 ✅, 출산율 ❌

셋 중 하나만 빠졌습니다. 에러도 없이요.

원인은 이랬습니다. nori가 출산율을 이렇게 쪼갠 겁니다.

 
출산율 → "출산" + "율"

율은 접미사라 필터에 걸려 사라지고, 단어 하나가 조각나면서 동의어 규칙이 깨졌습니다. 반면 저출생은 제가 사전에 등록해둔 단어라 안 쪼개졌고, 그래서 살아남았던 거죠.

해결은 간단했습니다. 사전에 출산율을 추가하는 것.

💡 한국어 ES의 철칙: 동의어에 쓸 단어는 사전에 먼저 등록하라. 단어가 쪼개지면 동의어는 작동하지 않습니다.

더 무서운 건 이 실패가 에러를 안 낸다는 점입니다. 설정은 성공하고 결과만 이상하죠. 그래서 검색 서비스는 "일단 돌아가니까 됐다"가 통하지 않습니다. 직접 검색해보고 확인해야 합니다.

함정 ② — 자동완성에 형태소 분석기를 쓰면 안 됩니다

검색창에 "저출"까지 쳤을 때 추천을 띄우려고 했더니, 이런 결과가 나왔습니다.

 
"저출" → 저, 제, 출, 추, ᆯ

당황스럽죠. 원인은 이겁니다. 저출은 한국어 단어가 아닙니다. 타이핑 중인 조각일 뿐이죠. nori는 이걸 어떻게든 형태소로 해석하려다 이상한 결과를 냈습니다.

역할을 나눠서 해결했습니다.

용도방식
의미 검색 nori (형태소 분석 O)
자동완성 글자 그대로 자르기 (형태소 분석 X)

💡 자동완성은 "사용자가 보는 글자 그대로" 매칭해야 합니다.

함정 ③ — 사람 이름에 형태소 분석기 금지

유지영이라는 저자를 nori에 넣으면 유지 + 영이 됩니다. 그러면 "유지보수" 관련 문서가 저자 검색에 딸려 나오죠.

인명, 회사명, 고유명사는 쪼개지 말고 통째로 저장해야 합니다.


4단계: 검색 API

Python의 FastAPI로 만들었습니다. 핵심 기능은 넷.

 
GET /api/search      검색 + 필터 + 하이라이트
GET /api/suggest     자동완성
GET /api/facets      분류별 개수 (좌측 필터 메뉴용)
GET /api/explain     이 문서가 왜 상위에 떴는지

쿼리에서 꼭 알아야 할 구분

 
json
{
  "must":   [ "저출산" ],           // 점수 계산 O
  "filter": [ "2024년 이후" ]       // 점수 계산 X, 캐시 O
}

"저출산과 얼마나 관련 있는가"는 점수를 매겨야 합니다. 하지만 "2024년 이후인가"는 예/아니오일 뿐이죠. 점수와 무관합니다.

filter에 넣으면 계산을 건너뛰고 결과를 재활용해서 훨씬 빠릅니다. 이 구분만 알아도 검색 성능이 달라집니다.


인덱스를 5번 갈아엎어도 서비스가 안 멈춘 비결

설정을 바꿀 때마다 인덱스를 새로 만들어야 했습니다. 그런데 만드는 동안 검색이 멈추면 안 되겠죠.

별명(alias) 을 씁니다.

 
앱은 "reports_search"만 호출
        ↓
    [별명] → reports_v3  (실제 인덱스)

앱은 별명만 압니다. 뒤에 뭐가 연결됐는지는 모르죠.

작업 순서는 이렇습니다.

 
1. reports_v4 새로 만들기        ← v3는 계속 서비스 중
2. v4에 데이터 넣기              ← v3는 계속 서비스 중
3. v4가 잘 되는지 확인
4. 별명을 v4로 옮기기            ← 여기서 순간 전환
5. v3 삭제

4번은 한 번의 요청으로 원자적으로 처리됩니다. "둘 다 없는" 순간이 존재하지 않아서 검색이 한 건도 실패하지 않아요. 코드 수정도, 서버 재시작도 없습니다.

문제가 생기면? 별명을 되돌리면 1초 만에 롤백됩니다.

💡 별명의 진짜 가치는 "되돌릴 수 있다"는 것입니다. 롤백이 쉬우면 과감하게 실험할 수 있습니다. 이건 ES뿐 아니라 배포 전략 전반에 통하는 원리예요.


정리하며

작은 프로젝트였지만 얻은 게 많았습니다.

설계

  • 원본과 사본을 분리하면 실험이 자유로워진다
  • 키는 "모든 데이터에 반드시 있는" 값으로
  • 되돌릴 수 있는 구조를 먼저 만들어라

한국어 검색

  • 형태소 분석기는 만능이 아니다 — 인명, 미완성 입력에는 독이 된다
  • 동의어보다 사전이 먼저다
  • 조용한 실패를 의심하라. 직접 검색해봐야 안다

태도

  • 추측하지 말고 관찰하라 (개발자도구 10분 = 몇 시간)
  • 수집과 파싱을 분리하라
  • 숫자로 검증하라

가장 크게 남은 건 이겁니다. 검색 서비스는 "돌아간다"와 "제대로 된다" 사이가 아주 멀다는 것. 에러 없이 잘못된 결과를 내놓는 경우가 너무 많아서, 직접 검색어를 넣어보고 결과를 눈으로 확인하는 과정을 건너뛸 수 없습니다.

혹시 검색 기능을 만들어보려는 분이라면, 작은 데이터로 먼저 끝까지 한 바퀴 돌려보시길 권합니다. 1,000건이든 100건이든요. 전체 흐름을 한 번 겪고 나면 그다음부터는 데이터가 늘어나도 같은 구조로 확장됩니다.

'강의&프로젝트' 카테고리의 다른 글

따라가는 공부는 남지 않는다 — 실무 기획자가 AI를 통해 성장하는 방식  (3) 2026.08.05
하네스는 언젠가 짐이 된다 — 그런데 A4 한 장은 끝까지 남는다  (0) 2026.08.04
하이브리드 RAG 검색 시스템, 설계하면서 부딪힌 4가지 갈림길  (1) 2026.07.13
[데이터 기획 실무] RDB의 한계를 넘는 온톨로지(Ontology) 활용법과 아키텍처 설계  (0) 2026.05.01
DB는 알지만 온톨로지는 이해하는 것: 왜 우리에게 '시맨틱 레이어'가 필요할까? 평범한 직장인 검색전문가 되기(1탄)  (0) 2026.04.29
'강의&프로젝트' 카테고리의 다른 글
  • 따라가는 공부는 남지 않는다 — 실무 기획자가 AI를 통해 성장하는 방식
  • 하네스는 언젠가 짐이 된다 — 그런데 A4 한 장은 끝까지 남는다
  • 하이브리드 RAG 검색 시스템, 설계하면서 부딪힌 4가지 갈림길
  • [데이터 기획 실무] RDB의 한계를 넘는 온톨로지(Ontology) 활용법과 아키텍처 설계
AI강선생
AI강선생
AI강선생의 블로그 입니다.
  • AI강선생
    나의 배움과 성장의 궤적
    AI강선생
  • 전체
    오늘
    어제
    • 분류 전체보기 (28) N
      • 강의&프로젝트 (10) N
      • 독서&지식 (9)
      • 생각과다짐 (9)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 개인정보처리방침
  • 링크

  • 공지사항

    • 소개 (About)
    • 개인정보처리방침
  • 인기 글

  • 태그

    지식그래프
    온톨로지
    길벗
    AI에이전트
    claude code
    rustfs
    국회
    시멘틱검색
    PostgreSQL
    한빛미디어
    인프런
    유리링
    벡엔드개발
    이지스퍼블리싱
    cursor
    클로드코드
    FastAPI
    혼공바이브코딩
    에이전트
    Rag
    AI agent
    java
    챌린지
    docker
    spring
    하네스엔지니어링
    게임기획
    Claude
    오레일리
    랭체인
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
AI강선생
검색 기능 하나 만드는 데 인덱스를 5번 갈아엎은 이야기
상단으로

티스토리툴바