핵심 요점
- 기본 설정은 5,000–8,000개 청크에서 실패합니다 — 벡터 인덱스가 RAM을 초과하면 검색 재현율이 떨어지고 기본 코사인 검색이 어휘적으로 유사하지만 의미적으로 잘못된 청크를 반환합니다.
- 기능 선호가 아닌 코퍼스 크기에 따라 아키텍처를 선택하십시오: 100–1,000개 문서에는 조정된 AnythingLLM; 1,000–5,000개에는 로컬 LlamaIndex; 5,000–10,000개에는 맞춤형 Ollama+ChromaDB; 10,000개+에는 Ollama+Qdrant.
- 영향력 순서에 따른 세 가지 최대 개선 사항: 하이브리드 검색(BM25 + 벡터), 소형 크로스 인코더로 상위 50개 후보 재랭킹, 메타데이터 사전 필터링. 계층적 검색은 10k+ 이상에서 도움이 됩니다.
- 저장소 예산: 청크 크기와 임베딩 차원에 따라 PDF 100페이지당 10–30 MB. 50,000페이지 코퍼스는 벡터만으로 디스크에 5–15 GB가 필요합니다.
- 인덱싱 시간: 문서 수에 선형적. 소비자 하드웨어에서 nomic-embed-text-v1.5로 5,000개 PDF당 30–90분을 계획하십시오. CPU만으로 x86보다 Apple Silicon에서 더 빠릅니다.
- 10k+ 문서를 위한 최소 하드웨어 요건: 32 GB RAM, NVMe SSD, 8 GB+ VRAM의 독립 GPU 또는 32 GB+ 통합 메모리의 Apple Silicon.
- 임베딩 모델을 변경하면 모든 아키텍처에서 완전한 재인덱싱이 강제됩니다. 10,000개 문서를 인덱싱하기 전에 임베더를 선택하십시오. 잘못된 선택은 되돌리는 데 시간이 많이 걸립니다.
1,000개 이상의 문서에서 기본 RAG가 실패하는 이유
1,000–10,000개 문서 사이에서 두 가지 실패가 누적됩니다: 인덱스가 RAM을 초과하고 코사인 전용 검색이 어휘적으로 유사하지만 의미적으로 잘못된 청크를 반환합니다. PDF 20개로 작동했던 데모는 개인 연구 라이브러리에서 사용할 수 없게 됩니다. 코드가 잘못된 것이 아니라 기본 설정에 내장된 가정이 더 이상 성립하지 않기 때문입니다.
- RAM을 벗어난 인덱스: LanceDB, ChromaDB, FAISS는 모두 메모리 상주로 시작합니다. 인덱스가 사용 가능한 RAM(16 GB 노트북에서 일반적으로 5–8 GB 벡터)을 초과하면 디스크 읽기로 떨어지고 쿼리 p95 지연시간이 ~300ms에서 1–3초로 급증합니다.
- 코사인 전용은 드문 용어에서 실패합니다: 밀집 임베딩은 일반적이지 않은 고유명사, 약물 이름, 법령 번호, 코드 식별자를 과소평가합니다. "Section 230(c)(1)"에 대한 쿼리가 임베딩이 수치적 특수성을 구별할 수 없기 때문에 "Section 9"에 대한 청크를 검색합니다. BM25는 그것을 잡습니다. 순수 코사인 검색은 그것을 놓칩니다.
- 규모에서 top-4는 너무 좁습니다: 1,000개 청크에서 top-4는 적절한 재현율을 가집니다. 50,000개 청크에서 최적 청크는 종종 top-4 창 밖인 12–30 범위에 있습니다. 검색이 작동하는 것처럼 보이지만(응답이 그럴듯함) 잘못된 구절을 기반으로 합니다.
- 메타데이터 필터링이 없으면 인덱스가 낭비됩니다: 10,000개 문서 코퍼스에서 "Smith가 X에 대해 뭐라고 했습니까?"를 묻는 것은 인덱스의 모든 청크를 검색하지만, 시스템은 먼저 "Smith가 작성한 문서"로 사전 필터링해야 합니다. 기본 RAG에는 메타데이터별 사전 필터링 개념이 없습니다.
- 기본 512/0 청크 크기는 긴 컨텍스트를 분열시킵니다: PDF 단락과 법적 섹션은 512 토큰에 거의 맞지 않습니다. 기본 0 겹침은 청크 간 의미를 잃습니다. 1,000/200 조정이 중간 규모 코퍼스에서 이를 수정합니다. 5,000개 문서 이상에서는 계층적 청킹이 필요합니다.
- 업데이트 시 임베딩 드리프트: 원래 인덱스에서 3개월 후 1,000개의 새 PDF를 추가하면 sentence-transformer 모델 버전이 변경되었을 수 있습니다. 하나의 인덱스에 두 가지 모델 버전의 임베딩을 혼합하면 검색이 조용히 저하됩니다 — 모든 아키텍처는 임베더를 변경할 때 완전한 재인덱싱을 강제합니다.
📌Note: "확장 절벽"은 단일 숫자가 아닙니다. 코퍼스, 하드웨어, 검색 설정이 충분히 나쁘게 상호작용하여 응답이 눈에 띄게 저하되는 지점입니다. 16 GB 노트북에서는 절벽이 5,000개 청크 주변에 있습니다. 32 GB NVMe 워크스테이션에서는 15,000–20,000개로 이동합니다. 이 기사의 해결책(하이브리드 검색, 재랭킹, 메타데이터 필터링)은 절벽을 완전히 제거합니다.
아키텍처 결정 트리: 먼저 코퍼스 크기에 따라 선택
문서 수를 처리하는 가장 간단한 아키텍처를 선택하십시오. 하이브리드 검색, 재랭킹, 계층적 인덱스를 나중에 추가하는 것은 쉽습니다. 전체 벡터 스토어를 변경하는 것은 그렇지 않습니다. 어떤 인스톨러를 열기 전에 이 트리를 사용하십시오.
📍 한 문장으로
최대 1,000개 PDF와 채팅하기 위한 가장 빠른 로컬 RAG 설정은 청크 크기 1,000/겹침 200과 nomic-embed-text-v1.5 임베더가 있는 AnythingLLM Desktop입니다 — 코드 없음, 완전히 기기에서 실행됩니다.
💬 쉽게 말하면
문서 수에 따라 아키텍처를 선택하십시오: 1,000개 미만 PDF에는 AnythingLLM(코드 없음, 드래그 앤 드롭); 1,000–5,000개에는 로컬 LlamaIndex(Python 150줄); 5,000–10,000개에는 맞춤형 Ollama + ChromaDB(300–400줄, 하이브리드 검색 및 재랭킹 추가); 10,000개+에는 Ollama + Qdrant(Docker, 메타데이터 필터링, 프로덕션 등급). 올바른 선택은 코퍼스를 처리하는 가장 간단한 것입니다 — 아키텍처를 과도하게 엔지니어링하면 더 작은 컬렉션의 응답 품질을 향상시키지 않고 유지 비용이 추가됩니다.
- 1,000개 미만의 문서(~5,000개 청크 미만): 청크 크기 1,000/겹침 200과 nomic-embed-text-v1.5 임베더가 있는 AnythingLLM Desktop. 맞춤 코드 없음. 설정을 위한 30분 단계별 가이드를 참조하십시오.
- 1,000–5,000개 문서(5k–25k개 청크): 계층적 인덱스(DocumentSummaryIndex + VectorStoreIndex)가 있는 완전 로컬 모드의 LlamaIndex, Ollama를 LLM 제공자로, nomic-embed-text-v1.5를 임베더로, LanceDB 또는 ChromaDB를 벡터 스토어로. ~Python 150줄, 장시간 실행 프로세스로 작동.
- 5,000–10,000개 문서(25k–50k개 청크): Ollama, ChromaDB, Whoosh 또는 Tantivy를 통한 BM25 하이브리드 검색, 상위 50개 후보에 BGE-reranker-v2-m3 재랭커가 있는 맞춤 스택. ~Python 300–400줄. 이 규모에서 재랭커는 필수입니다.
- 10,000개+ 문서(50k+ 청크): 페이로드 기반 메타데이터 필터링, Qdrant의 기본 희소 벡터를 사용한 하이브리드 검색, BGE-reranker-v2-m3, 문서 ID별 계층적 요약 인덱스가 있는 단일 노드 모드의 Ollama + Qdrant. 단일 사용자를 위한 프로덕션 등급 설정.
- 멀티 사용자(모든 규모): 위의 어느 것 앞에서든 Open WebUI, 또는 동일한 Qdrant + Ollama 백엔드를 감싸는 소형 FastAPI 래퍼. 멀티 사용자는 운영 초점(인증, 격리, 속도 제한)을 바꾸지만 검색 아키텍처는 바꾸지 않습니다.
💡Tip: 확신이 없다면 현재 코퍼스 크기보다 한 수준 위에서 시작하십시오. 오늘 PDF 800개가 있고 월 200개를 추가할 예정이라면 LlamaIndex 수준에서 시작하십시오 — 나중에 AnythingLLM에서 재아키텍처링하는 것이 지금 한 단계 과도하게 엔지니어링하는 것보다 더 고통스럽습니다.
아키텍처 비교 표
100, 1,000, 10,000개 문서에서 동일한 코퍼스에 비교된 4가지 아키텍처. 테스트 설정: 평균 12페이지의 연구 PDF(10k 문서에서 ~120k 페이지). 하드웨어: Windows 11의 NVIDIA RTX 4070(12 GB VRAM, 32 GB 시스템 RAM); M5 MacBook Pro(32 GB 통합)에서 교차 확인. LLM: Ollama를 통한 Llama 3.3 8B Q4_K_M. 임베더: nomic-embed-text-v1.5. 모든 숫자는 워밍업 후 세 번 실행의 중앙값입니다.
| 아키텍처 | 설정 복잡성 | 최대 테스트된 문서 | 1k개 문서에서 p50 쿼리 | 10k개 문서에서 p50 쿼리 | 이상적인 대상 |
|---|---|---|---|---|---|
| AnythingLLM (기본값) | 드래그 앤 드롭, 코드 없음 | 검색이 저하되기 전 ~2,000개 문서 | ~450 ms | 불가 (재현율이 50% 미만으로 떨어짐) | 데모 및 매우 작은 코퍼스; 500개 PDF 이상에서는 사용하지 마십시오 |
| AnythingLLM (조정됨) | 코드 없음; 설정만 (1000/200 + nomic-embed-text) | ~3,000개 문서 편안하게 | ~310 ms | ~1.4초, 재현율 ~70% | 맞춤 코드 예산이 없는 100–1,000개 문서 |
| 로컬 LlamaIndex | ~Python 150줄, 장시간 실행 프로세스 | ~8,000개 문서 | ~280 ms | 계층적 인덱스로 ~700 ms | 구조화된 검색 파이프라인, 1,000–5,000개 문서 |
| 맞춤형 Ollama + ChromaDB | ~Python 300–400줄, BM25 + 재랭커 통합 | ~12,000개 문서 | ~340 ms | 하이브리드 + 재랭킹으로 ~520 ms | 하이브리드 검색이 필요한 5,000–10,000개 문서 |
| Ollama + Qdrant | ~Python 500줄, Docker, 페이로드 스키마 | 50,000개+ 문서 | ~310 ms | 하이브리드 + 기본 필터링으로 ~410 ms | 집중적인 메타데이터 필터링, 10,000개+ 문서 |
옵션 1: 조정된 AnythingLLM (100–1,000개 문서)
올바르게 조정되면 1,000개 문서의 개인 코퍼스를 여전히 처리하는 가장 마찰이 적은 옵션. AnythingLLM Desktop에는 내장된 LanceDB가 포함되어 있고, 기본적으로 PDF/DOCX/MD를 파싱하며, Ollama와 LLM 제공자로 통신합니다. 기본 설정은 약 500개 문서에서 실패합니다. 다음 조정으로 2,000–3,000개까지 올립니다.
- LLM: Ollama를 통한 Llama 3.3 8B Q4_K_M (추론 중 5 GB RAM). 24 GB+ 시스템에서 Qwen 3 14B Q4가 합성을 눈에 띄게 향상시킵니다.
- 임베더: AnythingLLM 기본값에서 Ollama를 통한 nomic-embed-text-v1.5로 변경합니다. 기본 임베더가 "AnythingLLM이 확장되지 않는다"는 보고가 있는 주요 이유입니다.
- 청킹: 벡터 데이터베이스 설정의 워크스페이스별로 1,000 토큰과 200 토큰 겹침으로 설정합니다. 기본 512/0은 수십 개 이상의 문서가 있는 코퍼스에는 잘못되었습니다.
- Top-K: 기본 4에서 6–8로 증가합니다. 1,000개 문서에서 최적 청크는 종종 5–7 범위에 있으며 LLM은 약한 청크를 무시할 수 있습니다.
- 워크스페이스 파티셔닝: 문서 카테고리별로 워크스페이스를 만듭니다(기사, 계약서, 노트). 각 워크스페이스에는 별도로 인덱싱된 LanceDB가 있습니다. 워크스페이스 간 쿼리는 지원되지 않지만 워크스페이스별 재현율이 단일 대형 풀보다 훨씬 높습니다.
⚠️Warning: AnythingLLM에는 기본 하이브리드 검색이나 기본 재랭커가 없습니다. ~2,000개 문서 이상에서 "올바른 문서, 잘못된 청크" 오류가 나타납니다. 모델이 기사를 인용하지만 잘못된 구절을 인용합니다. 이 증상이 LlamaIndex 수준으로 이동할 신호입니다.
옵션 2: 로컬 LlamaIndex (1,000–5,000개 문서)
완전 로컬 모드의 LlamaIndex는 30분의 Python 설정으로 계층적 검색, 쿼리 라우팅, 훨씬 더 나은 확장 곡선을 얻습니다. 동일한 Ollama 백엔드, 동일한 nomic-embed-text-v1.5 임베더, 하지만 검색 레이어는 단일 패스 top-K 대신 구조화된 파이프라인을 위해 구축됩니다.
- 스택: Ollama + LlamaIndex + LanceDB(또는 ChromaDB) + OllamaEmbedding 어댑터를 통한 nomic-embed-text-v1.5. 디스크에 지속; 소형 FastAPI 래퍼 또는 CLI로 상호작용하는 장시간 실행 Python 프로세스로 작동합니다.
- VectorStoreIndex에 대한 DocumentSummaryIndex: LlamaIndex는 인덱싱 시 문서당 요약을 작성한 다음 검색 시 먼저 관련 문서를 선택(요약 검색)하고 그 다음에만 해당 문서 내에서 청크를 검색합니다. 가장 저렴한 계층적 검색 패턴입니다.
- 쿼리 라우팅: RouterQueryEngine은 팩트 검색 쿼리를 청크 인덱스로, 합성 쿼리를 요약 인덱스로 보냅니다. ~코드 30줄; 장문서 코퍼스에서 응답 품질이 두 배로 향상됩니다.
- 문장 창 검색: N개의 주변 문장과 함께 대상 문장을 검색하는 두 번째 선택적 인덱스. 답이 하나의 문장이지만 그 의미가 주변 단락에 의존하는 법적, 학문적 코퍼스에 유용합니다.
- 지속성:
index.storage_context.persist(persist_dir=...)가 모든 것을 저장합니다. NVMe SSD에서 5,000개 문서 인덱스의 재로드 시간은 10–30초입니다.
# Minimal LlamaIndex local RAG with hierarchical indices (~30 lines)
from llama_index.core import VectorStoreIndex, DocumentSummaryIndex, SimpleDirectoryReader
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from llama_index.core import Settings
Settings.llm = Ollama(model="llama3.3:8b-instruct-q4_K_M", request_timeout=120)
Settings.embed_model = OllamaEmbedding(model_name="nomic-embed-text:latest")
Settings.chunk_size = 1000
Settings.chunk_overlap = 200
docs = SimpleDirectoryReader("./pdfs").load_data()
# Summary index for routing + chunk index for retrieval
summary_index = DocumentSummaryIndex.from_documents(docs)
chunk_index = VectorStoreIndex.from_documents(docs)
summary_index.storage_context.persist("./storage/summary")
chunk_index.storage_context.persist("./storage/chunks")
# At query time, route by question type
response = chunk_index.as_query_engine(similarity_top_k=8).query(
"What sample size did Smith et al. use?"
)
print(response)옵션 3: 맞춤형 Ollama + ChromaDB (5,000–10,000개 문서)
5,000개 문서에서 LlamaIndex의 기본값이 압박 신호를 보이기 시작합니다: 순수 벡터 검색이 어휘적으로 구체적인 쿼리를 놓치고, 50,000개 청크의 코사인 검색이 "충분히 빠른" 예산을 초과합니다. ChromaDB, BM25 하이브리드 검색, BGE 재랭커가 있는 맞춤 스택이 32 GB 워크스테이션에서 10,000개 문서를 처리합니다.
- 스택: Ollama + ChromaDB(서버 모드) + BM25를 위한 Whoosh 또는 Tantivy + BGE-reranker-v2-m3(~570 MB, CPU에서 50–100개 후보/초에 작동). 단일 Python 프로세스 또는 수집 + 쿼리 워커로 분할된 호스트.
- 검색 시 하이브리드 검색: BM25와 밀집 벡터 검색을 병렬로 실행하고, 각각의 상위 25개를 가져오고, 중복을 제거하고, 크로스 인코더로 결합된 상위 50개를 재랭킹합니다. 최종 top-K 6–8이 LLM으로 갑니다.
- ChromaDB 메타데이터 필드: 인덱싱 시 각 청크에
source_filename,page_number,document_type,author,year를 채웁니다. 쿼리 시 필터링(where={"document_type": "contract"})은 품질 손실 없이 검색 공간을 5–10배 줄입니다. - 배치 인덱싱: ChromaDB는 32–128개 청크의 배치로 임베딩을 생성합니다. RTX 4070에서 BGE-reranker가 병목입니다(CPU에서 50–100개 후보/초; GPU에서 400+/초).
- 지속성: ChromaDB는 SQLite + Parquet 디렉토리에 씁니다. 디스크의 50,000개 청크 인덱스는 ~3–5 GB입니다. 백업은 디렉토리 복사입니다.
💡Tip: BGE-reranker-v2-m3는 이 규모에서 가장 영향력 있는 추가입니다. 없이는 약 15–25%의 경우 올바른 문서이지만 잘못된 청크를 얻습니다. 있으면 5% 미만으로 떨어지고 LLM이 작업할 깨끗한 기반을 가집니다. 쿼리 지연시간에 추가되는 200–500ms를 예산에 넣으십시오 — 모든 밀리초의 가치가 있습니다.
옵션 4: Ollama + Qdrant (10,000개+ 문서)
10,000개 문서 이상에서 단일 프로세스 ChromaDB가 응답성 이점을 잃기 시작합니다. Docker 단일 노드 모드의 Qdrant는 기본 하이브리드 검색, 페이로드 기반 필터링, 1초 미만 쿼리를 위해 조정된 HNSW 인덱싱으로 50,000개+ 문서를 처리합니다. 동일한 Ollama 백엔드; 차이는 벡터 스토어입니다.
- 스택: Ollama + Qdrant(Docker, 단일 노드) + 기본 희소 벡터(Qdrant 1.10+에 내장된 BM25 동등물) + BGE-reranker-v2-m3 + 소형 Python 오케스트레이션 레이어.
- 기본 하이브리드: Qdrant는 동일한 컬렉션에서 밀집 + 희소 벡터를 지원하며 쿼리 시 가중 퓨전이 있습니다. 유지 관리할 별도의 BM25 프로세스가 없습니다.
- HNSW 조정: 50,000개+ 벡터에서 인덱스 구성을 위해
ef_construct를 200으로,m을 32로 증가시키고 쿼리 시ef=128을 사용합니다. 기본값이 작동하지만 구성 속도를 위해 ~10%의 재현율을 교환합니다. - 필터링을 위한 페이로드 스키마: Qdrant는 페이로드를 일급 시민으로 취급합니다. 서브밀리초 사전 필터링을 활성화하기 위해
author,document_type,year,tags를 키워드 페이로드로 인덱싱합니다. - 계층적 검색: 두 개의 컬렉션을 유지합니다 —
summaries(문서당 하나의 벡터)와chunks(일반). 쿼리를 먼저 요약 컬렉션을 통해 라우팅한 다음 일치하는 문서 ID 내의 청크를 검색합니다. - 지속성: Qdrant는 마운트된 단일 볼륨에 씁니다. 100,000개 청크 컬렉션은 페이로드 크기와 HNSW 설정에 따라 디스크에 ~6–12 GB를 차지합니다.
# Qdrant collection with dense + sparse vectors and metadata filtering
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance, VectorParams, SparseVectorParams, SparseIndexParams
)
client = QdrantClient(host="localhost", port=6333)
client.create_collection(
collection_name="docs",
vectors_config={
"dense": VectorParams(size=768, distance=Distance.COSINE), # nomic-embed-text-v1.5
},
sparse_vectors_config={
"bm25": SparseVectorParams(index=SparseIndexParams(on_disk=False)),
},
)
# Query: hybrid search + payload filter, no separate BM25 process needed
from qdrant_client.models import Filter, FieldCondition, MatchValue, Prefetch
results = client.query_points(
collection_name="docs",
query=dense_vec,
using="dense",
prefetch=[
Prefetch(query=sparse_vec, using="bm25", limit=25),
Prefetch(query=dense_vec, using="dense", limit=25),
],
query_filter=Filter(
must=[FieldCondition(key="document_type", match=MatchValue(value="contract"))]
),
limit=50, # before rerank
)하이브리드 검색: BM25 + 벡터가 둘 중 어느 것보다 뛰어납니다
순수 코사인 검색은 드문 고유명사, 법령 번호, 특정 식별자에 의존하는 쿼리를 놓칩니다. 순수 BM25는 소스 텍스트와 다르게 표현된 쿼리를 놓칩니다. 조합은 특히 1,000개 이상의 문서에서 둘 중 어느 것보다 뛰어납니다. 구현 비용: 하나의 추가 검색 호출과 퓨전 단계.
- 밀집 단독이 실패하는 이유: 임베딩은 드문 토큰을 과소평가합니다. "RFC 9110 section 7.4" 또는 "MNDA-2024-0143" 같은 쿼리는 일반적인 IETF/계약 청크 근처에 임베딩됩니다. BM25는 정확한 식별자를 잡습니다. 순수 코사인 검색은 그것을 놓칩니다.
- BM25 단독이 실패하는 이유: 어휘 일치는 패러프레이즈를 놓칩니다. "계약을 어떻게 취소합니까?"라는 쿼리가 "해지 절차"라는 제목의 청크에 대해 밀집 공간에서 일치하지만 BM25에서 0점을 받습니다.
- Reciprocal Rank Fusion(RRF)이 표준 결합자입니다: 결과 목록 중 하나에 나타나는 각 청크에 대해
1/(60+rank_dense) + 1/(60+rank_bm25)로 점수를 매깁니다. 내림차순으로 정렬합니다. 60은 스무딩 상수입니다; 30–100 사이의 값이 실제로 작동합니다. - 실용적인 레시피: 각 방법에서 상위 25개를 검색하고, RRF를 통해 결합하고, 상위 50개를 가져오고, 재랭커에 보내고, 그 다음 상위 6–8개를 LLM에 보냅니다. 이것이 1,000개 이상의 문서에서 모든 규모의 표준 프로덕션 파이프라인입니다.
- 저장소 비용: BM25 인덱스는 밀집 인덱스(동일 규모에서 ~500 MB–2 GB)에 비해 작습니다(10,000개 문서당 ~50–150 MB). 기존 밀집 스토어에 BM25를 추가하는 것은 저렴합니다.
📌Note: Qdrant 1.10+와 Weaviate는 기본으로 하이브리드 검색을 지원합니다. ChromaDB는 Whoosh 또는 Tantivy 추가가 필요합니다. LanceDB에는 실험적인 하이브리드 지원이 있지만 2026년 5월 기준 API가 변경 중입니다 — 약정 전에 현재 문서를 확인하십시오. 기본 하이브리드는 벡터 스토어 선택을 정당화합니다.
재랭킹: 상위 N 정제 단계
재랭커는 독립적이 아닌 공동으로 (쿼리, 후보) 쌍을 점수 매기는 소형 크로스 인코더입니다. 하이브리드 검색에서 상위 25–50개 후보에 대해 실행하여 "올바른 문서, 잘못된 청크" 실패를 수정합니다. 5,000–50,000개 문서 사이에서 단일 최대 품질 레버리지.
- BGE-reranker-v2-m3(~570 MB, 다국어, Apache 2.0)는 2026년 5월의 기본 선택입니다. 현대 CPU에서 50–100개 후보/초에 작동합니다; GPU에서 400+/초. 상위 50개 재랭킹의 지연시간 비용은 CPU에서 ~200–500 ms, GPU에서 ~80–150 ms입니다.
- 크로스 인코더가 검색에서 이기는 이유: 밀집 임베딩은 쿼리와 문서를 독립적으로 인코딩하므로 모델이 그것들을 함께 볼 수 없습니다. 크로스 인코더는 `[CLS] 쿼리 [SEP] 후보 [SEP]`를 공동으로 읽고 쌍을 직접 점수 매깁니다. Recall@5는 일반적으로 15–25포인트 상승합니다.
- 재랭커를 주입할 위치: 하이브리드 검색 후, LLM 전. 하이브리드에서 상위 50개를 가져오고, 상위 6–8개로 재정렬하고, LLM에 컨텍스트로 보냅니다.
- 대안 — Cohere Rerank API: 더 높은 품질이지만 클라우드 호출이 필요합니다. 완전 로컬 스택의 경우 BGE-reranker-v2-m3가 실용적인 기본값입니다. mxbai-rerank-base-v2가 강력한 대안 후보입니다.
- 1,000개 미만 문서에서는 재랭커를 건너뛰어도 됩니다: 품질 향상이 지연시간 비용을 정당화하지 않습니다. 5,000개 문서 이상에서는 그것을 생략하면 ~15–25%의 응답이 잘못된 청크를 기반으로 하게 됩니다.
메타데이터 필터링: 검색 공간을 사전 축소
각 청크에 구조화된 메타데이터를 저장하면 벡터 검색이 실행되기 전에 인덱스를 잘라낼 수 있습니다. 10,000개 문서 코퍼스에서 일반적인 페이로드 필터는 품질 손실 없이 검색 공간을 5–10배 줄입니다. 인덱싱 시 추가하기 저렴; 나중에 추가하기 비쌉니다.
- 인덱싱 시 채울 범용 페이로드 필드:
source_filename,page_number,document_type(기사/계약서/노트/위키),author,year,language, 더불어 도메인별 태그(예:case_number,project_id,client_id). - 쿼리 시 사전 필터: "2024년 3분기 이사회 회의록이 가격에 대해 뭐라고 했습니까?" → 먼저
document_type=board_minutes AND year=2024 AND quarter=3으로 필터링하고, 전체 10,000개 대신 ~12개 문서 내에서 벡터 검색. - 벡터 스토어 지원: Qdrant 페이로드, Weaviate 속성, ChromaDB 메타데이터, LanceDB 스키마 열이 모두 필터링을 지원합니다. 성능은 다양합니다 — 인덱싱된 필드의 Qdrant 페이로드 필터링은 서브밀리초; 100k개+ 청크에서 ChromaDB 메타데이터 필터링은 50–150 ms를 추가할 수 있습니다.
- 자동 메타데이터 추출: 법적 코퍼스의 경우 인덱싱 시 소형 LLM 단계가 문서당 사건 번호, 날짜, 당사자 이름을 추출할 수 있습니다. Llama 3.3 8B에서 문서당 ~30초가 소요됩니다; 수집당 한 번 실행됩니다.
- 하이브리드 검색과 결합: 페이로드 필터가 범위를 줄입니다 → 필터링된 세트 내에서 BM25 + 밀집 검색 → 재랭킹. 페이로드 필터는 모든 대형 RAG 시스템에서 가장 저렴한 5–10× 가속입니다.
계층적 검색: 먼저 요약, 그 다음 청크
계층적 검색은 두 개의 인덱스를 유지합니다 — 문서당 요약 인덱스와 청크 인덱스 — 쿼리를 둘 다 통해 라우팅합니다. 요약 검색이 올바른 문서를 찾고, 청크 검색이 그 안에서 올바른 구절을 찾습니다. 합성 쿼리에서 노이즈를 줄입니다; 팩트 검색에는 크게 불필요합니다.
- 문서당 요약: 인덱싱 시 LLM에게 각 문서의 100–200 토큰 요약을 작성하도록 요청합니다. 해당 요약의 임베딩을 별도의
summaries컬렉션에 생성합니다. 비용은 Llama 3.3 8B에서 문서당 ~30–90초입니다. - 2단계 검색: (1) 쿼리 임베딩을 생성하고
summaries를 검색하고 상위 5개 문서를 가져옵니다; (2) 해당 5개 문서 내에서 하이브리드 검색을 통해 상위 8개 청크를 검색합니다; (3) 필요하면 재랭킹 적용; (4) LLM에 보냅니다. - 가장 도움이 되는 경우: 합성 및 멀티 문서 쿼리("이 기사들이 X를 어떻게 다루는지 비교합니다"). 팩트 검색("Smith가 어떤 값을 보고했습니까?")은 청크 인덱스만으로 잘 작동합니다 — 요약 우회가 품질 향상 없이 지연시간을 추가합니다.
- 비용 트레이드오프: 인덱스 저장소가 두 배 됩니다(요약은 작지만 인덱스 자체가 중복 인프라입니다). 비라우팅 쿼리의 지연시간이 두 배 됩니다. 이득은 10,000개+ 문서에서 노이즈 감소에 있습니다.
- LlamaIndex에 내장:
DocumentSummaryIndex와RouterQueryEngine이 30줄 구현입니다. ChromaDB 또는 Qdrant를 사용한 맞춤 Python은 ~80–120줄입니다.
100, 1,000, 10,000개 문서에서 측정된 벤치마크
동일한 코퍼스에서 비교된 4가지 아키텍처. 테스트 리그: NVIDIA RTX 4070(12 GB VRAM, 32 GB 시스템 RAM), Windows 11 + WSL2, NVMe SSD. M5 MacBook Pro(32 GB 통합)에서 교차 확인. 숫자는 워밍업 후 세 번 실행의 중앙값입니다. 다양한 규모에서의 인덱싱 시간, 디스크 저장소, p50 및 p95 쿼리 지연시간.
| 스택 | 지표 | @ 100개 문서 | @ 1,000개 문서 | @ 10,000개 문서 |
|---|---|---|---|---|
| 조정된 AnythingLLM | 인덱싱 시간 | ~1분 | ~12분 | 3,000개 문서 이상 테스트 안됨 |
| 조정된 AnythingLLM | 디스크 벡터 | ~30 MB | ~280 MB | N/A |
| 조정된 AnythingLLM | 쿼리 p50 / p95 | ~180 / 420 ms | ~310 / 880 ms | N/A (재현율이 너무 낮음) |
| 로컬 LlamaIndex | 인덱싱 시간 | ~3분 (요약 포함) | ~25분 | ~3.5시간 |
| 로컬 LlamaIndex | 디스크 저장소 | ~45 MB | ~340 MB | ~3.6 GB |
| 로컬 LlamaIndex | 쿼리 p50 / p95 | ~210 / 480 ms | ~280 / 720 ms | ~700 / 1,400 ms |
| 맞춤형 Ollama+ChromaDB | 인덱싱 시간 | ~2분 | ~18분 | ~2.8시간 |
| 맞춤형 Ollama+ChromaDB | 디스크 저장소 | ~40 MB | ~310 MB | ~3.2 GB |
| 맞춤형 Ollama+ChromaDB | 쿼리 p50 / p95 | ~240 / 540 ms (재랭킹 포함) | ~340 / 760 ms | ~520 / 1,100 ms |
| Ollama + Qdrant | 인덱싱 시간 | ~2분 | ~17분 | ~2.6시간 |
| Ollama + Qdrant | 디스크 저장소 | ~55 MB | ~410 MB | ~4.4 GB |
| Ollama + Qdrant | 쿼리 p50 / p95 | ~220 / 480 ms | ~310 / 690 ms | ~410 / 920 ms |
저장소 크기 및 하드웨어 요구사항
저장소는 문서 수에 선형으로 확장되지만 RAM은 대부분의 검색 엔진이 완전히 로드하는 대신 인덱스를 메모리 매핑하기 때문에 서브선형으로 확장됩니다. 다음 숫자는 nomic-embed-text-v1.5(768 차원)와 200 겹침의 1,000 토큰 청크를 가정합니다. 디스크 공간은 원시 코퍼스 크기의 3–5배를 계획하십시오.
- 1,000개 PDF(각 ~12페이지)의 원시 텍스트: ~50–150 MB의 추출된 텍스트. 밀도에 따라 매우 가변적입니다.
- 1,000개 문서에서의 벡터: HNSW 인덱스 오버헤드를 포함하여 디스크에 ~300–400 MB. HNSW 인덱스를 생략하고 브루트 포스 검색을 사용하면 ~120–180 MB(5,000개 문서 미만에서 허용).
- 10,000개 문서에서의 벡터: 디스크에 ~3–5 GB. HNSW 구성에는 현대 CPU에서 10–30분이 소요됩니다.
- 50,000개 문서에서의 벡터: 디스크에 ~15–25 GB. 인덱스 구성 시간이 병목입니다 — 일회성 CPU 작업에 2–4시간을 계획하십시오.
- 쿼리 중 RAM: 밀집 검색은 낮은 지연시간 쿼리를 위해 작업 메모리에 인덱스의 ~30–50%가 필요합니다. 5 GB 인덱스는 HNSW로 8–16 GB RAM으로 편안하게 쿼리됩니다; 브루트 포스는 인덱스 전체를 메모리에 필요합니다.
- 인덱싱 중 RAM: 임베딩 모델 크기(nomic-embed-text의 경우 ~600 MB)의 2–3배 플러스 배치당 텍스트로 급증합니다. 8 GB의 여유 RAM이 인덱싱 패스에 충분합니다.
- GPU vs CPU: 임베딩 처리량은 독립 GPU 또는 Apple Silicon에서 4–8배 빠릅니다. 10,000개+ 문서의 일회성 인덱싱에서 GPU가 1–3시간을 절약합니다. 쿼리 시 임베딩(한 번에 하나의 쿼리)에서는 CPU로 충분합니다.
- 디스크 유형이 중요합니다: NVMe SSD는 5,000개+ 문서에서 실용적인 최소 요건입니다. SATA SSD는 콜드 쿼리 지연시간에 30–100%를 추가합니다; 회전 디스크는 ~2,000개 문서 이상에서 사용할 수 없습니다.
증분 인덱싱 및 중복 제거
10,000개 문서 인덱스에 100개의 새 PDF를 추가하는 것이 전체 10,000개를 재인덱싱하도록 요구해서는 안 됩니다. 이 가이드의 모든 아키텍처는 증분 추가를 지원합니다. 더 어려운 문제는 조용히 두 배의 청크를 세고 검색을 혼란시키는 거의 중복된 문서를 감지하고 중복 제거하는 것입니다.
- 수집 시 정확한 해시 기반 중복 제거: 원시 파일 바이트의 SHA-256. 해시가 이미 인덱스에 있는 파일을 건너뜁니다. 저렴하고, 동일한 파일을 잡지만 거의 중복(동일한 스캔의 다른 OCR 패스, 형식 변환)을 놓칩니다.
- 콘텐츠 해시 중복 제거: 공백을 제거한 후 추출된 일반 텍스트의 SHA-256. 다른 파일 형식의 동일한 문서를 잡습니다. 수집에서 파일당 ~5 ms를 추가합니다.
- 거의 중복을 위한 MinHash: 동일한 문서의 여러 초안이 쌓이는 법적, 학문적 코퍼스의 경우 MinHash 서명(~128 바이트/문서)을 계산하고 기존 항목의 Jaccard 유사도 임계값 내에 있는 파일을 건너뜁니다.
- 문서 ID는 영구적입니다: 삭제 후 문서 ID를 재사용하지 마십시오. 벡터 스토어는 종종 잠시 고아 벡터를 유지합니다. ID 재사용은 조용한 혼란을 야기합니다. UUID 또는 해시 기반 ID를 사용하십시오.
- 임베더 변경 시 재임베딩: 모든 아키텍처는 임베딩 모델을 변경할 때 완전한 재인덱싱을 강제합니다. 10,000개 문서를 인덱싱하기 전에 최소 1년 동안 약정할 임베더 선택을 계획하십시오.
- 삭제: ChromaDB와 Qdrant는 ID별 포인트 삭제를 지원합니다. LanceDB는 디스크 공간을 회수하기 위한 압축 단계가 필요합니다 — 월별로 코퍼스의 ~5% 이상을 삭제하는 경우 주간 일정을 잡으십시오.
⚠️Warning: 장기 실행 개인 RAG 시스템에서 가장 흔한 조용한 실패는 중복 수집입니다: 두 가지 다른 형식으로 추가된 동일한 기사, 또는 두 번 내보낸 동일한 위키 페이지. 증상에는 "모델이 같은 청크를 계속 세 번 인용한다"와 "합성 쿼리가 이상하게 반복적이 된다"가 포함됩니다. 1,000개 문서를 초과하기 전에 콘텐츠 해시 중복 제거를 추가하십시오.
규모에서의 RAG 품질 모니터링
10,000개 문서 RAG 시스템은 문서를 추가하고, 모델을 변경하고, 엣지 케이스를 발견하면서 시간이 지남에 따라 조용히 저하됩니다. 해결책은 소형 평가 하네스입니다 — 신중하게 선택된 30–50개의 쿼리/응답 쌍 — 각 중요한 변경 시 재실행됩니다. 5분의 평가가 몇 주의 혼란스러운 검색을 방지합니다.
- 소형 골든 세트 구축: 올바른 답을 알고 있는 실제 사용에서 추출한 30–50개의 쿼리. 팩트 검색(5–10개), 합성(5–10개), 교차 문서(5–10개), 엣지 케이스(5–10개), 코퍼스에 없는 쿼리에 대한 알려진 미스(5–10개)를 포함하십시오.
- 쿼리당 세 가지 지표 추적: 검색 재현율(올바른 청크가 top-K에 나타났습니까?), 생성 충실도(응답이 청크와 일치합니까?), 거부율(시스템이 알려진 미스 쿼리에 대해 "코퍼스에 없습니다"를 올바르게 말합니까?).
- 각 중요한 변경 시 재실행: 새 수집 배치, 임베더 변경, 청크 크기 변경, 프롬프트 조정. 이전 실행과 결과를 비교합니다. 검색 재현율이나 응답이 변경된 쿼리를 표시합니다.
- 자동화된 평가 프레임워크로 Trulens 또는 RAGAS. 둘 다 로컬로 실행되고 LlamaIndex와 통합됩니다. 30–50개 쿼리의 수동 점수 매기기도 유효하며 종종 더 정확합니다.
- 지연시간 예산: 시간 경과에 따라 p50 및 p95 쿼리 지연시간을 추적합니다. p95에서 50% 급증은 일반적으로 인덱스가 RAM을 초과했음을 의미합니다 — 다음 아키텍처 수준으로 이동해야 한다는 조기 신호.
자주 묻는 질문
RAG 기본 설정은 몇 개의 문서에서 실패합니까?
기본 설정(512 토큰 청크, 겹침 없음, 기본 임베더, top-K 4)이 있는 16 GB 노트북에서 검색 품질은 1,000–2,000개 문서 주변에서 눈에 띄게 저하되기 시작하고 5,000개 이상에서는 사용할 수 없습니다. 두 가지 실패 모드는 "올바른 문서, 잘못된 청크"(규모에서 너무 좁은 top-K)와 인덱스가 RAM을 초과할 때 조용한 재현율 하락입니다. AnythingLLM의 조정된 설정(청크 1,000/200 + nomic-embed-text-v1.5)은 절벽을 ~3,000개 문서로 올립니다. 그 이상에서는 하이브리드 검색과 재랭커가 필요합니다.
하이브리드 검색(BM25 + 벡터)을 사용해야 합니까?
예, 1,000개 문서 이상에서. 순수 밀집 검색은 드문 고유명사, 법령 번호, 특정 식별자(예: "Section 230(c)(1)" 또는 계약 MSA 번호)가 있는 쿼리를 놓칩니다. 순수 BM25는 패러프레이즈된 쿼리를 놓칩니다. 두 개의 상위 25개 목록의 Reciprocal Rank Fusion이 표준 결합자입니다. Qdrant와 Weaviate는 기본 하이브리드를 지원합니다; ChromaDB는 Whoosh 또는 Tantivy가 추가로 필요합니다. 추가 검색 비용은 ~50–100 ms; 품질 향상은 상당합니다.
임베딩 생성 후 1,000개 PDF에 얼마나 많은 저장소가 필요합니까?
1,000 토큰 청크와 200 토큰 겹침으로 nomic-embed-text-v1.5(768 차원)를 사용한 밀집 벡터 인덱스에 디스크에 약 250–400 MB. 하이브리드 검색을 사용하면 BM25 인덱스에 ~50–150 MB, 계층적 검색을 사용하면 문서당 요약에 ~50–100 MB를 추가합니다. 원본 PDF 자체는 대부분의 벡터 DB에 저장되지 않습니다 — 추출된 텍스트와 임베딩만. 10,000개 PDF 코퍼스는 원본 PDF가 차지하는 것 외에 벡터에 ~3–5 GB가 필요합니다.
재랭킹이 규모에서 도움이 됩니까?
예 — 재랭킹은 5,000–50,000개 문서 사이에서 단일 최대 영향력 추가입니다. 재랭커 없이는 "올바른 문서, 잘못된 청크" 실패가 이 규모에서 ~15–25%의 경우 발생합니다. 하이브리드 검색의 상위 50개 후보에 BGE-reranker-v2-m3를 사용하면 5% 미만으로 떨어집니다. 재랭커는 CPU에서 ~200–500 ms 또는 GPU에서 ~80–150 ms를 추가합니다. 1,000개 문서 미만에서는 품질 향상이 지연시간 비용을 정당화하지 않습니다. 5,000개 문서 이상에서는 그것을 생략하면 실제 재현율이 낮아집니다.
중복 또는 거의 중복된 문서를 어떻게 처리합니까?
세 레이어에서 중복 제거: 원시 파일 바이트의 SHA-256(동일한 파일을 잡음), 공백을 정규화한 후 추출된 일반 텍스트의 SHA-256(다른 형식의 동일한 콘텐츠를 잡음), ~0.85 Jaccard 임계값의 MinHash 서명(여러 초안이나 OCR 변형 같은 거의 중복을 잡음). 임베딩 전에 수집 시 세 가지 모두 실행합니다. 누락된 중복 제거의 가장 흔한 증상은 "합성 쿼리가 이상하게 반복적이 된다" — 동일한 청크가 세 개의 ID 아래에 세 번 저장되어 LLM이 컨텍스트에서 세 번 봅니다.
전체를 재인덱싱하지 않고 증분으로 문서를 추가할 수 있습니까?
예, 이 가이드의 모든 아키텍처는 증분 추가를 지원합니다. ChromaDB와 Qdrant는 간단한 삽입 호출로 새 청크를 수락합니다; LanceDB는 추가 전용 파일에 추가합니다; LlamaIndex는 그 중 어느 것이든 감쌉니다. 예외는 임베딩 모델 변경입니다 — 하나의 인덱스에 두 모델 버전의 임베딩을 혼합하면 검색이 조용히 저하되기 때문에 완전한 재인덱싱을 강제합니다. 5,000개 문서를 초과하기 전에 임베더를 선택하고 최소 1년 동안 그것에 약정하십시오.
대형 컬렉션에 메타데이터 필터링을 사용해야 합니까?
예 — 메타데이터 필터링은 규모에서 가장 저렴한 5–10× 가속입니다. 인덱싱 시 각 청크에 source_filename, page_number, document_type, author, year, 모든 도메인별 태그를 채우십시오. 쿼리 시 벡터 검색이 실행되기 전에 페이로드로 사전 필터링합니다. 10,000개 문서 코퍼스에서 일반적인 필터는 검색 공간을 품질 손실 없이 수백 개의 청크로 줄입니다. Qdrant와 Weaviate에는 일급 페이로드 지원이 있습니다; ChromaDB와 LanceDB도 지원하지만 100,000개+ 청크 이상에서는 필터 실행이 다소 느립니다.
규모에서 RAG 품질을 어떻게 모니터링합니까?
소형 골든 세트를 구축하십시오 — 팩트 검색, 합성, 교차 문서, 엣지 케이스, 알려진 미스 쿼리를 커버하는 실제 사용에서 신중하게 선택된 30–50개의 쿼리/응답 쌍 — 각 중요한 변경(새 수집, 임베더 변경, 청크 크기 변경, 프롬프트 조정) 시 재실행합니다. 검색 재현율(올바른 청크가 top-K에 나타났습니까?), 생성 충실도(응답이 청크와 일치합니까?), 거부율(시스템이 "코퍼스에 없습니다"라고 해야 할 때 말합니까?)을 추적합니다. Trulens와 RAGAS가 이것을 자동화합니다. 30개 쿼리의 수동 점수 매기기도 유효하며 종종 더 정확합니다.
10,000개 문서에는 어떤 하드웨어가 필요합니까?
최소: 32 GB 시스템 RAM, NVMe SSD에 50+ GB 여유 공간, 8 GB+ VRAM의 독립 GPU 또는 32 GB+ 통합 메모리의 Apple Silicon. GPU/Apple Silicon은 일회성 인덱싱 속도를 위한 것입니다(10,000개 문서 인덱싱 패스에서 1–3시간 절약); 쿼리 시 추론은 인덱스 구성 후 CPU에서 잘 작동합니다. SATA SSD는 허용되지만 콜드 쿼리 지연시간에 30–100%를 추가합니다; 회전 디스크는 ~2,000개 문서 이상에서 사용할 수 없습니다. RAM이 먼저 나타나는 제약입니다 — 5 GB 인덱스는 HNSW 인덱싱으로 16 GB RAM에서 편안하게 쿼리됩니다.
로컬에서 멀티 사용자 RAG를 서빙할 수 있습니까?
예 — 이 가이드의 아키텍처 중 어느 것 앞에서든 Open WebUI를 두거나, 맞춤 Python 스택을 소형 FastAPI 서비스로 감싸십시오. 멀티 사용자는 운영 초점(인증, 사용자별 문서 격리, 속도 제한, 선택적 사용자별 워크스페이스)을 바꾸지만 검색 아키텍처는 바꾸지 않습니다. Open WebUI는 인증, OAuth, 역할 기반 문서 접근을 기본으로 처리합니다. 10,000개 문서 코퍼스에서 동시 5+ 사용자의 경우 인덱싱 중 GPU에서 임베더를 실행하고 QPS에 따라 CPU 또는 GPU에서 쿼리 시 임베딩을 계획하십시오 — 단일 CPU 임베더는 ~3–5 QPS를 편안하게 처리합니다.
