왜 나는 벡터 검색 위에 LLM 파서를 추가했는가 (그리고 그것이 바꾼 점)
Source: Dev.to
위에 제공된 소스 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역을 원하는 본문을 알려주시면 한국어로 번역해 드리겠습니다.
벡터 검색만으로 충분하다고 생각했어요
저는 Queryra 를 만들었습니다 – WooCommerce와 Shopify용 AI‑search 플러그인입니다.
키워드 매칭을 의미론적 임베딩으로 대체했기 때문에 고객은 “something warm for winter” 와 같이 입력하고 바로 스웨터, 플리스 재킷, 담요 등을 찾을 수 있었습니다. 결과가 전혀 없는 검색은 드물게 발생했죠. 잘 작동했습니다… 누군가가 다음과 같이 검색할 때까지:
“wireless headphones under $80, not Beats”
벡터 검색은 무선 헤드폰을 반환했지만, 그 중 많은 제품이 $200이고 몇 개는 Beats였습니다. 가격 제한과 브랜드 제외 조건이 임베딩 모델에 전혀 반영되지 않았습니다.
그때 저는 깨달았습니다: 벡터 검색은 레이어 1에 불과합니다. 저는 레이어 2를 놓치고 있었던 것이죠.
순수 벡터 검색의 문제점
임베딩은 한 가지에 뛰어납니다: 의미적 유사성을 인코딩하는 것.
- “Sneakers”(스니커즈)는 “trainers”(운동화)와 “running shoes”(러닝화)와 가깝게 위치합니다.
- “Gift for dad”(아빠에게 줄 선물)는 정원 도구, 바베큐 세트, 시계 등을 찾아줍니다 – 쿼리에 해당 단어가 없어도 말이죠.
하지만 “laptop under $1000 for video editing, not Chromebook”(비디오 편집용 노트북, 가격 $1000 이하, Chromebook 제외) 와 같은 쿼리는 근본적으로 두 종류의 정보를 담고 있습니다:
| 유형 | 설명 |
|---|---|
| 의미적 의도 | 고객이 원하는 것 (비디오 작업을 위한 강력한 노트북) |
| 구조적 제약 | 결과를 필터링하는 방법 (가격 제한, 카테고리 제외) |
임베딩은 의미적 의도를 잘 처리하지만 구조적 제약에 대한 메커니즘이 없습니다. “$1000 이하”를 벡터 공간의 방향으로 인코딩할 수 없으며, “Chromebook 제외”는 의미적 개념이 아니라 검색 시스템에 대한 지시이기 때문입니다. 모든 벡터 전용 구현은 이와 같은 사각지대를 가지고 있으며, 쿼리가 더 구체화될수록 문제는 심각해집니다.
가장 큰 피해자는 누구일까요? 바로 가장 높은 구매 의도를 가진 구매자들 – 즉, 지금 바로 구매를 결정하려는 사람들.
해결책: LLM 파서를 레이어 2로
벡터 검색보다 먼저 실행되는 쿼리 파서를 추가했습니다. 이 파서는 쿼리를 구조화된 구성 요소로 분해하는 역할을 합니다.
예시
{
"semantic_query": "organic shampoo",
"price_max": 25,
"attribute_exclude": ["sulfates"],
"sort_by": "rating"
}
각 구성 요소는 적절한 하위 시스템으로 전달됩니다:
| 구성 요소 | 대상 | 목적 |
|---|---|---|
semantic_query | 벡터 검색 | 의미적으로 관련된 제품을 찾음 |
price_max | 데이터베이스 필터 | $25에서 강제 제한 |
attribute_exclude | 포스트‑필터 | 황산염 함유 제품을 제거 |
sort_by | 결과 재정렬 | 평점이 가장 높은 제품을 먼저 표시 |
벡터 레이어는 고객이 의미하는 바를 파악하고; 파서 레이어는 고객이 말한 바를 적용합니다.
우회 문제 (지연)
파서는 약 700–800 ms의 지연을 추가합니다. “blue t‑shirt”와 같은 간단한 쿼리의 경우, 임베딩만으로 충분히 처리되므로 이 오버헤드는 불필요합니다.
라우팅 바로가기
import re
def should_parse(query: str) -> bool:
# Price signals
if re.search(r'under \$|below \$|\$\d+|budget|cheap|premium', query, re.I):
return True
# Exclusion signals
if re.search(r'\bnot\b|\bwithout\b|\bno\b|\bexclude\b', query, re.I):
return True
# Sorting signals
if re.search(r'best rated|top rated|newest|cheapest|most popular', query, re.I):
return True
# Brand signals (capitalized words that aren't at sentence start)
if re.search(r'(?<!^)(?<!\. )[A-Z][a-z]+(?:\s[A-Z][a-z]+)*', query):
return True
return False # Simple query — go straight to vector search
간단한 쿼리는 파서를 완전히 건너뜁니다. 복잡한 쿼리는 전체 의도 추출을 수행합니다. 라우팅은 사용자에게 보이지 않으며, 사용자는 더 나은 결과만 받게 됩니다.
변경 사항
| 쿼리 | 이전 (벡터만) | 이후 (벡터 + 파서) |
|---|---|---|
| “$80 이하 헤드폰” | 모든 헤드폰 | $80 이하 헤드폰만 |
| “BrandX 제외” | BrandX 포함 | BrandX 제외 |
| “최고 평점 커피 메이커” | 무작위 순서 | 평점 순으로 정렬 |
| “유기농, 황산염 무첨가” | 모든 유기농 샴푸 | 황산염‑무첨가 필터링 |
각 표의 첫 번째 행은 동일합니다 – 간단한 의미론적 쿼리는 동일하게 작동합니다. 다른 모든 행은 파서가 메우는 차이를 보여줍니다.
예상치 못한 한 가지 이점: 오타 + 제약조건
I expected the parser to help with structured queries, but it also solved a secondary problem: typos combined with constraints.
- Vector search 자체만으로도 오타를 잘 처리한다 – “moisturiser”가 “moisturizer”를 찾는다.
- “moisturiser under $20 without pareban”(오타가 난 parabens)은 임베딩 유사도가 오타가 난 용어에서 떨어져서 제외 조건이 깨졌다.
The LLM parser handles both in one pass: it corrects the typo, extracts the price constraint, and identifies the exclusion. This combined robustness was a pleasant surprise.
트레이드‑오프
파서는 복잡한 쿼리에서 LLM API 호출을 수행하기 때문에 비용이 발생합니다. 저는 gpt‑4.1‑nano를 사용합니다(gpt‑4o‑mini와 동일한 품질이며, 이 사용 사례에서는 약 33 % 저렴합니다). 우회 로직을 적용하면 전체 쿼리 중 일부만 파서를 통과하지만, 비용은 트래픽에 따라 여전히 증가합니다.
- 셀프‑호스팅 옵션: API 호출을 로컬 모델로 교체합니다(예: Ollama + Mistral 7B는 의도 추출에 꽤 잘 작동합니다).
- SaaS 제품: LLM 사용량을 가격에 반영합니다.
다음 단계
파서는 현재 다음을 추출합니다:
- 가격 범위
- 브랜드 언급
- 속성 필터 및 제외
- 정렬 선호도
- 기본 부정
다음 항목: 다중 의도 쿼리.
예시: “사무실용 제품과 체육관용 제품” – 두 개의 별도 의미 검색 결과를 병합합니다. 벡터 검색만으로는 의도를 분리할 수 없지만 파서는 가능합니다.
전자상거래 검색을 구축하면서 같은 문제에 직면한다면—첫 두 개의 의미 있는 단어 이후를 무시하는 벡터 결과—이 2단계 접근 방식은 추가 복잡성을 감수할 가치가 있습니다.
여기에서 매장 소유자를 위한 더 길고 비기술적인 버전을 작성했습니다: Why Vector Search Alone Isn’t Enough for Ecommerce Stores
질문에 기꺼이 답변해 드립니다!
Queryra는 WooCommerce와 Shopify용 AI 검색입니다. queryra.com