벡터 검색(임베딩)만이 시맨틱 검색의 전부는 아니다. 공통 표현, 유사도, 그리고 ‘매칭’ 기준을 갖춘 분류 체계(택소노미)와 BM25를 활용해 의미 기반 검색을 구현하는 방법을 비교한다.
2026년 1월 8일
추위를 피하려고 “long johns(내복)”나 “long underwear(긴 속옷)”를 검색하곤 한다. 하지만 요즘 아웃도어 의류 매장에서는 이런 제품을 “base layer(베이스 레이어)”라고 부른다. 방수 재킷을 찾을 때도 “slickers(레인코트)”나 “ski jacket(스키 재킷)”을 검색하지, 내가 사실 “shell(셸)”을 검색해야 한다는 걸 모른다.
내 용어가 구식인데도 검색은 여전히 잘 된다. 어찌 됐든 내 의도를 이해하고 올바른 콘텐츠를 보여준다.
우리는 이를 시맨틱 검색(semantic search) 이라고 부른다. 이 말을 들으면 임베딩을 떠올리기 쉽다. 하지만 오늘은 그 생각을 좀 더 넓혀보려 한다. 상상하고, 의미를 부여하자.
일부 팀은 시맨틱 검색을 오직 “벡터 검색(vector search)”으로만 동일시하는데, 이는 잘못된 가정일 수 있다. 당신에게는 더 많은 선택지가 있고, 그중에는 도메인과 조직이 이미 보유한 역량에 더 잘 맞는 것도 있다. LLM은 검색 쿼리와 정보를 조직하는 고전적인 방식들을 더 쉽게 만들어 준다.
여러 접근법을 대비해 보자. 그러다 보면, 당신에게 더 잘 맞는 또 다른 접근법이 보일지도 모른다.
시맨틱 검색에서 콘텐츠와 쿼리는 공유된 표현(shared representation) 으로 매핑된다. 이 공간에는 유사도(similarity) 함수가 있어, 비슷한 항목에 더 높은 점수를 준다.
예시로 살펴보자.
사용자가 “나무에서 자라는 둥글고 빨간 과일”을 검색했다면, 아마 사과를 원할 것이다.
인간은 이를 안다. 하지만 검색 엔진은 모른다. 그래서 우리가 도와줘야 한다.
쿼리를 세 가지 속성으로 매핑하자:
[round, red, fruit]
검색 인덱스에서도 세 가지 아이템을 같은 공간에 매핑해 둔다.
사과(apple):
[round, red, fruit]
오렌지(orange):
[round, orange, fruit]
야구공(baseball):
[round, white, ball]
이제 공유된 표현 이 생겼다. 다음 3-튜플이다: (
shape
,
color
,
item type
).
유사도는 어떻게 잴까? 각 속성(
shape
,
color
,
item type
)에 대해 값이 같으면 1, 아니면 0을 주고 합산할 수 있다:
Similarity(query, apple) = 1 + 1 + 1 = 3
Similarity(query, orange) = 1 + 0 + 1 = 2
Similarity(query, baseball) = 1 + 0 + 0 = 1
그러면 사용자는 다음과 같은 검색 결과를 보게 된다.
오케이, 꽤나 단순한(어쩌면 우스운) 시맨틱 검색 방법이다. (예를 들면 크기처럼) 중요한 속성은 무시한다. 모든 것을 포괄적으로 태깅해야 한다. 그럼에도 많은 팀이 태깅 + 동의어만으로 검색을 개선하려고 시도하고, 결과는 대체로 그저 그렇다.
“시맨틱 검색”에 대한 기본 답은 임베딩이다. 인터넷에는 벡터 검색이 동작하는 방식에 대한 문서가 널려 있다. 여기서는 초고속 요약으로만 다뤄보겠다(참고, 참고).
사과와 오렌지의 표현(임베딩)을 학습해서 다음이 성립하도록 만들 수 있다:
similarity(apple, orange) > similarity(apple, baseball)
임베딩은 학습 데이터로부터 배운다. 예를 들어, 같은 쿼리에서 함께 클릭되는 아이템은 “유사”하다고 선언할 수 있다. “나무에서 자라는 과일”이라는 쿼리에서 “사과”와 “오렌지”가 높은 빈도로 클릭되는 것을 관찰한다:
[나무에서 자라는 과일 🔍 ]
처음엔 각 아이템에 랜덤 벡터를 준다. 예를 들면 다음처럼 랜덤한 실수들의 리스트다:
[0.01, -0.1, 0.05]
그리고 이 초-비밀-초-하이테크 알고리즘으로 학습한다:
for session in sessions:
together = []
for lhs in session: # ie the apple
for rhs in session: # ie the orange
if lhs != rhs:
nudge_closer(lhs, rhs) # ie nudge apple closer to orange
(보이지 않는 부분: 음성(negative) 예시, 즉 서로 비유사한 아이템을 멀어지게 하는 과정)
많은 쿼리+세션을 학습한 뒤, 사과는
[0.1, -0.2, 0.05]
가 되고, 오렌지는 비슷한 벡터
[0.09, -0.21, 0.04]
가 될 수 있다. 유사도는 코사인 유사도, 유클리드 거리 등으로 측정한다. (“같은 검색 세션에서 함께 클릭됨”을 다른 “유사” 개념으로 바꿔도 된다: 같은 텍스트 구절에 함께 등장, 같은 장바구니에 함께 담김 등)
아이템마다 임베딩을 학습하는 방식엔 문제가 있다. 예를 들어, 이제 막 딸기 🍓를 상품 목록에 추가했다면, 학습 데이터가 없으므로
strawberry_vector = embeddings['strawberry']
는 랜덤 쓰레기 값에서 시작한다. 많은 사용자에게 노출되어야 의미 있는 값이 된다.
우리는 아이템의 특징(features) 에서 임베딩을 계산하는 편이 낫다. 그러면 딸기 🍓도 즉시 임베딩을 얻을 수 있다. 즉,
strawberry_embedding = embeddings['strawberry']
대신
strawberry_embedding = f(heart_shaped, red, fruit)
처럼 할 수 있다. 우리는 인코더(encoder) 를 만든다. 아이템의 속성으로부터, 기대하는 유사도를 만들어내는 임베딩을 계산한다. Hugging Face에서 내려받는 임베딩 모델들은 특정 작업(검색 등)을 위해 특정 유형/도메인의 콘텐츠를 인코딩하는 데 특화되어 있다.
인코더는 다양한 특징을 다룬다. 순서형(예: 모양/색상 목록), 원시 제목/설명 텍스트 자체, 이미지 등. 또한 투-타워 모델(two-tower model)을 학습할 수도 있다. 하나는 쿼리(예: “나무에서 자라는 둥근 과일”) 인코더, 다른 하나는 문서 인코더다.
다른 머신러닝 모델과 마찬가지로, 정밀도와 범용성 사이에서 트레이드오프가 있다. 적절한 특징이 필요하고, 그 특징들의 모든 변형을 커버할 만큼 충분한 학습 데이터가 필요하며(차원의 저주) 등등.
임베딩에서 곧바로 드러나는 고통 지점 하나가 있다: 매칭(matching) 부족이다.
사용자들이 이런 검색 결과에 실제로 어떻게 반응할까?
[나무에서 자라는 과일 🔍 ]
내 경험상, 부정적으로 반응한다.
랭킹은 완벽할 수 있다. 하지만 검색은 사용자가 의도하지 않은 아이템을 제외해야 한다. 사용자는 과일은 허용하지만, 야구공은 아니다.
그래서 아까는 사실 거짓말을 했다. 사용자가 시맨틱 검색에서 실제로 원하는 것은:
즉, 나는 시맨틱 검색의 정의를 불완전하게 제시했다. 임베딩은 앞의 2가지는 강하지만, 결과를 포함/제외하는 데는 그다지 뛰어나지 않다.
검색에서 사용자를(그리고 쿼리를) 이해하는 것은 매우 중요하다. 이는 사용자가 익숙한 용어와 카테고리를 바탕으로 적절한 아이템을 포함/제외해야 한다는 뜻이다.
“그럼 임베딩 유사도에 컷오프를 찾으면 되지 않을까?”라고 생각할 수 있다. 하지만 유감스럽게도 “유사도 0.8 이하이면 매치 아님” 같은 마법의 기준은 없다. 어떤 도메인/쿼리에서는 한 임계값이 통하지만, 다른 곳에서는 완전히 다른 임계값이 필요하다. 더 나아가 사용자가 여러 조건을 주면 임계값 적용은 더 어려워진다. 임베딩 모델은 “red apple(빨간 사과)” 쿼리에서 그린 애플보다 블러드 오렌지를 더 위에 올릴 수도 있다. 유사도에서 색상이 더 중요한가, 아이템 타입이 더 중요한가?
임계값은 명백히 무관한 결과를 일부 제외하고 성능을 개선하는 데는 도움이 되지만, 사용자 언어로 된 도메인 특화 기준을 사용해 결과를 포함/제외하진 못한다.
세 문제를 모두 풀려고 하는 시스템이 있다: 관리형 어휘(Managed vocabularies) 또는 택소노미(taxonomy).
택소노미는 쿼리+콘텐츠를 도메인 언어로 된 개념의 계층 구조로 매핑한다. 잘 정리된 디렉터리 트리나 듀이 십진 분류법(Dewey Decimal System)을 떠올리면 된다.
Wayfair WANDS 가구 데이터셋의 카테고리 트리 예시는 다음과 같다. “novelty rocking horses(장식/기발한 흔들말)”를 분류하는 위치가 이런 식이라니:
Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses / Novelty Rocking Horses
이제
hobby horse
라는 쿼리를 보자. 사람이 쿼리 규칙을 큐레이션해서 수동으로 노드에 매핑할 수 있다:
Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses
코퍼스가 제품을 올바르게 분류한다고 가정하면, 이를 활용해 검색/필터/부스팅 등을 할 수 있다.
표현 (카테고리 트리)는 갖췄다. 유사도 함수는 있을까?
있다. 예를 들어 다음처럼 랭킹할 수 있다:
Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses
Baby & Kids / Toddler & Kids Playroom / Indoor Play / Dollhouses
Baby & Kids / Toddler & Kids Playroom / Beanbag Chairs / …)은 더 낮게의미 있는 매치 기준도 정의할 수 있다. 예를 들어 같은 부모를 공유하는 카테고리(형제)는 포함하되, 필요에 따라 사촌/공유 조상(할아버지)보다 멀어지면 제외한다.
많은 팀은 이미 정보를 정리하고 카탈로깅하는 시스템을 운영한다. 많은 도메인에서 좋은 택소노미는 해자(moat)가 될 수 있다. 특히 법률/의료/패션/금융처럼 정밀한 분류가 중요한 기술 도메인에서는 더 그렇다. 이미 존재하는 택소노미도 있고, 임베딩의 통계적 ‘흐릿함’은 큰 다운사이드를 만들 수 있다.
과거에는 택소노미 관리 비용이 막대했다. 복잡한 규칙, 쿼리 구를 택소노미 노드로 매핑, 라벨러와 전문가 팀이 필요했다. 이런 점이 임베딩의 매력으로 오래 작용했다.
하지만 LLM은 이런 올드스쿨 정보 조직 방식을 은근히 초강화한다. 택소노미가 주어지면 LLM은 제품과 쿼리를 쉽고 저렴하게 분류할 수 있다. “파인튜닝”도 더 접근 가능해진다. AI 보강 라벨링은 지식 관리의 부담을 줄여 더 많은 사람들이 다룰 수 있게 한다.
위에서 유사도 함수를 설명했다(직접 매치, 그다음 부모, 그다음 조상). 토크나이징을 창의적으로 하면, 이 유사도는 일반 BM25 인덱스에서도 가능하다.
예를 들어
hobby horse
쿼리를 보자. 이것이 다음 노드에 매핑된다고 하자:
Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses
직접 매치가 형제보다 높게, 형제가 조상보다 높게 스코어링되려면 어떻게 해야 할까?
필요한 것은 계층형 토크나이저(hierarchical tokenizer)다. 모든 부모 디렉터리 경로를 토큰으로 생성하는 함수다:
In: hierarchical_tokenizer("Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses")
Out: ['Baby & Kids',
'Baby & Kids / Toddler & Kids Playroom',
'Baby & Kids / Toddler & Kids Playroom / Indoor Play',
'Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses']
이 토크나이저로 인덱싱하면, 앞서 말한 직접 > 부모 > 조상 유사도 함수에 필요한 스코어링을 재현할 수 있다.
핵심 이유: BM25는 흔한 매치보다 희귀한 매치를 더 보상한다.
루트 노드(조상)는 문서 빈도(document frequency)가 높다. 흔하다. 흔한 매치는 BM25에서 점수가 낮다.
왜냐하면
Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses
로 라벨링된 모든 아이템은 계층 토크나이징 시
Baby & Kids
라는 텀(term)을 가지기 때문이다.
Baby & Kids / Kids Furniture ... / Beanbag Chairs
로 라벨링된 문서도 마찬가지다.
Baby & Kids
는 여기저기에서 등장한다—인덱스의 10%쯤일 수도 있다.
반면 자식 노드는 문서 빈도가 낮다. 희귀하다. BM25에서는 희귀하고 더 구체적인 매치가 흔한 매치보다 위에 온다.
예를 들어
Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses / Traditonal Rocking Horses
로 라벨링된 제품은 토크나이징 시 전체 경로 토큰을 포함한다. 이 정확한 카테고리에 속한 제품이 인덱스 전체에 10개뿐일 수도 있다. 그 희귀성—그 높은 구체성—이 더 좁고 더 가까운 매치를 상단으로 끌어올린다.
좀 더 자세히 살펴보자.
내가 다음을 검색한다고 하자:
In: hierarchical_tokenizer(
"Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses / Novelty Rocking Horses"
)
Out: ['Baby & Kids',
'Baby & Kids / Toddler & Kids Playroom',
'Baby & Kids / Toddler & Kids Playroom / Indoor Play',
'Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses',
'Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses / Novelty Rocking Horses']
이제 다섯 개 토큰을 OR(실제로는 합산)로 검색한다:
"Baby & Kids" OR
"Baby & Kids / Toddler & Kids Playroom" OR
"Baby & Kids / Toddler & Kids Playroom / Indoor Play" OR
"Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses" OR
"Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses / Novelty Rocking Horses"
쿼리의 각 토큰은 역할이 있다. 서로 다른 구체성 레벨에 매치하며, 전체 분류 트리에 가까워질수록 점수가 올라간다.
| 검색어 | 역할 | 점수(문서 빈도 영향) | 매치 예시 |
|---|---|---|---|
| “Baby & Kids” | 최상위 카테고리 매치 | 최저 | Baby & Kids / Toddler & Kids Bedroom Furniture / Kids Headboards |
| “Baby & Kids / Toddler & Kids Playroom” | 다음으로 구체적인 카테고리 매치. 그냥 키즈가 아니라 아이들 ‘놀이’ 쪽으로 접근. 👍 | 낮음 | Baby & Kids / Toddler & Kids Playroom / Playroom Furniture / Baby Gyms & Playmats / Folding Baby Gyms & Playmats |
| … | … | … | … |
| Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses | 매우 좁고 구체적인 카테고리 매치. 말!🐴 “Novelty Rocking Horses”가 아니더라도 사용자에게 납득 가능한 “매치”다. | 높음 | Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses / Traditonal Rocking Horses |
말 → 장난감 → 놀이방 → 키즈 순서를 유지하더라도, 아직 3번째 문제(매치 제한)를 완전히 해결한 것은 아니다.
하지만 이건 쉽다. 쿼리 시점에 검색을 얼마나 제한할지 결정하면 된다. 예를 들어 직접 노드와 상위 1단계만 쿼리할 수도 있다:
"Baby & Kids / ... / Rocking Horses" OR
"Baby & Kids / ... / Rocking Horses / Novelty Rocking Horses"
사용자에게 중요한 카테고리에서 의미적으로(semantically) 한 단계 위까지 확장해도, 여전히 “키즈 놀이” 영역 안에 머물 수 있다.
택소노미가 사용자에게 타당하고, 실제 검색 행동을 반영한다면(큰 가정이지만), 왜 어떤 결과는 포함하고 어떤 결과는 제외했는지 사용자가 이해할 수 있다.
중요한 점: 이는 랭킹/필터링의 구성 요소 중 하나일 뿐이다. 여전히 다른 재료들도 함께 쓸 수 있다:
좋은 택소노미를 만들고 유지하는 건 결코 쉬운 일이 아니다. 그래서 실제로 ‘택소노미스트(taxonomist)’라는 직업이 있다. 그리고 여기서 임베딩과의 트레이드오프를 얼버무리고 싶지 않다. 정말 일이다.
택소노미는 내가 위에서 제시한 것처럼 깊고 복잡할 필요도 없다. 단순하게 시작할 수 있다. LLM에게 물어보라. 예를 들어 색상 택소노미를 이렇게 시작할 수 있다.
7가지 기본색을 가져와라. 그리고 각 색상 아래에 그 색의 하위 유형을 만들어라. 다음 형태의 디렉터리 트리처럼 리스트를 생성하라:
PRIMARY / SECONDARY
RED / Crimson
RED / Scarlet
RED / Burgundy
RED / Maroon
RED / Brick Red
RED / Cherry
ORANGE / Tangerine
ORANGE / Amber
ORANGE / Coral
ORANGE / Burnt Orange
ORANGE / Peach
ORANGE / Apricot
...
시작으로는 괜찮다.
대체로는 단순하게 시작하라.
Furniture
나
Baby & Kids
같은 넓은 상위 카테고리부터. 그러다 보면 그 카테고리가 너무 커지고 너무 다양해졌다는 걸 깨닫게 된다. 콘텐츠에서 자연스러운 분리가 보일 수 있다:
Baby & Kids / Toddler & Kids Playroom
Baby & Kids / Toddler & Kids Bedroom Furniture
어느 시점이 되면
Novelty Rocking Horse
와
Classic Rocking Horse
의 구분이 어떤 이유로 중요해진다.
여기에는 결함이 있을 수 있다: 각 제품이 단일 카테고리에만 속한다는 가정, 즉 카테고리가 상호 배타적이라는 가정이다. 현실은 지저분하다. 어떤 제품은 카테고리 경계에 걸칠 수 있다. 예를 들어 우스꽝스럽게 작은 소파는 다음 둘 다로 합리적으로 태깅될 수 있다:
Baby & Kids / Toddler & Kids Bedroom Furniture
또는
Living Room / Loveseats
패션에서는 어떤 액티브웨어가 속옷이기도 하다. 애슬레저(athleisure)는 또 어떨까?
한 가지 해결책은 아이템을 이중 분류하는 것이다. 두 분류를 모두 부여하되, 한 분류를 다른 분류보다 선호하도록 가중치를 둘 수 있다.
마지막으로 우려할 점: 가구를 분류하는 ‘공식적이고 순수한’ 방식과, 검색 사용자가 제품을 분류하는 방식은 큰 차이가 있다. 가구 전문가용 택소노미는 매우 정교하고(정확한) 분류를 제공할 수 있다. 하지만 사용자 사고방식과 맞는 더 ‘지저분한’ 분류가 결국 더 유용할 수 있다.
LLM으로 택소노미 분류기를 만드는 것은(내가 전체 강의까지 만든 주제이고, отдель пост로도 충분한 주제다) 여기서는 간단한 접근 몇 가지를 빠르게 훑어보자.
흥미롭게도 임베딩이 여기서 도움이 된다. 검색에서 임베딩의 ‘스위트 스폿’은 직접적인 랭킹/리트리벌이 아니라 더 나은 분류기를 만드는 데 있을지도 모른다.
예를 들어 제품들이 몇 천 개의 고유 카테고리에 속한다고 하자. 각 카테고리의 임베딩을 계산해 작은 인메모리 벡터 배열에 저장해 둔다. 이를 쿼리의 카테고리를 찾아내는 데 쓴다.
이제 “hobby horse” 쿼리를 임베딩으로 인코딩하고, 가장 유사한 카테고리 임베딩을 찾는다. 그러면
Baby & Kids / Toddler & Kids Playroom / Indoor Play / Rocking Horses
를 얻을 수 있다.
정확도가 부족하다면? 한 단계 올려보자.
분류 전에 LLM에게 ‘가짜 라벨’을 환각(hallucinate)하게 할 수 있다. 다음을 LLM에게 요청하자:
Be creative and hallucinate a set of classifications for the query below that
look like the real classifications.
Product classifications might look like:
'Furniture / Living Room Furniture / Coffee Tables & End Tables / Coffee Tables'
'Décor & Pillows / Decorative Pillows & Blankets / Throw Pillows'
'Furniture / Bedroom Furniture / Dressers & Chests'
'Outdoor / Outdoor & Patio Furniture / Patio Furniture Sets / Patio Conversation Sets'
'Home Improvement / Bathroom Remodel & Bathroom Fixtures / Bathroom Vanities / All Bathroom Vanities'
'Lighting / Wall Lights / Bathroom Vanity Lighting'
'Kitchen & Tabletop / Kitchen Organization / Food Storage & Canisters'
'School Furniture and Supplies / School Furniture / School Chairs & Seating / Stackable Chairs',
'Baby & Kids / Toddler & Kids Bedroom Furniture / Kids Beds',
If you feel inspired, return many unique values in a list. Be creative.
Cast a wide net with related but diverse categories.
Here's the query to invent categories for:
hobby horse
환각이 ‘필요할 때’는 아주 작은 모델로도 충분히 할 수 있다. 정확도를 LLM에 의존하는 게 아니라, 창의성과 그럴듯한 언어 생성에만 의존하기 때문이다.
이를 수행하면 다음 같은 분류를 얻을 수도 있다:
'Baby & Kids / Toys / Pretend Play & Dress Up / Pretend Play Toys / Hobby Horses'
더 설명적이고, 실제 분류에 쓰이는 언어에 더 가깝다. 그래서 실제의 고품질 분류를 되돌려받을 가능성이 더 크다.
이건 맛보기일 뿐이다. LLM과 임베딩 덕분에 대규모 멀티 라벨 분류기를 만드는 현대적 환경은 어느 때보다 쉬워졌다.
임베딩은 훌륭하다. 하지만 “시맨틱 검색”을 만들기 위해 임베딩이 반드시 필요한 것은 아니다. 오히려 엄밀하고 정밀한 매칭이 필요한 문제에서는 역효과일 수도 있다. 내가 주장했듯, 임베딩은 문제를 바라보는 잘못된 렌즈일 수 있다.
모든 것을 처음부터 다시 만들 필요는 없을지도 모른다. 대신 당신이 이미 잘하는 것을 활용하라. 거기에 LLM을 더하라. 더 체계적인 시맨틱 검색 접근을 택한다고 해서 변명할 필요는 없다.
Cheat at Search with LLMs에서 LLM을 검색 애플리케이션에 적용하는 방법을 함께 배워보길 바란다. 미리보기로는 이 포스트를 확인해 보라.

Twitter | LinkedIn | 뉴스레터 | Bsky
내 새 코스 수강하기 - Cheat at Search with LLMs
© 2026 SoftwareDoug LLC