|
| 1 | +# lang2sql v2 Usage Guide |
| 2 | + |
| 3 | +이 문서는 `src/lang2sql` 기준의 새로운 v2 API만 다룹니다. |
| 4 | +기존 `engine/`, `interface/`, `utils/llm/` 경로는 범위에서 제외합니다. |
| 5 | + |
| 6 | +자세한 단계별 실습은 [v2-complete-tutorial.md](./v2-complete-tutorial.md) 를 참고하세요. |
| 7 | + |
| 8 | +## 0) Why lang2sql |
| 9 | + |
| 10 | +- **운영 친화적인 기본 경로**: `Retriever -> Generator -> Executor`가 단순하고 디버깅 포인트가 명확합니다. |
| 11 | +- **명시적 인덱싱 파이프라인**: `chunker.split(docs)` → `VectorRetriever.from_chunks(chunks)` 패턴으로 split/embed/store 각 단계가 코드에 보입니다. |
| 12 | +- **프레임워크 락인 최소화**: 코어가 Protocol(`EmbeddingPort`, `VectorStorePort`, `DocumentChunkerPort`) 기반이라 구현체를 교체하기 쉽습니다. |
| 13 | +- **관측성 내장**: Hook(`TraceHook`, `MemoryHook`)으로 컴포넌트 단위 실행 이벤트를 바로 수집할 수 있습니다. |
| 14 | + |
| 15 | +## 0-1) 튜토리얼 데이터 자동 준비 |
| 16 | + |
| 17 | +```bash |
| 18 | +python scripts/setup_sample_db.py |
| 19 | +python scripts/setup_sample_docs.py |
| 20 | +``` |
| 21 | + |
| 22 | +문서 생성 후 `docs/business` 아래 파일을 로더 예제에서 그대로 사용합니다. |
| 23 | + |
| 24 | +## 1) v2에서 실제로 지원되는 기능 |
| 25 | + |
| 26 | +### Flows |
| 27 | +- `BaselineNL2SQL`: BM25 `KeywordRetriever` 기반 기본 파이프라인 |
| 28 | +- `HybridNL2SQL`: BM25 + Vector `HybridRetriever` 기반 파이프라인 |
| 29 | + |
| 30 | +### Retrievers |
| 31 | +- `KeywordRetriever` |
| 32 | +- `VectorRetriever` |
| 33 | +- `HybridRetriever` |
| 34 | + |
| 35 | +### Vector / Embedding (v2 내장) |
| 36 | +- Embedding: `OpenAIEmbedding` (내장 1개) |
| 37 | +- Vector store: `InMemoryVectorStore` (내장 1개) |
| 38 | + |
| 39 | +### Chunking / Loading |
| 40 | +- Chunkers: `CatalogChunker`, `RecursiveCharacterChunker`, `SemanticChunker` |
| 41 | + - 모두 `.split(list)` 메서드 제공 — LangChain 스타일 batch 입력/출력 |
| 42 | +- Loaders: `MarkdownLoader`, `PlainTextLoader`, `DirectoryLoader` |
| 43 | + - `PDFLoader` (optional, `pip install pymupdf`) |
| 44 | + |
| 45 | +### Extensibility (Protocol) |
| 46 | +- `EmbeddingPort`, `VectorStorePort`, `DocumentChunkerPort`, `DocumentLoaderPort` |
| 47 | +- 즉, 내장 구현 외에도 사용자 어댑터를 연결할 수 있습니다. |
| 48 | + |
| 49 | +## 2) 빠른 선택 가이드 |
| 50 | + |
| 51 | +### 가장 쉬운 시작 |
| 52 | +- 목적: 설치 후 바로 NL2SQL 확인 |
| 53 | +- 선택: `BaselineNL2SQL` |
| 54 | +- 특징: 벡터 인덱싱 없이 즉시 사용 |
| 55 | + |
| 56 | +### 검색 품질을 빠르게 올리고 싶을 때 |
| 57 | +- 목적: 키워드 매칭 한계를 보완 |
| 58 | +- 선택: `HybridNL2SQL` + `OpenAIEmbedding` |
| 59 | +- 특징: BM25 + Vector RRF 결합으로 안정적인 검색 품질 |
| 60 | + |
| 61 | +### 고급 제어가 필요할 때 |
| 62 | +- 목적: 청킹/임베딩/인덱싱/검색 파이프라인 세밀 제어 |
| 63 | +- 선택: `chunker.split()` + `VectorRetriever.from_chunks()` + 수동 컴포넌트 조합 |
| 64 | +- 특징: 증분 인덱싱, 커스텀 Chunker/VectorStore/Embedding 연동 가능 |
| 65 | + |
| 66 | +## 3) 최소 예제 |
| 67 | + |
| 68 | +### A. BaselineNL2SQL (키워드 기반) |
| 69 | +```python |
| 70 | +from lang2sql import BaselineNL2SQL |
| 71 | +from lang2sql.integrations.db import SQLAlchemyDB |
| 72 | +from lang2sql.integrations.llm import OpenAILLM |
| 73 | + |
| 74 | +catalog = [ |
| 75 | + { |
| 76 | + "name": "orders", |
| 77 | + "description": "order table", |
| 78 | + "columns": {"order_id": "pk", "amount": "order amount"}, |
| 79 | + } |
| 80 | +] |
| 81 | + |
| 82 | +pipeline = BaselineNL2SQL( |
| 83 | + catalog=catalog, |
| 84 | + llm=OpenAILLM(model="gpt-4o-mini"), |
| 85 | + db=SQLAlchemyDB("sqlite:///sample.db"), |
| 86 | + db_dialect="sqlite", |
| 87 | +) |
| 88 | + |
| 89 | +rows = pipeline.run("지난달 주문 건수") |
| 90 | +print(rows) |
| 91 | +``` |
| 92 | + |
| 93 | +### B. HybridNL2SQL (키워드 + 벡터) |
| 94 | +```python |
| 95 | +from lang2sql import HybridNL2SQL |
| 96 | +from lang2sql.integrations.db import SQLAlchemyDB |
| 97 | +from lang2sql.integrations.embedding import OpenAIEmbedding |
| 98 | +from lang2sql.integrations.llm import OpenAILLM |
| 99 | + |
| 100 | +catalog = [ |
| 101 | + { |
| 102 | + "name": "orders", |
| 103 | + "description": "order table", |
| 104 | + "columns": {"order_id": "pk", "amount": "order amount"}, |
| 105 | + } |
| 106 | +] |
| 107 | + |
| 108 | +docs = [ |
| 109 | + { |
| 110 | + "id": "biz_rules", |
| 111 | + "title": "매출 정의", |
| 112 | + "content": "매출은 반품 제외 순매출이다.", |
| 113 | + "source": "docs/biz_rules.md", |
| 114 | + } |
| 115 | +] |
| 116 | + |
| 117 | +pipeline = HybridNL2SQL( |
| 118 | + catalog=catalog, |
| 119 | + llm=OpenAILLM(model="gpt-4o-mini"), |
| 120 | + db=SQLAlchemyDB("sqlite:///sample.db"), |
| 121 | + embedding=OpenAIEmbedding(model="text-embedding-3-small"), |
| 122 | + documents=docs, |
| 123 | + db_dialect="sqlite", |
| 124 | + top_n=5, |
| 125 | +) |
| 126 | + |
| 127 | +rows = pipeline.run("지난달 순매출") |
| 128 | +print(rows) |
| 129 | +``` |
| 130 | + |
| 131 | +### C. 명시적 파이프라인: split → from_chunks (LangChain 스타일) |
| 132 | +```python |
| 133 | +from lang2sql import ( |
| 134 | + CatalogChunker, |
| 135 | + DirectoryLoader, |
| 136 | + RecursiveCharacterChunker, |
| 137 | + VectorRetriever, |
| 138 | +) |
| 139 | +from lang2sql.integrations.embedding import OpenAIEmbedding |
| 140 | + |
| 141 | +catalog = [ |
| 142 | + { |
| 143 | + "name": "orders", |
| 144 | + "description": "order table", |
| 145 | + "columns": {"order_id": "pk", "amount": "order amount"}, |
| 146 | + } |
| 147 | +] |
| 148 | + |
| 149 | +# 1) 문서 로딩 |
| 150 | +docs = DirectoryLoader("docs/business").load() |
| 151 | + |
| 152 | +# 2) 각 소스를 명시적으로 split |
| 153 | +catalog_chunks = CatalogChunker().split(catalog) |
| 154 | +doc_chunks = RecursiveCharacterChunker(chunk_size=800, chunk_overlap=80).split(docs) |
| 155 | + |
| 156 | +# 3) chunks를 합쳐서 retriever 생성 (embed + store 자동) |
| 157 | +retriever = VectorRetriever.from_chunks( |
| 158 | + catalog_chunks + doc_chunks, |
| 159 | + embedding=OpenAIEmbedding(model="text-embedding-3-small"), |
| 160 | + top_n=5, |
| 161 | +) |
| 162 | + |
| 163 | +result = retriever.run("순매출 계산 기준") |
| 164 | +print("schemas:", [s["name"] for s in result.schemas]) |
| 165 | +print("context:", result.context[:2]) |
| 166 | +``` |
| 167 | + |
| 168 | +명시적 플로우의 장점: |
| 169 | + |
| 170 | +1. split 단계가 코드에 보임 — `chunker.split(docs)`가 명시적 |
| 171 | +2. catalog chunks + doc chunks를 Python list로 자유롭게 조합 가능 |
| 172 | +3. `registry = {}` 같은 내부 상태를 사용자가 직접 관리할 필요 없음 |
| 173 | + |
| 174 | +증분 추가 시에는 chunks를 미리 split한 뒤 전달합니다: |
| 175 | + |
| 176 | +```python |
| 177 | +new_docs = DirectoryLoader("docs/new").load() |
| 178 | +retriever.add(RecursiveCharacterChunker().split(new_docs)) |
| 179 | +``` |
| 180 | + |
| 181 | +### D. DirectoryLoader → HybridNL2SQL 직결 |
| 182 | + |
| 183 | +문서를 로드한 뒤 바로 HybridNL2SQL에 전달하는 가장 간결한 패턴입니다. |
| 184 | + |
| 185 | +```python |
| 186 | +from lang2sql import DirectoryLoader, HybridNL2SQL |
| 187 | +from lang2sql.integrations.db import SQLAlchemyDB |
| 188 | +from lang2sql.integrations.embedding import OpenAIEmbedding |
| 189 | +from lang2sql.integrations.llm import OpenAILLM |
| 190 | + |
| 191 | +docs = DirectoryLoader("docs/business").load() |
| 192 | + |
| 193 | +pipeline = HybridNL2SQL( |
| 194 | + catalog=catalog, |
| 195 | + llm=OpenAILLM(model="gpt-4o-mini"), |
| 196 | + db=SQLAlchemyDB("sqlite:///sample.db"), |
| 197 | + embedding=OpenAIEmbedding(model="text-embedding-3-small"), |
| 198 | + documents=docs, |
| 199 | + db_dialect="sqlite", |
| 200 | +) |
| 201 | + |
| 202 | +rows = pipeline.run("지난달 순매출") |
| 203 | +print(rows) |
| 204 | +``` |
| 205 | + |
| 206 | +### E. PDFLoader — PDF 파일 인덱싱 |
| 207 | + |
| 208 | +PDF 파일은 `PDFLoader`로 로드합니다 (`pip install pymupdf` 필요). |
| 209 | + |
| 210 | +```python |
| 211 | +from lang2sql import DirectoryLoader, MarkdownLoader, VectorRetriever |
| 212 | +from lang2sql.integrations.embedding import OpenAIEmbedding |
| 213 | +from lang2sql.integrations.loaders import PDFLoader |
| 214 | + |
| 215 | +# PDFLoader를 DirectoryLoader에 등록 |
| 216 | +docs = DirectoryLoader( |
| 217 | + "docs/", |
| 218 | + loaders={ |
| 219 | + ".md": MarkdownLoader(), |
| 220 | + ".pdf": PDFLoader(), |
| 221 | + }, |
| 222 | +).load() |
| 223 | + |
| 224 | +# 이후 from_chunks 패턴으로 인덱싱 |
| 225 | +from lang2sql import CatalogChunker, RecursiveCharacterChunker |
| 226 | + |
| 227 | +chunks = ( |
| 228 | + CatalogChunker().split(catalog) + |
| 229 | + RecursiveCharacterChunker().split(docs) |
| 230 | +) |
| 231 | +retriever = VectorRetriever.from_chunks( |
| 232 | + chunks, |
| 233 | + embedding=OpenAIEmbedding(model="text-embedding-3-small"), |
| 234 | +) |
| 235 | +``` |
| 236 | + |
| 237 | +PDF는 페이지 단위로 `TextDocument`를 생성합니다: |
| 238 | +- `id`: `"{filename}__p{page_number}"` |
| 239 | +- `title`: `"{filename} page {page_number}"` |
| 240 | + |
| 241 | +## 4) 중요한 현재 제약 |
| 242 | + |
| 243 | +- v2 내장 VectorStore는 현재 `InMemoryVectorStore`만 공식 제공됩니다. |
| 244 | +- `BaselineNL2SQL`은 현재 `retriever` 주입 파라미터를 받지 않습니다. |
| 245 | + - 벡터 기반 파이프라인은 `HybridNL2SQL` 또는 수동 조합을 사용하세요. |
| 246 | +- `VectorRetriever` 결과의 `context`는 현재 `list[str]`입니다. |
| 247 | + - 문서 출처 구조화가 필요하면 `metadata`를 별도 조회하거나 커스텀 래퍼를 두세요. |
| 248 | +- `retriever.add()`는 **`list[IndexedChunk]`만 받습니다** — `TextDocument` 직접 전달 불가. |
| 249 | + - 추가 전 반드시 `chunker.split(docs)`로 split한 결과를 전달하세요: |
| 250 | + ```python |
| 251 | + # ❌ 동작 안 함 |
| 252 | + retriever.add(docs) |
| 253 | + |
| 254 | + # ✅ 올바른 방법 |
| 255 | + retriever.add(RecursiveCharacterChunker().split(docs)) |
| 256 | + ``` |
| 257 | + |
| 258 | +## 5) 추천 실습 순서 |
| 259 | + |
| 260 | +1. [v2-complete-tutorial.md](./v2-complete-tutorial.md) 1~4단계로 로컬 스모크 테스트 |
| 261 | +2. 동일 문서 5~8단계로 실제 DB/LLM 연결 |
| 262 | +3. 동일 문서 9~13단계로 벡터 인덱싱/문서 파싱/청킹 튜닝 |
| 263 | +4. 동일 문서 14~18단계로 고급 조합과 커스텀 어댑터 테스트 |
0 commit comments