키워드를 넘어, 의미를 찾는 시대

saewoohan·2025년 4월 27일
5

검색 시스템

목록 보기
1/1
post-thumbnail

검색은 단순히 텍스트를 찾는 기능이 아닙니다. 검색은 서비스가 가진 수많은 정보를 사용자가 가장 빠르고 편리하게 찾을 수 있도록 연결해주는 핵심 기능입니다.

최근의 검색 시스템은 텍스트를 찾는 것을 넘어, 사용자의 의도를 읽고, 의미를 파악해, 필요한 정보를 최적의 형태로 제공합니다. 또한, 대부분의 서비스에는 이런 편리한 사용자 경험을 위해 검색 기능이 필수적으로 들어가게 됩니다.

그렇다면, 실제 서비스를 구축할 때 검색 기능은 어떤 방식으로 구성할 수 있을까요? 대표적인 방식들을 소개하고 검색 시스템 아키텍처를 간단하게 살펴보겠습니다.


LIKE 기반 검색

처음 검색 기능을 만들 때는 대개 데이터베이스 하나에 의존합니다. 특별한 인프라도 필요 없습니다. 그냥 MySQL이나 PostgreSQL과 같은 DBMS 테이블에 데이터를 저장하고, 다음과 같이 쿼리하면 끝입니다.

SELECT * FROM posts WHERE title LIKE '%강아지%';

서버가 직접 DB에 접속해서 LIKE 쿼리를 던집니다. 추가 인덱스도, 별도의 검색 서버도 없습니다.

LIKE 기반 검색의 단점

하지만 이러한 아키텍처는 많은 분들이 이미 알고 있듯 여러가지 치명적인 단점을 안고 있습니다.

1. 인덱스 활용 불가

인덱스는 기본적으로 Leftmost Prefix Matching 방식을 사용합니다. 즉, 문자열의 가장 앞부분부터 차례로 비교하는 경우에만 인덱스를 사용할 수 있는 것입니다.

따라서 LIKE '강아지%' 처럼 앞에 %가 없고, ‘강아지’로 시작하는 패턴을 찾을 때는 인덱스를 활용할 수 있습니다. 반면에, LIKE '%강아지%' 처럼 앞쪽이 비어있거나, 중간에 키워드가 있는 경우 Leftmost 조건을 만족하지 않으므로, 결국 Full Table Scan이 발생하게 됩니다.

2. 정확도가 낮음

LIKE는 정확히 입력된 동일한 문자열만 찾습니다. 만약 오타가 있거나, 띄어쓰기 차이 하나로도 검색 자체가 안 됩니다. 예를 들어 LIKE '%강아지 산책%' 검색에서 “강아지산책”, “강아 지 산책”, “깡아지 산책”은 모두 검색이 불가능합니다.

3. 확장성 낮음

데이터가 늘어나면 LIKE 기반 검색은 Full Table Scan을 발생시키기 때문에 선형적으로 느려집니다. 파티셔닝, 샤딩 등의 기법을 사용하더라도 검색은 전체 파티션과 노드를 뒤져야하는 것은 동일합니다.

즉, 데이터가 늘어나면 어쩔 수 없이 LIKE 기반 검색은 한계점을 보이고 맙니다.


전문 검색

LIKE 검색이 한계에 도달했을 때 등장하는 솔루션이 바로 Elasticsearch과 같은 전문 검색 방식입니다.

Elasticsearch는 단순한 DB가 아니라, 텍스트 검색을 위해 특화된 검색 전용 서버입니다.

물론, Elasticsearch를 사용하지 않더라도 DBMS 자체에서 역인덱스를 활용한 전문 검색 인덱스(Full-Text Index) 를 지원하기는 합니다.

하지만 DBMS 기반 Full-Text 검색은 결국 하나의 테이블 범위에 국한되며, 데이터가 분산되었을 때 글로벌 검색과 글로벌 랭킹 정렬이 사실상 불가능하다는 치명적인 한계를 안고 갑니다.

반면, Elasticsearch는 애초에 분산 검색을 전제로 만들어졌기 때문에, 수백만, 수천만 건 이상의 데이터를 대상으로도 빠르고 안정적인 검색 경험을 제공합니다. 실제 많은 검색 시스템이 Elasticsearch로 이루어져 있으며 이를 통해 포스팅을 이어나가겠습니다.

역인덱스

전문 검색 엔진을 이야기할 때, 가장 중요한 구조를 하나만 꼽으라면 단연 역인덱스(Inverted Index) 입니다.

Elasticsearch가 빠르고, 대규모 데이터셋에서도 안정적으로 검색할 수 있는 이유에는 바로 이 역인덱스 구조가 존재합니다.

일반적인 데이터 저장 구조

우리가 흔히 알고 있는 RDBMS는 데이터를 다음과 같이 저장합니다.

문서 ID내용
1강아지와 산책을 즐기다
2고양이와 놀다

이 경우, 사용자가 “산책”이라는 단어를 검색하려면 앞서 본 'LIKE %산책%' 질의를 사용해야합니다. 인덱스를 사용하지 못하기 때문에 문서가 100개면 100개를, 100만 개면 100만 개를 읽어야 합니다. 데이터량이 많아질수록 검색 시간이 기하급수적으로 늘어나게 됩니다.

역인덱스 구조

반면, 검색 엔진은 데이터를 저장할 때 아예 다르게 접근합니다. 문서가 아니라, 단어 중심으로 인덱스를 만듭니다.예를 들어, 위 두 문서를 역인덱스 구조로 저장하면 이렇게 됩니다.

단어연결된 문서 ID
강아지1
산책1
고양이2
놀다2

즉, 문서 안에 어떤 단어가 있는지를 저장하는 것이 아니라, 단어를 키(key)로 삼고, 그 단어가 포함된 문서들의 목록을 저장하는 것입니다.

사용자가 “산책”을 검색하면, “산책”이라는 키워드로 바로 문서 1을 조회할 수 있습니다. 덕분에 전체 문서를 뒤질 필요 없이 빠르게 검색 결과를 반환할 수 있습니다.

역인덱스가 만들어지는 과정

그렇다면 “강아지와 산책을 즐기다” 같은 문장을 어떻게 단어 단위로 잘게 쪼개서 역인덱스를 만드는 걸까요?

이때 중요한 역할을 하는 것이 바로 분석기(Analyzer) 입니다. Elasticsearch는 문장을 저장할 때 자동으로 분석기를 적용해, 문장을 단어(토큰) 단위로 분리하고, 필요 없는 조사나 어미(예: “와”, “을”, “다”)를 제거하거나, 단어를 소문자화하는 등의 처리를 합니다.

입력 문장분석 결과
강아지와 산책을 즐기다강아지, 산책, 즐기

이러한 과정을 통해 문장 안에서 의미 있는 핵심 단어만 남게 되고, 이 핵심 단어들을 기반으로 역인덱스가 만들어집니다.

특히 한국어처럼 띄어쓰기만으로 단어를 명확히 분리하기 어려운 언어에서는, 형태소 분석기(Nori Analyzer)가 검색 품질에 큰 영향을 미칩니다.

예를 들어, “서비스를 제공합니다”라는 문장이 있을 때, 단순 띄어쓰기만 적용하면 “서비스를”이라는 덩어리가 남지만, 형태소 분석기를 적용하면 “서비스”라는 핵심 단어만 추출할 수 있습니다.

이러한 형태소 분석 덕분에, “서비스”를 검색했을 때도 정확하게 원하는 문서를 찾아낼 수 있게 됩니다.

색인과 검색 흐름

Elasticsearch는 RESTful API를 통해 데이터 색인과 검색 기능을 제공합니다.

하지만 단순히 데이터 저장과 검색만 하는 것이 아니라, 색인 과정에서 역인덱스를 생성하고, 검색 과정에서는 관련성 점수를 계산해 가장 적합한 결과를 반환하는 방식으로 동작합니다.

Elasticsearch full-text

색인 요청 흐름

색인은 새로운 문서를 Elasticsearch에 저장하는 과정입니다. 색인 요청은 보통 다음과 같은 흐름을 가집니다.

  1. Document 저장

    사용자가 JSON 형태의 문서를 Elasticsearch에 보냅니다.

    POST /posts-index/_doc
    {
      "title": "강아지와 산책을 즐기다",
      "content": "오늘은 강아지랑 한강 공원에 다녀왔다. 날이 풀려서 산책하기 좋은 날씨였다."
    }
  2. Analyzer 적용

    Elasticsearch는 문서의 각 텍스트 필드에 대해 Analyzer를 적용합니다.
    예를 들어 “강아지와 산책을 즐기다.”이라는 문장이 분석되면, “강아지”, “산책”, “즐기” 같은 핵심 단어들로 쪼개집니다.

  3. 역인덱스 생성

    분석된 단어를 키로 삼아, 해당 문서와 연결하는 역인덱스를 생성합니다.

  4. 샤드 저장

    생성된 문서는 지정된 Primary Shard에 저장되고, 동시에 Replica Shard에도 복제되어 고가용성을 보장합니다.

검색 요청 흐름

색인된 문서를 검색하는 과정은 다음과 같은 흐름을 가집니다.

  1. 쿼리 제출

    사용자가 검색 쿼리를 보냅니다.

    GET /posts-index/_search
    {
      "query": {
        "match": {
          "content": "강아지 산책"
        }
      }
    }
  2. 쿼리 파싱 및 분석

    Elasticsearch는 검색어를 Analyzer를 통해 다시 분석합니다.

  3. 역인덱스 조회

    분석된 단어를 기반으로, 해당 단어가 등장하는 문서 목록을 역인덱스에서 빠르게 조회합니다.

  4. 문서 스코어링

    조회된 문서들에 대해, 검색어와 문서 간의 관련성 점수 를 계산합니다.

  5. 결과 집계 및 정렬

    모든 샤드에서 결과를 수집한 뒤, 점수가 높은 순서대로 정렬하여 사용자에게 반환합니다.

랭킹 계산

Elasticsearch는 단순히 검색어가 들어간 문서를 나열하는 방식으로 결과를 보여주지 않고, 검색어와 문서 간의 관련성을 계산해서 점수를 매기고, 이 점수가 높은 문서부터 순서대로 정렬해 사용자에게 보여줍니다.

간단하게 어떠한 방식으로 점수를 계산하는지 알아보도록 하겠습니다.

BM25

현재 Elasticsearch가 기본적으로 사용하는 점수 계산 방식이며, BM25는 문서 안에서 검색어가 얼마나 자주 등장했는지, 그리고 그 검색어가 전체 문서 중 얼마나 희귀한지를 함께 고려해서 점수를 매기는 방식입니다.

자주 등장하는 단어를 가진 문서는 점수가 올라가고, 전체 문서에서 드물게 등장하는 단어일수록 가치를 더 높게 평가합니다. 여기에 문서 길이에 대한 보정도 들어가는데, 지나치게 긴 문서는 단순히 검색어가 우연히 포함될 가능성이 높기 때문에 상대적으로 점수를 깎습니다.

결과적으로 짧고 핵심이 잘 담긴 문서가 더 높은 점수를 받을 수 있도록 설계되어 있습니다.

TF-IDF

TF-IDF 역시 단어 빈도와 희귀도를 기반으로 점수를 계산하지만, 문서 길이에 대한 보정이 적용되지 않는다는 차이가 있습니다.

그래서 긴 문서가 검색어를 몇 번 포함하기만 해도 높은 점수를 받는 경우가 있었고, 이런 한계를 보완하기 위해 BM25가 등장하였습니다.

Function Score Query

기본 점수에 추가적인 가중치를 적용하는 기능도 지원합니다. Function Score Query를 이용하면, 단순한 키워드 일치 외에도 다양한 기준을 점수에 반영할 수 있습니다.

예를 들어, 사용자가 최근에 작성된 문서를 더 선호한다면 작성일자에 따라 가산점을 줄 수 있고, 좋아요 수가 많은 게시글을 더 먼저 보여주고 싶다면 좋아요 수를 기반으로 점수를 조정할 수도 있습니다.

이렇게 기본적인 검색어 일치 외에도 사용자의 니즈나 서비스의 특성을 반영한 점수 조정을 통해, 쿼리 튜닝이 가능합니다.


의미 기반 검색 (벡터 검색)

전문 검색이 키워드 중심으로 문서와 쿼리를 매칭하는 방식이라면, 의미 기반 검색은 문장 전체의 의미를 이해하고 유사한 내용을 찾는 방식입니다.

최근 사용자 쿼리는 점점 자연어에 가까워지고 있습니다. “회사 그만두면 받을 수 있는 돈은?” 같은 문장을 입력했을 때, 단어 몇 개 일치하는 것만으로는 정확한 결과를 찾기 어렵습니다.

이러한 의미 기반 검색을 가능하게 하는 것이 바로 벡터 검색 기술입니다. 의미 기반 검색은 벡터 검색을 통해, 키워드 매칭이 아니라 의미의 유사성을 기준으로 문서를 검색합니다.

임베딩과 벡터 변환

의미 기반 검색을 위해서는, 문장이나 문서를 단순한 텍스트 형태로 저장하는 것이 아니라, 숫자로 이루어진 벡터로 변환하는 과정이 필요합니다.

이 과정을 임베딩(Embedding) 이라고 합니다. 임베딩 과정에서는 보통 OpenAI의 Embedding 모델, HuggingFace의 SentenceTransformer 같은 사전학습된 모델을 사용합니다.

문서 한 편을 입력하면, 모델은 해당 문장의 의미를 압축한 고차원 벡터(768차원, 1536차원)를 반환합니다.

예를 들어, “강아지와 산책하는 방법”이라는 문장을 임베딩하면, 결과는 다음과 같은 벡터가 됩니다.

[0.12, -0.34, 0.08, ..., 0.56, -0.21]

벡터는 일련의 숫자 배열로, 이 숫자들은 단순히 텍스트의 단어를 나타내는 것이 아니라, 문장이 가진 의미를 수치적으로 표현한 것입니다.

벡터 공간에서는 이 숫자들의 패턴을 비교해, 서로 비슷한 의미를 가진 문장끼리는 가까운 위치에 있도록 배치할 수 있습니다. 문서를 임베딩한 후에는, 이 벡터들을 벡터 데이터베이스 에 저장하게 됩니다.

여기서부터는 키워드 인덱스를 사용하는 것이 아니라, 벡터 간의 거리를 계산해 가장 의미가 비슷한 문서를 찾게 됩니다.

벡터 유사도 검색

앞서 보았듯 문서나 문장을 임베딩하면, 각각은 숫자로 이루어진 고차원 벡터로 변환됩니다. 벡터 검색의 핵심은 사용자가 입력한 쿼리 벡터와 저장된 문서 벡터들 사이의 거리를 계산해, 의미상으로 가장 가까운 문서를 찾아내는 과정입니다.

벡터 간 거리 계산 방법

벡터 간 유사도는 다양한 방법으로 측정할 수 있지만, 주로 코사인 유사도 를 사용합니다. 코사인 유사도는 두 벡터가 이루는 각도의 코사인 값을 이용해, 벡터의 방향이 얼마나 비슷한지를 측정합니다.

이 방식은 벡터의 크기에는 영향을 받지 않고, 의미의 방향성만 고려하기 때문에 자연어 문장 검색에 특히 효과적입니다.

또 다른 방법으로는 유클리드 거리가 있습니다. 이는 두 점 사이의 직선 거리를 계산하는 방법이지만, 텍스트 검색보다는 수치 기반 데이터에 더 자주 사용됩니다.

그리고 한 가지 더, 내적도 벡터 유사도를 계산하는 방법 중 하나입니다. 내적은 두 벡터의 방향이 비슷할수록 값이 커지며, 특히 벡터 크기까지 함께 고려해서 유사도를 평가할 때 사용됩니다.

KNN과 ANN

KNN(K-Nearest Neighbor)은 가장 기본적인 방식으로 말 그대로, 쿼리 벡터와 모든 저장된 문서 벡터를 하나하나 거리 계산해서 가장 가까운 K개를 고르는 방식입니다.

그렇기 때문에 항상 정확한 유사한 결과를 가져올 수 있습니다. 하지만, 데이터가 굉장히 많아지게 된다면 모든 벡터와 비교해야하기 때문에 쿼리 속도가 매우 느려집니다.

벡터 검색에서는 사용자가 쿼리 하나를 보낼 때마다 데이터베이스 안에 저장된 수십만 ~ 수억 개의 벡터와 각각 거리를 계산해야 합니다. 이걸 전부 계산하는 건 물리적으로 너무 느립니다. 예를 들어 1억 개 벡터가 있는데, 쿼리 하나마다 1억 번 거리 계산을 해야 한다면? 아무리 서버를 많이 둬도 실시간 검색은 불가능해집니다.

그래서 등장한 것이 ANN(Approximate Nearest Neighbor)입니다. ANN은 전체 데이터를 다 보지 않고, ”유망한 후보”만 빠르게 추려내고, 그 안에서만 거리를 계산합니다. 대표적으로 데이터를 규칙에 따라 공간적으로 나눠두는 방식 혹은 벡터들을 연결해서 그래프를 만들어 두어, 비슷한 벡터끼리 링크를 걸어두는 방식을 통해 ANN을 구현합니다.

그렇기에 실제 대부분의 검색 시스템은 ANN 방식을 기반으로 구축됩니다.

의미 기반 검색 흐름

Elasticsearch는 기본적으로 텍스트 키워드 검색에 최적화되어 있지만, kNN search 기능을 통해 의미 기반 검색도 지원합니다.

Pinecone, Weaviate, Qdrant, Milvus등 다양한 Vector DB가 존재하지만 간단하게 Elasticsearch로 흐름을 알아보도록 하겠습니다.

색인 요청 흐름

의미 기반 검색에서는 단순 텍스트가 아니라, 벡터로 변환된 데이터를 색인합니다. 흐름은 다음과 같습니다.

  1. 문서 준비 및 임베딩 변환

    색인할 문장을 준비한 뒤, 사전 학습된 임베딩 모델(OpenAI, HuggingFace 등)을 이용해 고차원 벡터로 변환합니다. 예를 들어 “강아지와 한강 공원에서 산책하는 방법”이라는 문장을 1536차원 벡터로 변환하면 다음과 같습니다.

    [0.021, -0.042, 0.123, ..., 0.087, -0.032]
  2. 벡터 저장

    변환된 벡터를 Elasticsearch에 색인합니다. 문서 저장은 다음과 같은 형태로 진행됩니다.

    POST /semantic-posts/_doc
    {
      "title": "강아지와 한강 공원에서 산책하는 방법",
      "content_vector": [0.021, -0.042, 0.123, ..., 0.087, -0.032]
    }
  3. 벡터 인덱스 생성

    색인 시, dense_vector 타입 필드로 저장되며, 코사인 유사도 기반 검색이 가능하도록 설정됩니다.

  4. 샤드 저장

    생성된 문서는 지정된 Primary Shard에 저장되고, 동시에 Replica Shard에도 복제되어 고가용성이 보장됩니다.

검색 요청 흐름

색인된 벡터들을 검색하는 과정은 다음과 같은 흐름을 가집니다.

  1. 사용자 쿼리 및 임베딩 변환

    사용자가 자연어 형태로 검색어를 입력합니다. 예를 들어 “강아지랑 어디 산책 가면 좋을까?” 이 문장을 같은 임베딩 모델로 변환해 쿼리 벡터를 생성합니다.

  2. 벡터 기반 검색 요청

    변환된 쿼리 벡터를 이용해 Elasticsearch에 KNN 요청을 보냅니다. 아래와 같은 예시 요청을 통해 가장 유사한 3개의 문서를 반환 받을 수 있습니다.

    POST /semantic-posts/_search
    {
      "knn": {
        "field": "content_vector",
        "query_vector": [0.031, -0.055, 0.112, ..., 0.073, -0.021],
        "k": 3,
        "num_candidates": 100
      }
    }
  3. 벡터 간 유사도 계산

    Elasticsearch는 저장된 문서 벡터들과 쿼리 벡터 간의 코사인 유사도를 계산합니다. 유사도가 가장 높은 문서가 최상단에 위치합니다.

  4. 결과 집계 및 정렬

    가장 유사한 결과를 모아서, 관련성 높은 순서대로 사용자에게 반환합니다.


검색 아키텍처 구성

지금까지 검색 시스템의 다양한 방식과 기술을 살펴봤다면, 이제는 실제 서비스에서 검색 기능을 어떻게 구성할 수 있을지 간단한 예시를 짚어보려고 합니다.

검색 시스템을 설계할 때 추가적으로 고려되어야 할 것은 어떻게 하면 데이터를 빠르게, 정확하게, 그리고 최신 상태로 유지하면서 색인할 것인가 입니다.

검색 트래픽은 일반적인 CRUD 요청과 성격이 다릅니다. 단순히 하나의 레코드를 가져오는 것이 아니라, 수많은 데이터 중 최적의 결과를 찾아야 하기 때문에, 검색 연산은 훨씬 많은 CPU, 메모리, 디스크 I/O를 소모합니다. 그래서 일반적으로는 메인 DB 서버와 검색 서버를 분리합니다.

전체적인 아이디어는 다음과 같습니다.

  • DB 서버는 데이터 저장과 기본 트랜잭션 처리에만 집중합니다.
  • 검색 서버는 검색 요청만을 전담합니다.
  • 데이터 동기화는 Batch 색인Stream 색인을 사용하여 처리합니다.

전체 색인 (Batch Indexing)

전체 색인은 말 그대로, 데이터베이스 전체를 주기적으로 읽어와 Elasticsearch에 다시 색인하는 방법입니다.

보통 시스템을 처음 구축할 때, 대규모 데이터 구조 변경이 발생했을 때, 또는 장애 복구가 필요할 때 이러한 상황에서는 반드시 전체 색인이 필요합니다.

위 그림에서 ETL은 데이터베이스 전체를 주기적으로 읽어와(Extract), 필요한 가공(Transform) 과정을 거친 뒤, 최종적으로 Elasticsearch에 다시 색인하는 과정(Load)을 의미합니다.

주로 Airflow 같은 워크플로우 관리 시스템을 사용해, 매일 새벽, 혹은 주기적으로 이 작업을 트리거(trigger)합니다.

예를 들어, 매일 새벽 3시에 posts 테이블 전체를 읽어와 posts-index를 새로 덮어쓰는 방식이 될 수 있습니다.

증분 색인 (Stream Indexing)

하지만 모든 데이터를 매번 새로 색인하는 것은 비효율적입니다.

그래서 변경된 데이터만 빠르게 반영하는 증분 색인(Stream Indexing)를 사용할 수 있습니다.

증분 색인은, 데이터베이스에 Insert, Update, Delete 같은 변경 이벤트가 발생할 때마다 변경된 부분만 실시간으로 Elasticsearch에 업데이트하는 방식입니다.

이를 위해 보통 Debezium 같은 CDC 도구를 사용해 DB 변경 로그를 감지하고, 대표적으로 Kafka와 같은 메시지 브로커를 통해 이벤트를 전달한 다음, 별도의 Stream Consumer가 이벤트를 받아서 Elasticsearch에 색인합니다.

예를 들어, 사용자가 게시글을 수정하거나 새로운 댓글을 작성하면, 해당 변경 사항은 즉시 Elasticsearch에 반영되어 사용자 검색 결과에 바로 반영됩니다.

결론

단순한 문자열 매칭에서 출발해, 전문 검색, 그리고 의미 기반 벡터 검색까지 기술은 계속 진화해왔습니다.

최근 검색은 서비스의 경쟁력을 좌우하는 핵심 요소가 되고 있습니다. 검색 품질이 높을수록 사용자의 이탈률은 줄어들고, 체류 시간은 늘어나며, 커머스나 콘텐츠 플랫폼에서는 매출 상승으로도 이어집니다. 검색은 단순한 부가 기능이 아니라, 서비스 성공을 위한 핵심 전략으로 볼 수 있는 셈입니다.

또한 검색 기술은 빠르게 다른 영역으로 확장되고 있습니다. AI 추천 시스템, 챗봇 등 다양한 분야에서, 검색은 정보를 단순히 “찾아주는 것”을 넘어 사용자에게 맞춤형으로 최적화된 정보를 제공하고, 스스로 새로운 답변을 생성하는 능력까지 요구받고 있습니다.

이제 다음 글에서는, 이러한 흐름 속에서 특히 주목받고 있는 RAG(Retrieval-Augmented Generation) 기술에 대해 다뤄보려고 합니다. 검색과 생성이 결합되어, 어떻게 더욱 “지능적인 답변”을 만들어내는지 함께 살펴보겠습니다.

0개의 댓글