**๋ก์ปฌ ๋ฒกํฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค(Local Vector Database)**๋ ๋ฒกํฐ ์๋ฒ ๋ฉ์ ์ ์ฅํ๊ณ ์ ์ฌ๋ ๊ฒ์์ ์ํํ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก, ์๋ฒ ์์ด ๋ก์ปฌ ํ๊ฒฝ์์ ์คํ๋ฉ๋๋ค. AI/ML ์ ํ๋ฆฌ์ผ์ด์ ์์ ์๋ฏธ ๊ธฐ๋ฐ ๊ฒ์(semantic search)๊ณผ ์ถ์ฒ ์์คํ ์ ๊ตฌํํ๋ ํต์ฌ ์ธํ๋ผ์ ๋๋ค.
์ฃผ์ ํน์ง:
- ๋ฒกํฐ ์๋ฒ ๋ฉ ์ ์ฅ ๋ฐ ์ธ๋ฑ์ฑ
- ์ ์ฌ๋ ๊ฒ์ (์ฝ์ฌ์ธ ์ ์ฌ๋, L2 ๊ฑฐ๋ฆฌ ๋ฑ)
- ๋ก์ปฌ ํ์ผ ์์คํ ๊ธฐ๋ฐ (์๋ฒ ๋ถํ์)
- Python/JavaScript ๋ฑ ๋ค์ํ ์ธ์ด ์ง์
ํด๋น ๊ฐ๋ ์ด ํ์ํ ์ด์
- ์๋ฏธ ๊ธฐ๋ฐ ๊ฒ์: ํค์๋ ๋งค์นญ์ด ์๋ ๋ฌธ๋งฅ๊ณผ ์๋ฏธ๋ฅผ ์ดํดํ๋ ๊ฒ์
- RAG ๊ตฌํ: LLM์ ์ธ๋ถ ์ง์์ ์ฃผ์ ํ์ฌ hallucination ๊ฐ์
- ๋น ๋ฅธ ํ๋กํ ํ์ดํ: ์๋ฒ ์ค์ ์์ด ๋ก์ปฌ์์ ์ฆ์ ๊ฐ๋ฐ ์์
- ๋ฐ์ดํฐ ์ฃผ๊ถ: ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ฅผ ์ธ๋ถ ์๋ฒ๋ก ์ ์กํ์ง ์์
AS-IS: ์ ํต์ ์ธ ํค์๋ ๊ฒ์
sequenceDiagram autonumber participant User participant App participant DB as Relational DB User->>App: "apple product" App->>DB: SELECT * WHERE text LIKE '%apple%' DB-->>App: "apple fruit", "apple pie" Note over App: "apple ํ์ฌ" ์ ํ์<br/>์ฐพ์ง ๋ชปํจ App-->>User: ๋ถ์ ํํ ๊ฒฐ๊ณผ
TO-BE: ๋ฒกํฐ ๊ธฐ๋ฐ ์๋ฏธ ๊ฒ์
sequenceDiagram autonumber participant User participant App participant Embed as Embedding Model participant VDB as Vector DB User->>App: "apple product" App->>Embed: Encode query Embed-->>App: [0.2, 0.8, ...] App->>VDB: Similarity search VDB-->>App: "iPhone", "MacBook", "iPad" Note over VDB: ์๋ฏธ์ ์ผ๋ก<br/>์ ์ฌํ ๊ฒฐ๊ณผ ๋ฐํ App-->>User: ์ ํํ ์ ํ ์ ๋ณด
์ฃผ์ ๋ก์ปฌ ๋ฒกํฐ DB 3์ข ๋น๊ต
| ํน์ง | LanceDB | Chroma | FAISS |
|---|---|---|---|
| ๊ฐ๋ฐ์ฌ | LanceDB Inc. | Chroma | Meta AI Research |
| ์ฃผ์ ์ฉ๋ | ๋ฉํฐ๋ชจ๋ฌ AI ์ ํ๋ฆฌ์ผ์ด์ | RAG ๋ฐ ์๋ฏธ ๊ฒ์ | ๋๊ท๋ชจ ๋ฒกํฐ ์ ์ฌ๋ ๊ฒ์ |
| ๋ฐ์ดํฐ ํฌ๋งท | Lance (columnar) | ์์ฒด ํฌ๋งท | In-memory indices |
| ์ง์ ์ธ์ด | Python, JS, Rust | Python, JS | Python, C++ |
| ๋ฉํฐ๋ชจ๋ฌ ์ง์ | โ (ํ ์คํธ, ์ด๋ฏธ์ง, ๋น๋์ค ๋ฑ) | โ ๏ธ (์ฃผ๋ก ํ ์คํธ) | โ (๋ฒกํฐ๋ง) |
| ๋ฉํ๋ฐ์ดํฐ ํํฐ๋ง | โ SQL ์ฟผ๋ฆฌ ์ง์ | โ | โ ๏ธ (์ ํ์ ) |
| Full-text ๊ฒ์ | โ | โ | โ |
| GPU ๊ฐ์ | โ | โ | โ |
| ์๋ ๋ฒ์ ๋ | โ | โ | โ |
| Zero-copy | โ | โ | โ |
| ํด๋ฌ์คํฐ๋ง | โ | โ | โ |
| ์ธ๋ฑ์ค ํ์ | ์์ฒด + HNSW | ์์ฒด | ๋ค์ํ ANN ์๊ณ ๋ฆฌ์ฆ |
| ํ์ต ๊ณก์ | ์ค๊ฐ | ๋ฎ์ (๊ฐ์ฅ ์ฌ์) | ๋์ |
| ํ์ฅ์ฑ | Petabyte ๊ท๋ชจ | ์ค์ ๊ท๋ชจ | Billion ๊ท๋ชจ |
| ๋ฉ๋ชจ๋ฆฌ ํจ์จ | ๋์ (columnar) | ์ค๊ฐ | ๋์ (์์ถ ์ง์) |
| ๋ผ์ด์ ์ค | Apache 2.0 | Apache 2.0 | MIT |
์ฅ๋จ์ ์์ฝ
LanceDB:
- โ ์ฅ์ : ๋ฉํฐ๋ชจ๋ฌ, SQL ์ง์, ๋ฒ์ ๋, ๋์ ํ์ฅ์ฑ, columnar ํฌ๋งท์ผ๋ก ๋ถ์ ์นํ์
- โ ๋จ์ : ์๋์ ์ผ๋ก ์๋ก์ด ํ๋ก์ ํธ, ์ปค๋ฎค๋ํฐ ๊ท๋ชจ ์์, ํด๋ฌ์คํฐ๋ง ๋ฏธ์ง์
Chroma:
- โ ์ฅ์ : ๊ฐ์ฅ ์ฌ์ด ์ฌ์ฉ๋ฒ, ์๋ ์๋ฒ ๋ฉ ์์ฑ, LangChain/LlamaIndex ํตํฉ, ๋น ๋ฅธ ํ๋กํ ํ์ดํ
- โ ๋จ์ : ๋๊ท๋ชจ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ํ์ , GPU ๊ฐ์ ์์, ๋ฉํฐ๋ชจ๋ฌ ์ง์ ์ฝํจ
FAISS:
- โ ์ฅ์ : ๊ฒ์ฆ๋ ์ฑ๋ฅ, ๋ค์ํ ์ธ๋ฑ์ค ์๊ณ ๋ฆฌ์ฆ, GPU ๊ฐ์, ํด๋ฌ์คํฐ๋ง, ๋๊ท๋ชจ ์ฒ๋ฆฌ
- โ ๋จ์ : ๋ฎ์ ์์ค API (๋ณต์กํจ), ๋ฉํ๋ฐ์ดํฐ ํํฐ๋ง ์ฝํจ, ๋ฒกํฐ๋ง ์ ์ฅ ๊ฐ๋ฅ
LanceDB ์ฌ์ธต ๋ถ์
์ํคํ ์ฒ: Lance Columnar Format
LanceDB๋ Lance ๋ฐ์ดํฐ ํฌ๋งท์ ๊ธฐ๋ฐ์ผ๋ก ํฉ๋๋ค. Lance๋ Apache Arrow์ Parquet์์ ์๊ฐ์ ๋ฐ์ columnar ํฌ๋งท์ผ๋ก, ๋ค์๊ณผ ๊ฐ์ ํน์ง์ด ์์ต๋๋ค:
graph TB subgraph "Lance Format" A[Row Data] --> B[Columnar Storage] B --> C[Vector Column] B --> D[Metadata Columns] B --> E[Media Columns] C --> F[Index: HNSW/IVF] D --> G[SQL Engine] E --> H[Zero-copy Access] end style C fill:#e1f5ff style D fill:#fff5e1 style E fill:#ffe1f5
Columnar ํฌ๋งท์ ์ด์ :
- ์ ํ์ ์ฝ๊ธฐ: ํ์ํ ์ปฌ๋ผ๋ง ์ฝ์ด I/O ์ต์ํ
- ์์ถ ํจ์จ: ๊ฐ์ ํ์ ๋ฐ์ดํฐ๊ฐ ์ฐ์๋์ด ์์ถ๋ฅ ํฅ์
- ๋ถ์ ์ต์ ํ: SQL ์ฟผ๋ฆฌ์ ์ง๊ณ ์ฐ์ฐ์ ์ ๋ฆฌ
- Zero-copy: ๋ฉ๋ชจ๋ฆฌ ๋ณต์ฌ ์์ด ๋ฐ์ดํฐ ์ ๊ทผ
ํต์ฌ ๊ธฐ๋ฅ
1. ๋ฉํฐ๋ชจ๋ฌ ๋ฐ์ดํฐ ์ง์
# ํ
์คํธ, ์ด๋ฏธ์ง, ๋น๋์ค๋ฅผ ํ๋์ ํ
์ด๋ธ์
db = connect("./my-db")
table = create_table("multimodal", [
{
"text": "A cat playing piano",
"image_uri": "s3://bucket/cat.jpg",
"video_uri": "file://./cat_video.mp4",
"metadata": {"category": "pets"}
}
])2. Hybrid Search (Vector + Full-text + SQL)
# ๋ฒกํฐ ๊ฒ์ + SQL ํํฐ๋ง ๋์ ์ํ
results = table.search("cute animals") \
.where("metadata.category = 'pets'") \
.limit(10)3. ์๋ ๋ฒ์ ๋
# ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์๋์ผ๋ก ๋ฒ์ ๊ด๋ฆฌ
table.add([{"text": "new entry"}]) # version 1
table.add([{"text": "another entry"}]) # version 2
# ์ด์ ๋ฒ์ ์ผ๋ก ๋กค๋ฐฑ ๊ฐ๋ฅ
table.checkout(version=1)4. GPU ๊ฐ์ ์ธ๋ฑ์ฑ
# GPU๋ก ์๋ฐฑ๋ง ๋ฒกํฐ ์ธ๋ฑ์ฑ
table.create_index(
metric="cosine",
num_partitions=256,
use_gpu=True # GPU ๊ฐ์
)์ค์ ์ฌ์ฉ ์ผ์ด์ค
์ผ์ด์ค 1: Markdown ๋ฌธ์ ๊ฒ์ (โ LanceDB ์ ํฉ)
์๋๋ฆฌ์ค: ์ฌ์ฉ์๊ฐ ์ฌ๋ฌ ๊ฐ์ ๊ธฐ์ ๋ฌธ์(md ํ์ผ)๋ฅผ ์ ์ฅํ๊ณ , ๋์ค์ โOAuth ์ธ์ฆ flowโ์ ๋ํ ๋ด์ฉ์ ์ฐพ๊ณ ์ถ์ ๊ฒฝ์ฐ
sequenceDiagram autonumber participant User participant App participant Chunker as Document Chunker participant Embed as Embedding Model participant LDB as LanceDB participant LLM Note over User,LDB: 1๋จ๊ณ: ๋ฌธ์ ์ ์ฅ User->>App: auth.md ํ์ผ ์ ๋ก๋ Note over App: ํ์ผ ๋ด์ฉ:<br/>"OAuth 2.0 flow๋<br/>Authorization Code Grant..." App->>Chunker: ๋ฌธ์ ๋ถํ ์์ฒญ Note over Chunker: 512 ํ ํฐ ๋จ์๋ก<br/>chunk ์์ฑ Chunker-->>App: chunk ๋ฆฌ์คํธ ๋ฐํ App->>Embed: ๊ฐ chunk ์๋ฒ ๋ฉ ์์ฑ Embed-->>App: ๋ฒกํฐ ๋ฆฌ์คํธ ๋ฐํ App->>LDB: ๋ฒกํฐ + ๋ฉํ๋ฐ์ดํฐ ์ ์ฅ Note over LDB: vector, text, filename,<br/>chunk_id, timestamp LDB-->>App: ์ ์ฅ ์๋ฃ Note over User,LLM: 2๋จ๊ณ: ๊ฒ์ ๋ฐ ๋ต๋ณ ์์ฑ User->>App: "OAuth ์ธ์ฆ ์ ์ฐจ๊ฐ ๋ญ์์ง?" App->>Embed: ์ง์ ์๋ฒ ๋ฉ ์์ฑ Embed-->>App: ์ง์ ๋ฒกํฐ ๋ฐํ App->>LDB: ์ ์ฌ๋ ๊ฒ์ top 3 Note over LDB: Cosine similarity ๊ณ์ฐ LDB-->>App: ๊ด๋ จ chunk 3๊ฐ ๋ฐํ<br/>similarity scores ํฌํจ App->>LLM: ์ง์ + ๊ฒ์๋ ์ปจํ ์คํธ Note over LLM: RAG ํจํด:<br/>retrieved context๋ฅผ<br/>prompt์ ์ฃผ์ LLM-->>App: ์์ฑ๋ ๋ต๋ณ App-->>User: ๋ต๋ณ + ์ถ์ฒ
Pseudo ์ฝ๋:
# === 1๋จ๊ณ: ๋ฌธ์ ์ ์ฅ ===
db = connect("./docs-db")
model = load_embedding_model()
# Markdown ํ์ผ ์ฝ๊ธฐ ๋ฐ chunk ์์ฑ
content = read_file("auth.md")
chunks = split_into_chunks(content, max_tokens=512)
# ##์๋ฒ ๋ฉ ์์ฑ ๋ฐ ์ ์ฅ
data = []
for i, chunk in enumerate(chunks):
vector = model.encode(chunk)
data.append({
"vector": vector,
"text": chunk,
"filename": "auth.md",
"chunk_id": i
})
table = db.create_table("documents", data)
# === 2๋จ๊ณ: ๊ฒ์ ===
query = "OAuth ์ธ์ฆ ์ ์ฐจ๊ฐ ๋ญ์์ง?"
query_vector = model.encode(query)
results = table.search(query_vector).limit(3)
# LLM์ ์ปจํ
์คํธ ์ ๋ฌ
context = join_texts(results)
prompt = f"Context: {context}\n\nQuestion: {query}"
answer = llm.generate(prompt)ํต์ฌ ํฌ์ธํธ:
- ๋ฌธ์๋ฅผ ์์ chunk๋ก ๋ถํ (512 ํ ํฐ ๋จ์)
- ๊ฐ chunk๋ฅผ ๋ฒกํฐ๋ก ๋ณํํ์ฌ ๋ฉํ๋ฐ์ดํฐ์ ํจ๊ป ์ ์ฅ
- ์ง์๋ฅผ ๋ฒกํฐ๋ก ๋ณํ ํ ์ ์ฌ๋ ๊ฒ์
- ๊ฒ์๋ chunk๋ฅผ LLM ์ปจํ ์คํธ๋ก ํ์ฉ (RAG)
์ผ์ด์ค 2: ํต๊ณ ์ง๊ณ ์ง์ (โ LanceDB ๋ถ์ ํฉ)
์๋๋ฆฌ์ค: โ์ด๋ฒ์ฃผ TODO๋ฅผ ๊ฐ์ฅ ๋ง์ด ํ ๋ ์?โ - ๋จ์ ํต๊ณ ์ง์๋ SQL์ด ๋ ํจ์จ์
sequenceDiagram autonumber participant User participant App participant RDB as PostgreSQL participant LDB as LanceDB User->>App: "์ด๋ฒ์ฃผ TODO ๊ฐ์ฅ ๋ง์ด ํ ๋ ?" Note over App: ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ<br/>์ง๊ณ ์ฟผ๋ฆฌ ํ์ rect rgb(255, 200, 200) Note over App,LDB: โ LanceDB ์ฌ์ฉ ์ ๋ฌธ์ ์ App->>LDB: TODO ํ ์คํธ๋ก ๊ฒ์ ์๋ Note over LDB: ์๋ฏธ ๊ธฐ๋ฐ ๊ฒ์์<br/>COUNT์ ๊ฐ์<br/>ํต๊ณ ์ฐ์ฐ ๋ถ๊ฐ LDB-->>App: ๋ถ์ ํํ ๊ฒฐ๊ณผ end rect rgb(200, 255, 200) Note over App,RDB: โ RDB ์ฌ์ฉ์ด ์ ๋ต App->>RDB: SELECT date, COUNT(*)<br/>GROUP BY date<br/>ORDER BY count DESC Note over RDB: ์ธ๋ฑ์ค ์ค์บ<br/>์ง๊ณ ์ฐ์ฐ<br/>์ ๋ ฌ RDB-->>App: date: 2024-01-10, count: 15 end App-->>User: "์์์ผ์ 15๊ฐ๋ก ๊ฐ์ฅ ๋ง์์ต๋๋ค"
์ LanceDB๊ฐ ๋ถ์ ํฉํ๊ฐ:
| ์๊ตฌ์ฌํญ | LanceDB | RDB (SQL) |
|---|---|---|
| ์ง๊ณ ์ฐ์ฐ (COUNT, SUM) | โ ๋ฒกํฐ ๊ฒ์๋ง ๊ฐ๋ฅ | โ ์ต์ ํ๋ ์ง๊ณ |
| GROUP BY | โ ์ง์ ์ ํจ | โ ๋ค์ดํฐ๋ธ ์ง์ |
| ์ ๋ ฌ (ORDER BY count) | โ ์ ์ฌ๋๋ง ์ ๋ ฌ | โ ์์ ์ปฌ๋ผ ์ ๋ ฌ |
| ์ฑ๋ฅ | ๋๋ฆผ (๋ถํ์ํ ์๋ฒ ๋ฉ) | ๋น ๋ฆ (์ธ๋ฑ์ค ํ์ฉ) |
| ์ ํ์ฑ | ๋ณด์ฅ ์ ๋จ | 100% ์ ํ |
์ฌ๋ฐ๋ฅธ ์ ๊ทผ (Pseudo ์ฝ๋):
# โ
RDB๋ก ํต๊ณ ์ฒ๋ฆฌ
conn = connect_database()
result = conn.execute("""
SELECT date, COUNT(*) as count
FROM todos
WHERE created_at >= NOW() - INTERVAL '7 days'
GROUP BY date
ORDER BY count DESC
LIMIT 1
""")
# result: (2024-01-10, 15)ํต์ฌ ํฌ์ธํธ:
- ์ง๊ณ/ํต๊ณ ์ง์๋ ๊ตฌ์กฐํ๋ ์ฟผ๋ฆฌ๊ฐ ํ์
- LanceDB๋ ์๋ฏธ ๊ธฐ๋ฐ ๊ฒ์์ ํนํ, ์ง๊ณ ์ฐ์ฐ ๋ฏธ์ง์
- COUNT, SUM, GROUP BY โ RDB๊ฐ ์ ๋ต
์ผ์ด์ค 3: ํ์ด๋ธ๋ฆฌ๋ ๊ฒ์ (โ RDB + LanceDB)
์๋๋ฆฌ์ค: โ์ง๋๋ฌ ์์ฐ์ฑ์ด ๋ฎ์๋ ์ด์ ์ ๊ฐ์ ๋ฐฉ์์?โ - ํต๊ณ ๋ถ์ + ๋ฌธ์ ๊ฒ์ ๊ฒฐํฉ
sequenceDiagram autonumber participant User participant App participant RDB as PostgreSQL participant LDB as LanceDB participant Embed as Embedding Model participant LLM User->>App: "์ง๋๋ฌ ์์ฐ์ฑ์ด ๋ฎ์๋ ์ด์ ์<br/>๊ฐ์ ๋ฐฉ์์?" Note over App: ๋ณตํฉ ์ง์:<br/>ํต๊ณ ๋ถ์ + ๋ฌธ์ ๊ฒ์ rect rgb(230, 240, 255) Note over App,RDB: Part 1: ํต๊ณ ๋ฐ์ดํฐ ์์ง App->>RDB: SELECT date, COUNT(*)<br/>AVG time<br/>GROUP BY date Note over RDB: ์ง๊ณ ์ฟผ๋ฆฌ๋ก<br/>์์ฐ์ฑ ํต๊ณ ๊ณ์ฐ RDB-->>App: 12์: 8๊ฐ/์ผ<br/>11์: 12๊ฐ/์ผ (33% ๊ฐ์) end rect rgb(255, 245, 230) Note over App,LDB: Part 2: ๊ด๋ จ ๋ณด๊ณ ์ ๊ฒ์ App->>Embed: "์์ฐ์ฑ ๊ฐ์ ์์ธ" ์๋ฒ ๋ฉ Embed-->>App: ์ง์ ๋ฒกํฐ ๋ฐํ App->>LDB: ์ ์ฌ๋ ๊ฒ์<br/>where type = report Note over LDB: Hybrid search:<br/>Vector + Metadata filter LDB-->>App: ๊ด๋ จ ๋ณด๊ณ ์ 3๊ฐ ๋ฐํ end rect rgb(240, 255, 240) Note over App,LLM: Part 3: ํตํฉ ๋ถ์ App->>LLM: ํต๊ณ ๋ฐ์ดํฐ + ๋ณด๊ณ ์ ๋ฌธ์ Note over LLM: RAG + ํต๊ณ ๋ถ์<br/>๊ฒฐํฉํ์ฌ ๋ต๋ณ ์์ฑ LLM-->>App: ์์ธ ๋ถ์ + ๊ฐ์ ๋ฐฉ์ end App-->>User: ํตํฉ ๋ต๋ณ + ์ถ์ฒ
Pseudo ์ฝ๋:
# === Part 1: RDB์์ ํต๊ณ ์์ง ===
conn = connect_database()
stats = conn.execute("""
SELECT
DATE_TRUNC('month', created_at) as month,
COUNT(*) as todo_count,
AVG(completion_time) as avg_time
FROM todos
WHERE created_at >= '2024-11-01'
GROUP BY month
""")
# stats: [(2024-11, 360, 45.2), (2024-12, 248, 58.7)]
productivity_drop = calculate_drop_percentage(stats)
# -31.1%
# === Part 2: LanceDB์์ ๊ด๋ จ ๋ฌธ์ ๊ฒ์ ===
db = connect("./reports-db")
model = load_embedding_model()
query = "์์ฐ์ฑ ๊ฐ์ ์์ธ ๋ถ์ ๊ฐ์ ๋ฐฉ์"
query_vector = model.encode(query)
table = db.open_table("reports")
results = table.search(query_vector) \
.where("type = 'report' AND date >= '2024-12-01'") \
.limit(3)
# === Part 3: LLM์ ํตํฉ ์ ๋ณด ์ ๋ฌ ===
context = f"""
ํต๊ณ ๋ฐ์ดํฐ:
- 11์ TODO: 360๊ฐ
- 12์ TODO: 248๊ฐ
- ์์ฐ์ฑ ๊ฐ์: {productivity_drop}%
๊ด๋ จ ๋ฌธ์:
{join_texts(results)}
"""
prompt = f"{context}\n\nQuestion: ์ง๋๋ฌ ์์ฐ์ฑ์ด ๋ฎ์๋ ์ด์ ์ ๊ฐ์ ๋ฐฉ์์?"
answer = llm.generate(prompt)ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ์ ์ฅ์ :
| ๊ตฌ์ฑ ์์ | ์ญํ | ๊ฐ์ |
|---|---|---|
| RDB (SQL) | ์ ๋ ๋ฐ์ดํฐ ์ ๊ณต | ์ ํํ ํต๊ณ, ๋น ๋ฅธ ์ง๊ณ |
| LanceDB | ์ ์ฑ ์ ๋ณด ๊ฒ์ | ๊ด๋ จ ๋ฌธ์, ๋งฅ๋ฝ, ์ธ์ฌ์ดํธ |
| LLM | ํตํฉ ๋ถ์ | ๋ฐ์ดํฐ + ๋ฌธ์ ๊ฒฐํฉ ํด์ |
์ธ์ ํ์ด๋ธ๋ฆฌ๋๋ฅผ ์ฌ์ฉํ๋๊ฐ:
- โ โ์?โ ์ง๋ฌธ - ํต๊ณ + ์์ธ ๋ถ์
- โ โ์ด๋ป๊ฒ ๊ฐ์ ?โ - ๋ฐ์ดํฐ + ๊ณผ๊ฑฐ ์ธ์ฌ์ดํธ
- โ โ๋ฌด์จ ์ผ์ด ์์๋?โ - ํฉํธ + ๋งฅ๋ฝ
- โ โ๋ช ๊ฐ?โ - RDB๋ง์ผ๋ก ์ถฉ๋ถ
- โ โ๊ด๋ จ ๋ฌธ์๋?โ - LanceDB๋ง์ผ๋ก ์ถฉ๋ถ
ํต์ฌ ํฌ์ธํธ:
- RDB: ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ์ ํต๊ณ/์ง๊ณ (COUNT, AVG, GROUP BY)
- LanceDB: ๋น๊ตฌ์กฐํ ๋ฌธ์์ ์๋ฏธ ๊ฒ์ (๋ณด๊ณ ์, ํ๊ณ ๋ก ๋ฑ)
- LLM: ์์ชฝ ๊ฒฐ๊ณผ๋ฅผ ๊ฒฐํฉํ์ฌ ํตํฉ ์ธ์ฌ์ดํธ ์์ฑ
LanceDB vs ๋ค๋ฅธ DB: ์ ํ ๊ธฐ์ค
LanceDB๋ฅผ ์ ํํด์ผ ํ๋ ๊ฒฝ์ฐ:
- ๋ฉํฐ๋ชจ๋ฌ ๋ฐ์ดํฐ (ํ ์คํธ + ์ด๋ฏธ์ง + ๋น๋์ค) ์ฒ๋ฆฌ
- SQL ์ฟผ๋ฆฌ์ ๋ฒกํฐ ๊ฒ์์ ๋์์ ์ฌ์ฉ
- ๋ฐ์ดํฐ ๋ฒ์ ๋์ด ํ์ํ ๊ฒฝ์ฐ
- Petabyte ๊ท๋ชจ๋ก ํ์ฅ ๊ฐ๋ฅ์ฑ
- ๋ถ์ ์ํฌ๋ก๋์ ๊ฒ์์ ํจ๊ป ์ํ
Chroma๋ฅผ ์ ํํด์ผ ํ๋ ๊ฒฝ์ฐ:
- ๋น ๋ฅธ ํ๋กํ ํ์ดํ๊ณผ ๊ฐ๋จํ API ํ์
- LangChain/LlamaIndex ๊ธฐ๋ฐ RAG ๊ตฌํ
- ํ ์คํธ ์ค์ฌ ์ ํ๋ฆฌ์ผ์ด์
- ํ์ต ๊ณก์ ์ต์ํ
FAISS๋ฅผ ์ ํํด์ผ ํ๋ ๊ฒฝ์ฐ:
- ์ต๊ณ ์์ค์ ๊ฒ์ ์ฑ๋ฅ ํ์
- ๋ค์ํ ANN ์๊ณ ๋ฆฌ์ฆ ์คํ
- ๋ฒกํฐ ํด๋ฌ์คํฐ๋ง ํ์
- ์์ ๋ฒกํฐ ๊ฒ์์๋ง ์ง์ค
- ๋ฉํ๋ฐ์ดํฐ ํํฐ๋ง์ด ์ค์ํ์ง ์์
LanceDB ์ฑ๋ฅ ํน์ง
graph LR A[Query] --> B{Index Type} B -->|IVF| C[10ms<br/>99% recall] B -->|HNSW| D[1ms<br/>95% recall] B -->|Flat| E[100ms<br/>100% recall] style C fill:#90EE90 style D fill:#FFD700 style E fill:#FFB6C1
- ๋ฐ๋ฆฌ์ด ๋จ์ ์๋ต: ์์ต ๊ฐ ๋ฒกํฐ์์๋ ๋น ๋ฅธ ๊ฒ์
- ํ์ฅ์ฑ: ๋จ์ผ ์๋ฒ์์ ์์ญ์ต ๋ฒกํฐ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- ํธ๋ ์ด๋์คํ: ์ธ๋ฑ์ค ํ์ ์ ๋ฐ๋ผ ์๋/์ ํ๋ ์กฐ์
์ ์ฝ์ฌํญ
- ์๋์ ์ผ๋ก ์๋ก์ด ํ๋ก์ ํธ: ์ํ๊ณ๊ฐ ์ฑ์ํ์ง ์์
- ํด๋ฌ์คํฐ๋ง ๋ฏธ์ง์: FAISS์ ๋ฌ๋ฆฌ ๋ฒกํฐ ํด๋ฌ์คํฐ๋ง ์์
- ๋ฌธ์ ๋ถ์กฑ: ์ผ๋ถ ๊ณ ๊ธ ๊ธฐ๋ฅ์ ๋ฌธ์ํ๊ฐ ๋ฏธํก
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋: Columnar ํฌ๋งท์ผ๋ก ์ธํ ์ถ๊ฐ ๋ฉ๋ชจ๋ฆฌ ํ์
Q&A
Q) โOAuthโ ๊ฐ์ ํค์๋๋ฅผ ์ถ๊ฐํด์ ๊ฒ์์ ๋ ๋น ๋ฅด๊ณ ์ ํํ๊ฒ ํ ์๋ ์๋์?
โ ๊ฐ๋ฅํฉ๋๋ค! Hybrid Search (ํ์ด๋ธ๋ฆฌ๋ ๊ฒ์)๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
Hybrid Search๋? ๋ฒกํฐ ๊ฒ์(์๋ฏธ ๊ธฐ๋ฐ)๊ณผ ํค์๋ ๊ฒ์(์ ํํ ๋จ์ด ๋งค์นญ)์ ๊ฒฐํฉํ๋ ๋ฐฉ์์ ๋๋ค.
๋ฐฉ์: Vector DB ํ๋์๋ง ์ ์ฅ
sequenceDiagram autonumber participant User participant App participant KeywordExtractor as Keyword Extractor participant Embed as Embedding Model participant LDB as LanceDB Note over User,LDB: 1๋จ๊ณ: ๋ฐ์ดํฐ ์ ์ฅ (ํ ์คํธ + ๋ฒกํฐ + ํค์๋) User->>App: ๋ฌธ์ ์ ๋ก๋ Note over App: "OAuth 2.0 flow๋<br/>Authorization Code Grant..." App->>KeywordExtractor: ํค์๋ ์ถ์ถ ์์ฒญ Note over KeywordExtractor: TF-IDF, ํ์ฌ ํ๊น ๋ฑ KeywordExtractor-->>App: keywords ๋ฐํ App->>Embed: ํ ์คํธ ์๋ฒ ๋ฉ ์์ฑ Embed-->>App: ๋ฒกํฐ ๋ฐํ App->>LDB: ํ ์คํธ + ๋ฒกํฐ + ํค์๋ ์ ์ฅ Note over LDB: text: "OAuth 2.0..."<br/>vector: [0.2, 0.8, ...]<br/>keywords: ["OAuth", "Authorization"] LDB-->>App: ์ ์ฅ ์๋ฃ Note over User,LDB: 2๋จ๊ณ: Hybrid Search User->>App: "OAuth ์ธ์ฆ ๋ฐฉ๋ฒ" App->>Embed: ์ง์ ์๋ฒ ๋ฉ ์์ฑ Embed-->>App: ์ง์ ๋ฒกํฐ ๋ฐํ App->>LDB: Hybrid Search ์คํ Note over LDB: 1. ๋ฒกํฐ ์ ์ฌ๋ ๊ณ์ฐ (0.85)<br/>2. Full-text BM25 (12.3)<br/>3. ๊ฐ์คํฉ: 0.7*0.85 + 0.3*0.8 LDB-->>App: ๊ฒฐ๊ณผ ๋ฐํ (final_score ๋ด๋ฆผ์ฐจ์) App-->>User: "OAuth" ํค์๋ ํฌํจ + ์๋ฏธ ์ ์ฌ ๋ฌธ์
Pseudo ์ฝ๋:
# === 1๋จ๊ณ: ํค์๋ ํฌํจํ์ฌ ์ ์ฅ ===
db = connect("./docs-db")
model = load_embedding_model()
content = read_file("auth.md")
chunks = split_into_chunks(content, max_tokens=512)
data = []
for chunk in chunks:
# ํค์๋ ์ถ์ถ
keywords = extract_keywords(chunk) # ["OAuth", "Authorization", "Grant"]
# ์๋ฒ ๋ฉ ์์ฑ
vector = model.encode(chunk)
data.append({
"text": chunk,
"vector": vector,
"keywords": keywords # ํค์๋ ์ ์ฅ
})
table = db.create_table("documents", data)
# === 2๋จ๊ณ: Hybrid Search ===
query = "OAuth ์ธ์ฆ ๋ฐฉ๋ฒ"
query_vector = model.encode(query)
# ๋ฐฉ๋ฒ 1: SQL ํํฐ๋ง + ๋ฒกํฐ ๊ฒ์
results = table.search(query_vector) \
.where("'OAuth' = ANY(keywords)") \
.limit(10)
# ๋ฐฉ๋ฒ 2: ์ค์ฝ์ด ๊ฐ์คํฉ
vector_results = table.search(query_vector)
fulltext_results = table.search_fulltext(query)
# ๋ ์ค์ฝ์ด ๊ฒฐํฉ
final_results = []
for doc_id in all_doc_ids:
vector_score = get_vector_score(doc_id) # 0.85
fulltext_score = get_fulltext_score(doc_id) # 0.8 (normalized)
final_score = 0.7 * vector_score + 0.3 * fulltext_score
final_results.append((doc_id, final_score))
# ์ต์ข
์ ์๋ก ์ ๋ ฌ
final_results.sort(key=lambda x: x[1], reverse=True)ํต์ฌ ํฌ์ธํธ:
- ์๋ฏธ๋ ๋น์ทํ์ง๋ง ํค์๋๊ฐ ์๋ ๊ฒฝ์ฐ๋ ์ฐพ์ (๋ฒกํฐ ๊ฒ์)
- ์ ํํ ํค์๋๊ฐ ์๋ ๊ฒฝ์ฐ ์ฐ์ ์์ ์์น (Full-text ๊ฒ์)
- ๋ฐ์ดํฐ ์ค๋ณต ์ ์ฅ ๋ถํ์ (Vector DB ํ๋์์ ์ฒ๋ฆฌ)
- ์ ํ๋(Precision)์ ์ฌํ์จ(Recall) ๋ชจ๋ ํฅ์
๊ตฌํ ๋ณต์ก๋:
- LanceDB, Chroma: ๋ด์ฅ Full-text ๊ฒ์ ์ง์ โ ๊ฐ๋จ
- FAISS: Full-text ๋ฏธ์ง์ โ ๋ณ๋ ์ฒ๋ฆฌ ํ์
Q) ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์ด๋ค ๋ฌธ์์์ ์๋์ง ์ถ์ฒ๋ฅผ ํ์ํ ์ ์๋์?
โ ๊ฐ๋ฅํฉ๋๋ค! ๋ฉํ๋ฐ์ดํฐ์ ์ถ์ฒ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ฉด ๋ฉ๋๋ค.
sequenceDiagram autonumber participant User participant App participant Parser as Document Parser participant Embed as Embedding Model participant LDB as LanceDB participant LLM Note over User,LDB: 1๋จ๊ณ: ์ถ์ฒ ๋ฉํ๋ฐ์ดํฐ ํฌํจํ์ฌ ์ ์ฅ User->>App: ๋ฌธ์ ์ ๋ก๋ (auth.md) App->>Parser: ๋ฌธ์ ํ์ฑ + ๋ฉํ๋ฐ์ดํฐ ์ถ์ถ Note over Parser: ํ์ผ๋ช , ์ค ๋ฒํธ,<br/>์น์ ์ ๋ชฉ, URL ์ถ์ถ Parser-->>App: chunks + ๋ฉํ๋ฐ์ดํฐ App->>Embed: ๊ฐ chunk ์๋ฒ ๋ฉ ์์ฑ Embed-->>App: ๋ฒกํฐ ๋ฆฌ์คํธ ๋ฐํ App->>LDB: ๋ฒกํฐ + ํ ์คํธ + ์ถ์ฒ ๋ฉํ๋ฐ์ดํฐ ์ ์ฅ Note over LDB: vector, text, filename,<br/>line_range, section_title,<br/>url, page_number LDB-->>App: ์ ์ฅ ์๋ฃ Note over User,LLM: 2๋จ๊ณ: ๊ฒ์ + ์ถ์ฒ ํ์ User->>App: "OAuth ์ธ์ฆ ์ ์ฐจ๊ฐ ๋ญ์์ง?" App->>Embed: ์ง์ ์๋ฒ ๋ฉ ์์ฑ Embed-->>App: ์ง์ ๋ฒกํฐ ๋ฐํ App->>LDB: ์ ์ฌ๋ ๊ฒ์ LDB-->>App: ๊ฒฐ๊ณผ + ์ถ์ฒ ๋ฉํ๋ฐ์ดํฐ Note over App: ์ถ์ฒ ์ ๋ณด ํฌ๋งทํ :<br/>ํ์ผ๋ช , ์ค, ์น์ , URL App->>LLM: ์ง์ + ์ปจํ ์คํธ + ์ถ์ฒ ๋ฒํธ Note over LLM: ๋ต๋ณ ์์ฑ ์<br/>์ถ์ฒ ๋ฒํธ ์ธ์ฉ LLM-->>App: ๋ต๋ณ + [์ถ์ฒ 1], [์ถ์ฒ 2] App-->>User: ๋ต๋ณ + ์ถ์ฒ ๋ชฉ๋ก Note over User: ์ ๋ขฐ๋ ํ์ธ ๊ฐ๋ฅ<br/>์๋ณธ ๋ฌธ์ ์ง์ ์ ๊ทผ
Pseudo ์ฝ๋:
# === 1๋จ๊ณ: ์ถ์ฒ ๋ฉํ๋ฐ์ดํฐ ํฌํจํ์ฌ ์ ์ฅ ===
db = connect("./docs-db")
model = load_embedding_model()
content = read_file("auth.md")
chunks = parse_with_metadata(content) # ์ค ๋ฒํธ, ์น์
ํฌํจ
data = []
for chunk_info in chunks:
vector = model.encode(chunk_info.text)
data.append({
"vector": vector,
"text": chunk_info.text,
# ์ถ์ฒ ๋ฉํ๋ฐ์ดํฐ
"filename": "auth.md",
"line_range": chunk_info.line_range, # "45-67"
"section_title": chunk_info.section, # "## OAuth 2.0 Flow"
"url": "https://docs.oauth.net/2.0/",
"page_number": chunk_info.page # PDF์ธ ๊ฒฝ์ฐ
})
table = db.create_table("documents", data)
# === 2๋จ๊ณ: ๊ฒ์ + ์ถ์ฒ ํ์ ===
query = "OAuth ์ธ์ฆ ์ ์ฐจ๊ฐ ๋ญ์์ง?"
query_vector = model.encode(query)
results = table.search(query_vector).limit(3)
# ์ถ์ฒ ํฌํจ ์ปจํ
์คํธ ์์ฑ
context_with_sources = ""
for i, result in enumerate(results, 1):
context_with_sources += f"""
[์ถ์ฒ {i}] {result.filename} ({result.line_range}์ค, {result.section_title})
{result.text}
"""
# LLM ํ๋กฌํํธ
prompt = f"""
๋ค์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ฌ ์ง๋ฌธ์ ๋ต๋ณํ์ธ์.
๋ต๋ณ ์ ๋ฐ๋์ [์ถ์ฒ ๋ฒํธ]๋ฅผ ๋ช
์ํ์ธ์.
{context_with_sources}
์ง๋ฌธ: {query}
"""
answer = llm.generate(prompt)
# ์ฌ์ฉ์์๊ฒ ํ์
print(answer)
print("\n=== ์ฐธ๊ณ ๋ฌธ์ ===")
for i, result in enumerate(results, 1):
print(f"[{i}] {result.filename} ({result.line_range}์ค)")
print(f" ๐ {result.url}")์ถ๋ ฅ ์์:
OAuth ์ธ์ฆ์ Authorization Code Grant ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค[์ถ์ฒ 1].
ํ ํฐ ์ ์ก ์ ๋ฐ๋์ HTTPS๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค[์ถ์ฒ 2].
=== ์ฐธ๊ณ ๋ฌธ์ ===
[1] auth.md (45-67์ค)
๐ https://docs.oauth.net/2.0/
[2] security-guide.md (120-145์ค)
๐ https://docs.example.com/security
ํต์ฌ ํฌ์ธํธ:
- ๋ฉํ๋ฐ์ดํฐ์ ํ์ผ๋ช , ์ค ๋ฒํธ, ์น์ , URL ์ ์ฅ
- ๊ฒ์ ๊ฒฐ๊ณผ์ ํจ๊ป ์ถ์ฒ ์ ๋ณด ๋ฐํ
- LLM ๋ต๋ณ์ ์ถ์ฒ ๋ฒํธ ์ธ์ฉ ํฌํจ
- ์ฌ์ฉ์๊ฐ ์๋ณธ ๋ฌธ์๋ฅผ ์ง์ ํ์ธ ๊ฐ๋ฅ (์ ๋ขฐ๋ ํฅ์)