콘텐츠 대표 이미지 - Elasticsearch로 구축하는 초고속 전문 검색(Full-Text Search) 시스템: 데이터의 바다에서 순식간에 바늘 찾기

Elasticsearch로 구축하는 초고속 전문 검색(Full-Text Search) 시스템: 데이터의 바다에서 순식간에 바늘 찾기

자, 개발자 친구들! 오늘은 우리가 매일 마주하지만, 막상 직접 구현하려고 하면 등골이 서늘해지는 주제를 들고 왔어. 바로 '검색(Search)'이야. 우리가 흔히 쓰는 RDBMS의 LIKE %keyword% 쿼리로 수백만 건의 데이터를 뒤지던 시절은 이제 졸업할 때가 됐지? 사용자는 기다려주지 않거든. 검색창에 엔터를 치는 순간, 0.1초 만에 정확한 결과가 튀어나와야 비로소 "아, 이 서비스 좀 쓸만하네"라고 생각한다고.

특히 '재능넷'처럼 수많은 재능 상품과 전문가들이 모여 있는 플랫폼이라면 더더욱 그래. 사용자가 "로고 디자인"을 검색했는데, 엉뚱하게 "디자인 로고스"라는 닉네임을 가진 사람이 나오거나, 결과가 나오는 데 3초나 걸린다면? 그 사용자는 바로 뒤로 가기를 누를 거야. 그래서 오늘은 이 검색의 끝판왕, Elasticsearch(엘라스틱서치)를 활용해서 어떻게 넷플릭스나 우버처럼 빠르고 똑똑한 검색 시스템을 만들 수 있는지, 아주 깊고 진하게 파헤쳐 볼 거야. 커피 한 잔 준비해. 긴 여정이 될 테니까!

1. 왜 RDBMS로는 안 되는 걸까? (검색의 본질)

우리가 흔히 쓰는 MySQL이나 PostgreSQL 같은 관계형 데이터베이스는 '정형 데이터'를 저장하고 관리하는 데는 천재적이야. 하지만 '전문 검색(Full-Text Search)'의 영역으로 넘어오면 이야기가 달라져.

상상해봐. 두꺼운 백과사전이 있어. RDBMS의 LIKE 검색은 마치 "첫 페이지부터 끝 페이지까지 모든 문장을 다 읽으면서 내가 원하는 단어가 있는지 찾는 것"과 같아. 데이터가 적을 땐 괜찮아. 하지만 데이터가 100만 건, 1000만 건이 넘어가면? 서버 CPU는 비명을 지르고 사용자는 로딩 스피너만 멍하니 바라보게 되지.

반면에 Elasticsearch는 '역색인(Inverted Index)'이라는 기가 막힌 구조를 사용해. 이건 책의 맨 뒤에 있는 '색인(Index)' 페이지를 보는 것과 같아. 단어를 미리 정렬해두고, 그 단어가 어느 페이지(Document)에 있는지 기록해두는 거지. 그래서 데이터가 아무리 많아져도 검색 속도는 거의 일정하게 유지돼. 이게 바로 우리가 Elasticsearch를 배워야 하는 이유야.

RDBMS vs Elasticsearch 검색 구조 비교 RDBMS (Table Scan) Row 1: 안녕하세요 재능넷입니다. Row 2: 검색 시스템 개발 강좌 Row 3: Elasticsearch 튜토리얼 전체 행을 하나씩 확인 (느림) Elasticsearch (Inverted Index) Keyword Doc IDs 재능넷 [1] 개발 [2] 검색 [2, 3] 키워드로 바로 위치 확인 (빠름)

2. Elasticsearch 아키텍처: 기초 체력 다지기

자, 이제 본격적으로 엔진 뚜껑을 열어보자. Elasticsearch를 단순히 "검색 잘 되는 DB" 정도로만 알면 나중에 큰코다쳐. 클러스터링 구조를 제대로 이해해야 스케일 아웃(Scale-out)도 하고 고가용성(HA)도 챙길 수 있거든.

(1) 클러스터(Cluster)와 노드(Node)

Elasticsearch는 태생부터 분산 시스템이야. 여러 대의 서버가 마치 하나의 거대한 시스템처럼 동작하도록 설계되었어. 여기서 전체 시스템을 '클러스터'라고 하고, 그 클러스터를 구성하는 각각의 서버(인스턴스)를 '노드'라고 불러.

재미있는 건, 노드마다 역할이 다를 수 있다는 거야.
- Master Node: 클러스터의 반장이야. 인덱스를 생성하거나 삭제하고, 노드들의 상태를 관리해.
- Data Node: 실제 데이터를 저장하고 CRUD 작업과 검색 연산을 수행하는 일꾼이야. CPU와 RAM, I/O가 가장 많이 필요한 녀석이지.
- Ingest Node: 데이터가 저장되기 전에 전처리(파싱, 변환 등)를 담당해. Logstash의 경량화 버전이라고 보면 돼.

(2) 인덱스(Index)와 샤드(Shard)

RDBMS의 Database가 Elasticsearch의 Index라고 생각하면 쉬워. (엄밀히 말하면 Table에 더 가깝지만, 개념적으로는 그래). 그런데 이 Index가 너무 커지면 어떻게 될까? 예를 들어, 1TB짜리 인덱스가 있다고 치자. 이걸 노드 하나에 다 넣으면 그 노드는 터져버릴 거야.

그래서 Elasticsearch는 인덱스를 '샤드(Shard)'라는 조각으로 쪼개서 여러 노드에 분산 저장해. 이게 진짜 핵심 기술이야!
"데이터를 쪼개서 저장하니까, 검색할 때도 여러 노드가 동시에 병렬로 찾아서 결과를 합쳐줘. 그래서 데이터가 많아져도 속도가 빠른 거야."

3. 한국어 형태소 분석의 세계: '아버지가방에들어가신다'를 피하는 법

영어는 띄어쓰기만으로도 단어 분리가 꽤 잘 돼. "Elasticsearch is cool" -> [Elasticsearch, is, cool]. 심플하지? 하지만 우리 한국어는 '교착어'라는 아주 복잡한 특성을 가지고 있어. 조사, 어미가 덕지덕지 붙어 있잖아.

예를 들어 "재능넷에서 디자인을 검색했다"라는 문장을 기본 분석기(Standard Analyzer)로 뜯으면 어떻게 될까?
-> [재능넷에서, 디자인을, 검색했다]
이렇게 잘려. 그럼 사용자가 "재능넷"이나 "디자인"으로 검색하면? 검색이 안 돼! 왜냐하면 인덱스에는 "재능넷에서"라고 저장되어 있으니까. 완전히 다른 단어로 인식하는 거지.

Nori(노리) 분석기의 등장

그래서 우리는 반드시 한국어 형태소 분석기를 써야 해. 가장 대중적이고 성능이 좋은 게 바로 'Nori(노리)'야. (Elasticsearch 6.x부터 공식 플러그인으로 들어왔어). Nori를 적용하면 위 문장은 이렇게 변해.
-> [재능, 넷, 디자인, 검색] (조사 '에서', '을', '했다' 등은 불용어로 제거되거나 분리됨)

이제 "디자인"만 쳐도 정확하게 해당 문서를 찾아낼 수 있게 되는 거야. 재능넷 같은 플랫폼을 개발한다면, 상품명이나 설명 필드에 반드시 이 Nori 분석기를 적용해야 사용자가 원하는 재능을 찰떡같이 찾을 수 있어.

4. 실전 코딩: 인덱스 설계부터 검색 쿼리까지

이론은 충분해. 이제 손가락을 움직일 시간이야. 실제 인덱스를 만들고 데이터를 넣고 검색하는 과정을 시뮬레이션해보자. 우리는 REST API를 통해서 Elasticsearch와 대화해. (Kibana의 Dev Tools를 쓴다고 가정할게).

(1) 인덱스 매핑(Mapping) 정의

스키마리스(Schemaless)라고 해서 그냥 막 넣으면 안 돼. 데이터 타입을 명확히 하고 분석기를 지정해줘야 해.

PUT /jaenung_items
{
  "settings": {
    "index": {
      "number_of_shards": 3,
      "number_of_replicas": 1
    },
    "analysis": {
      "tokenizer": {
        "nori_user_dict": {
          "type": "nori_tokenizer",
          "decompound_mode": "mixed",
          "user_dictionary_rules": ["재능넷"]
        }
      },
      "analyzer": {
        "my_korean_analyzer": {
          "type": "custom",
          "tokenizer": "nori_user_dict",
          "filter": ["lowercase", "nori_part_of_speech"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "my_korean_analyzer"
      },
      "description": {
        "type": "text",
        "analyzer": "my_korean_analyzer"
      },
      "price": {
        "type": "integer"
      },
      "category": {
        "type": "keyword"
      },
      "created_at": {
        "type": "date"
      }
    }
  }
}

* Tip: `category`는 `keyword` 타입으로 잡았어. 이건 형태소 분석을 하지 않고 정확히 일치하는 값만 찾거나 집계(Aggregation)를 할 때 사용해.

(2) Query DSL: 검색의 마법 주문

이제 데이터를 조회해볼까? Elasticsearch의 쿼리는 JSON 형태로 작성해. 이걸 Query DSL(Domain Specific Language)이라고 불러. 가장 많이 쓰는 건 bool 쿼리야. 여러 조건을 조합할 수 있거든.

사용자가 "디자인" 카테고리에서 "로고"라는 단어가 포함된 상품을 찾고 싶은데, 가격은 5만원 이하였으면 좋겠대. 이걸 쿼리로 짜보면:

GET /jaenung_items/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "로고" } }
      ],
      "filter": [
        { "term": { "category": "디자인" } },
        { "range": { "price": { "lte": 50000 } } }
      ]
    }
  }
}

여기서 중요한 포인트! `must`와 `filter`의 차이를 알아야 해.
- must: 검색어와의 '유사도(Score)'를 계산해. 더 정확한 결과가 상위에 노출돼.
- filter: 단순히 예/아니오만 판단해. 점수 계산을 안 하니까 속도가 훨씬 빠르고 캐싱도 돼. 가격 범위나 카테고리 필터링은 무조건 `filter` 절에 넣는 게 성능 최적화의 지름길이야.

분산 검색 실행 흐름 (Query Phase -> Fetch Phase) Client Coordinating Node 요청 분배 & 결과 병합 Shard A Shard B Shard C 1. 검색 요청 2. 각 샤드로 분산 3. Local Search 각 샤드에서 Top N 결과(ID+Score) 추출 4. Global Merge 모든 샤드의 결과를 모아 정렬 후 실제 문서 내용(Fetch) 가져옴

5. 검색 경험(UX)을 극대화하는 고급 기능들

단순히 결과를 보여주는 것만으로는 부족해. 사용자가 "오, 센스 있는데?"라고 느끼게 하려면 몇 가지 양념이 필요해.

(1) 자동완성 (Autocomplete)

사용자가 '디'만 쳐도 '디자인', '디지털 마케팅'이 주르륵 나와야지. 이걸 구현하는 방법은 여러 가지가 있는데, completion suggester나 edge_ngram 토크나이저를 사용해. 특히 edge_ngram은 단어를 앞에서부터 잘라서 인덱싱하는 방식이야. (디, 디자, 디자인...). 이렇게 해두면 앞글자만 쳐도 매칭이 아주 잘 돼.

(2) 오타 교정 (Fuzzy Search)

급하게 치다 보면 "디자인"을 "디지안"이라고 칠 수도 있잖아. RDBMS는 "결과 없음"을 뱉겠지만, Elasticsearch는 Fuzziness(유사도) 옵션을 통해 철자가 한두 개 틀려도 찰떡같이 알아듣고 결과를 찾아줘. Levenshtein 거리 알고리즘을 사용해서 편집 거리가 가까운 단어를 찾는 원리야.

(3) 하이라이팅 (Highlighting)

구글 검색해보면 내가 검색한 단어가 본문에서 볼드체로 표시되잖아? 그게 바로 하이라이팅이야. Elasticsearch에 요청할 때 `highlight` 필드를 추가하면, 응답 결과에 `검색어` 태그가 감싸진 채로 돌아와. 프론트엔드에서는 이걸 그냥 뿌려주기만 하면 끝! 사용자의 시선을 확 잡아끌 수 있는 필수 기능이지.

6. 재능넷 개발자를 위한 실전 팁 & 성능 튜닝

마지막으로, 실제 운영 환경에서 피와 땀으로 얻은 팁들을 방출할게. 이 부분은 진짜 메모해둬야 해.

  • 1. Refresh Interval 조정하기
    기본적으로 ES는 1초마다 인덱싱된 데이터를 검색 가능하게 만들어(Refresh). 하지만 대량의 데이터를 한꺼번에 넣을 때(Bulk Insert)는 이 작업이 엄청난 부하가 돼. 대량 등록 중에는 refresh_interval을 -1이나 30s 정도로 늘려줘. 속도가 몇 배는 빨라질 거야.
  • 2. 힙 메모리(Heap Memory) 설정
    Elasticsearch는 자바 기반이야. 힙 메모리는 보통 물리 메모리의 50%를 할당하는 게 국룰이야. 하지만 절대 32GB를 넘기지는 마. 32GB를 넘어가면 자바의 Compressed OOPs(객체 포인터 압축) 기능이 꺼져서 오히려 성능이 떨어져.
  • 3. 인덱스 수명 주기 관리 (ILM)
    로그 데이터 같은 시계열 데이터는 시간이 지나면 잘 안 보잖아? Hot-Warm-Cold 아키텍처를 구성해서, 오래된 데이터는 저렴한 장비(Cold Node)로 옮기는 정책을 세워야 비용을 아낄 수 있어.

마치며...
Elasticsearch는 단순한 검색 엔진을 넘어, 로그 분석, 보안 모니터링, 그리고 최근에는 벡터 검색(Vector Search)을 통한 AI 추천 시스템까지 영역을 확장하고 있어. 오늘 우리가 다룬 내용은 빙산의 일각일지도 몰라. 하지만 이 기초가 튼튼하다면, 재능넷 같은 플랫폼에서 수천만 건의 재능 속에서도 사용자가 원하는 그 '보석'을 0.1초 만에 찾아주는 마법 같은 경험을 선사할 수 있을 거야. 자, 이제 터미널을 열고 도커 이미지를 당겨올 시간이야! Happy Coding!

Writing Complete.
댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.