05-infrastructure-vector-index

Dec 7, 2025

Vector Index & Similarity Search

코드: lib/graph/vector.ts

개요

FalkorDB의 벡터 인덱스를 사용한 시맨틱 메모리 검색입니다.

핵심 개념

용어

설명

Embedding

텍스트를 고차원 벡터로 변환한 것 (1536차원)

Vector Index

벡터 간 유사도 검색을 위한 인덱스

Cosine Similarity

두 벡터의 방향 유사도 (0~1)

설정

기본 구성

// lib/graph/vector.ts
export const MEMORY_VECTOR_INDEX = {
  label: 'Memory',
  property: 'embedding',
  dimension: 1536,           // OpenAI text-embedding-3-small
  similarityFunction: 'cosine'
};

인덱스 초기화

import { client, initializeVectorIndex } from '@/lib/graph';

// 앱 시작 시 한 번 호출
await initializeVectorIndex(client);

임베딩 저장

단일 저장

import { client, storeEmbedding } from '@/lib/graph';

// OpenAI로 임베딩 생성 후 저장
const embedding = await openai.embeddings.create({
  model: 'text-embedding-3-small',
  input: 'Meeting about Q1 goals'
});

await storeEmbedding(client, 'mem-123', embedding.data[0].embedding);

배치 저장

import { client, storeEmbeddingsBatch } from '@/lib/graph';

const embeddings = [
  { id: 'mem-1', embedding: [...] },
  { id: 'mem-2', embedding: [...] },
  // ...
];

await storeEmbeddingsBatch(client, embeddings);

유사도 검색

기본 검색

import { client, searchSimilarMemories } from '@/lib/graph';

// 쿼리 임베딩으로 검색
const results = await searchSimilarMemories(client, queryEmbedding, {
  topK: 10,      // 상위 10개
  minScore: 0.7  // 유사도 0.7 이상
});

for (const { node, score } of results) {
  console.log(`${node.id}: ${score.toFixed(2)} - ${node.content}`);
}

필터 적용 검색

// 특정 카테고리의 메모리만 검색
const results = await searchSimilarMemories(client, queryEmbedding, {
  topK: 10,
  minScore: 0.6,
  filters: {
    category: 'meeting',
    importance: 0.8  // >= 0.8은 지원 안됨, 정확한 값만
  }
});

특정 메모리와 유사한 것 찾기

import { client, findSimilarToMemory } from '@/lib/graph';

// mem-123과 유사한 메모리 찾기
const similar = await findSimilarToMemory(client, 'mem-123', {
  topK: 5,
  minScore: 0.75
});

유틸리티 함수

임베딩 존재 여부 확인

import { client, hasEmbedding } from '@/lib/graph';

if (await hasEmbedding(client, 'mem-123')) {
  console.log('Embedding exists');
}

임베딩 없는 메모리 조회

import { client, getMemoriesWithoutEmbeddings } from '@/lib/graph';

// 임베딩 생성이 필요한 메모리 가져오기
const needsEmbedding = await getMemoriesWithoutEmbeddings(client, 100);

for (const { id, content } of needsEmbedding) {
  // 임베딩 생성 후 저장
}

설계 결정

왜 1536 차원인가?

OpenAI의 text-embedding-3-small 모델 출력 차원입니다.

모델

차원

비용

성능

text-embedding-3-small

1536

저렴

충분

text-embedding-3-large

3072

비쌈

높음

text-embedding-ada-002

1536

중간

레거시

대부분의 use case에서 small 모델로 충분합니다.

왜 Cosine Similarity인가?

  • 정규화된 벡터: OpenAI 임베딩은 정규화됨

  • 방향 기반: 벡터 크기가 아닌 방향으로 유사도 측정

  • 효율적: 계산이 빠름

대안:

  • Euclidean: 거리 기반, 정규화 필요

  • Dot Product: 크기에 민감

왜 Memory 노드만 임베딩을 가지는가?

  • Person/Entity/Thread: 관계를 통해 탐색

  • Memory: 내용 기반 검색 필요

[Query]  [Similar Memories]  [Related Entities/People]

관계 그래프를 활용한 2단계 검색이 더 효과적입니다.

성능 고려사항

인덱스 빌드 시간

벡터 인덱스는 데이터가 많을수록 빌드 시간이 늘어납니다:

  • 1,000개: 즉시

  • 10,000개: 수 초

  • 100,000개: 수 분

검색 성능

FalkorDB의 벡터 인덱스는 HNSW 알고리즘 기반:

  • 검색 시간: O(log n)

  • 정확도: 근사치 (정확한 결과가 아닐 수 있음)

메모리 사용량

벡터당 메모리: dimension * 4 bytes = 1536 * 4 = 6KB

메모리 수

벡터 저장 공간

1,000

~6 MB

10,000

~60 MB

100,000

~600 MB

RAG 파이프라인 통합

전체 흐름

1. 사용자 쿼리 임베딩 생성
2. 벡터 검색 관련 메모리 (top-K)
3. 그래프 탐색 관련 Person/Entity 확장
4. 컨텍스트 조립 LLM 프롬프트
5. LLM 응답 생성

코드 예시

import { client, searchSimilarMemories, getOutgoingRelationships } from '@/lib/graph';

async function retrieveContext(query: string, queryEmbedding: number[]) {
  // 1. 유사한 메모리 검색
  const memories = await searchSimilarMemories(client, queryEmbedding, {
    topK: 5,
    minScore: 0.7
  });

  // 2. 관련 엔티티 확장
  const context = [];
  for (const { node, score } of memories) {
    const people = await getOutgoingRelationships(client, {
      sourceId: node.id,
      type: 'MENTIONS',
      minConfidence: 0.8
    });

    context.push({
      memory: node,
      relevance: score,
      relatedPeople: people
    });
  }

  return context;
}

다음 문서

  • FalkorDB 연결 - 클라이언트 설정

  • 그래프 스키마 - 노드 인덱스