벡터 데이터베이스
벡터 데이터베이스 (Vector Databases)
벡터 데이터베이스는 AI 애플리케이션에서 핵심적인 역할을 하는 특수한 데이터베이스예요. 전통적인 관계형 데이터베이스와 달리 정확히 일치하는 값을 찾는 대신 **유사도 검색(similarity search)**을 수행해요. 쿼리 벡터가 주어지면 그와 "유사한" 벡터를 돌려주죠. 이 글에서는 Spring AI가 벡터 DB를 사용하는 방식과 VectorStore 인터페이스를 자세히 살펴볼게요.
출처: 문서
본문
벡터 데이터베이스 (Vector Databases)
벡터 데이터베이스는 AI 애플리케이션에서 필수적인 역할을 하는 특수한 데이터베이스 타입이에요. 벡터 데이터베이스에서 쿼리는 전통적인 관계형 데이터베이스와 달라요. 정확히 일치하는 값을 찾는 대신 유사도 검색을 수행해요. 벡터가 쿼리로 주어지면 벡터 데이터베이스는 쿼리 벡터와 "유사한" 벡터를 반환해요. 이런 유사도가 높은 수준에서 어떻게 계산되는지에 대한 자세한 내용은 Vector Similarity에 제공돼 있어요.
벡터 데이터베이스는 데이터를 AI 모델과 통합하는 데 사용돼요. 사용의 첫 단계는 데이터를 벡터 데이터베이스에 로드하는 것이에요. 그런 다음 사용자 질의를 AI 모델에 보낼 때, 먼저 유사한 문서 집합을 검색해요. 이 문서들은 사용자 질문의 컨텍스트가 되어 사용자 질의와 함께 AI 모델에 보내져요. 이 기법을 Retrieval Augmented Generation (RAG)이라고 해요.
다음 섹션에서는 여러 벡터 데이터베이스 구현과 몇 가지 높은 수준의 샘플 사용법에 대한 Spring AI 인터페이스를 설명해요. 마지막 섹션은 벡터 데이터베이스에서 유사도 검색의 기본 접근 방식을 명확히 설명하기 위한 것이에요.
API 개요 (API Overview)
이 섹션은 Spring AI 프레임워크 내의 VectorStore 인터페이스와 관련 클래스에 대한 안내예요. Spring AI는 VectorStore 인터페이스와 그 읽기 전용 대응물인 VectorStoreRetriever 인터페이스를 통해 벡터 데이터베이스와 상호작용하기 위한 추상화된 API를 제공해요.
VectorStoreRetriever 인터페이스 (VectorStoreRetriever Interface)
Spring AI는 문서 검색 기능만 노출하는 읽기 전용 인터페이스인 VectorStoreRetriever를 제공해요:
@FunctionalInterface
public interface VectorStoreRetriever {
List<Document> similaritySearch(SearchRequest request);
default List<Document> similaritySearch(String query) {
return this.similaritySearch(SearchRequest.builder().query(query).build());
}
}
이 함수형 인터페이스는 어떠한 변경 작업도 수행하지 않고 벡터 스토어에서 문서만 검색하면 되는 사용 사례를 위해 설계됐어요. 문서 검색에 필요한 기능만 노출함으로써 최소 권한(principle of least privilege) 원칙을 따르요.
VectorStore 인터페이스 (VectorStore Interface)
VectorStore 인터페이스는 VectorStoreRetriever를 확장하고 변경 기능을 추가해요:
public interface VectorStore extends DocumentWriter, VectorStoreRetriever {
default String getName() {
return this.getClass().getSimpleName();
}
void add(List<Document> documents);
void delete(List<String> idList);
void delete(Filter.Expression filterExpression);
default void delete(String filterExpression) { ... }
default <T> Optional<T> getNativeClient() {
return Optional.empty();
}
}
VectorStore 인터페이스는 읽기와 쓰기 작업을 모두 결합해서, 벡터 데이터베이스에서 문서를 추가·삭제·검색할 수 있게 해 줘요.
SearchRequest 빌더 (SearchRequest Builder)
public class SearchRequest {
public static final double SIMILARITY_THRESHOLD_ACCEPT_ALL = 0.0;
public static final int DEFAULT_TOP_K = 4;
private String query = "";
private int topK = DEFAULT_TOP_K;
private double similarityThreshold = SIMILARITY_THRESHOLD_ACCEPT_ALL;
@Nullable
private Filter.Expression filterExpression;
public static Builder from(SearchRequest originalSearchRequest) {
return builder().query(originalSearchRequest.getQuery())
.topK(originalSearchRequest.getTopK())
.similarityThreshold(originalSearchRequest.getSimilarityThreshold())
.filterExpression(originalSearchRequest.getFilterExpression());
}
public static class Builder {
private final SearchRequest searchRequest = new SearchRequest();
public Builder query(String query) {
Assert.notNull(query, "Query can not be null.");
this.searchRequest.query = query;
return this;
}
public Builder topK(int topK) {
Assert.isTrue(topK >= 0, "TopK should be positive.");
this.searchRequest.topK = topK;
return this;
}
public Builder similarityThreshold(double threshold) {
Assert.isTrue(threshold >= 0 && threshold <= 1, "Similarity threshold must be in [0,1] range.");
this.searchRequest.similarityThreshold = threshold;
return this;
}
public Builder similarityThresholdAll() {
this.searchRequest.similarityThreshold = 0.0;
return this;
}
public Builder filterExpression(@Nullable Filter.Expression expression) {
this.searchRequest.filterExpression = expression;
return this;
}
public Builder filterExpression(@Nullable String textExpression) {
this.searchRequest.filterExpression = (textExpression != null)
? new FilterExpressionTextParser().parse(textExpression) : null;
return this;
}
public SearchRequest build() {
return this.searchRequest;
}
}
public String getQuery() {...}
public int getTopK() {...}
public double getSimilarityThreshold() {...}
public Filter.Expression getFilterExpression() {...}
}
벡터 데이터베이스에 데이터를 넣으려면 Document 객체로 캡슐화해요. Document 클래스는 PDF나 Word 문서 같은 데이터 소스의 콘텐츠를 캡슐화하고 문자열로 표현된 텍스트를 포함해요. 또한 파일 이름 같은 세부 정보를 포함한 키-값 쌍 형태의 메타데이터도 갖고 있어요.
벡터 데이터베이스에 삽입하면 텍스트 콘텐츠는 임베딩 모델을 사용해 숫자 배열, 즉 float[]인 벡터 임베딩으로 변환돼요. Word2Vec, GLoVE, BERT 또는 OpenAI의 text-embedding-ada-002 같은 임베딩 모델은 단어, 문장, 문단을 이런 벡터 임베딩으로 변환하는 데 사용돼요.
벡터 데이터베이스의 역할은 이런 임베딩을 저장하고 유사도 검색을 용이하게 하는 것이에요. 임베딩 자체를 생성하지는 않아요. 벡터 임베딩을 만들려면 EmbeddingModel을 사용해야 해요.
인터페이스의 similaritySearch 메서드는 주어진 쿼리 문자열과 유사한 문서를 검색할 수 있게 해 줘요. 이 메서드들은 다음 파라미터로 미세 조정할 수 있어요:
k: 반환할 최대 유사 문서 수를 지정하는 정수. 흔히 'top K' 검색 또는 'K nearest neighbors' (KNN)라고 불러요.threshold: 0에서 1 사이의 double 값으로, 1에 가까울수록 유사도가 높음을 나타내요. 예를 들어 기본적으로 0.75 임계값을 설정하면 이 값보다 높은 유사도를 가진 문서만 반환돼요.Filter.Expression: SQL의 'where' 절과 비슷하게 동작하지만Document의 메타데이터 키-값 쌍에만 적용되는 플루언트 DSL(도메인 특화 언어) 표현식을 전달하는 데 사용되는 클래스.filterExpression: 문자열로 필터 표현식을 받는 ANTLR4 기반의 외부 DSL. 예를 들어 country, year,isActive같은 메타데이터 키가 있다면country == 'UK' && year >= 2020 && isActive == true.같은 표현식을 사용할 수 있어요.
Filter.Expression에 대한 자세한 정보는 Metadata Filters 섹션에서 확인할 수 있어요.
스키마 초기화 (Schema Initialization)
일부 벡터 스토어는 사용 전에 백엔드 스키마를 초기화해야 해요. 기본적으로는 초기화되지 않아요. 해당 생성자 인자에 boolean을 전달하거나, Spring Boot를 사용한다면 application.properties 또는 application.yml에서 적절한 initialize-schema 프로퍼티를 true로 설정해 선택해야 해요. 특정 프로퍼티 이름은 사용 중인 벡터 스토어의 문서를 확인하세요.
배칭 전략 (Batching Strategy)
벡터 스토어에서 작업할 때는 많은 수의 문서를 임베딩해야 하는 경우가 많아요. 한 번에 모든 문서를 임베딩하는 단일 호출이 단순해 보일 수 있지만, 이 방식은 문제를 일으킬 수 있어요. 임베딩 모델은 텍스트를 토큰으로 처리하며 컨텍스트 윈도우 크기라고 하는 최대 토큰 제한이 있어요. 이 제한은 단일 임베딩 요청에서 처리할 수 있는 텍스트 양을 제한해요. 한 번에 너무 많은 토큰을 임베딩하려 하면 오류나 잘린 임베딩이 생길 수 있어요.
이 토큰 제한을 해결하기 위해 Spring AI는 배칭 전략을 구현해요. 이 방식은 큰 문서 집합을 임베딩 모델의 최대 컨텍스트 윈도우에 맞는 더 작은 배치로 나눠요. 배칭은 토큰 제한 문제를 해결할 뿐 아니라 성능 향상과 API 속도 제한의 더 효율적인 사용으로도 이어질 수 있어요.
Spring AI는 문서를 토큰 수에 따라 하위 배치로 처리할 수 있게 해 주는 BatchingStrategy 인터페이스를 통해 이 기능을 제공해요. 핵심 BatchingStrategy 인터페이스는 다음과 같이 정의돼요:
public interface BatchingStrategy {
List<List<Document>> batch(List<Document> documents);
}
이 인터페이스는 문서 목록을 받아 문서 배치 목록을 반환하는 단일 메서드 batch를 정의해요.
기본 구현 (Default Implementation)
Spring AI는 TokenCountBatchingStrategy라는 기본 구현을 제공해요. 이 전략은 문서를 토큰 수에 따라 배칭해서 각 배치가 계산된 최대 입력 토큰 수를 초과하지 않도록 보장해요.
TokenCountBatchingStrategy의 핵심 기능:
- OpenAI의 최대 입력 토큰 수 (8191)를 기본 상한으로 사용.
- 잠재적 오버헤드에 대한 버퍼를 제공하기 위해 예약 비율(reserve percentage, 기본 10%)을 포함.
- 실제 최대 입력 토큰 수를
actualMaxInputTokenCount = originalMaxInputTokenCount * (1 - RESERVE_PERCENTAGE)로 계산.
이 전략은 각 문서의 토큰 수를 추정하고, 최대 입력 토큰 수를 초과하지 않도록 배치로 그룹화하며, 단일 문서가 이 한도를 초과하면 예외를 던져요.
TokenCountBatchingStrategy를 특정 요구사항에 맞게 커스터마이즈할 수도 있어요. Spring Boot @Configuration 클래스에서 커스텀 파라미터로 새 인스턴스를 만들어 이 작업을 할 수 있어요. 커스텀 TokenCountBatchingStrategy 빈을 만드는 예제는 다음과 같아요:
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customTokenCountBatchingStrategy() {
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE, // Specify the encoding type
8000, // Set the maximum input token count
0.1 // Set the reserve percentage
);
}
}
이 구성에서:
EncodingType.CL100K_BASE: 토큰화에 사용되는 인코딩 타입을 지정해요. 이 인코딩 타입은JTokkitTokenCountEstimator가 토큰 수를 정확히 추정하는 데 사용해요.8000: 최대 입력 토큰 수를 설정해요. 이 값은 임베딩 모델의 최대 컨텍스트 윈도우 크기보다 작거나 같아야 해요.0.1: 예약 비율을 설정해요. 최대 입력 토큰 수에서 예약할 토큰 비율로, 처리 중 가능한 토큰 수 증가에 대한 버퍼를 만들어 줘요.
기본적으로 이 생성자는 콘텐츠 포맷팅에 Document.DEFAULT_CONTENT_FORMATTER를, 메타데이터 처리를 위해 MetadataMode.NONE을 사용해요. 이 파라미터를 커스터마이즈해야 한다면 추가 파라미터가 있는 완전한 생성자를 사용할 수 있어요.
정의되면 이 커스텀 TokenCountBatchingStrategy 빈은 애플리케이션의 EmbeddingModel 구현이 자동으로 사용해 기본 전략을 대체해요.
TokenCountBatchingStrategy는 내부적으로 TokenCountEstimator(구체적으로 JTokkitTokenCountEstimator)를 사용해 효율적인 배칭을 위한 토큰 수를 계산해요. 이는 지정된 인코딩 타입에 기반한 정확한 토큰 추정을 보장해요.
또한 TokenCountBatchingStrategy는 TokenCountEstimator 인터페이스의 직접 구현을 전달할 수 있게 해 줘요. 이 기능은 특정 필요에 맞춘 커스텀 토큰 계산 전략을 사용할 수 있게 해 줘요. 예를 들어:
TokenCountEstimator customEstimator = new YourCustomTokenCountEstimator();
TokenCountBatchingStrategy strategy = new TokenCountBatchingStrategy(
this.customEstimator,
8000, // maxInputTokenCount
0.1, // reservePercentage
Document.DEFAULT_CONTENT_FORMATTER,
MetadataMode.NONE
);
자동 절단(auto-truncation) 작업 (Working with Auto-Truncation)
Vertex AI text embedding 같은 일부 임베딩 모델은 auto_truncate 기능을 지원해요. 활성화하면 모델이 최대 크기를 초과하는 텍스트 입력을 조용히 잘라내고 계속 처리하며, 비활성화하면 너무 큰 입력에 대해 명시적 오류를 던져요.
배칭 전략과 함께 자동 절단을 사용할 때는 모델의 실제 최대치보다 훨씬 높은 입력 토큰 수로 배칭 전략을 구성해야 해요. 이렇게 하면 큰 문서에 대해 배칭 전략이 예외를 던지는 것을 막아서, 임베딩 모델이 내부적으로 절단을 처리하게 해요.
자동 절단 구성 (Configuration for Auto-Truncation)
자동 절단을 활성화할 때는 배칭 전략의 최대 입력 토큰 수를 모델의 실제 한도보다 훨씬 높게 설정하세요. 이렇게 하면 큰 문서에 대해 배칭 전략이 예외를 던지지 않아서, 임베딩 모델이 내부적으로 절단을 처리할 수 있어요.
자동 절단과 커스텀 BatchingStrategy로 Vertex AI를 사용하고 이를 PgVectorStore에서 사용하는 예제 구성은 다음과 같아요:
@Configuration
public class AutoTruncationEmbeddingConfig {
@Bean
public VertexAiTextEmbeddingModel vertexAiEmbeddingModel(
VertexAiEmbeddingConnectionDetails connectionDetails) {
VertexAiTextEmbeddingOptions options = VertexAiTextEmbeddingOptions.builder()
.model(VertexAiTextEmbeddingOptions.DEFAULT_MODEL_NAME)
.autoTruncate(true) // Enable auto-truncation
.build();
return new VertexAiTextEmbeddingModel(connectionDetails, options);
}
@Bean
public BatchingStrategy batchingStrategy() {
// Only use a high token limit if auto-truncation is enabled in your embedding model.
// Set a much higher token count than the model actually supports
// (e.g., 132,900 when Vertex AI supports only up to 20,000)
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE,
132900, // Artificially high limit
0.1 // 10% reserve
);
}
@Bean
public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel, BatchingStrategy batchingStrategy) {
return PgVectorStore.builder(jdbcTemplate, embeddingModel)
// other properties omitted here
.build();
}
}
이 구성에서:
- 임베딩 모델에는 자동 절단이 활성화되어 있어서 너무 큰 입력을 우아하게 처리해요.
- 배칭 전략은 실제 모델 한도(20,000)보다 훨씬 큰 인위적으로 높은 토큰 한도(132,900)를 사용해요.
- 벡터 스토어는 구성된 임베딩 모델과 커스텀
BatchingStrategy빈을 사용해요.
왜 이렇게 동작하는가 (Why This Works)
이 방식이 동작하는 이유:
TokenCountBatchingStrategy는 단일 문서가 구성된 최대치를 초과하는지 확인하고, 초과하면IllegalArgumentException을 던져요.- 배칭 전략에서 매우 높은 한도를 설정하면 이 검사가 절대 실패하지 않도록 보장해요.
- 모델 한도를 초과하는 문서나 배치는 조용히 잘려나가고 임베딩 모델의 자동 절단 기능으로 처리돼요.
모범 사례 (Best Practices)
자동 절단을 사용할 때:
- 배칭 전략의 최대 입력 토큰 수를 모델의 실제 한도보다 최소 5-10배 크게 설정해서 배칭 전략의 조기 예외를 피하세요.
- 임베딩 모델의 절단 경고에 대해 로그를 모니터링하세요 (참고: 모든 모델이 절단 이벤트를 로깅하지는 않아요).
- 조용한 절단이 임베딩 품질에 미치는 영향을 고려하세요.
- 샘플 문서로 테스트해 잘린 임베딩이 여전히 요구사항을 충족하는지 확인하세요.
- 이 구성은 비표준이므로 향후 유지보수자를 위해 문서화하세요.
참고: 자동 절단은 오류를 방지하지만 불완전한 임베딩으로 이어질 수 있어요. 긴 문서 끝의 중요한 정보가 손실될 수 있어요. 애플리케이션이 모든 콘텐츠를 임베딩해야 한다면, 임베딩 전에 문서를 더 작은 청크로 나누세요.
Spring Boot 자동 설정 (Spring Boot Auto-Configuration)
Spring Boot 자동 설정을 사용한다면 Spring AI에 기본으로 딸려 온 것을 재정의하기 위해 커스텀 BatchingStrategy 빈을 제공해야 해요:
@Bean
public BatchingStrategy customBatchingStrategy() {
// This bean will override the default BatchingStrategy
return new TokenCountBatchingStrategy(
EncodingType.CL100K_BASE,
132900, // Much higher than model's actual limit
0.1
);
}
애플리케이션 컨텍스트에 이 빈이 있으면 모든 벡터 스토어가 사용하는 기본 배칭 전략을 자동으로 대체해요.
커스텀 구현 (Custom Implementation)
TokenCountBatchingStrategy는 견고한 기본 구현을 제공하지만, 특정 필요에 맞게 배칭 전략을 커스터마이즈할 수 있어요. 이는 Spring Boot의 자동 설정을 통해 할 수 있어요. 배칭 전략을 커스터마이즈하려면 Spring Boot 애플리케이션에서 BatchingStrategy 빈을 정의하세요:
@Configuration
public class EmbeddingConfig {
@Bean
public BatchingStrategy customBatchingStrategy() {
return new CustomBatchingStrategy();
}
}
이 커스텀 BatchingStrategy는 애플리케이션의 EmbeddingModel 구현이 자동으로 사용하게 돼요.
참고: Spring AI가 지원하는 벡터 스토어는 기본
TokenCountBatchingStrategy를 사용하도록 구성돼 있어요.
VectorStore 구현 (VectorStore Implementations)
VectorStore 인터페이스의 사용 가능한 구현은 다음과 같아요:
- Azure Vector Search - Azure 벡터 스토어.
- Apache Cassandra - Apache Cassandra 벡터 스토어.
- Chroma Vector Store - Chroma 벡터 스토어.
- Elasticsearch Vector Store - Elasticsearch 벡터 스토어.
- GemFire Vector Store - GemFire 벡터 스토어.
- MariaDB Vector Store - MariaDB 벡터 스토어.
- Milvus Vector Store - Milvus 벡터 스토어.
- MongoDB Atlas Vector Store - MongoDB Atlas 벡터 스토어.
- Neo4j Vector Store - Neo4j 벡터 스토어.
- OpenSearch Vector Store - OpenSearch 벡터 스토어.
- Oracle Vector Store - Oracle Database 벡터 스토어.
- PgVector Store - PostgreSQL/PGVector 벡터 스토어.
- Pinecone Vector Store - Pinecone 벡터 스토어.
- Qdrant Vector Store - Qdrant 벡터 스토어.
- Redis Vector Store - Redis 벡터 스토어.
- Typesense Vector Store - Typesense 벡터 스토어.
- Weaviate Vector Store - Weaviate 벡터 스토어.
- S3 Vector Store - AWS S3 벡터 스토어.
- SimpleVectorStore - 벡터 저장의 간단한 구현으로 테스트 목적으로만 좋아요.
참고: Azure Cosmos DB 벡터 스토어 지원은 Azure Cosmos DB 팀이 유지보수하는 외부 모듈로 제공돼요. 자세한 내용은 azurecosmosdb.github.io/spring-ai/docs/index.html을 참고하세요.
참고:
SimpleVectorStore구현은 프로덕션 사용을 위해 설계되지 않았으며 테스트나 데모 목적으로만 사용해야 해요.
향후 릴리스에서 더 많은 구현이 지원될 수 있어요. Spring AI가 지원해야 할 벡터 데이터베이스가 있다면 GitHub에서 이슈를 열거나, 더 좋게는 구현으로 풀 리퀘스트를 제출해 주세요. 각 VectorStore 구현에 대한 정보는 이 장의 하위 섹션에서 찾을 수 있어요.
사용 예시 (Example Usage)
벡터 데이터베이스의 임베딩을 계산하려면 사용 중인 더 높은 수준의 AI 모델과 일치하는 임베딩 모델을 선택해야 해요. 예를 들어 OpenAI의 ChatGPT와 함께라면 OpenAiEmbeddingModel과 text-embedding-ada-002라는 모델을 사용해요. OpenAI에 대한 Spring Boot starter의 자동 설정은 의존성 주입을 위해 Spring 애플리케이션 컨텍스트에 EmbeddingModel 구현을 제공해요.
벡터 스토어에 쓰기 (Writing to a Vector Store)
벡터 스토어에 데이터를 로드하는 일반적인 사용법은 배치 형태의 작업에서 하는 것이에요. 먼저 데이터를 Spring AI의 Document 클래스에 로드한 다음 VectorStore 인터페이스의 add 메서드를 호출해요. 벡터 데이터베이스에 로드하려는 데이터가 있는 JSON 파일을 나타내는 소스 파일의 String 참조가 주어지면, Spring AI의 JsonReader를 사용해 JSON의 특정 필드를 로드하고, 이를 작은 조각으로 나눈 다음 그 조각들을 벡터 스토어 구현에 전달해요. VectorStore 구현이 임베딩을 계산하고 JSON과 임베딩을 벡터 데이터베이스에 저장해요:
@Autowired
VectorStore vectorStore;
void load(String sourceFile) {
JsonReader jsonReader = new JsonReader(new FileSystemResource(sourceFile),
"price", "name", "shortDescription", "description", "tags");
List<Document> documents = jsonReader.get();
this.vectorStore.add(documents);
}
벡터 스토어에서 읽기 (Reading from a Vector Store)
나중에 사용자 질문이 AI 모델에 전달될 때, 유사 문서를 검색하기 위해 유사도 검색이 수행되고, 그 문서들은 사용자 질문의 컨텍스트로 프롬프트에 "채워 넣어져요(stuffed)". 읽기 전용 작업에는 VectorStore 인터페이스나 더 집중된 VectorStoreRetriever 인터페이스를 사용할 수 있어요:
@Autowired
VectorStoreRetriever retriever; // Could also use VectorStore here
String question = "<question from user>";
List<Document> similarDocuments = retriever.similaritySearch(question);
// Or with more specific search parameters
SearchRequest request = SearchRequest.builder()
.query(question)
.topK(5) // Return top 5 results
.similarityThreshold(0.7) // Only return results with similarity score >= 0.7
.build();
List<Document> filteredDocuments = retriever.similaritySearch(request);
similaritySearch 메서드에 추가 옵션을 전달해 검색할 문서 수와 유사도 검색의 임계값을 정의할 수 있어요.
읽기·쓰기 작업 분리 (Separation of Read and Write Operations)
별도의 인터페이스를 사용하면 어떤 컴포넌트가 쓰기 접근이 필요하고 어떤 컴포넌트가 읽기 접근만 필요로 하는지 명확히 정의할 수 있어요:
// Write operations in a service that needs full access
@Service
class DocumentIndexer {
private final VectorStore vectorStore;
DocumentIndexer(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
public void indexDocuments(List<Document> documents) {
vectorStore.add(documents);
}
}
// Read-only operations in a service that only needs retrieval
@Service
class DocumentRetriever {
private final VectorStoreRetriever retriever;
DocumentRetriever(VectorStoreRetriever retriever) {
this.retriever = retriever;
}
public List<Document> findSimilar(String query) {
return retriever.similaritySearch(query);
}
}
이런 관심사 분리는 변경 작업에 대한 접근을 진정으로 필요한 컴포넌트로만 제한해서 더 유지보수 가능하고 안전한 애플리케이션을 만드는 데 도움이 돼요.
VectorStoreRetriever를 사용한 검색 작업 (Retrieval Operations with VectorStoreRetriever)
VectorStoreRetriever 인터페이스는 벡터 스토어의 읽기 전용 뷰를 제공하며 유사도 검색 기능만 노출해요. 이는 최소 권한 원칙을 따르며, 기저 데이터를 수정하지 않고 문서만 검색하면 되는 RAG(Retrieval-Augmented Generation) 애플리케이션에서 특히 유용해요.
VectorStoreRetriever 사용의 이점 (Benefits of Using VectorStoreRetriever)
- 관심사 분리 (Separation of Concerns): 읽기 작업을 쓰기 작업과 명확히 분리해요.
- 인터페이스 분리 (Interface Segregation): 검색 기능만 필요한 클라이언트는 변경 메서드에 노출되지 않아요.
- 함수형 인터페이스 (Functional Interface): 간단한 사용 사례에는 람다 표현식이나 메서드 참조로 구현할 수 있어요.
- 의존성 감소 (Reduced Dependencies): 검색만 수행하면 되는 컴포넌트는 전체
VectorStore인터페이스에 의존할 필요가 없어요.
사용 예시 (Example Usage)
유사도 검색만 수행하면 될 때 VectorStoreRetriever를 직접 사용할 수 있어요:
@Service
public class DocumentRetrievalService {
private final VectorStoreRetriever retriever;
public DocumentRetrievalService(VectorStoreRetriever retriever) {
this.retriever = retriever;
}
public List<Document> findSimilarDocuments(String query) {
return retriever.similaritySearch(query);
}
public List<Document> findSimilarDocumentsWithFilters(String query, String country) {
SearchRequest request = SearchRequest.builder()
.query(query)
.topK(5)
.filterExpression("country == '" + country + "'")
.build();
return retriever.similaritySearch(request);
}
}
이 예제에서 서비스는 VectorStoreRetriever 인터페이스에만 의존하므로, 검색 작업만 수행하고 벡터 스토어를 수정하지 않는다는 것이 분명해요.
RAG 애플리케이션과의 통합 (Integration with RAG Applications)
VectorStoreRetriever 인터페이스는 AI 모델에 컨텍스트를 제공하기 위해 관련 문서를 검색해야 하는 RAG 애플리케이션에서 특히 유용해요:
@Service
public class RagService {
private final VectorStoreRetriever retriever;
private final ChatModel chatModel;
public RagService(VectorStoreRetriever retriever, ChatModel chatModel) {
this.retriever = retriever;
this.chatModel = chatModel;
}
public String generateResponse(String userQuery) {
// Retrieve relevant documents
List<Document> relevantDocs = retriever.similaritySearch(userQuery);
// Extract content from documents to use as context
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
// Generate response using the retrieved context
String prompt = "Context information:\n" + context + "\n\nUser query: " + userQuery;
return chatModel.generate(prompt);
}
}
이 패턴은 RAG 애플리케이션에서 검색 컴포넌트와 생성 컴포넌트를 깔끔하게 분리할 수 있게 해 줘요.
메타데이터 필터 (Metadata Filters)
이 섹션은 쿼리 결과에 대해 사용할 수 있는 다양한 필터를 설명해요.
필터 문자열 (Filter String)
similaritySearch 오버로드 중 하나에 SQL과 유사한 필터 표현식을 String으로 전달할 수 있어요. 다음 예를 살펴볼게요:
"country == 'BG'""genre == 'drama' && year >= 2020""genre in ['comedy', 'documentary', 'drama']"
Filter.Expression
FilterExpressionBuilder로 Filter.Expression 인스턴스를 만들 수 있는데, 플루언트 API를 노출해요. 간단한 예는 다음과 같아요:
FilterExpressionBuilder b = new FilterExpressionBuilder();
Expression expression = this.b.eq("country", "BG").build();
다음 연산자를 사용해 정교한 표현식을 만들 수 있어요:
EQUALS: '=='
MINUS : '-'
PLUS: '+'
GT: '>'
GE: '>='
LT: '<'
LE: '<='
NE: '!='
다음 연산자로 표현식을 결합할 수 있어요:
AND: 'AND' | 'and' | '&&';
OR: 'OR' | 'or' | '||';
다음 예를 고려해 볼게요:
Expression exp = b.and(b.eq("genre", "drama"), b.gte("year", 2020)).build();
다음 연산자도 사용할 수 있어요:
IN: 'IN' | 'in';
NIN: 'NIN' | 'nin';
NOT: 'NOT' | 'not';
다음 예를 고려해 볼게요:
Expression exp = b.and(b.in("genre", "drama", "documentary"), b.not(b.lt("year", 2020))).build();
다음 연산자도 사용할 수 있어요:
IS: 'IS' | 'is';
NULL: 'NULL' | 'null';
NOT NULL: 'NOT NULL' | 'not null';
다음 예를 고려해 볼게요:
Expression exp = b.and(b.isNull("year")).build();
Expression exp = b.and(b.isNotNull("year")).build();
참고:
IS NULL과IS NOT NULL은 아직 모든 벡터 스토어에 구현되지 않았어요.
공유 인덱스 파티셔닝 (Partitioning a Shared Index)
단일 벡터 스토어는 종종 많은 논리적 문서 그룹이 공유해요 — 예를 들어 여러 고객, 프로젝트, 지식 베이스의 콘텐츠를 담은 하나의 인덱스가 그러하죠. VectorStore는 전체 인덱스에 대해 동작하고 유사도와 제공한 필터에 따라 문서를 반환하거나 제거하므로, 필터 표현식이 작업하려는 그룹을 선택하는 방법이에요.
접근 방식은 각 문서를 쓸 때 그 그룹을 식별하는 메타데이터로 태그하고, 검색과 삭제에 일치하는 필터를 적용하는 것이에요:
// When writing, record the group the document belongs to.
Document document = new Document(content, Map.of("group", groupId));
vectorStore.add(List.of(document));
// When searching, select documents from that group.
List<Document> results = vectorStore.similaritySearch(SearchRequest.builder()
.query(query)
.filterExpression("group == '" + groupId + "'")
.build());
// When deleting by filter, target the same group.
Filter.Expression scoped = new FilterExpressionBuilder().eq("group", groupId).build();
vectorStore.delete(scoped);
그룹을 깔끔하게 분리해 주는 몇 가지 관행이 있어요:
- 식별 메타데이터를 쓰기 시점에 모든 문서에 추가해서 나중에 항상 필터링할 수 있게 하세요.
- 검색과 삭제에 일관되게 해당 필터를 적용해서 작업이 의도한 그룹에만 영향을 주게 하세요.
벡터 스토어에서 문서 삭제 (Deleting Documents from Vector Store)
Vector Store 인터페이스는 문서 삭제를 위한 여러 메서드를 제공해요. 특정 문서 ID로 또는 필터 표현식을 사용해 데이터를 제거할 수 있어요.
문서 ID로 삭제 (Delete by Document IDs)
문서를 삭제하는 가장 간단한 방법은 문서 ID 목록을 제공하는 것이에요:
void delete(List<String> idList);
이 메서드는 제공된 목록의 ID와 일치하는 모든 문서를 제거해요. 목록의 어떤 ID가 스토어에 없으면 무시돼요.
// Create and add document
Document document = new Document("The World is Big",
Map.of("country", "Netherlands"));
vectorStore.add(List.of(document));
// Delete document by ID
vectorStore.delete(List.of(document.getId()));
필터 표현식으로 삭제 (Delete by Filter Expression)
더 복잡한 삭제 기준에는 필터 표현식을 사용할 수 있어요:
void delete(Filter.Expression filterExpression);
이 메서드는 어떤 문서를 삭제할지 기준을 정의하는 Filter.Expression 객체를 받아요. 문서의 메타데이터 속성을 기준으로 삭제해야 할 때 특히 유용해요.
// Create test documents with different metadata
Document bgDocument = new Document("The World is Big",
Map.of("country", "Bulgaria"));
Document nlDocument = new Document("The World is Big",
Map.of("country", "Netherlands"));
// Add documents to the store
vectorStore.add(List.of(bgDocument, nlDocument));
// Delete documents from Bulgaria using filter expression
Filter.Expression filterExpression = new Filter.Expression(
Filter.ExpressionType.EQ,
new Filter.Key("country"),
new Filter.Value("Bulgaria")
);
vectorStore.delete(filterExpression);
// Verify deletion with search
SearchRequest request = SearchRequest.builder()
.query("World")
.filterExpression("country == 'Bulgaria'")
.build();
List<Document> results = vectorStore.similaritySearch(request);
// results will be empty as Bulgarian document was deleted
문자열 필터 표현식으로 삭제 (Delete by String Filter Expression)
편의를 위해 문자열 기반 필터 표현식으로도 문서를 삭제할 수 있어요:
void delete(String filterExpression);
이 메서드는 제공된 문자열 필터를 내부적으로 Filter.Expression 객체로 변환해요. 필터 기준이 문자열 형식일 때 유용해요.
// Create and add documents
Document bgDocument = new Document("The World is Big",
Map.of("country", "Bulgaria"));
Document nlDocument = new Document("The World is Big",
Map.of("country", "Netherlands"));
vectorStore.add(List.of(bgDocument, nlDocument));
// Delete Bulgarian documents using string filter
vectorStore.delete("country == 'Bulgaria'");
// Verify remaining documents
SearchRequest request = SearchRequest.builder()
.query("World")
.topK(5)
.build();
List<Document> results = vectorStore.similaritySearch(request);
// results will only contain the Netherlands document
Delete API 호출 시 오류 처리 (Error Handling When Calling the Delete API)
모든 삭제 메서드는 오류가 발생하면 예외를 던질 수 있어요. 가장 좋은 방법은 삭제 작업을 try-catch 블록으로 감싸는 것이에요:
try {
vectorStore.delete("country == 'Bulgaria'");
}
catch (Exception e) {
logger.error("Invalid filter expression", e);
}
문서 버전 관리 사용 사례 (Document Versioning Use Case)
흔한 시나리오는 문서 버전을 관리하면서 새 버전을 업로드하고 이전 버전을 제거해야 하는 경우예요. 필터 표현식을 사용해 다음과 같이 처리할 수 있어요:
// Create initial document (v1) with version metadata
Document documentV1 = new Document(
"AI and Machine Learning Best Practices",
Map.of(
"docId", "AIML-001",
"version", "1.0",
"lastUpdated", "2024-01-01"
)
);
// Add v1 to the vector store
vectorStore.add(List.of(documentV1));
// Create updated version (v2) of the same document
Document documentV2 = new Document(
"AI and Machine Learning Best Practices - Updated",
Map.of(
"docId", "AIML-001",
"version", "2.0",
"lastUpdated", "2024-02-01"
)
);
// First, delete the old version using filter expression
Filter.Expression deleteOldVersion = new Filter.Expression(
Filter.ExpressionType.AND,
new Filter.Expression(
Filter.ExpressionType.EQ,
new Filter.Key("docId"),
new Filter.Value("AIML-001")
),
new Filter.Expression(
Filter.ExpressionType.EQ,
new Filter.Key("version"),
new Filter.Value("1.0")
)
);
vectorStore.delete(deleteOldVersion);
// Add the new version
vectorStore.add(List.of(documentV2));
// Verify only v2 exists
SearchRequest request = SearchRequest.builder()
.query("AI and Machine Learning")
.filterExpression("docId == 'AIML-001'")
.build();
List<Document> results = vectorStore.similaritySearch(request);
// results will contain only v2 of the document
문자열 필터 표현식으로도 같은 작업을 할 수 있어요:
// Delete old version using string filter
vectorStore.delete("docId == 'AIML-001' AND version == '1.0'");
// Add new version
vectorStore.add(List.of(documentV2));
문서 삭제 시 성능 고려 사항 (Performance Considerations While Deleting Documents)
- 제거할 문서를 정확히 알고 있다면 ID 목록 삭제가 일반적으로 더 빠르거나다.
- 필터 기반 삭제는 일치하는 문서를 찾기 위해 인덱스 스캔이 필요할 수 있어요. 하지만 이는 벡터 스토어 구현에 따라 달라요.
- 큰 삭제 작업은 시스템에 부담을 주지 않도록 배칭해야 해요.
- ID를 먼저 수집하는 대신 문서 속성을 기준으로 삭제할 때는 필터 표현식을 사용하는 것을 고려하세요.