LabHub

블로그

Elasticsearch와 OpenSearch, Lucene의 내부 — Inverted Index, BM25, Sharding, Vector Search, Hybrid RAG까지 (2025)

한국어English日本語

"Search is not a feature. It's a philosophy of how humans interact with information." — Doug Cutting (creator of Lucene, 1999)

Google이 없던 시절을 기억하는가? 1999년 Doug Cutting이 자바로 Lucene을 만들었을 때, 그는 "누구나 자기 데이터에 Google급 검색을 달 수 있어야 한다"고 믿었다. 26년이 지난 지금, Elasticsearch, OpenSearch, Solr는 모두 Lucene 위에 서 있다. 로그 분석, 상품 검색, 자동완성, 그리고 2024년부터는 RAG(Retrieval-Augmented Generation)의 핵심 인프라까지.

그러나 "Elasticsearch 쓴다"와 "Lucene을 이해한다"는 하늘과 땅 차이다. 이 글은 검색의 본질부터 2025년 하이브리드 검색까지, 한 번에 꿰뚫는 지도다.


1. 왜 관계형 DB의 LIKE '%keyword%'는 안 되는가

선형 스캔의 벽

SELECT * FROM articles WHERE content LIKE '%postgresql%';

Postgres의 GIN도 일부 해결하지만:

검색 전용 엔진이 필요한 이유다.


2. Inverted Index — 검색의 수학적 심장

기본 아이디어

Document 1: "The quick brown fox"
Document 2: "The lazy brown dog"
Document 3: "Foxes and dogs"

이를 단어 → 문서 리스트로 뒤집으면:

brown  → [1, 2]
dog    → [2]
dogs   → [3]
fox    → [1]
foxes  → [3]
lazy   → [2]
quick  → [1]
the    → [1, 2]

질의 "brown dog"는:

이것이 Inverted Index(역인덱스)의 본질이다. 수십억 문서에서도 O(logN)O(\log N)에 가까운 속도.

Lucene의 구현 — 왼쪽에서 오른쪽으로

Lucene이 디스크에 저장하는 단위:

  1. Term Dictionary — 모든 term의 정렬된 사전 (FST, Finite State Transducer)
  2. Postings List — 각 term이 나타난 문서 리스트 + 빈도/위치
  3. Stored Fields — 원본 문서 (압축)
  4. Doc Values — 컬럼나(columnar) 저장, 집계/정렬용
  5. Norms — 필드 길이 정규화 값

FST — 접두사 공유로 메모리 절약

"fox, foxes, foxy" 같은 term들의 공통 접두사를 유한 상태 전이기로 압축. 수천만 term의 사전을 수 MB로 저장. 자동완성의 핵심 구조이기도 하다.


3. Segment — Lucene의 불변성 원칙

불변(Immutable) Segment

Lucene에서 한 번 쓴 파일은 변경되지 않는다. 이것이 Lucene을 빠르고 안전하게 만든다.

Segment 구조

한 segment는 여러 파일로 구성:

_0.cfs     — composite file (여러 파일을 하나로)
_0.cfe     — 진입점
_0.si      — segment info
_0.fdt/fdx — field data
_0.tim/tip — term dictionary
_0.doc/pos — postings
_0.dvm/dvd — doc values
_0.liv     — live docs (삭제 비트맵)

Merge 정책

Refresh / Flush / Commit의 차이

용어시점
Refresh메모리 buffer를 검색 가능한 segment로 만듦기본 1초마다
Flushsegment를 디스크로 fsync자동 (메모리 임계치)
Commit트랜잭션 로그 포함 완전 영속덜 자주

"Near Real-Time Search"의 비밀: refresh는 1초, flush는 나중에. 그래서 "1초 지연"이 Elasticsearch의 트레이드마크.

Translog — 내구성을 지키는 로그


4. BM25 — TF-IDF를 대체한 점수

TF-IDF의 한계

tf-idf(t,d)=tf(t,d)×logNdf(t)\text{tf-idf}(t, d) = \text{tf}(t, d) \times \log\frac{N}{\text{df}(t)}

BM25 공식

score(d,q)=tqIDF(t)f(t,d)(k1+1)f(t,d)+k1(1b+bdavgdl)\text{score}(d, q) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{f(t, d) \cdot (k_1 + 1)}{f(t, d) + k_1 \cdot (1 - b + b \cdot \frac{|d|}{\text{avgdl}})}

핵심 변경:

BM25가 기본값인 이유

파라미터 튜닝

제품명/쿼리 로그 분석은 b=0.3 정도로 낮추는 게 흔한 패턴.


5. 분석기 (Analyzer) — 토큰화의 예술

3단계 파이프라인

  1. Character Filter — HTML 제거, 문자 교체
  2. Tokenizer — 문장을 단어로 자름
  3. Token Filter — 소문자화, 어간 추출, 동의어, 불용어

한국어의 지옥

영어: 공백 기준 토큰화 쉬움.

한국어: 교착어 + 어미 변화 + 조사.

예: nori 분석

POST _analyze
{
  "analyzer": "nori",
  "text": "Elasticsearch는 검색엔진입니다"
}

-> "elasticsearch", "는", "검색", "엔진", "입니다"

, 입니다 같은 조사/어미 제거:

"filter": ["nori_part_of_speech"]

동의어 확장

"shoe, sneaker, runner" 
-> 질의 "shoe" 시 자동으로 sneaker/runner 문서도 매칭

검색 품질의 절반은 동의어 사전에 달려 있다. 구축은 지루하지만 투자 대비 효과 최고.


6. Shard & Replica — 분산의 기본

Primary Shard

Replica Shard

한계와 원칙

Cluster State & Split Brain


7. Query DSL — JSON의 미로

주요 쿼리 타입

쿼리용도
match분석기 거친 풀텍스트 매칭
term분석기 거치지 않는 정확 일치 (keyword 필드)
match_phrase순서 보존 구문
multi_match여러 필드 동시 검색
boolAND/OR/NOT 조합 (핵심)
range범위
function_score커스텀 점수
rank_feature부스팅 필드

bool 쿼리의 4절(clause)

{
  "bool": {
    "must": [...],      // AND, 점수 기여
    "should": [...],    // OR, 점수 기여
    "filter": [...],    // AND, 점수 기여 안함, 캐시됨
    "must_not": [...]   // NOT
  }
}

filter 절을 최대한 활용하라 — 캐시되고 빠르다. match로 점수를 매기고, 고정 조건은 filter로.

Term vs Match — 가장 흔한 실수

// text 필드 "User Name"이 저장되어 있음
{"term": {"name": "User Name"}}   // 매칭 안 됨! (소문자화되었음)
{"term": {"name": "user name"}}   // 이것도 안 됨 (공백 토큰화 됨)
{"match": {"name": "User Name"}}  // OK

text 필드에는 match, keyword 필드에는 term.

집계(Aggregation) — 분석 DB로서의 ES

{
  "aggs": {
    "by_category": {
      "terms": {"field": "category"},
      "aggs": {
        "avg_price": {"avg": {"field": "price"}}
      }
    }
  }
}

SQL의 GROUP BY에 해당. 로그 분석, 대시보드 구축에 필수.


8. Vector Search — 2022년 이후의 혁명

왜 벡터인가

ES/OpenSearch의 kNN

Elasticsearch 8.0(2022)부터 native kNN. Lucene 9.0의 HNSW 구현 활용.

// 인덱스 매핑
{
  "mappings": {
    "properties": {
      "title_vector": {
        "type": "dense_vector",
        "dims": 768,
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}

// 질의
{
  "knn": {
    "field": "title_vector",
    "query_vector": [0.1, 0.2, ...],
    "k": 10,
    "num_candidates": 100
  }
}

HNSW 파라미터

저장 효율 — Quantization


9. Hybrid Search — RAG 시대의 정답

측면BM25Vector
정확 일치 (상품 코드)강함약함
의미 유사성약함강함
희귀어 (전문용어)강함약함
오타약함중간
다국어약함강함

둘 다 필요하다.

RRF — Reciprocal Rank Fusion

두 랭킹의 결과를 순위 기반으로 결합:

RRF(d)=i1k+ranki(d)\text{RRF}(d) = \sum_i \frac{1}{k + \text{rank}_i(d)}

ES의 rank: rrf

{
  "retriever": {
    "rrf": {
      "retrievers": [
        {"standard": {"query": {"match": {"content": "검색어"}}}},
        {"knn": {"field": "vec", "query_vector": [...], "k": 50}}
      ],
      "rank_window_size": 50,
      "rank_constant": 60
    }
  }
}

Cross-Encoder Reranker


10. 2021년 라이선스 전쟁 — OpenSearch 탄생

배경

결과

2024-2025 상황

제품라이선스주도
Elasticsearch 8Elastic License 2 / SSPLElastic
Elasticsearch 8.14+AGPL 추가 (2024.8)Elastic (회복 시도)
OpenSearch 2.xApache 2.0AWS → Linux Foundation

선택 가이드


11. 운영의 지옥 — 흔한 장애 패턴

JVM Heap 관리

Circuit Breaker

circuit_breaking_exception: Data too large

Hot Shard

Shard 수 폭발

Snapshot & Restore


12. Ingest 파이프라인 — 데이터 투입의 예술

Logstash — 전통적 ETL

Beats — 경량 에이전트

Elastic Agent + Fleet (2021+)

OpenTelemetry 통합

Ingest Node Pipeline

ES 자체에 간단한 파이프라인 정의:

{
  "processors": [
    {"set": {"field": "indexed_at", "value": "{{_ingest.timestamp}}"}},
    {"grok": {"field": "message", "patterns": ["%{COMBINEDAPACHELOG}"]}}
  ]
}

13. ES|QL — SQL의 귀환 (2024)

Elastic이 오랜 숙원 SQL-like 질의 언어를 내놓았다.

FROM logs-*
| WHERE status >= 500
| STATS count = COUNT(*) BY host
| SORT count DESC
| LIMIT 10

OpenSearch도 2024년 PPL (Piped Processing Language)로 유사 기능 추가.


14. 검색 품질 평가

nDCG (Normalized Discounted Cumulative Gain)

Precision / Recall

클릭 로그로 학습 (Learning to Rank)

A/B Test


15. 안티패턴 TOP 10

  1. 기본 1-shard/1-replica로 수십 TB 인덱스 — shard 당 10-50GB 유지
  2. _id를 수동 지정해 해싱 불균형 — routing 편향
  3. 너무 많은 인덱스/shard — cluster state 폭발
  4. JVM heap 64GB 설정 — Compressed OOPs 한계 (32GB 이하)
  5. deep pagination (from=10000) — scroll/search_after 사용
  6. 분석된 text 필드에 term 쿼리 — keyword 사용 또는 match
  7. 매 질의마다 클러스터 헬스 체크 — 과부하
  8. 빈번한 대량 삭제 — segment 폭발, _forcemerge 필요
  9. 백업 없음 — snapshot 필수
  10. 벡터만 쓰고 BM25 버림 — Hybrid가 거의 항상 이김

16. Elasticsearch/OpenSearch 현명하게 쓰기 체크리스트


마치며 — Lucene 위에 선 거인들

Elasticsearch, OpenSearch, Solr — 이름은 다르지만 모두 Lucene이라는 거인의 어깨 위에 서 있다. 그리고 그 Lucene은 1999년, Doug Cutting이 "검색을 민주화하자"고 만든 단일 자바 라이브러리에서 시작했다.

2025년 검색은:

모든 곳에 있지만, 모두가 제대로 이해하는 것은 아니다. Inverted Index와 Segment, BM25와 벡터 검색, Shard와 Routing — 이 용어들이 "아하!" 하고 연결되는 순간, 당신은 검색을 사용하는 사람에서 설계하는 사람이 된다.


다음 글 예고 — 분산 시스템의 합의 — Paxos, Raft, ZAB 완전 분해

ES의 cluster state 관리, etcd, ZooKeeper, PostgreSQL Patroni HA — 모두 합의 알고리즘 위에 서 있다. 다음 글에서는:

분산 시스템의 진짜 심장을 여는 여정.


"A good search engine doesn't just find what you typed. It finds what you meant." — Peter Norvig (Google Research Director)

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다