벡터 스토어 커넥터용 임베딩 생성하기
벡터 스토어 커넥터용 임베딩 생성하기
벡터 스토어 커넥터로 데이터를 넣을 때 임베딩을 어떻게 만들지가 관건이에요. Semantic Kernel은 크게 두 가지 경로를 지원해요.
- 개발자가 임베딩을 직접 만들어 레코드에 담아 전달하기
- 벡터 스토어가 내부에서 자동으로 생성해 주기
후자를 쓰면 upsert와 검색 양쪽에서 임베딩이 자동 생성되어, 수동 전처리 단계가 사라져요. 먼저 자동 생성부터 살펴볼게요.
벡터 스토어가 임베딩을 생성하게 하기
벡터 저장소에 임베딩 생성기를 설정하면, upsert와 검색 중에 자동으로 벡터가 만들어져요. 자동 생성이 되게 하려면 데이터 모델의 벡터 속성을 소스 타입(예: string)으로 정의하되, 여전히 VectorStoreVectorPropertyAttribute 같은 어트리뷰트로 장식하면 돼요.
[VectorStoreVector(1536)]
public string Embedding { get; set; }
upsert 전에 Embedding 속성에는 벡터로 만들 문자열이 들어 있어야 해요. DB에 저장될 벡터 타입(예: float32, float16)은 설정한 임베딩 생성기에서 파생돼요.
[!IMPORTANT] 이렇게 자동 생성된 벡터 속성은 생성된 벡터나 원본 텍스트를 다시 조회할 수 없어요. 원본 텍스트를 저장해야 한다면 따로 Data 속성을 추가해서 저장해야 합니다.
Microsoft.Extensions.AI 추상화를 구현한 임베딩 생성기는 여러 단계에서 설정할 수 있어요.
- Vector Store에 설정 — 벡터 스토어 전체의 기본 임베딩 생성기로, 모든 컬렉션·속성에 적용되고 하위에서 재정의하지 않으면 사용돼요.
- 컬렉션에 설정 — 특정 컬렉션만의 임베딩 생성기로, 스토어 레벨 생성기를 덮어써요.
- 레코드 정의에 설정 —
VectorStoreCollectionDefinition으로 속성을 프로그래밍 방식으로 정의할 때 모든 속성에 적용돼요. - 벡터 속성 정의에 설정 — 속성을 프로그래밍 방식으로 정의할 때 속성 하나하나에 직접 넣을 수 있어요.
컬렉션 레벨 설정에서 쓰는 구조는 이렇게 생겼어요. 임베딩 생성기는 OpenAI 클라이언트에서 AsIEmbeddingGenerator()로 만들어요.
using Microsoft.Extensions.AI;
using CommunityToolkit.VectorData.Qdrant;
using OpenAI;
using Qdrant.Client;
var embeddingGenerator = new OpenAIClient("your key")
.GetEmbeddingClient("your chosen model")
.AsIEmbeddingGenerator();
var collectionOptions = new QdrantCollectionOptions
{
EmbeddingGenerator = embeddingGenerator
};
var collection = new QdrantCollection<ulong, MyRecord>(
new QdrantClient("localhost"),
"myCollection",
ownsClient: true,
collectionOptions);
자동 생성 예시
아래 예시는 upsert와 검색 도중 임베딩이 자동으로 만들어지는 흐름을 보여줘요. 흥미로운 점은 벡터 속성이 string 타입이고 Embedding => this.Text처럼 다른 속성 값을 그대로 쓰는 read-only 속성이라는 거예요. 즉 개발자는 벡터를 전혀 신경 쓸 필요가 없어요.
[!IMPORTANT] 이 패턴에서는
Embedding벡터 속성으로 생성된 벡터나 원본 텍스트를 다시 가져올 수 없어요. 원본 텍스트 저장이 필요하면 별도 Data 속성을 두세요.
// 데이터 모델
internal class FinanceInfo
{
[VectorStoreKey]
public string Key { get; set; } = string.Empty;
[VectorStoreData]
public string Text { get; set; } = string.Empty;
// 벡터 속성이 string 타입이고, 값은 Text 속성에서 나온다.
// upsert 시 문자열이 벡터로 변환되어 DB에 벡터로 저장된다.
[VectorStoreVector(1536)]
public string Embedding => this.Text;
}
// OpenAI 임베딩 생성기를 만들어 벡터 스토어에 연결한다.
var embeddingGenerator = new OpenAIClient("your key")
.GetEmbeddingClient("your chosen model")
.AsIEmbeddingGenerator();
var vectorStore = new InMemoryVectorStore(new() { EmbeddingGenerator = embeddingGenerator });
var collection = vectorStore.GetCollection<string, FinanceInfo>("finances");
await collection.EnsureCollectionExistsAsync();
// 테스트 데이터 준비
string[] budgetInfo =
{
"The budget for 2020 is EUR 100 000",
"The budget for 2021 is EUR 120 000",
"The budget for 2022 is EUR 150 000",
"The budget for 2023 is EUR 200 000",
"The budget for 2024 is EUR 364 000"
};
// upsert 시 임베딩이 자동 생성된다.
var records = budgetInfo.Select((input, index) => new FinanceInfo { Key = index.ToString(), Text = input });
await collection.UpsertAsync(records);
// 검색용 임베딩도 검색 시 자동 생성된다.
var searchResult = collection.SearchAsync("What is my budget for 2024?", top: 1);
await foreach (var result in searchResult)
{
Console.WriteLine($"Key: {result.Record.Key}, Text: {result.Record.Text}");
}
직접 임베딩 생성하기
임베딩을 직접 만드는 경로에서는 ITextEmbeddingGenerationService를 활용해요. Microsoft.SemanticKernel.Embeddings 네임스페이스의 GenerateEmbeddingAsync로 문자열을 벡터로 바꿀 수 있어요.
upsert 시에 직접 생성해 넣는 코드를 보면, 임베딩 생성기와 컬렉션을 받아서 임베딩을 만든 뒤 레코드에 담아 upsert해요.
public async Task GenerateEmbeddingsAndUpsertAsync(
ITextEmbeddingGenerationService textEmbeddingGenerationService,
VectorStoreCollection<ulong, Hotel> collection)
{
// 레코드 하나를 upsert하려 한다.
string descriptionText = "A place where everyone can be happy.";
ulong hotelId = 1;
// 임베딩 생성
ReadOnlyMemory<float> embedding =
await textEmbeddingGenerationService.GenerateEmbeddingAsync(descriptionText);
// 이미 생성한 임베딩을 담아 upsert
await collection.UpsertAsync(new Hotel
{
HotelId = hotelId,
HotelName = "Hotel Happy",
Description = descriptionText,
DescriptionEmbedding = embedding,
Tags = new[] { "luxury", "pool" }
});
}
검색 시에도 동일하게, 검색 질의 텍스트를 먼저 벡터로 만든 뒤 그 벡터로 SearchAsync를 호출해요.
public async Task GenerateEmbeddingsAndSearchAsync(
ITextEmbeddingGenerationService textEmbeddingGenerationService,
VectorStoreCollection<ulong, Hotel> collection)
{
string descriptionText = "Find me a hotel with happiness in mind.";
// 검색용 임베딩 생성
ReadOnlyMemory<float> searchEmbedding =
await textEmbeddingGenerationService.GenerateEmbeddingAsync(descriptionText);
// 이미 생성한 임베딩으로 검색
IAsyncEnumerable<VectorSearchResult<Hotel>> searchResult = collection.SearchAsync(searchEmbedding, top: 1);
List<VectorSearchResult<Hotel>> resultItems = await searchResult.ToListAsync();
Console.WriteLine("Score for first result: " + resultItems.FirstOrDefault()?.Score);
Console.WriteLine("Hotel description for first result: " + resultItems.FirstOrDefault()?.Record.Description);
}
임베딩 차원(dimensions) 다루기
벡터 데이터베이스는 보통 컬렉션을 만들 때 각 벡터의 차원 수를 지정하길 요구해요. 임베딩 모델마다 차원 수가 달라요. 예를 들어 OpenAI text-embedding-ada-002는 1536차원 벡터를 만들고, Google text-embedding-004는 기본 768차원이지만 1~768 사이 아무 값이나 고를 수 있어요.
항상 임베딩 모델이 만든 벡터 차원과 DB의 대응 벡터 차원을 일치시켜야 해요. Semantic Kernel 벡터 스토어 추상화로 컬렉션을 만들 땐, 각 벡터 속성에 필요한 차원 수를 어트리뷰트 또는 레코드 정의로 지정해 주면 됩니다.
[VectorStoreVector(Dimensions: 1536)]
public ReadOnlyMemory<float>? DescriptionEmbedding { get; set; }
new VectorStoreVectorProperty("DescriptionEmbedding", typeof(float), dimensions: 1536);