logo
|
Blog
  • 합류하러 가기 🚀
ArticleProduct

퀸잇 검색 시스템의 여정: WHERE title LIKE '%keyword%'에서 Hybrid Search까지

Server Engineer, 박서준 님
채용팀's avatar
채용팀
Apr 17, 2026
퀸잇 검색 시스템의 여정: WHERE title LIKE '%keyword%'에서 Hybrid Search까지
Contents
들어가며1. PR #7과 #8 사이, 4시간 — MySQL LIKE에서 Elasticsearch로문제: 검색이 없었습니다.첫 번째 시도: MySQL LIKE해결: 같은 날 Elasticsearch로 전환한계2. 좋은 상품을 골라주는 검색으로 — Reranking문제: 텍스트 매칭 점수가 높다고 좋은 상품은 아닙니다.해결: function_score 기반 리랭킹한계3. 의미를 이해하고, 사람마다 다른 검색으로 — 벡터 검색 도입문제: 개인의 선호를 반영하지 못하고, 단어가 일치해야만 검색이 됩니다.해결: 벡터 검색개인화 벡터 검색 — 사용자별 다른 결과Semantic Search — "캠핑 갈 때 입기 좋은 점퍼"를 검색할 수 있게한계4. 여러 검색 방식을 하나로 — RRF문제: 서로 다른 검색 결과를 하나로 합쳐야 합니다해결: RRF(Reciprocal Rank Fusion)결과5. 우리가 틀렸던 것들BM25 ∩ Semantic 교집합 선노출 — "관련성이 높다"와 "클릭한다"는 다르다럭퀸세일 장애 — 검색이 멈추면 앱 전체가 멈춘다오타 교정 — ‘검색 실패율 감소’가 곧 ‘품질 개선’은 아니다6. 앞으로의 방향Retrieval 소스 일원화ML RankerQuery Understanding검색 인프라 고도화마무리시리즈 안내

들어가며

2020년 9월 출시 직후, 퀸잇에는 검색이 없었습니다.

퀸잇은 4050 여성을 위한 패션 커머스입니다. 사용자가 "블라우스"를 찾으려면 카테고리를 하나하나 탐색 했고, 우리는 그날 MySQL LIKE '%keyword%'로 첫 검색을 만들었습니다.

그로부터 5년이 지났습니다. 검색은 키워드 매칭에서 의미를 이해하고 개인화된 결과를 보여주는 단계까지 진화했습니다. 저는 이 과정을 주니어 엔지니어로 합류해 처음부터 함께했고, 지금도 인텔리전스 스쿼드에서 검색 시스템 전체를 직접 설계하고 개선하고 있습니다.

이 글은 그 여정의 요약이자 시리즈의 첫 번째 글입니다. 어떤 문제를 만났고, 어떤 선택을 했고, 그 과정 속에서 틀린 선택지까지 다뤄보려 합니다. 시리즈다보니 각 주제의 구현과 실험은 후속 글에서 더 깊이 다룰 예정입니다. 검색 시스템이 실제 서비스에서 어떻게 성장해 왔는지 궁금한 분이라면 재미있게 읽으실 수 있을 겁니다.


1. PR #7과 #8 사이, 4시간 — MySQL LIKE에서 Elasticsearch로

문제: 검색이 없었습니다.

2020년 9월, 사용자가 원하는 상품을 찾는 방법은 카테고리를 탐색하는 것뿐이었습니다.

첫 번째 시도: MySQL LIKE

가장 빠르게 검색을 제공할 수 있는 방법은 이미 있는 MySQL을 쓰는 것이었습니다. PR #7 "검색 1단계"로 첫 검색 API가 탄생했고, 이는 의도된 결정이었습니다. 당시에는 지금만큼 상품 수가 많지 않았고, 하루라도 빨리 검색을 붙이는 게 더 중요했기 때문입니다.

다만 LIKE '%keyword%' 에는 근본적인 한계가 있었습니다. 인덱스를 탈 수 없어 상품이 늘어날수록 검색 속도가 느려질 뿐만 아니라, 의미가 아닌 문자열 일치에만 의존하기 때문에 ‘봄 재킷’을 검색해도 ‘봄 트위드 재킷’은 찾지 못하고, ‘니트’를 검색해도 상품명이 ‘스웨터’로 등록되어 있으면 결과가 나오지 않습니다. 동의어, 자동완성, 오타 보정 같은 기능을 RDBMS 위에 쌓는 것도 현실적이지 않았습니다.

해결: 같은 날 Elasticsearch로 전환

PR #7이 머지되고 4시간 후, PR #8 "검색 phase 2"가 열렸습니다.

MySQL LIKE는 "일단 동작하는 검색을 가장 빨리 제공" 하기 위한 스캐폴딩이었고, Elasticsearch가 이를 대체하면서 검색다운 검색이 시작되었습니다. 역인덱스 기반의 밀리초 단위 매칭, 한국어 형태소 분석, BM25 관련도 스코어링을 지원합니다. 동의어/사용자 사전 관리와 자동완성도 기능도 쉽게 활용할 수 있게 되었습니다.

한계

하지만 당시의 단순한 BM25 쿼리는 텍스트 매칭 점수만으로 정렬했기 때문에, 인기도나 판매량 같은 비즈니스 지표가 반영되지 않았습니다. 좋은 상품이 상위에 나오지 못하는 문제가 바로 드러났습니다.


2. 좋은 상품을 골라주는 검색으로 — Reranking

문제: 텍스트 매칭 점수가 높다고 좋은 상품은 아닙니다.

BM25는 검색어와 문서 간의 텍스트 관련성을 계산합니다. 하지만 BM25(텍스트 관련성)는 상품의 품질을 대변하진 않습니다. 그건 텍스트 매칭만으로는 알 수 없습니다.

해결: function_score 기반 리랭킹

ES의 function_score 쿼리를 활용해 BM25 점수 위에 비즈니스 시그널을 결합하는 리랭킹 시스템을 구축했습니다. 구매량·CTR·거래액 기반의 상품 품질 점수를 산출하고, 이를 Elasticsearch에 색인하는 파이프라인을 만들었습니다. 리뷰 가중치, 카테고리 매칭, 퀸잇 고객층의 선호 가격대 등을 추가로 반영했고, 이 시그널들의 가중치는 Admin API를 통해 실시간 조정하고 A/B 테스트로 검증할 수 있게 만들었습니다.

한계

하지만 여전히 아래와 같은 두 가지 한계를 맞이하게 됩니다.

  1. 모든 사용자에게 같은 순서를 보여줍니다.
    function_score의 시그널들은 상품 자체의 품질이지 사용자별로 달라지는 값이 아닙니다. 캐주얼 브랜드를 선호하는 사용자와 포멀 브랜드를 선호하는 사용자가 같은 "원피스" 검색 결과를 보게 됩니다.

  2. BM25가 찾지 못하는 상품은 리랭킹으로 풀 수 없습니다.
    동의어 사전으로 보완할 수는 있지만, 엔지니어와 PO가 직접 관리하는 사전에서는 누락이 빈번했습니다. 게다가 "캠핑 갈 때 입기 좋은 점퍼"처럼 자연어로 표현된 검색어는 사전에 등록할 수 있는 성격이 아니었습니다. 또한, 홈쇼핑 인수로 패션을 넘어 리빙·식품 등 카테고리가 급격히 확장될 상황에서 수작업 사전 관리는 더 이상 유지할 수 없었습니다.


3. 의미를 이해하고, 사람마다 다른 검색으로 — 벡터 검색 도입

문제: 개인의 선호를 반영하지 못하고, 단어가 일치해야만 검색이 됩니다.

리랭킹이 남긴 두 가지 숙제(개인화 부재와 키워드 매칭의 한계)를 함께 풀어야 했습니다.

해결: 벡터 검색

벡터 검색으로 두 문제를 함께 풀었습니다. 텍스트를 고차원 벡터로 변환하고, 벡터 공간에서의 거리로 관련성을 판단합니다. 키워드가 달라도 의미가 비슷하면 가까운 벡터로 표현되고, 사용자의 행동 패턴도 같은 공간에 표현할 수 있습니다.

개인화 벡터 검색 — 사용자별 다른 결과

먼저 도입한 것은 유저 임베딩 기반의 개인화입니다. 추천 모델이 클릭·구매·체류 등 행동 신호로 학습한 유저·상품 임베딩을 검색에도 활용했습니다. 상품 임베딩은 ES에 사전 색인해 두고, 검색 시점에 ML 서버로 유저 ID를 보내 유저 임베딩을 조회한 뒤, ES KNN으로 유사도를 계산해 스코어에 반영합니다.

같은 "원피스"를 검색해도 사용자마다 다른 상품이 상위에 올라옵니다. Control(기존 검색) 대비 Variant(개인화 적용)를 A/B 실험한 결과, 아래와 같은 비즈니스 지표 증분을 얻었습니다.

지표

변화

전체 거래액

+2.38%

검색 기여 거래액

+20.33%

Semantic Search — "캠핑 갈 때 입기 좋은 점퍼"를 검색할 수 있게

기존 검색이 단어가 일치해야만 결과가 나오는 구조이다 보니, 검색 실패율(검색 후 결과가 없는 비율)이 높았습니다. 이를 해결하기 위해 단어 기반이 아닌, 검색어의 의미를 이해하는 Semantic Search를 도입했습니다.

여기서 아키텍처 선택의 갈림길이 있었습니다. 정석대로라면 Semantic 임베딩을 ES에 함께 색인하고 하나의 쿼리로 처리하는 게 맞지만, 개인화 벡터 위에 Semantic까지 추가하면 색인 시간과 검색 레이턴시가 동시에 늘어나는 문제가 있었습니다. 완벽한 구조를 먼저 만드는 대신, ML 서버의 FAISS로 semantic 검색을 수행했습니다. 정렬 방식은 기존 bm25 결과 뒤에 semantic search를 단순 병합하는 방식을 택했습니다. 핵심가치인 ‘Super Fast’처럼 빠르게 효과를 검증하는 것이 우선이었습니다.

1차 실험

1차 실험(Control: BM25만, Variant: BM25 결과 10개 이하일 때 Semantic으로 대체)에서 실패에 가까운 검색어에 한 해 Semantic을 보여준 결과입니다.

지표

변화

전체 거래액

+3.43%

검색 기여 거래액

+7.74%

검색 실패율(검색 후 검색 결과가 없는 비율)

4.3% → 0.47% (−89%)

하루 100만 건 이상의 검색 중 빈 화면을 보는 경우가 거의 사라졌고, "캠핑 갈 때 입기 좋은 점퍼" 같은 자연어 검색도 가능해졌습니다.

2차 실험

1차 실험에서 Semantic의 효과는 확실했지만, 단어가 일치하는 BM25 결과를 통째로 버리는 데서 부작용이 나타났습니다.

위 사진의 예시와 같이 특정 상품의 모델명을 검색하는 패턴에서, BM25는 해당 상품을 정확히 찾아냅니다. 그러나, 결과가 10개 이하라는 이유로 Semantic 결과로 대체되면서 고객이 실제로 찾던 상품이 노출되지 않는 경우가 있었습니다.

그래서 단어 매칭이 강한 BM25를 앞에 두고 Semantic을 보강재로 뒤에 이어 붙이는 방식으로 2차 실험을 진행했습니다.

지표

변화

전체 거래액

+1.62%

검색 기여 거래액

+7.45%

1, 2차 실험 이터레이션은 3주간 진행되었습니다. 완벽한 구조를 먼저 만들었다면 이 검증에 수개월이 더 걸렸을 겁니다.

한계

하지만, 이 구조에서는 BM25 결과가 항상 앞에, Semantic 결과가 뒤에 배치됩니다. 의미적으로 더 적절한 상품이 있어도 뒤로 밀리고, 두 결과는 점수 체계가 달라 일관된 정렬 기준을 만들 수 없었습니다.


4. 여러 검색 방식을 하나로 — RRF

문제: 서로 다른 검색 결과를 하나로 합쳐야 합니다

그렇다면 BM25 1위 상품과 Semantic 1위 상품이 완전히 다를 때, 우리는 어떤 걸 먼저 보여줘야 할까요? 단순히 점수를 합산하자니 스케일이 다르고, 한쪽을 앞에 고정하자니 나머지가 묻힙니다.

실제 데이터가 이 어려움을 보여줍니다. 브랜드 검색어 1,374개를 대상으로 BM25와 Semantic Search의 품질을 비교한 결과입니다.

지표

BM25

Semantic

F1 기준 승률

96.1%

3.9%

recall=0 (상품 아예 못 찾음)

0개

443개 (32.2%)

브랜드 검색에서는 BM25가 압도적입니다. 반면 "캠핑 갈 때 입기 좋은 점퍼" 같은 검색에서는 Semantic이 필수적입니다. 결론적으로 둘 중 하나를 고르는 문제가 아니었습니다.

해결: RRF(Reciprocal Rank Fusion)

단순히 결과를 이어 붙이는 방식은 특정 검색 방식에 지나치게 의존하게 만들고, 각각의 결과가 갖는 상대적인 중요도를 반영하기 어렵습니다. RRF는 이 문제를 점수(score)가 아닌 순위(rank) 기반으로 해결합니다.

각 소스에서 상위에 랭크된 결과일수록 높은 점수를 부여하기 때문에, 서로 다른 점수 체계를 별도 정규화 없이 자연스럽게 결합할 수 있습니다. 특정 검색 방식에 치우치지 않으면서도 각 방식의 강점을 유지한 채 하나의 일관된 랭킹을 만들 수 있게 되었습니다.

k는 순위 간 점수 차이를 얼마나 완만하게 만들지 결정합니다. k가 작으면 상위 순위에 점수가 집중되고, k가 크면 순위 간 차이가 줄어듭니다. k=60(RRF 논문 기본값)을 사용하고 있습니다.

세 소스의 가중치는 환경변수로 외부화해서 실험할 수 있게 했습니다.

결과

RRF 기반 검색 개선 실험에서 다음과 같은 지표 개선을 확인했습니다.

구매 지표 관련 개선

전체 지면

지표

변화

전체 거래액

+2.67%

검색 지면

지표

변화

검색 구매 수량

+6.08%

검색 기여 거래액

+3.91%

탐색 지표 관련 개선

전체 지면

지표

변화

전체 CTR

+0.84%

검색 지면

지표

변화

유저당 상품 클릭 수

+3.01%

유저당 상품 노출 수

+1.68%

CTR

+1.39%

검색 당 평균 상품 클릭 횟수

+4.00%

현재 검색 서버는 세 소스를 코루틴으로 병렬 실행하고 RRF로 합산하는 구조입니다. BM25는 검색 서버가 ES에 직접 쿼리하고, Semantic은 ML 서버에서 가져오며, 개인화 벡터 검색은 ES KNN으로 수행합니다.


5. 우리가 틀렸던 것들

지난 5년이 순탄하지만은 않았습니다. 돌이켜보면 실패한 실험들이 오히려 방향을 잡아줬습니다.

BM25 ∩ Semantic 교집합 선노출 — "관련성이 높다"와 "클릭한다"는 다르다

RRF를 도입하면서, 양쪽에서 모두 찾은 상품을 상단에 배치하면 더 좋지 않을까 하는 가설을 세웠습니다.

A/B 테스트 결과, CTR이 유의미하게 하락(1차 p=0.001, 재실험에서도 p=0.009로 재현)했습니다. 원인을 분석해보니 교집합이 작은 검색어에서 개인화의 영향력이 희석되는 것이 문제였습니다. 교집합을 강제로 올리면 개인화가 추천한 상품이 밀려납니다.

이 실패가 개인화를 별도 신호로 유지해야 한다는 결론으로 이어졌습니다.

럭퀸세일 장애 — 검색이 멈추면 앱 전체가 멈춘다

럭퀸세일(퀸잇의 블랙 프라이데이) 티징데이에 검색 서버가 2.5시간 동안 완전히 멈췄습니다. 예상치 못한 데이터 패턴 변화와 트래픽 증가가 겹쳤습니다. 문제는 검색만 멈춘 게 아니었습니다. 기획전, 홈 등 앱의 핵심 지면이 모두 검색 서버에 의존하고 있었기 때문에 서비스 전체가 영향을 받았습니다.

이 장애가 Circuit Breaker와 단계별 Fallback 설계의 직접적인 계기가 되었습니다. Resilience4j 기반으로 ML 호출과 ES 호출 각각에 Circuit Breaker를 적용하고, slow call 비율과 failure rate를 모니터링하며 임계치 초과 시 회로를 차단합니다.

  • ES 서킷 발동 시: 사전에 캐싱해둔 BM25 Top-K 결과로 fallback. 첫 페이지 요청 시 BM25 Top-K 결과를 비동기로 선제 적재해두어, 장애 시점에 캐시가 이미 준비되어 있도록 합니다.

  • ML(Semantic) 실패 시: BM25 + Personalized Vector 2-way RRF로 fallback

  • 개인화 실패 시: BM25 + Query Vector 2-way RRF로 fallback

  • Hybrid 검색 전체 빈 결과 시: 일반 BM25 검색으로 fallthrough

외부 시스템의 장애가 서비스 전체로 번지지 않도록, 호출하는 쪽에서 격리하고 흡수할 수 있는 구조를 만들었습니다. 뼈아팠지만, 이 장애가 없었다면 이 설계도 없었을 겁니다.

오타 교정 — ‘검색 실패율 감소’가 곧 ‘품질 개선’은 아니다

한편, 오타 교정을 도입하면 검색 실패율을 줄일 수 있을 거라는 기대로 실험을 진행했습니다. 실패율은 약 16% 감소했지만, 구매전환율은 오히려 소폭 하락했고 ARPU에도 유의미한 개선이 없었습니다.

하나씩 분석해보니 상위 100개 오타 교정 키워드에서 33%가 오교정이었습니다. "운동복"이 "아동복"으로, "쿠폰"이 "쿠론"으로 바뀌는 식이었습니다. 교정 신뢰도 기반으로 선별 적용하는 방향으로 개선했지만 숏테일 검색어(1~2개의 단어로 구성된 일반적이고 광범위한 핵심 검색어)에서의 오교정이 지속되어 일시 중단했습니다. 현재는 교정 대상을 정밀 관리하는 방식으로 재도입해 운영 중입니다.

결국 오타 교정을 통해 지표 하나로만 판단할 수 없다는 레슨런을 얻었습니다.


6. 앞으로의 방향

Retrieval 소스 일원화

Semantic Search와 개인화를 빠르게 도입하는 과정에서, 검색 로직의 소유권이 검색 서버와 ML 서버에 나뉘었습니다. "이 검색어에서 왜 이 상품이 3위인가?"를 추적하려면 두 서비스를 오가야 했고, 실험 하나에 양쪽을 함께 수정해야 했습니다. 복잡도가 쌓이면 실험 속도가 느려지고, 결국 개선이 멈춥니다.

현재까지의 개선 방향을 요약하면 다음과 같습니다.

FAISS는 pre-filtering이 불가능해 post-filtering 과정에서 후보군의 수와 품질이 함께 떨어지는 문제가 있습니다. ES KNN으로 통합하면 pre-filtering이 가능해지고, 검색 품질과 시스템 복잡도를 동시에 개선할 수 있습니다.

ML Ranker

현재 스쿼드에서 진행 중인 가장 큰 변화입니다. BM25와 Semantic으로 후보를 추출한 뒤, ML 모델이 최종 랭킹을 결정하는 구조입니다.

핵심은 관련성과 선호도의 분리 학습입니다. 기존 검색 랭킹 모델들은 "검색어와 상품이 관련 있는가"와 "이 사용자가 이 상품을 좋아하는가"를 하나의 공간에서 학습하는데, 이러면 선호도가 높은 상품이 관련 없는 검색어에서도 상위에 올라옵니다. 예를 들어 "골프웨어"를 검색했는데 구매 이력이 많다는 이유로 "트레이닝복"이 상위에 노출되는 식입니다. 두 신호를 명시적으로 분리하고, 검색어·사용자·상품 조합에 따라 모델이 최적의 균형을 찾도록 개발하고 있습니다.

Query Understanding

같은 ‘쿠션’이라도 침구류인지 화장품인지, 현재 검색 시스템은 구분하지 못합니다. 또한 위에서 언급한 것처럼 BM25와 Semantic Search가 방식이 잘하는 영역은 검색어의 종류에 따라 다릅니다. 검색어 자체의 의도를 파악하고 그것에 맞는 결과를 도출하는 것이 다음 단계입니다.

검색어를 토큰 단위로 분절하고, 각 토큰이 브랜드인지, 카테고리인지를, 상품 속성인지를 분류하는 Query Understanding 모듈을 개발하고 있습니다. 예를 들어, ‘나이키 면 원피스’가 입력되면 ‘나이키’는 브랜드, ‘면’은 소재, ‘원피스’는 카테고리로 분류되고, 이 정보가 이후 단계의 필터링과 부스팅에 활용됩니다. 초기에는 ML 모델 없이 규칙 기반으로 시작하고, 추후 ML/LLM을 기반으로 확장할 계획입니다.

검색 인프라 고도화

이 외에도 검색 레이턴시 최적화, 오프라인 평가 환경, 인덱싱 파이프라인 안정성, 관측성 같은 시스템 엔지니어링 과제들을 함께 풀고 있습니다. 예를 들어, 검색 결과에서 stored_fields: _none_, _source: false로 상품 ID만 반환하고 필요한 필드는 doc_values에서 조회하는 방식으로 ES 응답 크기와 직렬화 비용을 줄이고 있고, 인덱싱 파이프라인에서는 여러 데이터 소스를 하나의 검색 문서로 통합하는 과정에서 CDC 이벤트의 순서 보장과 멱등성을 관리하고 있습니다. 이 주제들은 후속 글에서 깊이 다룰 예정입니다.


마무리

퀸잇의 검색은 WHERE title LIKE '%니트%'에서 시작해 5년 넘게 진화해왔고, 지금도 계속 변화하고 있습니다.

단순히 기능을 개발하는 것을 넘어, 검색 시스템 전체 구조를 이해하고 각 단계의 문제를 정의하고 해결하는 과정에 참여할 수 있었습니다. 성능, 품질, 복잡도 사이의 트레이드오프를 고민하며 설계하고, 그 선택이 실제 서비스 지표로 이어지는 과정을 직접 경험할 수 있었습니다.

돌이켜보면 실패도, 성공도 같은 원칙에서 나왔습니다. MySQL LIKE query로 임시 조치를 해두고 같은 날 ES로 전환했고, Semantic Search는 완벽한 구조 대신 단순 병합으로 빠르게 검증했으며, 오타 교정을 데이터로 부정하고 중단했습니다. 빠르게 실행하고, 데이터로 검증하고, 틀리면 바꿨습니다. 그게 지난 5년간 우리가 검색을 만들어온 방식이었습니다.

이 변화들은 소수로 구성된 인텔리전스 스쿼드가 만들어낸 결과입니다. Server Engineer, Frontend Engeineer, ML Engineer, PO, DA, PD가 하나의 팀에서 검색과 추천을 함께 담당합니다. 작은 팀이지만, 한 명의 엔지니어가 검색의 특정 기능이 아니라 전체 흐름을 이해하고 개선에 참여합니다. 그만큼 만들어내는 영향 역시 적지 않습니다.

앞서 6장에서 다룬 과제들(Retrieval 소스 일원화, ML Ranker, Query Understanding)은 아직 완전히 해결되지 않았고, 지금도 계속 진행 중입니다. 여기에 더해 시스템 전반에 걸친 변화도 예정되어 있습니다. 이미 완성된 시스템이 아니라, 직접 문제를 정의하고 풀어가야 하는 환경이기에 검색 시스템을 이해하고, 직접 개선해보고 싶은 분이라면 이 경험이 의미 있을 것이라 생각합니다.

이 여정을 함께할 엔지니어를 찾고 있어서 관심 있으시다면 [라포랩스 채용]에서 확인해주세요.


시리즈 안내

이 글은 퀸잇 검색 시스템 시리즈의 첫 번째 글입니다. 후속 글에서는 각 주제의 구현과 실험을 깊이 다룹니다.

  • Hybrid Search: BM25와 벡터 검색을 하나의 엔진에서 처리하기

  • Vector Search: 개인화와 시맨틱 검색의 구현과 실험

  • ML Reranker: 관련성과 선호도를 분리 학습하는 랭킹 모델

  • Query Understanding: 검색어의 의도를 이해하는 시스템

  • Indexing Pipeline: 실시간 CDC 기반 색인 시스템의 설계와 운영

Share article

라포랩스

RSS·Powered by Inblog