11차시. Retriever 기초 실습
4. 실습 ① — Retriever 만들고 비교하기
출처: 원본 PDF 11차시_Retriever기초실습.pdf 9~10쪽 · 쪽 그림 9쪽 10쪽
📝 src/retriever_test.py
from indexer import get_store # 10차시 함수
store = get_store()
QUESTION = "환불과 교환은 어떻게 다른가요?"
def show(name, retriever):
docs = retriever.invoke(QUESTION)
print(f"■ {name} ({len(docs)}개)")
for d in docs:
preview = d.page_content[:55].replace("\n", " ")
print(f" p.{d.metadata['page_no']} {preview}...")
print()
# ① 기본 유사도 검색
show("similarity k=3",
store.as_retriever(search_kwargs={"k": 3}))
# ② MMR — 다양성 확보
show("MMR k=3 fetch_k=10",
store.as_retriever(search_type="mmr",
search_kwargs={"k": 3, "fetch_k": 10,
"lambda_mult": 0.5}))
# ③ 임계값 필터
show("threshold 0.47",
store.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"k": 5, "score_threshold": 0.47}))실행 결과 예시 (코드를 읽고 만든 예상 화면입니다. 실제 실행 결과, 특히 모델의 답은 다를 수 있습니다)
→ 저장된 인덱스를 불러옵니다 (비용 0)
■ similarity k=3 (3개)
p.2 4. 환불 및 교환 규정 환불 상품 수령 후 7 일 이내에 환불을 신청하실 수 있습니다. 단순 변심...
p.2 자동 모드에서는 내장된 먼지 센서가 실내 공기질을 감지하여 풍량을 자동으로 조절합니다. 필터 교체 ...
p.1 1 제품 사용 설명서 가정용 공기청정기 AP-3300 목차 1. 제품 개요 2. 설치 방법 3. 사...
■ MMR k=3 fetch_k=10 (3개)
p.2 4. 환불 및 교환 규정 환불 상품 수령 후 7 일 이내에 환불을 신청하실 수 있습니다. 단순 변심...
p.2 교환 상품 불량의 경우 30 일 이내에 교환이 가능합니다. 이 경우 배송비는 당사가 부담합니다. 5...
p.2 5. 보증 및 무상 A/S 본 제품은 구매일로부터 1 년간 무상 A/S 를 제공합니다....
■ threshold 0.47 (2개)
p.2 4. 환불 및 교환 규정 환불 상품 수령 후 7 일 이내에 환불을 신청하실 수 있습니다. 단순 변심...
p.2 자동 모드에서는 내장된 먼지 센서가 실내 공기질을 감지하여 풍량을 자동으로 조절합니다. 필터 교체 ...실습 코드 · 11/retriever_test.py
실습 코드: 실습/src/11/retriever_test.py · 위의 원본 PDF 11차시 9쪽 「4️⃣ 실습 ① — Retriever 만들고 비교하기」과 이어집니다.
# ============================================================
# 11차시 | PPT: 11차시_Retriever기초실습.pptx
# 역할: 저장된 인덱스에서 source가 설명서인 조각만 retriever로 검색
# 입력: 10차시 FAISS 인덱스, 질문 "환불과 교환은 어떻게 다른가요?"
# 출력: 결과 개수, 쪽 번호, 본문 앞 55자
# 흐름:
# get_store() -> FAISS
# -> as_retriever(similarity, k=3, source 필터)
# -> show_result()
# ============================================================
# retriever: 질문을 받아 가까운 문서 조각을 돌려주는 검색기
# FAISS: 벡터(숫자 목록)를 저장한 인덱스. embedding은 질문을 그 숫자로 바꿀 때 씁니다.
# metadata: source, page_no처럼 조각에 붙은 부가 정보. filter가 source를 비교합니다.
import os
import sys
import warnings
# 이 파일 위치를 기준으로 10차시 폴더 경로를 만듭니다. 어디서 실행해도 같은 곳을 가리킵니다.
CURRENT_DIR = os.path.dirname(__file__)
INDEXER_DIR = os.path.join(CURRENT_DIR, "..", "10")
# 0번 자리에 넣어 10차시 indexer.py를 가장 먼저 찾게 합니다.
sys.path.insert(0, INDEXER_DIR)
from indexer import get_store
# 저장해 둔 인덱스를 열어 FAISS 저장소를 받습니다. 질문을 임베딩할 때 API 키가 필요합니다.
store = get_store()
QUESTION = "환불과 교환은 어떻게 다른가요?"
# compare_k가 기준 미달 조각을 셀 때 쓰는 점수 기준입니다.
MIN_SCORE = 0.0
# 점수 범위 경고 같은 문구가 출력을 가리지 않게 숨깁니다.
warnings.filterwarnings("ignore")
def compare_k(question, ks=(1, 3, 5)):
# k를 바꿔 관련도 점수 목록을 비교합니다. 아래 실행부에서는 호출하지 않습니다.
print(f"질문: {question}")
print("=" * 58)
# k마다 검색을 따로 한 번씩 돌립니다.
for k in ks:
# (문서, 점수) 쌍을 k개 돌려받습니다. 점수가 클수록 질문과 가깝습니다.
pairs = store.similarity_search_with_relevance_scores(
question,
k=k
)
scores = []
low_count = 0
# 점수를 모으면서 기준 미달 개수도 함께 셉니다.
for doc, score in pairs:
scores.append(score)
if score < MIN_SCORE:
low_count = low_count + 1
# 화면에서 읽기 쉽게 소수점 셋째 자리까지 반올림합니다.
rounded_scores = []
for score in scores:
rounded_score = round(float(score), 3)
rounded_scores.append(rounded_score)
# k:<3 은 k 값을 왼쪽 정렬로 세 칸에 맞춰 줄을 가지런히 합니다.
print(f"k={k:<3} 점수={rounded_scores}")
print(f" 기준 미달({MIN_SCORE}) 조각: {low_count}개")
print()
def show_result(name, retriever):
# retriever.invoke는 질문과 가까운 Document 리스트를 돌려줍니다.
docs = retriever.invoke(QUESTION)
print(f"{len(docs)}개")
# 조각마다 쪽 번호와 본문 앞 55자를 보여 줍니다.
for doc in docs:
preview = doc.page_content[:55]
# replace의 반환값을 다시 담지 않으므로 줄바꿈은 그대로 출력됩니다.
preview.replace("\n", " ")
# page_no는 10차시에서 인덱스를 만들 때 metadata에 넣어 둔 쪽 번호입니다.
page_no = doc.metadata["page_no"]
print(f"{page_no} {preview}")
print()
# similarity_retriever = store.as_retriever(
# search_type = "similarity",
# search_kwargs = {
# "k":3
# }
# )
# show_result("similarity k=3", similarity_retriever)
# mmr_retriever = store.as_retriever(
# search_type = "mmr",
# search_kwargs = {
# "k":3
# }
# )
# show_result("mmr k=3", mmr_retriever)
# k별 점수 표를 보고 싶으면 아래 두 줄의 주석을 풉니다.
# compare_k("환불은 며칠 이내인가요?")
# compare_k("환불과 교환은 어떻게 다른가요?")
# metadata의 source가 이 경로인 조각만 검색합니다. k는 최대 3개입니다.
manual_retriever = store.as_retriever(
search_type = "similarity",
search_kwargs = {
"k":3,
"filter" : {
"source" : "../../data/manual.pdf"
}
}
)
# 필터 값이 metadata의 source와 한 글자라도 다르면 결과가 0개입니다.
show_result("메뉴얼 필터", manual_retriever)실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
실행: cd 실습/src/11
python retriever_test.py
예시 출력 (API 키 필요, 실행하지 않음)
(get_store가 기존 인덱스를 열 때 prepare import로 아래 로딩 문장이 먼저 나옵니다.)
3쪽 로딩 완료 (총 1529 자)
3개
2 4. 환불 및 교환 규정
환불
상품 수령 후 7 일 이내에 환불을 신청하실 수 있습니다.
2 교환
상품 불량의 경우 30 일 이내에 교환이 가능합니다.
이 경우 배송비는 당사가 부담합니다.
3 7. 고객센터 안내
운영 시간은 평일 09:00 ~ 18:00 입니다.
(쪽과 본문은 manual.pdf 문장을 출력 형식에 맞춘 예시입니다.
실제 검색 순위는 질문 embedding 결과이며, 유사도 숫자는 이 실행 경로에서 출력하지 않습니다.
compare_k는 주석이라 점수 줄은 나오지 않습니다.)코드 해설
저장된 인덱스에서 source가 설명서인 조각만 retriever로 검색입니다. 입력은 10차시 FAISS 인덱스, 질문 "환불과 교환은 어떻게 다른가요?"이고, 출력은 결과 개수, 쪽 번호, 본문 앞 55자입니다.
주석에 적힌 흐름입니다.
get_store() -> FAISS
-> as_retriever(similarity, k=3, source 필터)
-> show_result()코드 앞에서 고정해 두는 말입니다.
- retriever: 질문을 받아 가까운 문서 조각을 돌려주는 검색기
- FAISS: 벡터(숫자 목록)를 저장한 인덱스. embedding은 질문을 그 숫자로 바꿀 때 씁니다.
- metadata: source, page_no처럼 조각에 붙은 부가 정보. filter가 source를 비교합니다.
함수와 클래스로 나누어 읽습니다.
compare_k: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.show_result: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.
실습 코드 · 11/retriever_test_fast.py
실습 코드: 실습/src/11/retriever_test_fast.py · 위의 원본 PDF 11차시 9쪽 「4️⃣ 실습 ① — Retriever 만들고 비교하기」과 이어집니다.
# ============================================================
# 11차시 | PPT: 11차시_Retriever기초실습.pptx
# 역할: 이미 만든 FAISS 인덱스로 설명서만 검색하고 걸린 시간을 출력
# 입력: src/11 또는 src/10의 faiss_index, 질문 1개
# 출력: 로드 시간, 검색 시간, 결과 개수, 순위·쪽·미리보기
# 흐름:
# load_store() -> retriever(source 필터) -> invoke -> print
# ============================================================
# retriever: 질문으로 가까운 문서 조각을 찾는 검색기
# FAISS: 저장해 둔 벡터 인덱스. embedding은 질문 문장을 숫자 목록으로 바꿉니다.
# metadata: source, page_no 같은 부가 정보
"""기존 인덱스를 재사용해 manual 문서만 빠르게 검색하는 예제입니다."""
from pathlib import Path
import sys
from time import perf_counter
CURRENT_DIR = Path(__file__).resolve().parent
# 같은 폴더의 빠른 인덱스 로더를 현재 작업 위치와 무관하게 가져옵니다.
sys.path.insert(0, str(CURRENT_DIR))
from retriever_fast import QUESTION, RESULT_COUNT, load_store
# 이 값은 저장된 문서의 source metadata와 정확히 일치해야 합니다.
MANUAL_SOURCE = "../../data/manual.pdf"
def main():
# 기존 FAISS 인덱스를 로드하며 PDF를 다시 분할하거나 임베딩하지 않습니다.
load_started = perf_counter()
store = load_store()
print(f"인덱스 로드: {perf_counter() - load_started:.2f}초")
# source 조건에 맞는 manual 청크 중 유사도가 높은 결과만 검색합니다.
manual_retriever = store.as_retriever(
search_type="similarity",
search_kwargs={
"k": RESULT_COUNT,
"filter": {"source": MANUAL_SOURCE},
},
)
# 질문 임베딩 요청은 한 번만 발생하므로 검색 결과도 한 번만 조회합니다.
search_started = perf_counter()
documents = manual_retriever.invoke(QUESTION)
print(f"검색 및 질문 임베딩: {perf_counter() - search_started:.2f}초")
print(f"질문: {QUESTION}\nmanual 검색 결과: {len(documents)}개")
# 페이지 번호와 본문 일부를 출력해 각 검색 결과를 확인합니다.
for rank, document in enumerate(documents, start=1):
page = document.metadata.get("page_no", "-")
preview = " ".join(document.page_content.split())[:120]
print(f"{rank}. 페이지 {page}: {preview}")
# 이 파일을 직접 실행할 때만 검색을 시작합니다.
if __name__ == "__main__":
main()실행 결과 샘플 (교안 기록. API 키가 필요한 코드는 키 없이 맞춘 예시이고, 모델의 답은 실행할 때마다 달라집니다)
===== 실행 결과 샘플 =====
실행: cd 실습/src/11
python retriever_test_fast.py
예시 출력 (API 키 필요, 실행하지 않음)
인덱스 로드: (초 단위 시간 미기록)
검색 및 질문 임베딩: (초 단위 시간 미기록)
질문: 환불과 교환은 어떻게 다른가요?
manual 검색 결과: 3개
1. 페이지 2: 4. 환불 및 교환 규정 환불 상품 수령 후 7 일 이내에 환불을 신청하실 수 있습니다. 단순 변심의 경우 왕복 배송비는 고객 부담입니다.
2. 페이지 2: 교환 상품 불량의 경우 30 일 이내에 교환이 가능합니다. 이 경우 배송비는 당사가 부담합니다.
3. 페이지 3: 7. 고객센터 안내 운영 시간은 평일 09:00 ~ 18:00 입니다. 전화 1588-0000 / 이메일 help@example.com
(3개는 RESULT_COUNT입니다. 본문은 manual.pdf 문장을 형식에 맞춘 예시이며 검색 순위가 아닙니다.
경과 시간과 유사도 점수는 기록된 값이 없습니다.)코드 해설
이미 만든 FAISS 인덱스로 설명서만 검색하고 걸린 시간을 출력입니다. 입력은 src/11 또는 src/10의 faiss_index, 질문 1개이고, 출력은 로드 시간, 검색 시간, 결과 개수, 순위·쪽·미리보기입니다.
주석에 적힌 흐름입니다.
load_store() -> retriever(source 필터) -> invoke -> print코드 앞에서 고정해 두는 말입니다.
- retriever: 질문으로 가까운 문서 조각을 찾는 검색기
- FAISS: 저장해 둔 벡터 인덱스. embedding은 질문 문장을 숫자 목록으로 바꿉니다.
- metadata: source, page_no 같은 부가 정보
함수와 클래스로 나누어 읽습니다.
main: 이름 그대로 한 가지 일만 합니다. 본문 주석을 같이 읽습니다.
파일 맨 아래 실행 결과 샘플 주석은 실행되지 않습니다. 화면 출력은 다음 절에 다시 적어 두었습니다.
4-1) 결과 비교
■ similarity k=3 (3개)
p.2 상품을 받은 후 7일 이내에는 환불을...
p.2 3. 환불 및 교환 규정
p.2 문의사항은 고객센터(1588-0000)로...
■ MMR k=3 fetch_k=10 (3개)
p.2 상품을 받은 후 7일 이내에는 환불을...
p.2 30일 이내에 무상으로 교환해드리며... ← 교환 내용!
p.1 벽면에서 30cm 이상 떨어진 평평한...
■ threshold 0.47 (2개)
p.2 상품을 받은 후 7일 이내에는 환불을...
p.2 30일 이내에 무상으로 교환해드리며...🎯 여기서 확인할 것
질문은 "환불과 교환의 차이"였습니다.
similarity → 환불 얘기만 3개 가져왔습니다
→ 교환에 대해 답할 수 없습니다!
MMR → 환불 조각 + 교환 조각을 함께 가져왔습니다
→ 두 가지를 비교해 답할 수 있습니다
이것이 MMR을 쓰는 이유입니다.