sonsdf's picture
feat: 감리지적사례 사이드카 추가 (audit_cases 컬렉션, 답변과 분리된 참고 정보)
59506e2 verified
Raw
History Blame Contribute Delete
4.26 kB
# -*- coding: utf-8 -*-
"""임베딩 + ChromaDB 적재 (일괄 작업, BGE-M3만 로드).
- 청크 = 레코드 1개 (추가 분할 없음). 8192 초과는 모델이 자동 절단.
- 질의회신은 question+answer 합쳐 하나의 임베딩 (common.embed_text).
- 배치 처리 + 진행률. 재개: 이미 적재된 id는 스킵(재인코딩 안 함).
사용법: python3 -m rag.embed (전체)
python3 -m rag.embed --batch 32
"""
import argparse
import sys
import time
from collections import defaultdict
from rag import common as C
# 배치 어텐션 메모리 ∝ batch × maxlen². 토큰 예산으로 배치 크기를 적응시켜
# 긴 문서(8192)는 배치 1, 짧은 문서는 배치 32까지 → MPS OOM 방지.
AREA_BUDGET = 8192 # batch × approx_tokens 상한
MAX_BATCH = 32
def approx_tokens(text):
# 한국어 서브워드 ~1.5~2자/토큰. 안전하게 과대추정(작은 배치) + 8192 상한.
return min(max(len(text) // 2, 1), C.MAX_SEQ)
def adaptive_batches(items):
"""items(id,text,meta)를 길이 내림차순 정렬 후 토큰예산 기반 가변배치로 yield."""
ordered = sorted(items, key=lambda x: -approx_tokens(x[1]))
i = 0
while i < len(ordered):
maxtok = approx_tokens(ordered[i][1])
bs = max(1, min(MAX_BATCH, AREA_BUDGET // maxtok))
yield ordered[i:i + bs]
i += bs
def existing_ids(coll):
try:
return set(coll.get(include=[])["ids"])
except Exception: # noqa: BLE001
return set()
def run(collections=None):
"""collections(기본 COLLECTIONS) 매핑을 임베딩·적재. AUDIT_COLLECTIONS 등도 재사용 가능."""
# 컬렉션별 레코드 수집
buckets = defaultdict(list) # coll -> [(id, text, metadata)]
for coll, fn, i, rec in C.iter_records(collections):
text = C.embed_text(rec)
if not text.strip():
continue
buckets[coll].append((C.record_id(fn, i), text, C.to_metadata(rec, coll)))
print("적재 대상:", {k: len(v) for k, v in buckets.items()}, flush=True)
client = C.get_chroma()
print("BGE-M3 로드 중...", flush=True)
emb = C.load_embedder()
print("device =", emb.device, flush=True)
grand_done = grand_total = 0
t0 = time.time()
for coll_name, items in buckets.items():
col = client.get_or_create_collection(
coll_name, metadata={"hnsw:space": "cosine"})
done = existing_ids(col)
todo = [x for x in items if x[0] not in done]
grand_total += len(items)
grand_done += len(items) - len(todo)
print(f"\n[{coll_name}] 전체 {len(items)} / 기적재 {len(items)-len(todo)} / 신규 {len(todo)}",
flush=True)
try:
import torch
mps_clear = torch.mps.empty_cache if torch.backends.mps.is_available() else None
except Exception: # noqa: BLE001
mps_clear = None
for chunk in adaptive_batches(todo):
ids = [c[0] for c in chunk]
docs = [c[1] for c in chunk]
metas = [c[2] for c in chunk]
vecs = emb.encode(docs, normalize_embeddings=True,
batch_size=len(chunk), show_progress_bar=False)
col.upsert(ids=ids, embeddings=[v.tolist() for v in vecs],
documents=docs, metadatas=metas)
if mps_clear:
mps_clear() # 배치 간 MPS 메모리 반환 (긴 문서 후 누적 방지)
grand_done += len(chunk)
pct = 100 * grand_done / grand_total
rate = grand_done / max(time.time() - t0, 1e-6)
sys.stdout.write(
f"\r 진행 {grand_done}/{grand_total} ({pct:.1f}%) "
f"{rate:.0f} rec/s ")
sys.stdout.flush()
print(f"\n[{coll_name}] 완료: {col.count()}건", flush=True)
print(f"\n전체 적재 완료: {grand_done}건, {time.time()-t0:.0f}s", flush=True)
print("컬렉션 현황:", {c.name: c.count() for c in client.list_collections()},
flush=True)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--batch", type=int, default=32)
ap.parse_args()
run()
if __name__ == "__main__":
main()