**๋กœ์ปฌ ๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค(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์ข… ๋น„๊ต

ํŠน์ง•LanceDBChromaFAISS
๊ฐœ๋ฐœ์‚ฌLanceDB Inc.ChromaMeta AI Research
์ฃผ์š” ์šฉ๋„๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜RAG ๋ฐ ์˜๋ฏธ ๊ฒ€์ƒ‰๋Œ€๊ทœ๋ชจ ๋ฒกํ„ฐ ์œ ์‚ฌ๋„ ๊ฒ€์ƒ‰
๋ฐ์ดํ„ฐ ํฌ๋งทLance (columnar)์ž์ฒด ํฌ๋งทIn-memory indices
์ง€์› ์–ธ์–ดPython, JS, RustPython, JSPython, C++
๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ง€์›โœ… (ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, ๋น„๋””์˜ค ๋“ฑ)โš ๏ธ (์ฃผ๋กœ ํ…์ŠคํŠธ)โŒ (๋ฒกํ„ฐ๋งŒ)
๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋งโœ… SQL ์ฟผ๋ฆฌ ์ง€์›โœ…โš ๏ธ (์ œํ•œ์ )
Full-text ๊ฒ€์ƒ‰โœ…โœ…โŒ
GPU ๊ฐ€์†โœ…โŒโœ…
์ž๋™ ๋ฒ„์ €๋‹โœ…โŒโŒ
Zero-copyโœ…โŒโŒ
ํด๋Ÿฌ์Šคํ„ฐ๋งโŒโŒโœ…
์ธ๋ฑ์Šค ํƒ€์ž…์ž์ฒด + HNSW์ž์ฒด๋‹ค์–‘ํ•œ ANN ์•Œ๊ณ ๋ฆฌ์ฆ˜
ํ•™์Šต ๊ณก์„ ์ค‘๊ฐ„๋‚ฎ์Œ (๊ฐ€์žฅ ์‰ฌ์›€)๋†’์Œ
ํ™•์žฅ์„ฑPetabyte ๊ทœ๋ชจ์ค‘์†Œ ๊ทœ๋ชจBillion ๊ทœ๋ชจ
๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ๋†’์Œ (columnar)์ค‘๊ฐ„๋†’์Œ (์••์ถ• ์ง€์›)
๋ผ์ด์„ ์ŠคApache 2.0Apache 2.0MIT

์žฅ๋‹จ์  ์š”์•ฝ

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 ํฌ๋งท์˜ ์ด์ :

  1. ์„ ํƒ์  ์ฝ๊ธฐ: ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์ฝ์–ด I/O ์ตœ์†Œํ™”
  2. ์••์ถ• ํšจ์œจ: ๊ฐ™์€ ํƒ€์ž… ๋ฐ์ดํ„ฐ๊ฐ€ ์—ฐ์†๋˜์–ด ์••์ถ•๋ฅ  ํ–ฅ์ƒ
  3. ๋ถ„์„ ์ตœ์ ํ™”: SQL ์ฟผ๋ฆฌ์™€ ์ง‘๊ณ„ ์—ฐ์‚ฐ์— ์œ ๋ฆฌ
  4. 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)

ํ•ต์‹ฌ ํฌ์ธํŠธ:

  1. ๋ฌธ์„œ๋ฅผ ์ž‘์€ chunk๋กœ ๋ถ„ํ•  (512 ํ† ํฐ ๋‹จ์œ„)
  2. ๊ฐ chunk๋ฅผ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์™€ ํ•จ๊ป˜ ์ €์žฅ
  3. ์งˆ์˜๋ฅผ ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ ํ›„ ์œ ์‚ฌ๋„ ๊ฒ€์ƒ‰
  4. ๊ฒ€์ƒ‰๋œ 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๊ฐ€ ๋ถ€์ ํ•ฉํ•œ๊ฐ€:

์š”๊ตฌ์‚ฌํ•ญLanceDBRDB (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๋งŒ์œผ๋กœ ์ถฉ๋ถ„

ํ•ต์‹ฌ ํฌ์ธํŠธ:

  1. RDB: ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ์˜ ํ†ต๊ณ„/์ง‘๊ณ„ (COUNT, AVG, GROUP BY)
  2. LanceDB: ๋น„๊ตฌ์กฐํ™” ๋ฌธ์„œ์˜ ์˜๋ฏธ ๊ฒ€์ƒ‰ (๋ณด๊ณ ์„œ, ํšŒ๊ณ ๋ก ๋“ฑ)
  3. 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
  • ๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„ ์‘๋‹ต: ์ˆ˜์–ต ๊ฐœ ๋ฒกํ„ฐ์—์„œ๋„ ๋น ๋ฅธ ๊ฒ€์ƒ‰
  • ํ™•์žฅ์„ฑ: ๋‹จ์ผ ์„œ๋ฒ„์—์„œ ์ˆ˜์‹ญ์–ต ๋ฒกํ„ฐ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
  • ํŠธ๋ ˆ์ด๋“œ์˜คํ”„: ์ธ๋ฑ์Šค ํƒ€์ž…์— ๋”ฐ๋ผ ์†๋„/์ •ํ™•๋„ ์กฐ์ ˆ

์ œ์•ฝ์‚ฌํ•ญ

  1. ์ƒ๋Œ€์ ์œผ๋กœ ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ: ์ƒํƒœ๊ณ„๊ฐ€ ์„ฑ์ˆ™ํ•˜์ง€ ์•Š์Œ
  2. ํด๋Ÿฌ์Šคํ„ฐ๋ง ๋ฏธ์ง€์›: FAISS์™€ ๋‹ฌ๋ฆฌ ๋ฒกํ„ฐ ํด๋Ÿฌ์Šคํ„ฐ๋ง ์—†์Œ
  3. ๋ฌธ์„œ ๋ถ€์กฑ: ์ผ๋ถ€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์€ ๋ฌธ์„œํ™”๊ฐ€ ๋ฏธํก
  4. ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰: 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

ํ•ต์‹ฌ ํฌ์ธํŠธ:

  1. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ํŒŒ์ผ๋ช…, ์ค„ ๋ฒˆํ˜ธ, ์„น์…˜, URL ์ €์žฅ
  2. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์™€ ํ•จ๊ป˜ ์ถœ์ฒ˜ ์ •๋ณด ๋ฐ˜ํ™˜
  3. LLM ๋‹ต๋ณ€์— ์ถœ์ฒ˜ ๋ฒˆํ˜ธ ์ธ์šฉ ํฌํ•จ
  4. ์‚ฌ์šฉ์ž๊ฐ€ ์›๋ณธ ๋ฌธ์„œ๋ฅผ ์ง์ ‘ ํ™•์ธ ๊ฐ€๋Šฅ (์‹ ๋ขฐ๋„ ํ–ฅ์ƒ)

์ฐธ๊ณ  ๋ฌธ์„œ