RAG와 Vector Database — LLM이 모르는 것을 어떻게 찾아내는가
처음으로 LLM 기반 애플리케이션에 RAG를 붙여 보려는 백엔드 개발자를 대상으로 합니다. RAG가 왜 필요한지, Vector Database는 기존 RDB와 어떻게 다른지, 임베딩과 유사도 함수는 어떤 의미를 갖는지, 그리고 Spring AI에서 Vector Store를 설정할 때 왜 dimensions 값을 손으로 적어 줘야 하는지를 한 흐름으로 정리합니다.
1. RAG가 풀려는 문제
GPT-4, Claude, Gemini 같은 대형 언어 모델은 학습 시점까지의 텍스트를 가중치(weight) 안에 압축해 들고 있습니다. 사용자가 "어제 우리 회사 사내 위키에 올라온 배포 절차"를 물으면, 모델은 자신이 본 적 없는 정보를 그럴듯하게 만들어 내거나(hallucination) 모른다고 답합니다. 모델을 다시 파인튜닝하지 않는 이상 가중치 안의 지식은 갱신되지 않습니다.
2020년 Lewis 등이 제안한 RAG(Retrieval-Augmented Generation)는 이 문제를 두 종류의 메모리를 결합하는 방식으로 풀었습니다.
- Parametric memory: 모델 가중치 안에 압축된 지식. seq2seq 모델(원논문에서는 BART)이 담당합니다. 일반 상식, 문법, 추론 능력은 여기서 나옵니다.
- Non-parametric memory: 외부에 별도로 저장한 문서 인덱스. 원논문에서는 Wikipedia 위키피디아 지문을 dense vector로 색인하고 DPR(Dense Passage Retrieval) 리트리버로 꺼냈습니다. 인덱스를 바꾸면 모델 재학습 없이 지식이 갱신됩니다.
flowchart LR
Q[User Query] --> R[Retriever]
R --> VDB[(Vector Database)]
VDB --> R
R --> CTX[Top-K Documents]
CTX --> G[Generator / LLM]
Q --> G
G --> A[Answer]
핵심 아이디어는 단순합니다. 질문을 받으면 외부 인덱스에서 관련 문서 K개를 검색해 프롬프트에 끼워 넣고, LLM이 그 문맥 안에서 답하게 합니다. 모델이 모르는 사실은 인덱스가 책임지고, 자연어 생성은 모델이 책임집니다. 책임 분리가 명확합니다.
이 구조에서 가장 중요한 결정은 "관련 문서를 어떻게 빠르고 정확하게 찾을 것인가"입니다. 그래서 RAG의 출발점은 항상 Vector Database입니다.
2. RAG 파이프라인의 두 단계
RAG는 시간 축이 다른 두 단계로 나뉩니다.
2.1 인덱싱 단계 (offline)
문서를 사전에 잘게 잘라(chunking) 임베딩한 뒤 벡터 DB에 저장하는 단계입니다. 사용자가 질문하기 전에 미리 끝내 둡니다.
flowchart LR
DOC[Source Documents] --> SPLIT[Text Splitter]
SPLIT --> CHUNK[Chunks]
CHUNK --> EMB[Embedding Model]
EMB --> VEC[Float Vectors]
VEC --> STORE[(Vector Store)]
CHUNK --> META[Metadata]
META --> STORE
- Splitter: 토큰 한도와 의미 단위를 고려해 문서를 청크로 자릅니다. 너무 크면 검색이 둔해지고, 너무 작으면 문맥이 끊어집니다.
- Embedding Model: 각 청크를 고정 차원의 실수 벡터로 변환합니다.
- Vector Store: 벡터와 원문, 메타데이터를 함께 저장합니다. 검색 시 메타데이터로 필터링도 가능해야 합니다.
2.2 질의 단계 (online)
flowchart LR
Q[User Query] --> EMB[Embedding Model]
EMB --> QV[Query Vector]
QV --> SEARCH[Similarity Search]
SEARCH --> STORE[(Vector Store)]
STORE --> SEARCH
SEARCH --> TOPK[Top-K Documents]
TOPK --> PROMPT[Prompt Builder]
Q --> PROMPT
PROMPT --> LLM[LLM]
LLM --> ANS[Answer]
질문도 같은 임베딩 모델로 벡터화한 뒤, 인덱싱 때 저장한 벡터들과 유사도 비교를 합니다. 가장 가까운 K개(보통 4개 전후)를 꺼내 프롬프트의 "참고 문맥"으로 끼워 넣습니다.
이 두 단계의 관계에서 한 가지 중요한 제약이 나옵니다. 인덱싱과 질의는 반드시 같은 임베딩 모델을 써야 합니다. 모델이 다르면 벡터가 놓이는 의미 공간이 달라서 거리 비교가 무의미해집니다. 모델을 바꾸면 인덱스를 통째로 다시 만들어야 한다는 뜻입니다. 이 점은 6장에서 차원 이야기로 다시 돌아옵니다.
3. 임베딩 — 텍스트를 의미 좌표로 옮기다
임베딩(embedding)은 텍스트를 고정 길이의 실수 벡터로 바꾸는 함수입니다. 입력은 임의 길이의 문자열, 출력은 정해진 차원 D의 부동소수점 배열입니다.
"고양이는 포유류다" ──┐
├──► EmbeddingModel ──► [0.012, -0.443, 0.881, ..., 0.057] (D=1536)
"개는 포유류다" ──┘ (의미 공간의 한 점)
핵심 성질은 "의미가 비슷한 문장은 좌표 공간 위에서 가까이에 놓인다" 입니다. "고양이는 포유류다"와 "개는 포유류다"는 이 공간에서 가깝고, "오늘 날씨가 좋다"는 멀리 떨어져 있습니다. 이 성질이 만들어지려면 임베딩 모델이 대규모 코퍼스에서 의미 유사성 신호로 학습되어 있어야 합니다.
차원이 모델에 묶이는 이유
OpenAI의 임베딩 모델은 다음과 같은 기본 차원을 가집니다.
| 모델 | 기본 차원 |
|---|---|
text-embedding-3-small |
1536 |
text-embedding-3-large |
3072 |
text-embedding-ada-002 |
1536 |
이 숫자는 임의로 고른 값이 아니라 모델 아키텍처의 출력층이 그렇게 설계되어 있어서 나오는 값입니다. 모델이 만들어 내는 좌표 공간 자체가 D차원이라는 뜻입니다. 따라서 인덱스를 만들 때 사용하는 차원과 질의 시 모델이 출력하는 차원이 다르면 거리 계산이 성립하지 않습니다.
text-embedding-3 계열은 Matryoshka Representation Learning을 적용해 출력 벡터의 앞부분만 잘라 써도 의미가 보존되도록 학습됐습니다. 그래서 1536차원 모델을 256차원으로 줄여 저장하는 것이 가능합니다. 다만 줄이고 나면 그 차원이 새로운 인덱스의 고정 차원이 됩니다. 즉 차원 축소도 한 번 정하면 끝까지 따라갑니다.
Spring AI의 EmbeddingModel
Spring AI는 임베딩 호출을 EmbeddingModel 인터페이스로 추상화합니다. 사용자는 OpenAI, Azure OpenAI, Ollama, Bedrock, Vertex AI 등 어떤 백엔드를 쓰더라도 같은 코드를 작성합니다.
@Autowired
EmbeddingModel embeddingModel;
float[] vector = embeddingModel.embed("고양이는 포유류다");
int dim = embeddingModel.dimensions(); // 1536
dimensions() 메서드는 백엔드에 한 번 호출해 보고 결과 길이를 캐시합니다. Vector Store 구현체는 인덱스를 만들 때 이 값을 보고 컬럼/스키마를 정의합니다.
4. Vector Database가 RDB와 다른 점
RDB의 인덱스는 정렬 가능한 키를 전제로 합니다. B-Tree는 키를 비교 가능한 순서로 정렬해 두고 등호·범위 검색을 로그 시간 안에 처리합니다. 풀텍스트 검색을 위한 GIN/inverted index 역시 토큰의 등호 매칭이 출발점입니다.
벡터 검색이 풀어야 하는 문제는 종류가 다릅니다. "정확히 같은 벡터"를 찾는 것이 아니라 "가장 가까운 K개"를 찾는 nearest-neighbor 문제입니다. 두 벡터의 거리는 차원만큼 곱셈을 해야 나오고, 비교 가능한 단일 키로 환원되지 않습니다. 그래서 B-Tree로는 처리할 수 없습니다.
| 항목 | RDB (B-Tree) | Vector Database |
|---|---|---|
| 인덱스 키 | 정렬 가능한 스칼라 (int, varchar) | 고정 차원의 실수 벡터 |
| 검색 질문 | "키가 X인 행" / "키가 [a, b]에 있는 행" | "이 벡터에 가장 가까운 K개" |
| 정확도 | 정확 매칭 | 근사(approximate) 허용 |
| 자료구조 | B-Tree, Hash, GIN | HNSW, IVFFlat, ScaNN, FAISS IVF, ... |
| 시간 복잡도 | O(log N) | 정확: O(N), 근사: O(log N) 수준 |
| 정렬 키 개수 | 보통 1개 (복합 키도 정렬됨) | 차원 D개 (1000+ 흔함) |
차원이 1000을 넘는 공간에서는 brute-force(모든 벡터와의 거리 계산)가 순식간에 비싸집니다. 1억 개 벡터에 대해 매 질의마다 1억 번의 거리 계산을 도는 구조는 실서비스에 못 씁니다. 그래서 Vector Database는 거의 모두 정확도를 약간 양보하고 속도를 얻는 ANN(Approximate Nearest Neighbor) 인덱스를 채택합니다. 이 점은 RDB에서는 보기 어려운 결정입니다.
5. 유사도 함수
벡터끼리 "얼마나 비슷한가"를 재는 함수는 여러 가지가 있습니다. RAG 시스템에서 자주 쓰는 세 가지를 정리합니다. 차원 D의 두 벡터 a, b에 대해서:
5.1 Cosine similarity
cos(a, b) = (a · b) / (||a|| * ||b||)
cosine_distance = 1 - cos(a, b)
벡터의 방향만 봅니다. 길이는 무시합니다. "문서가 길어서 임베딩 norm이 큰지 작은지"에 영향을 받지 않습니다. 텍스트 의미 검색에서 가장 무난한 선택이라 대부분의 임베딩 모델 가이드가 권장합니다.
5.2 Inner product (dot product)
a · b = Σ a_i * b_i
방향과 길이를 모두 반영합니다. 길이가 의미를 갖는 추천 시스템(인기도, 신뢰도 등)에서 유용합니다. 임베딩 벡터가 단위 벡터(L2 norm = 1) 로 정규화되어 있으면 inner product와 cosine similarity는 수학적으로 동일합니다. 이 경우 정규화를 한 번만 해 두면 곱셈만 하는 inner product가 가장 빠릅니다.
5.3 Euclidean distance (L2)
d(a, b) = sqrt( Σ (a_i - b_i)^2 )
직관적인 직선 거리입니다. 좌표 평면에서 두 점의 거리를 떠올리면 됩니다. 단위 벡터에서는 cosine과 단조 관계를 가지지만 일반 벡터에서는 다른 순위를 줍니다.
5.4 어떤 것을 골라야 하나
대부분의 임베딩 모델은 출력 벡터를 단위 벡터로 정규화해서 내보냅니다(혹은 사용자가 정규화하는 것을 권장합니다). 그러면 cosine과 inner product는 같은 결과를 줍니다. 따라서 실용적인 선택은 다음과 같이 좁혀집니다.
- 모델이 정규화 벡터를 내놓는다 → cosine 또는 inner product (둘 중 빠른 쪽)
- 정규화하지 않은 임의 벡터를 그대로 쓴다 → cosine
- 절대 거리가 의미 있는 도메인(예: 좌표/시간) → euclidean
Spring AI의 PgVectorStore는 기본값으로 COSINE_DISTANCE를 쓰고, 필요할 때 EUCLIDEAN_DISTANCE나 NEGATIVE_INNER_PRODUCT로 바꿀 수 있습니다.
5.5 pgvector의 거리 연산자
PostgreSQL 확장인 pgvector는 SQL 연산자 형태로 거리를 노출합니다.
| 연산자 | 의미 |
|---|---|
<-> |
L2 거리 (Euclidean) |
<#> |
음수 내적 (negative inner product) |
<=> |
cosine 거리 |
<+> |
L1 거리 (Manhattan) |
<~> |
Hamming 거리 (bit) |
<%> |
Jaccard 거리 (bit) |
내적이 음수로 표현되는 이유는 pgvector의 ORDER BY 의미를 "작은 값이 가깝다"로 통일하기 위해서입니다. 그래서 inner product가 클수록(=유사할수록) <#> 연산자의 값은 작아집니다.
6. ANN 인덱스 — IVFFlat과 HNSW
ANN 인덱스는 정확도와 속도를 맞바꾸는 자료구조입니다. 모든 벡터와 거리를 계산하는 brute-force(flat 또는 sequential scan) 대신, 일부만 보고 "충분히 가까운" 후보를 빠르게 골라냅니다. pgvector가 제공하는 두 인덱스를 비교하면 ANN 인덱스 일반의 trade-off가 잘 드러납니다.
6.1 IVFFlat — 클러스터링 기반
전체 벡터를 K개의 클러스터로 나누고(보통 K-means), 각 클러스터의 중심점만 먼저 비교합니다. 질의 벡터에 가장 가까운 몇 개 클러스터(probes) 안에서만 정확 검색을 합니다.
flowchart LR
Q[Query Vector] --> CMP[Compare with cluster centroids]
CMP --> SEL[Select top probes clusters]
SEL --> SCAN[Brute-force inside selected clusters]
SCAN --> RES[Top-K Result]
- 빌드 시간 짧음, 메모리 적음
probes값을 키우면 정확도 증가, 속도 감소- 데이터가 추가되어 분포가 바뀌면 재학습이 필요한 경우가 있음
6.2 HNSW — 다층 그래프 기반
벡터들을 작은 세계 그래프(small-world graph)로 연결하고, 위층은 듬성하게 아래층은 빽빽하게 만든 다층 구조에서 그리디 탐색을 합니다.
flowchart TB
L2["Layer 2 (sparse)"] --> L1["Layer 1 (medium)"]
L1 --> L0["Layer 0 (dense)"]
Q[Query] --> L2
- 위층에서 큰 보폭으로 빠르게 이동
- 아래층에서 정밀하게 좁혀 들어감
- 빌드 시간 길고 메모리 큼, 그러나 같은 정확도 기준에서 검색 속도가 가장 빠름
6.3 비교
| 항목 | IVFFlat | HNSW |
|---|---|---|
| 자료구조 | 클러스터 + flat 스캔 | 다층 그래프 |
| 빌드 시간 | 짧음 | 김 |
| 메모리 | 적음 | 큼 |
| 검색 정확도/속도 | 보통 | 가장 좋음 |
| 동적 추가 | 분포 변화에 약함 | 강함 |
| pgvector 권장 | 기존 데이터 많을 때 | 일반적으로 우선 |
운영에서는 데이터가 충분히 안정적이고 메모리에 여유가 있으면 HNSW가 보통 우월합니다. 데이터가 자주 바뀌거나 빌드 시간을 줄이고 싶으면 IVFFlat이 합리적입니다. 어느 쪽을 쓰든 인덱스 없이도 정확 검색이 동작하므로(느릴 뿐) 초기에는 인덱스 없이 시작해도 무방합니다.
7. Spring AI에서 차원을 세팅하는 이유
Spring AI에서 PgVectorStore를 설정할 때 다음과 같은 코드를 자주 봅니다.
@Bean
VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) {
return PgVectorStore.builder(jdbcTemplate, embeddingModel)
.dimensions(1536)
.distanceType(PgVectorStore.PgDistanceType.COSINE_DISTANCE)
.indexType(PgVectorStore.PgIndexType.HNSW)
.initializeSchema(true)
.build();
}
dimensions(1536) 한 줄이 왜 필요한지를 두 층에서 봐야 합니다.
7.1 PostgreSQL 측: vector(N) 컬럼은 N에 묶인다
pgvector는 vector(N) 형태의 컬럼 타입을 정의합니다. 여기서 N이 차원입니다.
CREATE TABLE vector_store (
id uuid PRIMARY KEY,
content text,
metadata jsonb,
embedding vector(1536)
);
- N은 컬럼 정의 시점에 고정됩니다.
- 차원이 다른 벡터를 INSERT 하면 에러가 납니다.
- 인덱스(
vector_cosine_ops등)도 컬럼 타입에 묶입니다. - 차원을 바꾸려면 컬럼을 새로 만들고 데이터를 다시 채워 넣어야 합니다.
저장소 측면에서도 vector(N)은 4 * N + 8 바이트를 사용합니다. 1536차원이면 한 행당 약 6.1KB가 임베딩 컬럼만으로 들어갑니다. 잘못된 차원으로 시작했다가 갈아 끼우는 비용은 데이터 크기에 비례합니다.
7.2 Spring AI 측: 차원 결정 우선순위
PgVectorStore는 컬럼을 만들 때 다음 순서로 차원을 결정합니다.
- 빌더에
dimensions(...)로 명시적으로 지정한 값 embeddingModel.dimensions()호출 결과- fallback 1536
우선순위가 이렇게 정해진 이유는 운영 신뢰성 때문입니다.
embeddingModel.dimensions()는 실제로 임베딩 API를 한 번 호출해 결과 길이를 봅니다. 즉 네트워크 호출과 비용이 발생합니다. 부팅 시점에 외부 API가 느리거나 비싸면 곤란합니다.- 모델 종류를 바꿀 때(예: text-embedding-3-small → text-embedding-3-large) 차원이 바뀌어 기존 테이블과 충돌하는 사고를 막아야 합니다. 명시적 값이 있으면 빌더가 부팅 시점에 검증을 도울 수 있습니다.
- 한 애플리케이션에서 같은 임베딩 모델을 쓰면서도 일부러 차원을 줄여(예: text-embedding-3-large 3072 → 1024) 저장하고 싶은 경우가 있습니다.
따라서 운영 코드에서는 dimensions(...)를 명시적으로 적어 두는 편이 안전합니다. "이 인덱스는 이 차원으로 박혀 있다"가 코드와 스키마 양쪽에서 같은 값으로 보이게 만들어야 하기 때문입니다.
7.3 차원 불일치 사고 패턴
운영에서 흔한 실수 두 가지를 정리합니다.
- 모델 교체 시 dimensions만 갈아 끼우면 INSERT 실패: text-embedding-3-large(3072)로 모델을 바꿨는데 테이블은 vector(1536)으로 정의되어 있으면 INSERT 시점에 실패합니다. 마이그레이션 절차는 새 컬럼/테이블 생성 → 전체 재인덱싱 → 라우팅 전환입니다.
- dimensions를 비웠을 때 첫 부팅이 느림: 명시 값이 없으면 부팅 중 임베딩 API를 호출합니다. 외부 API 장애가 부팅 실패로 이어집니다.
8. Spring AI VectorStore와 RAG
Spring AI는 RAG 구성 요소를 VectorStore 인터페이스, Document, SearchRequest, Advisor 체인으로 분해해 둡니다. 핵심만 추리면 다음 세 메서드입니다.
public interface VectorStore {
void add(List<Document> documents);
void delete(List<String> idList);
void delete(Filter.Expression filterExpression);
List<Document> similaritySearch(SearchRequest request);
}
SearchRequest의 빌더 옵션은 다음과 같습니다.
| 옵션 | 기본값 | 의미 |
|---|---|---|
query(String) |
"" | 임베딩 비교용 텍스트 |
topK(int) |
4 | 상위 K개 결과 |
similarityThreshold(double) |
0.0 (모두 허용) | 0.0~1.0 사이 임계값. 1.0이면 정확 일치만 |
filterExpression(...) |
null | 메타데이터 필터 (예: tenant, source) |
호출은 다음과 같은 모양입니다.
List<Document> ctx = vectorStore.similaritySearch(SearchRequest.builder()
.query("Spring Bean 생명주기에서 BeanPostProcessor의 역할은?")
.topK(4)
.similarityThreshold(0.7)
.filterExpression("tenant == 'prod' && lang == 'ko'")
.build());
이렇게 꺼낸 Document 목록을 프롬프트에 끼워 넣어 LLM을 호출하는 부분은 QuestionAnswerAdvisor나 RetrievalAugmentationAdvisor가 담당합니다. 다음 코드 한 덩어리가 RAG 한 사이클입니다.
String answer = ChatClient.builder(chatModel)
.build()
.prompt()
.user("Spring Bean 생명주기에서 BeanPostProcessor의 역할은?")
.advisors(new QuestionAnswerAdvisor(vectorStore))
.call()
.content();
QuestionAnswerAdvisor는 사용자의 질문 텍스트를 그대로 임베딩해서 vectorStore에서 K개를 꺼내 프롬프트의 시스템 메시지 자리에 끼워 넣습니다. 즉 5장에서 본 cosine 유사도, 7장에서 본 차원 합의, 6장에서 본 ANN 인덱스, 그리고 이 어드바이저가 한 줄짜리 RAG로 보이게 만드는 추상화의 안쪽 층입니다.
9. 운영에서 자주 만나는 함정
9.1 청크 크기와 검색 품질
청크가 너무 길면 임베딩이 평균값에 가까워져 변별력이 떨어집니다. 너무 짧으면 문맥이 끊겨 LLM이 이어 붙이지 못합니다. 도메인에 따라 다르지만 일반 텍스트는 200~800 토큰, 코드는 함수/클래스 단위가 무난한 출발점입니다.
9.2 similarity threshold 튜닝
기본값 0.0은 "모든 결과 반환"입니다. 이대로 두면 무관한 문서까지 컨텍스트에 끼어 LLM이 산만해집니다. 도메인 데이터로 검색 결과의 상하한을 직접 본 뒤 0.6~0.8 사이에서 잡는 것이 보통입니다. 너무 높이면 답할 자료가 사라져 hallucination을 유도합니다.
9.3 메타데이터 필터링은 필수에 가깝다
멀티 테넌트 시스템이라면 tenant, 다국어라면 lang, 시계열이라면 created_at 같은 메타데이터를 임베딩 옆에 같이 저장합니다. 유사도만으로 거르지 말고 SQL의 WHERE에 해당하는 필터를 항상 같이 걸어야 권한 누출 같은 사고를 막을 수 있습니다.
9.4 인덱싱과 질의의 임베딩 모델은 같아야 한다
2장에서 짚은 제약입니다. 모델을 바꾸려면 전체 재인덱싱입니다. 임베딩 모델 버전을 메타데이터에 함께 적어 두면 잘못된 인덱스에 쿼리가 날아갈 때 빠르게 알아챌 수 있습니다.
9.5 정확 검색이 충분히 빠른 영역도 있다
수만~수십만 행 수준이면 인덱스 없이 sequential scan으로도 100ms 안에 답이 옵니다. ANN 인덱스를 도입하기 전에 데이터 규모와 응답 시간을 먼저 측정해 보는 편이 정확도 손실 없이 이득을 봅니다.
10. 정리
- RAG는 LLM의 가중치 안 지식(parametric)과 외부 인덱스(non-parametric)를 결합해 "모르는 사실"을 외부에서 가져옵니다.
- Vector Database는 RDB와 검색 모델 자체가 다릅니다. 정렬 가능한 키가 아니라 고차원 벡터의 nearest-neighbor 문제를 풉니다.
- 임베딩 모델은 차원 D의 의미 공간을 정의합니다. 차원은 모델 아키텍처가 정한 고정값이며 인덱스 차원과 일치해야 합니다.
- 유사도 함수는 cosine, inner product, euclidean이 주로 쓰이며 정규화 벡터에서 cosine과 inner product는 동일합니다.
- ANN 인덱스(IVFFlat, HNSW)는 정확도-속도 trade-off의 산물입니다. 데이터 규모와 빌드/메모리 비용에 따라 고릅니다.
- Spring AI에서
dimensions(...)를 명시하는 이유는 (1)vector(N)스키마가 N에 묶이고 (2) 부팅 시점에 외부 임베딩 API에 의존하지 않기 위해서입니다. - RAG 사이클은 결국 한 줄짜리 advisor 추상화 뒤에 임베딩, 차원, 인덱스, 유사도 함수, 메타데이터 필터가 차곡차곡 쌓인 구조입니다.
참고자료
- Lewis et al., "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks" (2020): https://arxiv.org/abs/2005.11401
- Spring AI Reference, Vector Databases: https://docs.spring.io/spring-ai/reference/api/vectordbs.html
- Spring AI Reference, Embeddings: https://docs.spring.io/spring-ai/reference/api/embeddings.html
- Spring AI Reference, Retrieval Augmented Generation: https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html
- Spring AI API, VectorStore: https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/vectorstore/VectorStore.html
- Spring AI API, SearchRequest: https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/vectorstore/SearchRequest.html
- Spring AI API, PgVectorStore: https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/vectorstore/pgvector/PgVectorStore.html
- pgvector GitHub: https://github.com/pgvector/pgvector
- OpenAI Embeddings Guide: https://platform.openai.com/docs/guides/embeddings
- OpenAI New embedding models (text-embedding-3): https://openai.com/index/new-embedding-models-and-api-updates/
- HNSW (Hierarchical Navigable Small World) Wikipedia: https://en.wikipedia.org/wiki/Hierarchical_navigable_small_world
- pgvector Index Performance and Comparison: https://deepwiki.com/pgvector/pgvector/5.3-index-performance-and-comparison

