✅ 개요
이전 포스트에서는 생성된 질문의 중복여부를 검증하기 위한 텍스트 임베딩 모델을 조사하고 결정하였다.
그런데 여기서 고려해야할 한가지 문제가 있다.
질문들은 매일 6개씩 새로 생겨 누적된다.
질문의 중복을 검증하려면 이전의 생성된 모든 질문들을 텍스트 임베딩 모델을 돌려야하는데 서비스가 유지될 수록 그 비용이 배로 늘어갈 것이다. 따라서 생성된 질문들의 벡터값을 별도로 저장할 수 있는 곳이 필요하다.
1. RDBMS? -> ❌
현재 프로젝트에서는 RDBMS로 MySQL을 사용하고 있다. 따라서 질문들의 벡터값을 MySQL에 저장한다면 데이터 일관성과 접근성을 높인다는 장점이 있다.
하지만 텍스트 임베딩을 통해 만들어진 벡터는 상당한 고차원 데이터이다. 이번 프로젝트에서 사용하는 OpenAI의 text-embedding-3-large의 경우 3072차원이다. 그렇기 때문에 그대로 저장한다면 매우 비효율적일 것이다.
또한 코사인 유사도 계산의 경우 MySQL에서 별도로 제공하지 않기 때문에 애플리케이션에서 직접해야한다. 성능에 영향이 있을 것이다.
2. Vector Database -> 🅾️
Vector Database는 앞서 말했던 RDBMS의 단점들을 상쇄해준다.
Vector Database를 사용하면 벡터를 더 효율적으로 저장할 수 있고 Vector Database만의 인덱싱 방식은 벡터를 검색하는 속도를 매우 빠르게 만들어준다. 또한 자체적으로 유사도 계산을 지원하기 때문에 애플리케이션에서 별도의 계산로직을 작성하지 않아도 된다.
✅ Redis Vector Database & Vector Search
시중에는 수 많은 Vector Database가 존재하지만 Redis Vector Database & Vector Search를 선택한 이유는 바로 학습곡선이 낮기 때문이다.
현재 프로젝트내에서 Redis를 활용하고 있고 프로젝트 기간이 한달로 매우 짧다는 점을 감안했을 때 새로운 Vector Database를 사용하기에는 시간이 부족하다고 판단했다.
Redis Vector Database & Vector Search는 Redis에 벡터를 저장하여 관리하는 것이다. 기존 Redis에 Vector Simliarity Search라는 모듈만 추가하면 사용할 수 있고 Spring AI에서도 공식적으로 라이브러리를 제공하기 때문에 쉽게 사용할 수 있다.
🐳 Docker로 Redis Vector Database & Vector Search 환경 구축하기
Redis Vector Database & Vector Search를 사용하기 위해선 Redis-stack이라는 라이브러리를 사용하면 된다.
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 -e REDIS_ARGS="--requirepass mypassword" redis/redis-stack:latest
6379번 포트에 Redis를 띄웠고 8001번에 띄운 건 redis insight라는 GUI툴이다. 현재는 redis insight는 사용하지 않으니 크게 신경쓰지 않아도 된다.
정상적으로 실행되고 있는 모습이다.
🌱 Spring AI - Redis Vector Database & Vector Search 사용방법
Spring에선 Redis Vector DB에 대한 라이브러리를 제공해준다.
인덱스 생성, 외부 임베딩 모델 API 요청, 벡터 데이터 저장, 벡터 데이터 조회 등의 기능을 추상화해 편리하게 사용할 수 있게 도와준다.
0️⃣ 라이브러리 추가
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
implementation 'org.springframework.ai:spring-ai-redis-store-spring-boot-starter'
임베딩 모델 사용을 위한 spring ai - open ai와 벡터 데이터베이스를 위한 spring ai - redis store를 추가했다.
1️⃣ Embedding Model 빈등록
Redis Vector Database는 기본적으로 임베딩 기능을 제공하지 않기 때문에 별도로 임베딩 모델을 구축해주어야 한다.
Spring AI의 EmbeddingModel이라는 객체를 통해 text-embedding-3-large모델을 등록해주었다.
@Configuration
public class EmbeddingModelConfig {
@Value("${spring.ai.openai.api-key}")
private String AI_API_KEY;
@Bean
public EmbeddingModel embeddingModel() {
return new OpenAiEmbeddingModel(
new OpenAiApi(AI_API_KEY),
MetadataMode.EMBED,
OpenAiEmbeddingOptions.builder()
.withModel("text-embedding-3-large")
.build()
);
}
}
2️⃣ Vector Index 생성
Redis에서 Vector를 저장하고 검색하기 위해선 Vector Index가 필요하다.
하지만 Spring AI에서는 자동으로 Index를 생성해주는 기능을 제공해준다.
spring.ai.vectorstore.redis.initialize-schema=true
jpa의 ddl-auto: update와 비슷한 기능이다. 관련 설정 정보를 읽어서 해당 Index가 없으면 자동으로 Index를 만들어준다.
Index에 대한 정보도 지정해줄 수 있다.
spring.ai.vectorstore.redis.uri="redis://localhost:6379"
spring.ai.vectorstore.redis.index=qfeedV-index
spring.ai.vectorstore.redis.prefix=qfeedV
- redis.index: Index 명
- redis.prefix: Key값 앞에 붙는 접두사
[Index가 Redis에 잘 생성되었다.]
만약 Index를 커스텀하고 싶다면 initialize-schema 옵션을 false로 하고 아래 링크를 참고해 직접 만들면된다.
참고로 Spring이 기본적으로 생성해주는 Index에 대한 명령어는 다음과 같다.
FT.CREATE <Index 명> ON JSON PREFIX 1 <Key 접두사> SCHEMA
$.content AS content TEXT WEIGHT 1
$.embedding AS embedding VECTOR HNSW 10 TYPE FLOAT32 DIM 3072 DISTANCE_METRIC COSINE M 16 EF_CONSTRUCTION 200
Spring은 해당 정보를 기반으로 VectorStore객체를 생성해 주입해준다.
백터 데이터베이스에 접근할 수 있게 해주는 객체이다.
private final VectorStore vectorStore;
3️⃣ 데이터 저장
public void storeVector() {
List<Document> documents = List.of(new Document("서울의 날씨는 오늘 맑습니다."));
vectorStore.add(documents);
}
- Spring ai에서는 VectorStore의 add() 메서드를 통해 벡터 데이터베이스에 벡터값을 저장한다.
내부적으로 외부 임베딩 모델 API를 호출해주고 받아온 벡터 값을 Redis에 저장한다.
public void doAdd(List<Document> documents) {
Pipeline pipeline = this.jedis.pipelined();
try {
// 임베딩 모델 호출
this.embeddingModel.embed(documents, EmbeddingOptionsBuilder.builder().build(), this.batchingStrategy);
Iterator var3 = documents.iterator();
// 벡터값 저장
while(var3.hasNext()) {
Document document = (Document)var3.next();
document.setEmbedding(document.getEmbedding());
HashMap<String, Object> fields = new HashMap();
fields.put(this.config.embeddingFieldName, document.getEmbedding());
fields.put(this.config.contentFieldName, document.getContent());
fields.putAll(document.getMetadata());
pipeline.jsonSetWithEscape(this.key(document.getId()), JSON_SET_PATH, fields);
}
// 일부 로직 생략...
}
4️⃣ 데이터 조회
public void search() {
List<Document> results = this.vectorStore.similaritySearch(SearchRequest.query("서울의 날씨는 오늘 맑습니다.")
.withSimilarityThreshold(0.8)
.withTopK(5));
System.out.println(results);
}
- VectorStore의 similaritySearch() 메서드를 통해 조회를 할 수 있다
- SearchRequest라는 객체를 통해 쉽게 세부쿼리가 작성이 가능하다.
- withSimilarityThreshold: 유사도 임계치. 기본값은 0
- withTopK: 높은 순으로 몇 개의 데이터를 받아올건지. 기본값은 4
내부적으로 외부 임베딩 모델 API를 호출해주고 받아온 벡터 값을 가지고 쿼리를 날려 조건에 맞는 데이터들을 가져옴
public List<Document> doSimilaritySearch(SearchRequest request) {
// 로직 일부 생략
float[] embedding = this.embeddingModel.embed(request.getQuery()); // 임베딩 모델 호출
Query query = (new Query(queryString)).addParam("BLOB", RediSearchUtil.toByteArray(embedding)).returnFields((String[])returnFields.toArray(new String[0])).setSortBy("vector_score", true).limit(0, request.getTopK()).dialect(2);
SearchResult result = this.jedis.ftSearch(this.config.indexName, query);
return result.getDocuments().stream().filter((d) -> {
return (double)this.similarityScore(d) >= request.getSimilarityThreshold();
}).map(this::toDocument).toList(); // 쿼리후 결과 반환
}
[출력값]
[Document{id='b25d021e-bf6f-4c02-80dc-ae2d98e6d4bb',
metadata={vector_score=0.3924256}, content='달리기는 건강에 좋다.', media=[]}]
여기서 vector_score는 코사인 유사도를 나타내는 것이 아닌 코사인 거리를 나타내는 것이다.
metadata.put("vector_score", 1.0F - this.similarityScore(doc));
private float similarityScore(redis.clients.jedis.search.Document doc) {
return (2.0F - Float.parseFloat(doc.getString("vector_score"))) / 2.0F;
}
- 코사인 유사도 = 1 - 코사인 거리이기 때문에 내부적으로 변환하는 과정이 존재한다.
- 다만 스프링 ai에선 코사인 유사도 범위를 0~1로 정규화하기 때문에 위와같은 계산과정을 거친다.
✅ 마치며
이제 질문 생성 기능을 위한 모든 준비는 끝났다. 이제 본격적으로 어플리케이션 로직을 구현해보도록 하겠다.
✅ 참고자료
'데브코스 > 실습 & 프로젝트' 카테고리의 다른 글
[최종 프로젝트] 질문 생성 기능 - 5. 질문 생성 로직 개선하기(질문 생성 횟수, 데이터 불일치 개선) (0) | 2024.12.30 |
---|---|
[최종 프로젝트] 질문 생성 기능 - 4. 질문 생성 로직 구현하기 (0) | 2024.12.30 |
[최종 프로젝트] 질문 생성 기능 - 2. 중복 질문 방지를 위한 텍스트 임베딩 모델 조사 (2) | 2024.12.29 |
[최종 프로젝트] 질문 생성 기능 - 1. 질문 생성을 위한 생성형 AI 모델 조사 (2) | 2024.12.28 |
[최종 프로젝트] Jira 도입기 ( + 회고) (2) | 2024.12.27 |