persist · collection · metadata filter · 중복방지 · 재색인
박수현 · (사)한국정보공학기술사회 · 2026
📂 소스:
2.langchain/7.RAG/3.vectorstore/
$eq/$gt/$in/$and 문법으로 정밀 검색~/documents/ 통째로 색인📂 소스:
2.langchain/7.RAG/3.vectorstore/
FAISS 의 한계와 디스크 영속화의 가치 — 약 15분
🔗 벡터 DB 4종 비교·ANN 색인 구조는 Session 13(PART A·C) 소유. 여기서는 그중 Chroma 의 영속화·컬렉션·필터 구조에 집중한다.
| 한계 | 증상 | 운영 영향 |
|---|---|---|
| 메모리 기반 | 프로세스 죽으면 색인 사라짐 (save_local() 매번 직렬화) |
재시작마다 로드, 분산 불가 |
| 메타데이터 필터 약함 | LangChain 래퍼가 retrieval 후 후처리로만 필터 | "특정 PDF 의 12 페이지만 검색" 불편 |
| 동시 쓰기 X | 단일 프로세스 mutation | 색인 중 검색 불가 |
| 하이브리드 검색 X | 벡터만 — BM25 (키워드) 결합 어려움 | 약어·고유명사 검색 부정확 |
persist_directory 폴더에 디스크 저장| 구분 | InMemory | Chroma |
|---|---|---|
| 저장 위치 | 메모리 | persist_directory (디스크) |
| 재실행 시 | 매번 재임베딩, API 비용 | 즉시 로드, 추가 비용 0 |
| 용도 | 빠른 데모 | 반복 실험·운영 |
# 있으면 로드, 없으면 생성 — 비용을 아끼는 핵심 패턴
if os.path.exists(PERSIST_DIR) and os.listdir(PERSIST_DIR):
store = load_store() # 재임베딩 없음
else:
store = build_store() # 첫 1회만 임베딩
🎯 InMemory 와의 결정적 차이. Chroma 를 채택하는 가장 직접적인 이유는 영속화를 통한 임베딩 비용 0 이다.
| DB | 호스팅 | 메타데이터 필터 | BM25 하이브리드 | 가격 | 적합 |
|---|---|---|---|---|---|
| FAISS | 라이브러리 | 약함 | ❌ | 무료 | 학습·실험 |
| ChromaDB ⭐ | 로컬 + 클라이언트-서버 | 강함 | ❌ (외부 BM25 결합) | 무료 | 본 코스 · 개인 RAG |
| Qdrant | 로컬 + 클라우드 | 강함 | ✅ | 무료 + 호스팅 | 단일 회사 운영 |
| Weaviate | 로컬 + 클라우드 | 강함 | ✅ | 무료 + 호스팅 | 멀티모달·하이브리드 |
| Pinecone | 클라우드 전용 | 강함 | ✅ | 유료 (월 $70+) | SaaS 운영 |
| Milvus | 클러스터 | 강함 | ✅ | 무료 | 대규모 (수억+) |
| pgvector | PostgreSQL 확장 | SQL | ✅ (Postgres FTS) | 무료 | 이미 Postgres 쓰는 팀 |
| 레이어 | 구현 | 역할 |
|---|---|---|
| Client API | Python / TypeScript | add / query / get / update / delete |
| Embedding Function | OpenAI / SentenceTransformers / 커스텀 | 텍스트 → 벡터 |
| Index | HNSW (Session 13 동일 구조) | 벡터 → top-K 검색 |
| Storage | SQLite + Parquet | 메타데이터 · 문서 본문 · 임베딩 |
| Persistence | local dir 또는 Server (FastAPI) | persist_directory 지정 |
add(documents)
├─→ Embedding Function 호출 → 벡터
├─→ HNSW 인덱스에 벡터 + ID
└─→ SQLite + Parquet 에 (id, document, metadata, embedding) 저장
query(query_text, where={...})
├─→ Embedding Function 호출 → 질의 벡터
├─→ HNSW 에서 top-K 후보
├─→ SQLite 에서 where 조건 일치하는 것만 필터
└─→ (ids, distances, documents, metadatas) 반환

ChromaDB 컬렉션 · 레코드 {id·embedding·document·metadata}, HNSW 인덱스로 similarity_search, persist_directory 영속화
설치 → CRUD → 검색 — 약 20분
# 본 코스 (개인 RAG)
pip install chromadb langchain-chroma
import chromadb
# 로컬 디렉토리에 저장
client = chromadb.PersistentClient(path="./chroma_db")
# 디렉토리 구조:
# ./chroma_db/
# ├─ chroma.sqlite3 ← collection 메타·문서
# └─ <uuid>/ ← collection 별 HNSW + parquet
🎯 본 코스는 Persistent 로 진행한다. 운영 환경에서는 server 모드 + 별도 Docker 를 사용한다.
# 서버 실행 (별도 터미널)
chroma run --path ./chroma_db --host 0.0.0.0 --port 8000
client = chromadb.HttpClient(host="localhost", port=8000)
client = chromadb.Client() # 프로세스 종료 시 사라짐
🎯 운영 환경에서는 HttpClient + 별도 Docker 컨테이너로 분리한다.
import chromadb
client = chromadb.PersistentClient(path="./chroma_db")
# === Collection 생성 / 가져오기 ===
collection = client.get_or_create_collection(
name="my_notes",
metadata={"hnsw:space": "cosine"}, # 거리 함수 (cosine|l2|ip)
)
# === C — add ===
collection.add(
ids=["doc1_p1", "doc1_p2", "doc1_p3"],
documents=[
"RAG 는 retrieval augmented generation 의 약자다.",
"벡터 DB 는 임베딩을 저장하고 유사도 검색을 한다.",
"ChromaDB 는 SQLite + Parquet 으로 영속화한다.",
],
metadatas=[
{"source": "rag_intro.md", "page": 1},
{"source": "rag_intro.md", "page": 2},
{"source": "rag_intro.md", "page": 3},
],
)
⬇️ 코드가 길어 다음 장에 이어집니다.
# === R — query (유사도 검색) ===
results = collection.query(
query_texts=["벡터 검색이 뭔가요?"],
n_results=2,
)
# results = {ids, distances, documents, metadatas, embeddings}
# === R — get (ID 또는 조건으로) — 검색 아닌 단순 조회 ===
collection.get(ids=["doc1_p1"])
collection.get(where={"source": "rag_intro.md"})
collection.count() # 저장된 청크 수 — 색인 검증의 첫걸음
collection.get(include=["documents", "metadatas"], limit=3) # 내용·메타 직접 확인
💡
ids는 client 가 제공 — auto-generated 가 아님. 중복 방지·재색인의 핵심 (뒤 슬라이드).
# === U — update (기존 ID 의 내용·메타 교체) ===
collection.update(
ids=["doc1_p1"],
documents=["RAG = 검색을 통해 LLM 응답을 보강하는 패턴"],
)
# === D — delete (ID 또는 조건으로) ===
collection.delete(ids=["doc1_p3"])
collection.delete(where={"source": "old_doc.md"})
# === collection 자체 삭제 ===
client.delete_collection("my_notes")
| 동사 | 메서드 | 용도 |
|---|---|---|
| C | add |
청크 + 메타 + ID 삽입 |
| R | query |
유사도 검색 (벡터) |
| R | get / count |
ID·조건 조회, 색인 검증 |
| U | update |
기존 ID 내용 교체 |
| D | delete |
ID·조건 삭제 |
💡
update는 같은 ID 가 이미 있어야 동작. 없는 ID 는 무시 → 신규 삽입은add/upsert사용.
import chromadb
from chromadb.utils import embedding_functions
# === 1. Sentence Transformers (기본, 영어 위주) ===
default_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="all-MiniLM-L6-v2" # 384d, 빠름, 무료
)
# === 2. OpenAI (1536d, 한국어 우수) ===
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.environ["OPENAI_API_KEY"],
model_name="text-embedding-3-small",
)
# === 3. 한국어 특화 — multilingual-e5-large ===
multilingual_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="intfloat/multilingual-e5-large" # 1024d, 100+ 언어
)
# Collection 생성 시 임베딩 함수 지정
collection = client.get_or_create_collection(
name="ko_notes",
embedding_function=multilingual_ef, # ← 한국어면 이거 권장
)
⚠️ 임베딩 함수는 collection 생성 후 변경 불가. 모델 교체 시 collection 재생성 + 재색인이 필요하다.
| 모델 | 차원 | 한국어 품질 | 속도 | 가격 |
|---|---|---|---|---|
all-MiniLM-L6-v2 |
384 | ❌ 약함 | ⚡⚡⚡ | 무료 |
multilingual-e5-large |
1024 | ✅ 우수 | ⚡⚡ | 무료 (로컬) |
| OpenAI text-embedding-3-small | 1536 | ✅ 우수 | ⚡⚡⚡ (API) | $0.02/M 토큰 |
BAAI/bge-m3 |
1024 | ✅ 최상위 | ⚡⚡ | 무료 (로컬) |
⚠️ 임베딩 함수는 collection 생성 후 변경 불가. 모델 교체 시 collection 재생성 + 재색인이 필요하다.
langchain-chroma 추상화from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
# 1. 첫 색인 (디렉토리에 저장)
docs = [
Document(page_content="...", metadata={"source": "doc1.md", "page": 1}),
# ...
]
vectorstore = Chroma.from_documents(
documents=docs,
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
persist_directory="./chroma_db",
collection_name="my_notes",
)
⬇️ 코드가 길어 다음 장에 이어집니다.
langchain-chroma 추상화 (이어서)# 2. 다음번 — 기존 디렉토리에서 로드 (재임베딩 X, API 비용 0)
vectorstore = Chroma(
persist_directory="./chroma_db",
collection_name="my_notes",
embedding_function=OpenAIEmbeddings(model="text-embedding-3-small"),
)
# "있으면 로드, 없으면 생성" 패턴:
# from_documents = 임베딩 후 저장 (첫 1회) / Chroma(...) = 재임베딩 없는 로드
# 3. 추가 — 점진 색인 (기존에 더 얹기)
vectorstore.add_documents(more_docs)
# 4. 검색 + 메타데이터 필터
results = vectorstore.similarity_search(
"벡터 검색이란?",
k=4,
filter={"source": "rag_intro.md"}, # 특정 문서로 한정
)
# 5. retriever 추상화 (Session 13 의 RAG 체인과 동일)
retriever = vectorstore.as_retriever(
search_kwargs={"k": 4, "filter": {"source": "rag_intro.md"}}
)
🎯 FAISS → Chroma 마이그레이션은 3줄 변경으로 충분 (
FAISS→Chroma+persist_directory+collection_name).💡
persist_directory가 핵심: InMemory 는 프로세스 종료 시 사라져 매 실행 재임베딩(비용↑). Chroma 는 디스크에 영속화 → 재실행 시 즉시 로드, 임베딩 API 재호출 0.
$eq · $gt · $in · $and — 약 12분
where 절 문법 정리| 연산자 | 의미 | 예시 |
|---|---|---|
$eq (기본) |
정확히 같음 | {"page": 12} 또는 {"page": {"$eq": 12}} |
$ne |
같지 않음 | {"source": {"$ne": "draft.md"}} |
$gt / $gte |
보다 큼 / 이상 | {"page": {"$gte": 10}} |
$lt / $lte |
보다 작음 / 이하 | {"page": {"$lt": 50}} |
$in |
리스트 중 하나 | {"category": {"$in": ["AI", "ML"]}} |
$nin |
리스트 중 어느 것도 아님 | {"status": {"$nin": ["draft", "deprecated"]}} |
$and |
모두 만족 | {"$and": [{"src": "x.pdf"}, {"page": {"$gte": 10}}]} |
$or |
하나만 만족 | {"$or": [{"src": "a.pdf"}, {"src": "b.pdf"}]} |
$contains (document) |
본문 substring | vectorstore.similarity_search(q, filter={...}, where_document={"$contains":"강아지"}) |
where — metadata 필터where_document — 문서 본문 substring 필터 (검색 hybrid 효과)# 1. 특정 PDF 만
vectorstore.similarity_search("Q", filter={"source": "manual.pdf"})
# 2. 페이지 범위 — Chapter 3 (page 30~50)
vectorstore.similarity_search("Q", filter={
"$and": [
{"source": "book.pdf"},
{"page": {"$gte": 30}},
{"page": {"$lte": 50}},
]
})
# 3. 여러 출처 — A 또는 B
vectorstore.similarity_search("Q", filter={
"source": {"$in": ["doc_a.md", "doc_b.md", "doc_c.md"]}
})
# 4. draft 제외
vectorstore.similarity_search("Q", filter={
"status": {"$ne": "draft"}
})
⬇️ 코드가 길어 다음 장에 이어집니다.
# 5. 최근 1개월 색인된 문서만
import time
one_month_ago = time.time() - 30 * 86400
vectorstore.similarity_search("Q", filter={
"embedded_at_ts": {"$gte": one_month_ago}
})
# 6. 사용자별 격리 (멀티테넌트)
vectorstore.similarity_search("Q", filter={"user_id": current_user})
# 7. 본문 + 메타 결합 — "강아지" 단어가 들어간 manual.pdf chunk
vectorstore.similarity_search(
"Q",
filter={"source": "manual.pdf"},
where_document={"$contains": "강아지"},
)
🎯 필터는 retrieval 정확도를 2~3배 향상시킨다. 답변 근거의 출처 범위를 검색 단계에서 지정하는 수단이다.
5개 PDF 통째로 색인 — 약 20분
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("./DATA/Python_시큐어코딩_가이드(2023).pdf")
pages = loader.load() # 페이지마다 Document 1개
print(f"총 페이지: {len(pages)}") # 예: 87
print(f"3페이지 본문 일부: {pages[2].page_content[:200]}")
print(f"3페이지 메타데이터: {pages[2].metadata}")
# {'source': './DATA/Python_시큐어코딩_가이드.pdf', 'page': 2}
| 이슈 | 증상 | 해결 |
|---|---|---|
| 스캔 이미지 PDF | page_content 가 빈 문자열 |
OCR 필요 — easyocr 또는 pytesseract |
| 암호화 PDF | 로드 자체 실패 | pypdf.PdfReader(pwd=...) |
| CID 인코딩 | (cid:1234) 같은 문자열 |
pdfplumber 또는 pdfminer.six 로 폴백 |
| 글자 깨짐 | 한글이 □□□ 로 | 폰트 임베딩 안 됨 → OCR 필요 |
💡 본 코스 권장:
PyPDFLoader가 정상 동작하면 그대로 사용하고, 본문이 깨지면UnstructuredFileLoader또는pdfplumber로 폴백한다.
pdfloader.py 핵심 — 페이지 단위 + 청킹from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from pathlib import Path
import hashlib
pdf_path = Path("./DATA/Python_시큐어코딩_가이드.pdf")
loader = PyPDFLoader(str(pdf_path))
pages = loader.load()
# 청킹 — 토큰 단위 (비용 추정 정확)
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
separator="\n\n",
chunk_size=1000,
chunk_overlap=200,
)
chunks = splitter.split_documents(pages)
⬇️ 코드가 길어 다음 장에 이어집니다.
pdfloader.py 핵심 — 페이지 단위 + 청킹 (이어서)# 메타데이터 풍부화
for i, c in enumerate(chunks):
c.metadata["doc_id"] = pdf_path.stem # 파일명만
c.metadata["chunk_idx"] = i
c.metadata["chunk_total"] = len(chunks)
# 결정적 ID — 같은 내용은 항상 같은 ID
h = hashlib.sha256(c.page_content.encode()).hexdigest()[:16]
c.metadata["chunk_id"] = f"{pdf_path.stem}_p{c.metadata['page']}_h{h}"
# 색인
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
persist_directory="./chroma_db",
collection_name="my_library",
ids=[c.metadata["chunk_id"] for c in chunks], # ← 결정적 ID 전달!
)
print(f"{len(chunks)} chunks 색인 완료")
collection.delete(ids=[...]))~/documents/*.pdf 한 번에from pathlib import Path
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
# === 옵션 1 — DirectoryLoader (LangChain 추상화) ===
loader = DirectoryLoader(
path="./my_library/",
glob="**/*.pdf",
loader_cls=PyPDFLoader,
use_multithreading=True,
show_progress=True,
)
docs = loader.load()
print(f"PDF 합계 페이지: {len(docs)}")
💡 가장 간단한 일괄 로딩 방식. 메타데이터를 세밀히 제어하려면 다음 장의 옵션 2 를 사용한다.
~/documents/*.pdf 한 번에 — 직접 제어# === 옵션 2 — 직접 (더 세밀한 제어) ===
all_chunks = []
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=1000, chunk_overlap=200
)
for pdf in sorted(Path("./my_library").glob("**/*.pdf")):
pages = PyPDFLoader(str(pdf)).load()
for p in pages:
p.metadata["doc_id"] = pdf.stem
p.metadata["doc_path"] = str(pdf.relative_to("./my_library"))
chunks = splitter.split_documents(pages)
all_chunks.extend(chunks)
print(f" ✓ {pdf.name}: {len(pages)} pages → {len(chunks)} chunks")
# 한 collection 에 통째 색인
vectorstore = Chroma.from_documents(
documents=all_chunks,
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
persist_directory="./chroma_db",
collection_name="library",
)
print(f"총 {len(all_chunks)} chunks 색인 완료")
💡 5만 chunks 수준까지 는 노트북에서 무리 없이 처리된다. 그 이상은 색인 시간(~30분)과 비용($1~$5)을 고려한다.
2.langchain/7.RAG/3.vectorstore/3.chromadb2_saveload_multifiles.py 패턴# 이미 색인된 vectorstore 로드
vectorstore = Chroma(
persist_directory="./chroma_db",
collection_name="library",
embedding_function=OpenAIEmbeddings(model="text-embedding-3-small"),
)
# === 패턴 1 — 전체 컬렉션 검색 ===
results = vectorstore.similarity_search("RAG 성능 평가", k=5)
# === 패턴 2 — 특정 책만 ===
results = vectorstore.similarity_search(
"RAG 성능 평가",
k=5,
filter={"doc_id": "RAG_handbook_2024"}
)
⬇️ 코드가 길어 다음 장에 이어집니다.
2.langchain/7.RAG/3.vectorstore/3.chromadb2_saveload_multifiles.py 패턴 (이어서)# === 패턴 3 — 여러 책 중에서 ===
results = vectorstore.similarity_search(
"RAG 성능 평가",
k=5,
filter={"doc_id": {"$in": ["book_a", "book_b", "paper_x"]}}
)
# === 패턴 4 — 답변 + 출처 인용 ===
for d in results:
print(f"[{d.metadata['doc_id']} p.{d.metadata['page']}]")
print(d.page_content[:200])
print("---")
# === 패턴 5 — Retriever 추상화 (RAG chain 에서) ===
retriever = vectorstore.as_retriever(
search_kwargs={
"k": 4,
"filter": {"doc_id": current_book_id},
}
)
본선: 결정적 ID 로 중복 방지 · 심화: 배치·재색인 비용·백엔드 내부 — 약 15분
# ❌ 한 chunk 씩 호출 — 매 호출당 latency
for chunk in chunks: # 1000 chunks
emb = client.embeddings.create(model=..., input=chunk.text)
# → 약 1000 × 200ms = 200초 (네트워크 + 모델 호출)
# ✅ 배치 호출 — 한 번에 100개씩
BATCH = 100
for i in range(0, len(chunks), BATCH):
batch = chunks[i:i+BATCH]
emb = client.embeddings.create(
model="text-embedding-3-small",
input=[c.text for c in batch], # 리스트 전달
)
# → 약 10 호출 × 500ms = 5초
Chroma.from_documents 의 자동 배치embed_documents() 호출chunk_size=512 처럼 더 작게 (안정성)| 방식 | 시간 | 비용 (small) |
|---|---|---|
| 1개씩 호출 | ~50분 | $0.20 |
| 배치 100 | ~3분 | $0.20 (동일) |
| 배치 1000 | ~2분 | $0.20 |
import hashlib
from langchain_chroma import Chroma
def deterministic_id(chunk) -> str:
"""문서 내용 + 출처 + 페이지로 고유 ID 생성."""
payload = f"{chunk.metadata['doc_id']}|p{chunk.metadata['page']}|{chunk.page_content}"
return hashlib.sha256(payload.encode()).hexdigest()[:24]
# 색인 시 ids 전달 — ChromaDB 는 같은 ID upsert (덮어쓰기)
vectorstore.add_documents(
documents=chunks,
ids=[deterministic_id(c) for c in chunks],
)
collection.delete(ids=[...]) 로 정밀 삭제# 파일이 바뀌었는지 mtime + hash 로 체크
existing_ids = set(collection.get(where={"doc_id": pdf_name})["ids"])
new_ids = {deterministic_id(c) for c in new_chunks}
to_add = new_ids - existing_ids
to_remove = existing_ids - new_ids
# 없어진 chunk 삭제
if to_remove:
collection.delete(ids=list(to_remove))
# 새 chunk 만 추가
add_chunks = [c for c in new_chunks if deterministic_id(c) in to_add]
collection.add(documents=add_chunks, ids=[deterministic_id(c) for c in add_chunks])
💡 색인 전체를 다시 만들지 않고 바뀐 chunk 만 갱신 — 대용량 컬렉션 운영의 핵심.
# 1. 새 collection 생성 (이름 다르게)
new_vs = Chroma(
persist_directory="./chroma_db",
collection_name="library_v2", # ← 새 이름
embedding_function=OpenAIEmbeddings(model="text-embedding-3-large"),
)
# 2. 원본 문서로부터 다시 색인 (chunk 텍스트 그대로 재사용)
old_collection = chromadb.PersistentClient("./chroma_db").get_collection("library")
all_docs = old_collection.get(include=["documents", "metadatas"])
new_vs.add_texts(
texts=all_docs["documents"],
metadatas=all_docs["metadatas"],
ids=all_docs["ids"], # 같은 ID 유지 → citation 호환
)
# 3. 검증 후 교체 — 만족하면 delete_collection("library"), 아니면 v2 폐기
💡 임베딩 모델 변경 = 색인 비용 + 검증 시간. 모델 선택은 처음에 신중히.
./chroma_db/ 디렉토리 구성./chroma_db/
├── chroma.sqlite3 # collection 메타·doc 본문·메타데이터
├── 32a4f1c0-.../ # collection UUID
│ ├── header.bin # HNSW 인덱스 헤더
│ ├── data_level0.bin # HNSW 그래프 (level 0)
│ ├── length.bin
│ ├── link_lists.bin # 노드 간 엣지
│ └── *.parquet # 임베딩 + ID
└── 9b71... / # 다른 collection
💡 디스크 용량이 부족하면 차원 축소 (Matryoshka
dimensions=512) 또는 PQ 압축 (Milvus/Qdrant).
# SQLite — 메타데이터
sqlite3 chroma_db/chroma.sqlite3 \
"SELECT collection_id, collection_name, count(*) FROM embeddings_queue GROUP BY collection_id"
# Parquet — 벡터 (pandas 로)
python -c "
import pandas as pd, pathlib
for p in pathlib.Path('chroma_db').rglob('*.parquet'):
df = pd.read_parquet(p)
print(p, df.shape, df.columns.tolist())
"
💡
chroma.sqlite3는 메타·문서, collection UUID 폴더의*.parquet는 임베딩 벡터를 담는다.
PDF 5개 → 자기만의 Q&A 봇 — 약 30분
./my_library/ 디렉토리에 모아두기# 색인 진행 측정
import time
t = time.time()
vectorstore = Chroma.from_documents(...)
print(f"색인 시간: {time.time()-t:.1f}s, chunks: {len(all_chunks)}")
# 패턴 A — 전체 검색
vectorstore.similarity_search("Q", k=5)
# 패턴 B — 특정 책만
vectorstore.similarity_search("Q", k=5, filter={"doc_id": "book_a"})
# 패턴 C — 페이지 범위
vectorstore.similarity_search("Q", k=5, filter={
"$and": [{"doc_id": "book_a"}, {"page": {"$gte": 10}}, {"page": {"$lte": 30}}]
})
[doc_id p.N] 형식으로 출처 표시add / query / get / update / delete$eq · $gt · $in · $and · where_document.$contains2.langchain/7.RAG/8.web_app/ 실행 — Flask UI 챗봇과 결합