Skip to content

필사 모드: PostgreSQL + pgvector 벡터 검색 실전 가이드: RAG부터 하이브리드 검색까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

1. 왜 PostgreSQL + pgvector인가

별도 벡터 DB(Pinecone, Qdrant, Weaviate) 대신 PostgreSQL + pgvector를 선택하는 이유:

| 특성 | 전용 벡터 DB | PostgreSQL + pgvector |

| ---------------- | -------------- | ----------------------- |

| 추가 인프라 | ✅ 필요 | ❌ 기존 PostgreSQL 활용 |

| ACID 트랜잭션 | ❌ 제한적 | ✅ 완전 지원 |

| JOIN/관계형 쿼리 | ❌ 불가 | ✅ 자유롭게 조합 |

| 하이브리드 검색 | ⚠️ 제한적 | ✅ tsvector + vector |

| 운영 복잡도 | 높음 | 낮음 (기존 DBA 활용) |

| 확장성 | ✅ 수십억 벡터 | ⚠️ 수천만 수준 |

**결론**: 벡터 수가 수천만 이하이고, 관계형 데이터와 함께 관리해야 한다면 pgvector가 최적.

2. 설치 및 설정

2.1 pgvector 설치

Ubuntu/Debian

sudo apt install postgresql-17-pgvector

macOS (Homebrew)

brew install pgvector

Docker

docker run -d --name pgvector \

-e POSTGRES_PASSWORD=secret \

-p 5432:5432 \

pgvector/pgvector:pg17

2.2 확장 활성화

-- pgvector 확장 설치

CREATE EXTENSION IF NOT EXISTS vector;

-- 버전 확인

SELECT extversion FROM pg_extension WHERE extname = 'vector';

-- 0.8.0

3. 기본 사용법

3.1 테이블 생성

-- 문서 테이블 (1536차원 = OpenAI text-embedding-3-small)

CREATE TABLE documents (

id BIGSERIAL PRIMARY KEY,

title TEXT NOT NULL,

content TEXT NOT NULL,

embedding vector(1536), -- 벡터 컬럼!

metadata JSONB DEFAULT '{}',

created_at TIMESTAMPTZ DEFAULT NOW()

);

-- 384차원 (sentence-transformers/all-MiniLM-L6-v2)

CREATE TABLE chunks (

id BIGSERIAL PRIMARY KEY,

doc_id BIGINT REFERENCES documents(id),

chunk_text TEXT NOT NULL,

embedding vector(384),

chunk_index INT

);

3.2 데이터 삽입

-- 단일 삽입

INSERT INTO documents (title, content, embedding)

VALUES (

'Kubernetes RBAC Guide',

'RBAC is a method of regulating access...',

'[0.1, 0.2, 0.3, ...]'::vector -- 1536차원 벡터

);

-- Python에서 배치 삽입

from pgvector.psycopg2 import register_vector

conn = psycopg2.connect("dbname=mydb user=postgres password=secret")

register_vector(conn)

cur = conn.cursor()

OpenAI 임베딩 생성

from openai import OpenAI

client = OpenAI()

texts = ["Kubernetes RBAC guide", "Docker networking basics", ...]

response = client.embeddings.create(

model="text-embedding-3-small",

input=texts

)

배치 삽입

for text, emb_data in zip(texts, response.data):

embedding = np.array(emb_data.embedding)

cur.execute(

"INSERT INTO documents (title, content, embedding) VALUES (%s, %s, %s)",

(text, text, embedding)

)

conn.commit()

3.3 유사도 검색

-- 코사인 유사도 (가장 일반적)

SELECT id, title,

1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS similarity

FROM documents

ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector

LIMIT 10;

-- L2 거리

SELECT id, title,

embedding <-> '[0.1, 0.2, ...]'::vector AS distance

FROM documents

ORDER BY embedding <-> '[0.1, 0.2, ...]'::vector

LIMIT 10;

-- 내적 (Inner Product) — 정규화된 벡터에서 코사인과 동일

SELECT id, title,

(embedding <#> '[0.1, 0.2, ...]'::vector) * -1 AS similarity

FROM documents

ORDER BY embedding <#> '[0.1, 0.2, ...]'::vector

LIMIT 10;

**연산자 정리:**

| 연산자 | 의미 | 용도 |

| ------ | ----------- | ------------------------- |

| `<->` | L2 거리 | 유클리드 거리 기반 |

| `<=>` | 코사인 거리 | 방향 유사도 (가장 일반적) |

| `<#>` | 내적 (음수) | 정규화 벡터에서 사용 |

4. 인덱스: HNSW vs IVFFlat

4.1 IVFFlat

-- IVFFlat 인덱스 생성

CREATE INDEX ON documents

USING ivfflat (embedding vector_cosine_ops)

WITH (lists = 100);

-- 검색 시 probe 수 설정 (정확도 vs 속도 트레이드오프)

SET ivfflat.probes = 10;

| 파라미터 | 설명 | 권장값 |

| -------- | ---------------- | --------------------- |

| `lists` | 클러스터 수 | √(row수) ~ row수/1000 |

| `probes` | 검색 클러스터 수 | lists/10 ~ lists/5 |

4.2 HNSW (권장)

-- HNSW 인덱스 생성 (빌드 시간 길지만 검색 빠름)

CREATE INDEX ON documents

USING hnsw (embedding vector_cosine_ops)

WITH (m = 16, ef_construction = 200);

-- 검색 시 ef_search 설정

SET hnsw.ef_search = 100;

| 파라미터 | 설명 | 권장값 |

| ----------------- | --------- | ------ |

| `m` | 연결 수 | 16~64 |

| `ef_construction` | 빌드 품질 | 200+ |

| `ef_search` | 검색 품질 | 40~200 |

4.3 HNSW vs IVFFlat 비교

| 특성 | IVFFlat | HNSW |

| --------- | ----------------- | -------------- |

| 빌드 속도 | ✅ 빠름 | ❌ 느림 |

| 검색 속도 | 보통 | ✅ 빠름 |

| Recall | 보통 (probe 의존) | ✅ 높음 |

| 메모리 | ✅ 적음 | ❌ 많음 |

| 업데이트 | ❌ 재빌드 필요 | ✅ 실시간 가능 |

**권장**: 대부분의 경우 **HNSW**. 데이터가 자주 변경되거나 메모리가 제한적이면 IVFFlat.

5. 하이브리드 검색: 벡터 + 전문 검색

5.1 Full-Text Search + Vector Search

-- tsvector 컬럼 추가

ALTER TABLE documents ADD COLUMN tsv tsvector

GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || content)) STORED;

CREATE INDEX ON documents USING gin(tsv);

-- 하이브리드 검색 함수

CREATE OR REPLACE FUNCTION hybrid_search(

query_text TEXT,

query_embedding vector(1536),

match_count INT DEFAULT 10,

vector_weight FLOAT DEFAULT 0.7,

text_weight FLOAT DEFAULT 0.3

)

RETURNS TABLE (id BIGINT, title TEXT, score FLOAT) AS $$

BEGIN

RETURN QUERY

WITH vector_results AS (

SELECT d.id, d.title,

1 - (d.embedding <=> query_embedding) AS vector_score

FROM documents d

ORDER BY d.embedding <=> query_embedding

LIMIT match_count * 3

),

text_results AS (

SELECT d.id, d.title,

ts_rank(d.tsv, plainto_tsquery('english', query_text)) AS text_score

FROM documents d

WHERE d.tsv @@ plainto_tsquery('english', query_text)

LIMIT match_count * 3

),

combined AS (

SELECT

COALESCE(v.id, t.id) AS id,

COALESCE(v.title, t.title) AS title,

COALESCE(v.vector_score, 0) * vector_weight +

COALESCE(t.text_score, 0) * text_weight AS score

FROM vector_results v

FULL OUTER JOIN text_results t ON v.id = t.id

)

SELECT c.id, c.title, c.score

FROM combined c

ORDER BY c.score DESC

LIMIT match_count;

END;

$$ LANGUAGE plpgsql;

-- 사용

SELECT * FROM hybrid_search(

'Kubernetes RBAC security',

'[0.1, 0.2, ...]'::vector(1536)

);

5.2 Reciprocal Rank Fusion (RRF)

CREATE OR REPLACE FUNCTION rrf_search(

query_text TEXT,

query_embedding vector(1536),

match_count INT DEFAULT 10,

rrf_k INT DEFAULT 60

)

RETURNS TABLE (id BIGINT, title TEXT, rrf_score FLOAT) AS $$

BEGIN

RETURN QUERY

WITH vector_ranked AS (

SELECT d.id, d.title,

ROW_NUMBER() OVER (ORDER BY d.embedding <=> query_embedding) AS rank

FROM documents d

LIMIT match_count * 5

),

text_ranked AS (

SELECT d.id, d.title,

ROW_NUMBER() OVER (

ORDER BY ts_rank(d.tsv, plainto_tsquery('english', query_text)) DESC

) AS rank

FROM documents d

WHERE d.tsv @@ plainto_tsquery('english', query_text)

LIMIT match_count * 5

),

fused AS (

SELECT

COALESCE(v.id, t.id) AS id,

COALESCE(v.title, t.title) AS title,

COALESCE(1.0 / (rrf_k + v.rank), 0) +

COALESCE(1.0 / (rrf_k + t.rank), 0) AS rrf_score

FROM vector_ranked v

FULL OUTER JOIN text_ranked t ON v.id = t.id

)

SELECT f.id, f.title, f.rrf_score

FROM fused f

ORDER BY f.rrf_score DESC

LIMIT match_count;

END;

$$ LANGUAGE plpgsql;

6. RAG 파이프라인 통합

6.1 Python 전체 예제

from pgvector.psycopg2 import register_vector

from openai import OpenAI

client = OpenAI()

conn = psycopg2.connect("dbname=ragdb user=postgres password=secret")

register_vector(conn)

def embed(text: str) -> list[float]:

resp = client.embeddings.create(

model="text-embedding-3-small", input=text

)

return resp.data[0].embedding

def rag_query(question: str, top_k: int = 5) -> str:

query_vec = embed(question)

cur = conn.cursor()

cur.execute("""

SELECT title, content,

1 - (embedding <=> %s::vector) AS similarity

FROM documents

WHERE 1 - (embedding <=> %s::vector) > 0.7

ORDER BY embedding <=> %s::vector

LIMIT %s

""", (query_vec, query_vec, query_vec, top_k))

results = cur.fetchall()

context = "\n\n".join([

f"[{r[0]}] (similarity: {r[2]:.3f})\n{r[1]}"

for r in results

])

response = client.chat.completions.create(

model="gpt-4o",

messages=[

{"role": "system", "content": f"Answer based on context:\n{context}"},

{"role": "user", "content": question}

]

)

return response.choices[0].message.content

사용

answer = rag_query("Kubernetes RBAC에서 ClusterRole과 Role의 차이는?")

print(answer)

7. 성능 튜닝

7.1 핵심 설정

-- 작업 메모리 (인덱스 빌드/검색 시)

SET maintenance_work_mem = '2GB'; -- HNSW 빌드 시

SET work_mem = '256MB'; -- 검색 시

-- 병렬 처리

SET max_parallel_workers_per_gather = 4;

SET max_parallel_maintenance_workers = 4;

-- HNSW 빌드 최적화

SET maintenance_work_mem = '4GB';

-- 빌드 후 원복

7.2 벤치마크

100만 벡터 (1536차원), PostgreSQL 17 + pgvector 0.8.0:

| 인덱스 | 빌드 시간 | 검색 지연 (p50) | Recall@10 | 메모리 |

| ------------------------------- | --------- | --------------- | --------- | ------ |

| 없음 (brute) | - | 850ms | 100% | 0 |

| IVFFlat (lists=1000, probes=50) | 45s | 8ms | 95% | 1.2GB |

| HNSW (m=16, ef=200) | 12min | **3ms** | **99%** | 2.8GB |

8. 퀴즈

**코사인 거리** (1 - cosine_similarity). 값이 작을수록 유사하다. 유사도로 변환하려면 `1 - (a <=> b)`.

**HNSW**. 빌드는 느리지만 검색은 빠르고 Recall도 높다. 대부분의 프로덕션 환경에서 권장.

벡터 검색은 의미적 유사도에 강하지만 키워드 정확 매칭에 약함. 전문 검색과 결합하면 **의미적 유사도 + 키워드 정확도** 모두 확보.

각 검색 결과의 **순위의 역수**를 합산. 점수 스케일이 다른 검색 결과를 정규화 없이 결합할 수 있는 장점.

`lists`는 클러스터 수, `probes`는 검색 시 탐색할 클러스터 수. probes가 클수록 정확하지만 느림. 보통 probes = lists/10 ~ lists/5.

(1) 관계형 데이터와 JOIN 필요 (2) ACID 트랜잭션 필요 (3) 벡터 수 수천만 이하 (4) 추가 인프라 운영 부담 최소화.

현재 단락 (1/262)

별도 벡터 DB(Pinecone, Qdrant, Weaviate) 대신 PostgreSQL + pgvector를 선택하는 이유:

작성 글자: 0원문 글자: 7,208작성 단락: 0/262