벡터 스토어란 무엇인가요?
벡터 스토어란 무엇인가요? (Preview)
벡터 데이터베이스는 자연어 처리(NLP), 컴퓨터 비전(CV), 추천 시스템(RS)처럼 '의미를 이해하고 매칭해야 하는' 작업에서 정말 유용해요. 그중에서도 LLM(대규모 언어 모델) 응답 품질을 높이는 용도로 많이 쓰이죠.
LLM은 답을 만들 때 자꾸 틀리거나, 근거가 약하거나, 모순된 말을 하거나 하는 문제를 겪기 쉬워요. 정확한 최신 정보를 보충해 주면 이런 문제를 크게 줄일 수 있어요. 벡터 데이터베이스에 도메인 관련 주제·키워드·사실·의견·출처를 저장해 두면, 질문과 관련된 부분만 빠르게 찾을 수 있고, 그 정보를 프롬프트와 함께 LLM에 넘겨서 더 정확한 콘텐츠를 만들 수 있죠.
예를 들어 '최신 AI 트렌드'에 대한 블로그 포스트를 쓰고 싶다면, 그 주제의 최신 정보를 벡터 데이터베이스에 저장해 두고 질문과 함께 LLM에 전달하면 최신 내용을 반영한 글을 얻을 수 있어요.
Semantic Kernel은 이런 벡터 스토어를 다루는 추상화(abstraction) 와, 각 데이터베이스용으로 이 추상화를 구현한 아웃-오브-더-박스 구현체들을 제공해요. 레코드 컬렉션의 생성·목록 조회·삭제, 그리고 레코드의 업로드·검색·삭제가 가능하죠. 이 추상화 덕분에 무료나 로컬 호스팅 벡터 스토어로 먼저 실험해 보고, 규모가 필요해질 때 서비스로 쉽게 바꿀 수 있어요.
RAG에 벡터 스토어 쓰기
벡터 스토어 추상화는 데이터를 넣고 꺼내는 저수준 API예요. 그러면서도 Semantic Kernel은 어떤 벡터 스토어 구현이든 RAG에 쓸 수 있게 기본 지원을 해 줘요. Python 기준으로는 VectorSearchBase[TKey, TModel]를 VectorizedSearchMixin[TModel], VectorizableTextSearchMixin[TModel], VectorTextSearch[TModel] 중 하나로 감싸서 텍스트 검색(Text Search) 구현으로 노출하는 방식이에요. 즉 검색 기능 하나만으로도 LLM 앱에 RAG를 얹을 수 있는 거죠.
잠깐만요, '레거시 메모리 스토어'에 대한 정보를 찾고 계신 거라면 메모리 스토어 페이지로 이동해 주세요.
벡터 스토어 추상화 구조
추상화의 핵심은 세 인터페이스예요.
- VectorStore: 컬렉션 전체에 걸친 연산을 담당해요. 대표적으로
ListCollectionNames로 컬렉션 이름 목록을 조회하죠. 또한VectorStoreCollection<TKey, TRecord>인스턴스를 얻는 기능도 제공해요. - VectorStoreCollection<TKey, TRecord>: 하나의 컬렉션을 표현해요. 컬렉션이 존재하는지 확인하고, 생성하거나 삭제할 수 있어요. 레코드를 upsert·조회·삭제할 수 있고,
IVectorSearchable<TRecord>를 상속받아 벡터 검색 능력도 갖추고 있어요. - IVectorSearchable
: SearchAsync<TRecord>를 제공해요. 두 가지 방식으로 검색할 수 있는데, 하나는 등록된 임베딩 생성기(또는 DB가 지원하면 DB 자체)가 벡터화할 수 있는 입력을 받는 벡터 검색이고, 다른 하나는 이미 만들어진 벡터를 직접 입력받는 벡터 검색이에요.
데이터 모델 정의하기
벡터 스토어 추상화는 모델 우선(model first) 접근 방식을 써요. 즉 상호작용의 첫 단계가 저장 스키마에 매핑되는 데이터 모델을 정의하는 거예요. 각 속성이 어떤 역할인지 표시하기 위해 모델에 주석을 붙이면, 구현체가 컬렉션을 만들고 스키마에 맞게 매핑해 줘요.
Python에서는 vectorstoremodel 데코레이터와 VectorStoreField 어노테이션을 써서 모델을 정의합니다.
from dataclasses import dataclass, field
from typing import Annotated
from semantic_kernel.data.vector import (
DistanceFunction,
IndexKind,
VectorStoreField,
vectorstoremodel,
)
@vectorstoremodel
@dataclass
class Hotel:
hotel_id: Annotated[str, VectorStoreField('key')] = field(default_factory=lambda: str(uuid4()))
hotel_name: Annotated[str, VectorStoreField('data', is_filterable=True)]
description: Annotated[str, VectorStoreField('data', is_full_text_searchable=True)]
description_embedding: Annotated[list[float], VectorStoreField('vector', dimensions=4, distance_function=DistanceFunction.COSINE, index_kind=IndexKind.HNSW)]
tags: Annotated[list[str], VectorStoreField('data', is_filterable=True)]
C#에서는 [VectorStoreKey], [VectorStoreData], [VectorStoreVector] 어트리뷰트를 씁니다.
using Microsoft.Extensions.VectorData;
public class Hotel
{
[VectorStoreKey]
public ulong HotelId { get; set; }
[VectorStoreData(IsIndexed = true)]
public string HotelName { get; set; }
[VectorStoreData(IsFullTextIndexed = true)]
public string Description { get; set; }
[VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineSimilarity, IndexKind = IndexKind.Hnsw)]
public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }
[VectorStoreData(IsIndexed = true)]
public string[] Tags { get; set; }
}
데이터 모델을 정의하는 방법은 데이터 모델 정의하기에서, 어트리뷰트 대신 레코드 정의로 스키마를 정의하는 대안은 레코드 정의로 스키마 정의하기에서 다뤄요.
DB에 연결하고 컬렉션 선택하기
데이터 모델을 정했다면, 원하는 DB의 VectorStore 인스턴스를 만들고 레코드 컬렉션을 선택할 차례예요. DB마다 지원하는 키·레코드 타입이 다르므로 제네릭으로 키와 레코드 타입을 지정해요. 아래는 Qdrant 예시입니다. hotel_id가 str 타입이고 Qdrant가 str·int 키만 지원하므로 키 타입을 str로 잡았어요.
from semantic_kernel.connectors.qdrant import QdrantCollection
# 컬렉션 생성 시 제네릭 파라미터로 키와 레코드 타입을 지정합니다.
collection: QdrantCollection[str, Hotel] = QdrantCollection(
record_type=Hotel,
collection_name="skhotels" # 선택 사항, vectorstoremodel 데코레이터에 지정할 수도 있어요.
)
컬렉션이 존재하지 않으면 만들어 주고, 레코드를 upsert한 뒤 다시 조회하는 흐름은 이렇게 돼요.
# 컬렉션이 없다면 생성합니다.
await collection.ensure_collection_exists()
# 레코드를 upsert합니다.
description = "A place where everyone can be happy."
hotel_id = "1"
await collection.upsert(Hotel(
hotel_id = hotel_id,
hotel_name = "Hotel Happy",
description = description,
description_embedding = await GenerateEmbeddingAsync(description),
tags = ["luxury", "pool"]
))
# upsert한 레코드를 다시 조회합니다.
retrieved_hotel = await collection.get(hotel_id)
벡터 검색하기
search 메서드는 컬렉션의 레코드를 검색해요. 모델이나 컬렉션에 설정된 임베딩 생성기를 통해 벡터화되는 문자열을 받을 수도 있고, 이미 생성된 벡터를 직접 받을 수도 있어요.
# 검색을 수행합니다.
search_result = await collection.search("I'm looking for a hotel where customer happiness is the priority.", vector_property_name="description_embedding", top=3)
# 반환된 호텔들을 확인합니다.
async for result in search_result.results:
print(f"Found hotel description: {result.record.description}")
검색 함수 만들기
LLM의 함수 호출(function calling)에서 쓸 간단한 검색 함수는 create_search_function 메서드로 만들 수 있어요. 함수 이름·설명·파라미터 설명들은 LLM에 전달되는 함수 시그니처를 만드는 데 쓰여서, LLM이 올바른 함수 호출을 생성하도록 튜닝하는 용도로 유용해요. 기본은 search_type="vector"입니다.
collection.create_search_function(
function_name="hotel_search",
description="A hotel search engine, allows searching for hotels in specific cities, "
"you do not have to specify that you are searching for hotels, for all, use `*`.",
search_type="keyword_hybrid", # 기본값은 "vector" 입니다
parameters=[
KernelParameterMetadata(
name="query",
description="The terms you want to search for in the hotel database.",
type="str",
is_required=True,
type_object=str,
),
KernelParameterMetadata(
name="tags",
description="The tags you want to search for in the hotel database, use `*` to match all.",
type="str",
type_object=str,
default_value="*",
),
KernelParameterMetadata(
name="top",
description="Number of results to return.",
type="int",
default_value=5,
type_object=int,
),
],
# 레코드를 문자열로 변환하는 string_mapper 함수를 지정해
# LLM에 필요한 정보만 전달되도록 합니다.
string_mapper=lambda x: f"Hotel {x.record.hotel_name}: {x.record.description}. Tags: {x.record.tags} (hotel_id: {x.record.hotel_id}) ",
)
출처: https://learn.microsoft.com/en-us/semantic-kernel/concepts/vector-store-connectors/