카탈로그와 비즈니스 문서를 벡터로 인덱싱해 의미 기반 검색을 수행합니다.
export OPENAI_API_KEY="sk-..."
python scripts/setup_sample_db.py
python scripts/setup_sample_docs.py # docs/business/ 샘플 문서 생성청킹·임베딩·저장을 한 번에 처리합니다.
from lang2sql import VectorRetriever
from lang2sql.integrations.embedding import OpenAIEmbedding
catalog = [
{
"name": "orders",
"description": "고객 주문 정보. 주문 건수·금액·날짜 조회에 사용.",
"columns": {
"order_id": "주문 고유 ID",
"amount": "주문 금액",
"order_date": "주문 일시",
"status": "주문 상태",
},
},
]
docs = [
{
"id": "revenue_def",
"title": "매출 정의",
"content": "매출은 반품을 제외한 순매출이다. cancelled 상태 주문은 제외한다.",
"source": "docs/business/revenue.md",
},
]
retriever = VectorRetriever.from_sources(
catalog=catalog,
documents=docs,
embedding=OpenAIEmbedding(model="text-embedding-3-small"),
top_n=5,
)
result = retriever.run("지난달 순매출 기준 집계")
print("schemas:", [s["name"] for s in result.schemas])
print("context:", result.context)from_sources() 내부 동작:
CatalogChunker로 catalog splitRecursiveCharacterChunker로 docs splitfrom_chunks()로 embed + store (기본:InMemoryVectorStore)
split 단계를 직접 제어하거나 영속 store(FAISS, pgvector)를 쓸 때 사용합니다.
from lang2sql import CatalogChunker, DirectoryLoader, RecursiveCharacterChunker, VectorRetriever
from lang2sql.integrations.embedding import OpenAIEmbedding
# 1) 문서 로딩
docs = DirectoryLoader("docs/business").load()
# 2) 명시적 split
catalog_chunks = CatalogChunker().split(catalog)
doc_chunks = RecursiveCharacterChunker(chunk_size=800, chunk_overlap=80).split(docs)
# 3) embed + store
retriever = VectorRetriever.from_chunks(
catalog_chunks + doc_chunks,
embedding=OpenAIEmbedding(model="text-embedding-3-small"),
top_n=5,
)
result = retriever.run("순매출 집계 기준")
print("schemas:", [s["name"] for s in result.schemas])
print("context:", result.context)증분 추가:
new_docs = DirectoryLoader("docs/new").load()
retriever.add(RecursiveCharacterChunker().split(new_docs)) # pre-split 필수from lang2sql import MarkdownLoader, PlainTextLoader
md_docs = MarkdownLoader().load("docs/business/revenue.md")
txt_docs = PlainTextLoader().load("docs/business/rules.txt")from lang2sql import DirectoryLoader
# 기본: .md → MarkdownLoader, .txt → PlainTextLoader
docs = DirectoryLoader("docs/business").load()
print(f"로드된 문서 수: {len(docs)}")pip install pymupdffrom lang2sql import DirectoryLoader, MarkdownLoader
from lang2sql.integrations.loaders import PDFLoader
docs = DirectoryLoader(
"docs/",
loaders={
".md": MarkdownLoader(),
".pdf": PDFLoader(),
},
).load()PDFLoader는 페이지 단위로 TextDocument를 생성합니다:
id:"{filename}__p{page_number}"title:"{filename} page {page_number}"
from lang2sql import CatalogChunker, VectorRetriever
from lang2sql.integrations.embedding import OpenAIEmbedding
from lang2sql.integrations.vectorstore import FAISSVectorStore
store = FAISSVectorStore(index_path="./index/catalog.faiss")
chunks = CatalogChunker().split(catalog)
retriever = VectorRetriever.from_chunks(
chunks,
embedding=OpenAIEmbedding(model="text-embedding-3-large"),
vectorstore=store,
)
# 벡터 인덱스 + registry 파일로 저장
retriever.save("./index/catalog")
# → ./index/catalog.faiss
# → ./index/catalog.faiss.meta
# → ./index/catalog.registryfrom lang2sql import VectorRetriever
from lang2sql.integrations.embedding import OpenAIEmbedding
from lang2sql.integrations.vectorstore import FAISSVectorStore
embedding = OpenAIEmbedding(model="text-embedding-3-large")
store = FAISSVectorStore.load("./index/catalog.faiss")
retriever = VectorRetriever.load(
"./index/catalog",
vectorstore=store,
embedding=embedding,
)
result = retriever.run("주문 건수 집계")DataHub 없이 CSV로 카탈로그를 만들고 FAISS로 인덱싱합니다.
import csv, os
from collections import defaultdict
from lang2sql import CatalogChunker, VectorRetriever
from lang2sql.integrations.embedding import OpenAIEmbedding
from lang2sql.integrations.vectorstore import FAISSVectorStore
CSV_PATH = "./dev/table_catalog.csv"
OUTPUT_DIR = "./index"
tables: dict = defaultdict(lambda: {"desc": "", "columns": {}})
with open(CSV_PATH, newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
t = row["table_name"].strip()
tables[t]["desc"] = row["table_description"].strip()
tables[t]["columns"][row["column_name"].strip()] = row["column_description"].strip()
catalog = [
{"name": t, "description": info["desc"], "columns": info["columns"]}
for t, info in tables.items()
]
os.makedirs(OUTPUT_DIR, exist_ok=True)
store = FAISSVectorStore(index_path=f"{OUTPUT_DIR}/catalog.faiss")
chunks = CatalogChunker().split(catalog)
retriever = VectorRetriever.from_chunks(
chunks,
embedding=OpenAIEmbedding(model="text-embedding-3-large"),
vectorstore=store,
)
retriever.save(f"{OUTPUT_DIR}/catalog")
print(f"저장 완료: {OUTPUT_DIR}/catalog")docker run -d \
--name pgvector \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
pgvector/pgvector:pg16from lang2sql import CatalogChunker, VectorRetriever
from lang2sql.integrations.embedding import OpenAIEmbedding
from lang2sql.integrations.vectorstore import PGVectorStore
store = PGVectorStore(
connection="postgresql://postgres:postgres@localhost:5432/postgres",
table_name="lang2sql_vectors", # 첫 upsert 시 자동 생성
)
chunks = CatalogChunker().split(catalog)
retriever = VectorRetriever.from_chunks(
chunks,
embedding=OpenAIEmbedding(model="text-embedding-3-large"),
vectorstore=store,
)
# save() 없음 — upsert()마다 DB에 즉시 반영
# 동일 chunk_id 재실행 시 덮어씀 (ON CONFLICT DO UPDATE)InMemoryVectorStore |
FAISSVectorStore |
PGVectorStore |
|
|---|---|---|---|
| 영속성 | 없음 | 파일 | PostgreSQL |
| Upsert | true upsert | append-only | true upsert |
| 멀티 서버 | 불가 | 불가 | 가능 |
| 권장 규모 | < 50k chunks | < 500k chunks | 500k+ chunks |
vectorstore= 파라미터만 교체하면 나머지 코드는 동일합니다.
retriever = VectorRetriever.from_sources(
catalog=catalog,
embedding=OpenAIEmbedding(),
top_n=3, # 반환할 최대 스키마/문서 수 (기본값: 5)
score_threshold=0.3, # 이 점수 이하는 결과에서 제외 (기본값: 0.0)
)관련 없는 테이블이 검색될 때 score_threshold를 0.3~0.5로 올려보세요.
BM25 + 벡터 하이브리드 검색 → 04-hybrid.md