Redis 벡터 유사도 검색
Redis 벡터 유사도 검색 (Vector similarity)
벡터 검색은 단어가 아니라 '의미'로 데이터를 찾는 방법이에요. 텍스트를 벡터(숫자로 된 리스트)로 바꾸고, 서로 의미가 가까운 벡터끼리 공간상 가까이 위치한다는 점을 이용해서 검색합니다. 이 문서는 Redis Search 튜토리얼의 마지막 단계로, 지금까지 만든 카탈로그·인덱스·검색 문법 위에 벡터 검색을 얹는 과정을 보여줘요. 커머스 검색처럼 '정확한 단어'보다 '바라는 의미'로 찾아야 할 때 특히 유용하죠.
참고: 이 문서에서 하이브리드 검색 부분은
FT.HYBRID를 사용하는데, 이는 Redis 8.8 이상이 필요해요. 벡터 검색(FT.SEARCH) 자체는 Redis Search가 있는 이전 버전에서도 동작합니다.
벡터 검색이 뭔지 먼저
지금까지는 상품을 '포함하는 단어'와 '필드의 정확한 값'으로 매칭했죠. 그런데 "달리면서 음악 들을 것"을 찾는 고객은 headphones나 earbuds라는 단어를 쓰지 않아요. 키워드 검색이라면 그 상품을 놓칠 수밖에 없죠. 벡터 검색은 이 문제를 '단어의 의미'로 해결합니다.
동작 원리는 이렇게 세 단계예요.
- 각 상품에 대해 임베딩(embedding)을 생성하고 문서에 저장한다. 여기서는 상품 설명을 임베딩합니다.
- 인덱스에
VECTOR필드를 추가해서 Redis가 그 임베딩을 검색할 수 있게 한다. - 쿼리 시점에 검색 문구를 임베딩하고, Redis에게 그 벡터와 가장 가까운 상품을 물어본다.
여기서 '가까움'은 거리 메트릭(distance metric)으로 측정해요. 이 튜토리얼은 **코사인 거리(cosine distance)**를 쓰는데, 거리가 작을수록 더 유사하다는 뜻입니다.
임베딩 생성하고 저장하기
임베딩은 모델이 만들어내므로 이 단계는 redis-cli가 아니라 클라이언트 라이브러리를 써요. 아래 예시는 Python의 SentenceTransformers 프레임워크로 각 상품 설명을 임베딩한 뒤 $.embedding 필드에 저장합니다. 여기서 쓰는 모델은 768차원 벡터를 만들어요.
from redis import Redis
from sentence_transformers import SentenceTransformer
r = Redis(host="localhost", port=6379, decode_responses=True)
embedder = SentenceTransformer("msmarco-distilbert-base-v4") # 768-dimensional vectors
# Embed each product's description and store it on the document.
for key in r.scan_iter(match="product:*"):
description = r.json().get(key, "$.description")[0]
embedding = embedder.encode(description).astype("float32").tolist()
r.json().set(key, "$.embedding", embedding)
Redis는 벡터를 해시(hash)나 JSON 문서 어디든 저장할 수 있어요. 이 튜토리얼은 JSON을 쓰므로 각 임베딩이 숫자 배열로 저장되고, 결과적으로 모든 상품이 기존 속성 옆에 embedding 필드를 갖게 됩니다.
인덱스에 벡터 필드 추가하기
앞서 만든 인덱스는 새 embedding 필드를 모르는 상태예요. VECTOR 필드를 포함하도록 다시 만들어야 합니다. 인덱스를 내려도 문서는 삭제되지 않으니, 새 인덱스가 만들어지는 순간 방금 저장한 임베딩이 바로 색인됩니다.
> FT.DROPINDEX idx:catalog
OK
> FT.CREATE idx:catalog ON JSON PREFIX 1 product: SCHEMA $.name AS name TEXT $.brand AS brand TAG SORTABLE $.category AS category TAG $.description AS description TEXT $.price AS price NUMERIC SORTABLE $.rating AS rating NUMERIC SORTABLE $.features[*] AS features TAG $.embedding AS embedding VECTOR FLAT 6 TYPE FLOAT32 DIM 768 DISTANCE_METRIC COSINE
OK
벡터 필드 정의를 풀어보면 이래요. $.embedding을 VECTOR 필드로 색인하는데, 알고리즘은 FLAT이고 뒤에 속성 6개가 따라옵니다 — TYPE FLOAT32, DIM 768(모델의 차원), DISTANCE_METRIC COSINE. FLAT은 정확한 검색을 수행해서 작은 데이터셋에 좋은 기본값이에요. 데이터가 크다면 HNSW를 고르면 됩니다. 전체 옵션은 벡터 검색 개념 문서에서 다뤄요.
K-최근접 이웃 (KNN) 검색
KNN 쿼리는 쿼리 벡터에 가장 가까운 임베딩 k개를 요청하는 거예요. 검색 문구를 같은 모델로 임베딩한 뒤, 그 결과 벡터를 FT.SEARCH에 넘기면 됩니다.
FT.SEARCH idx:catalog "(*)=>[KNN 3 @embedding $query_vector AS score]" PARAMS 2 query_vector "\x9a\x99\x19\x3f..." SORTBY score ASC RETURN 2 score name DIALECT 2
각 부분이 무슨 일을 하는지 볼게요.
(*)는 벡터 검색 전에 실행되는 **사전 필터(pre-filter)**예요.(*)는 "모든 상품을 고려한다"는 뜻이고, 여기에 아무 쿼리나 넣어서 후보를 제한할 수 있어요 (바로 아래 사전 필터 절에서 확인).=>[KNN 3 @embedding $query_vector AS score]는embedding필드에서 최근접 이웃 3개를 요청하고, 각 결과의 거리 점수를score라는 이름으로 붙이는 부분이에요.PARAMS 2 query_vector "..."는 쿼리 벡터의 이진값을 전달해요.2는 뒤에 인자 두 개(파라미터 이름과 값)가 온다는 뜻입니다.SORTBY score ASC는 거리가 가까운 순(처음이 가장 가까움)으로 정렬하고,DIALECT 2는 벡터 검색이 요구하는 쿼리 다이얼렉트를 선택해요.
참고: 쿼리 벡터의 이진값은 길어서 위 예시에서는 짧게 줄였어요. 실제 애플리케이션에서는 위 임베딩 단계에서처럼 클라이언트 라이브러리가 모델 출력으로부터 이 값을 만들어 줍니다.
예를 들어 "portable music for the outdoors" 같은 문구로 검색하면, 쿼리와 정확한 키워드를 공유하지 않아도 의미상 가장 가까운 상품들 — 휴대용 스피커와 이어버드 — 이 높은 순위로 나옵니다.
후보군 사전 필터링
사전 필터는 벡터 검색과 아는 필터링이 만나는 지점이에요. (*) 대신 아무 FT.SEARCH 쿼리나 넣으면 부분집합 안에서 유사한 상품을 찾습니다. 아래는 Audio 상품만 골라서 최근접 3개를 찾는 예시예요.
FT.SEARCH idx:catalog "(@category:{Audio})=>[KNN 3 @embedding $query_vector AS score]" PARAMS 2 query_vector "\x9a\x99\x19\x3f..." SORTBY score ASC RETURN 2 score name DIALECT 2
하이브리드 검색 (Hybrid search)
키워드 검색과 벡터 검색은 저마다 강점이 있어요. 키워드는 사용자가 정확한 용어를 알 때 정밀하고, 벡터는 사용자가 원하는 바를 자기 말로 표현할 때 관대하죠. 하이브리드 검색은 둘을 동시에 실행하고 결과를 융합해서, 각각의 장점을 취합니다.
FT.HYBRID 명령은 SEARCH 절(FT.SEARCH와 똑같은 전문 쿼리)과 VSIM 절(벡터 유사도 쿼리)을 받아 순위를 결합해요. 아래는 키워드 wireless를 찾으면서, 동시에 쿼리 벡터(여기서는 "wireless headphones for listening to music"의 임베딩)에 의미상 유사한 상품을 찾는 예시입니다.
FT.HYBRID idx:catalog SEARCH "wireless" VSIM @embedding $query_vector KNN 2 K 5 LOAD 1 @name PARAMS 2 query_vector "\x9a\x99\x19\x3f..."
KNN 예시와 마찬가지로 쿼리 벡터의 이진값은 위에서 짧게 줄였고, 클라이언트 라이브러리가 모델 출력에서 만들어 줍니다.
결과는 두 순위를 섞어요. 말 그대로 wireless를 언급하는 상품과, 쿼리 벡터와 의미상 가장 가까운 상품을 함께 보여주는 거죠. 이 쿼리에서는 무선 헤드폰과 이어버드가 위로 올라옵니다 — 키워드와 의미를 모두 만족하거든요 — 그 뒤로 다른 무선 상품들, 그리고 휴대용 스피커 같은 가장 가까운 의미 매칭이 따라옵니다.
기본적으로 FT.HYBRID는 Reciprocal Rank Fusion이라는 방식으로 두 순위를 융합해요. COMBINE 절로 균형을 조절할 수 있고, 애그리게이션에서 쓰던 것처럼 FILTER, LOAD, APPLY, SORTBY 단계도 추가할 수 있습니다. 전체 문법은 FT.HYBRID 레퍼런스를 보세요.
정리하며
이번 튜토리얼을 마치면 빈 데이터베이스에서 시작해 하이브리드 의미 검색(RAG 검색의 핵심)까지 단계적으로 올라오게 됐어요. 그동안 JSON 문서로 데이터를 모델링하고, TEXT·TAG·NUMERIC 필드 타입으로 인덱스를 만들고, FT.SEARCH로 검색·필터·프로젝션을 다루고, FT.AGGREGATE로 묶고 요약했죠. 그리고 마지막으로 벡터 KNN으로 의미를 검색하고 FT.HYBRID로 키워드와 결합하는 법까지 배웠습니다.
더 알아보기 (Learn more)
- 쿼리 심화 — 쿼리 문서에서 퍼지 매칭, 지오스페이셜 쿼리, 스코어링 등을 더 다룹니다.
- 벡터 튜닝 — 벡터 검색 개념 문서에서
FLAT과HNSW인덱스 타입, 벡터 범위 쿼리, 둘 사이의 선택 기준을 설명해요. - 벡터 전용 라이브러리 — RedisVL은 Redis 위에서 벡터 검색과 AI 애플리케이션을 만드는 더 높은 수준의 Python API를 제공합니다.
- AI 애플리케이션 — RAG 퀵 스타트와 Redis for AI에서 Redis가 검색 증강 생성(RAG)을 어떻게 지원하는지 볼 수 있어요.
- 참고 — 전체 검색 인덱스 없이 독립형 유사도 검색이 필요하다면, Redis가 제공하는 vector sets 데이터 타입도 확인해 보세요.