RAG2026-5일11차시 · Retriever 기초 실습7 / 18← 이전목차다음 →MetaContents

11차시. Retriever 기초 실습

4. 실습 ① — Retriever 만들고 비교하기

출처: 원본 PDF 11차시_Retriever기초실습.pdf 9~10쪽 · 쪽 그림 9쪽 10쪽

📝 src/retriever_test.py

Python
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 만들고 비교하기」과 이어집니다.

Python
# ============================================================
# 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 만들고 비교하기」과 이어집니다.

Python
# ============================================================
# 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을 쓰는 이유입니다.

← 이전 절11차시 · 3. Top-K를 몇으로 할까?다음 절 →11차시 · 5. 실습 ② — Top-K 실험