본문 바로가기
인공지능/[인공지능] RAG 검색 성능 최적화와 최신 Retrieval 전략

8. multihop_retrieval

by Toddler_AD 2025. 6. 7.

멀티홉 검색 실험

코드 셀 1: 환경 변수 로드

이 셀은 OpenAI API 키와 Pinecone 설정 등 외부 서비스 연결에 필요한 환경 변수를 불러오는 역할을 합니다. dotenv 패키지의 load_dotenv() 함수를 사용하여 .env 파일에 저장된 환경 변수를 로드하고, 이를 통해 OPENAI_API_KEY, OPENAI_LLM_MODEL, OPENAI_EMBEDDING_MODEL 등 다양한 설정 변수를 가져옵니다. 이러한 변수들은 OpenAI의 언어 모델 및 임베딩 모델 호출과 Pinecone 벡터 데이터베이스 연결 등에 사용됩니다. 마지막 줄의 print("환경 변수 로딩 완료")는 모든 환경 변수가 정상적으로 로드되었음을 확인하는 메시지를 출력합니다.

import os
from dotenv import load_dotenv

load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_LLM_MODEL = os.getenv("OPENAI_LLM_MODEL")  # 예: 'gpt-4o-mini'
OPENAI_EMBEDDING_MODEL = os.getenv("OPENAI_EMBEDDING_MODEL")  # 예: 'text-embedding-3-small'
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
PINECONE_INDEX_REGION = os.getenv("PINECONE_INDEX_REGION")
PINECONE_INDEX_CLOUD = os.getenv("PINECONE_INDEX_CLOUD")
PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME")  # 'ir'
PINECONE_INDEX_METRIC = os.getenv("PINECONE_INDEX_METRIC")
PINECONE_INDEX_DIMENSION = int(os.getenv("PINECONE_INDEX_DIMENSION"))

print("환경 변수 로딩 완료")

출력된 환경 변수 로딩 완료 메시지를 통해 .env 파일에 저장된 환경 변수들이 모두 성공적으로 불러와졌음을 알 수 있습니다.

환경 변수 로딩 완료

코드 셀 2: Pinecone 벡터 스토어 설정

이 코드 셀에서는 Pinecone 벡터 데이터베이스를 초기화하고, 임베딩 모델 및 벡터 스토어를 설정합니다. 먼저 Pinecone(api_key=...)를 통해 Pinecone 클라이언트를 생성하고, pc.Index(PINECONE_INDEX_NAME)로 사전에 준비된 Pinecone 인덱스에 연결합니다. 그런 다음 OpenAI의 임베딩 모델(OpenAIEmbeddings)을 API 키와 모델 이름으로 초기화하여 텍스트를 벡터로 변환할 준비를 합니다. 마지막으로 PineconeVectorStore 객체를 생성하여 Pinecone 인덱스를 기반으로 한 벡터 검색 기능을 구성합니다. 이 과정을 통해 주어진 질의에 대해 Pinecone 상에서 유사한 문서를 검색할 수 있는 환경이 갖추어집니다. 마지막의 print("Pinecone 설정 완료")는 Pinecone 및 임베딩 모델 설정이 완료되었음을 알리는 확인용 출력입니다.

from pinecone import Pinecone
from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore

# Pinecone 클라이언트 연결
pc = Pinecone(api_key=PINECONE_API_KEY)
index = pc.Index(PINECONE_INDEX_NAME)

# 임베딩 모델 생성
embedding_model = OpenAIEmbeddings(model=OPENAI_EMBEDDING_MODEL, openai_api_key=OPENAI_API_KEY)
# Dense 벡터 스토어 설정
vector_store = PineconeVectorStore(index_name=PINECONE_INDEX_NAME, embedding=embedding_model)

print("Pinecone 설정 완료")

출력의 첫 부분에 나타난 TqdmWarning: IProgress not found... 경고는 Jupyter 노트북 환경에서 진행 표시바를 표시하기 위한 모듈이 없다는 알림이며, 검색 기능 자체에는 영향을 주지 않습니다. 이어서 출력된 Pinecone 설정 완료 메시지는 Pinecone 클라이언트 연결과 벡터 스토어 구성이 정상적으로 완료되었음을 나타냅니다.

c:\Users\ssampooh\RAG-Retrieval\.conda\Lib\site-packages\tqdm\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
Pinecone 설정 완료

코드 셀 3: 멀티홉 질의 데이터 로드

이 셀에서는 실험에 사용할 멀티홉 질의 데이터를 불러옵니다. pandas 라이브러리를 이용하여 queries_multihop.csv 파일을 읽고, 이를 queries_multihop_df 데이터프레임에 저장합니다. 이 데이터프레임에는 복합 질문(멀티홉 질의)과 각 질문에 대한 정답 문서들의 ID 목록 등이 포함되어 있을 것으로 보입니다. print 함수를 통해 "멀티홉 질의 데이터 로드 완료"라는 메시지와 함께 불러온 질의의 개수를 출력하여, 데이터셋에 몇 개의 질의가 있는지 (예에서는 30개) 확인하고 있습니다.

import pandas as pd

# 멀티홉 질의 파일 queries_multihop.csv 로드
queries_multihop_df = pd.read_csv("queries_multihop.csv")

print("멀티홉 질의 데이터 로드 완료")
print(f"멀티홉 질의 수: {len(queries_multihop_df)}")

데이터 로드 완료 메시지와 함께 멀티홉 질의 수: 30이라는 출력이 나타났습니다. 이를 통해 이 실험에서 사용될 멀티홉 질의가 총 30개임을 확인할 수 있습니다.

멀티홉 질의 데이터 로드 완료
멀티홉 질의 수: 30

코드 셀 4: 1-hop 검색 함수 정의

이 셀에서는 1-hop(단일 단계) 검색을 수행하는 함수를 정의합니다. retrieve_1hop(query: str, k: int = 5) 함수는 주어진 질의에 대해 백터 스토어에서 상위 k개의 유사 문서를 검색합니다. 함수 내부에서 vector_store.similarity_search_with_score(query, k=k)를 호출하여 Pinecone 벡터 인덱스에서 해당 질의와 가장 유사한 상위 k개의 문서를 찾고, 결과로 얻은 문서 객체 리스트에서 각 문서의 메타데이터 doc_id를 추출하여 retrieved_ids 리스트를 구성합니다. 동시에 검색 시작 시각과 종료 시각을 비교하여 latency(검색 지연 시간)를 계산합니다. 이 함수는 검색된 문서 ID들의 리스트와 검색에 걸린 시간을 반환하며, 기본적으로 k=5로 설정되어 있어 상위 5개의 결과를 가져옵니다. 마지막으로 print("1-hop 검색 함수 정의 완료")로 함수 정의 완료를 알리는 메시지를 출력합니다.

import time

def retrieve_1hop(query: str, k: int = 5):
    start = time.time()
    docs_and_scores = vector_store.similarity_search_with_score(query, k=k)
    retrieved_ids = [doc.metadata['doc_id'] for doc, _ in docs_and_scores]
    latency = time.time() - start
    return retrieved_ids, latency

print("1-hop 검색 함수 정의 완료")

1-hop 검색 함수 정의 완료라는 출력이 나타나며, 이는 1-hop 검색을 수행하는 함수가 제대로 정의되었음을 알려줍니다 (이 함수는 아직 실행된 것이 아니라 정의만 된 상태입니다).

1-hop 검색 함수 정의 완료

코드 셀 5: 2-hop 후속 질의 생성 체인 설정

이 셀에서는 첫 번째 검색 결과를 바탕으로 두 번째 검색에 사용할 후속 질의(follow-up query)를 생성하는 체인을 구성합니다. PromptTemplate을 이용해 LLM에게 제공할 프롬프트를 정의하는데, 이 프롬프트에는 사용자로부터 받은 원래 복합 질문({original_query})과 1-hop 검색 결과 문서들의 내용({hop1_docs_contents})이 채워질 자리표시자가 포함되어 있습니다. 프롬프트의 내용은 주어진 정보에 기초하여 복합 질문을 해결하기 위한 2-hop용 추가 질의를 한 문장으로 작성하라는 요구사항으로 구성됩니다.

이어지는 부분에서 StrOutputParser는 LLM의 응답을 문자열로 추출하는 파서이고, ChatOpenAI는 OpenAI의 챗봇 모델(예: GPT-4)을 불러와 후속 질의를 생성하는 역할을 합니다. 여기서는 temperature=0.0으로 설정하여 응답의 일관성을 높였습니다. 마지막으로 followup_prompt | chat_model | output_parser 형태로 프롬프트, 모델, 파서를 하나의 체인으로 연결하여 followup_chain을 구성합니다. 이제 이 체인을 사용하면 원래 질문과 1-hop 결과를 입력으로 주었을 때 LLM이 자동으로 2-hop 질의를 생성해줄 것입니다. 코드 마지막의 print("Follow-up Query 생성 체인 설정 완료")는 이러한 LLM 기반 질의 생성 체인이 성공적으로 준비되었음을 알려줍니다.

from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

# Follow-up Query 생성 체인 설정 (LCEL 스타일)
followup_prompt = PromptTemplate(
    input_variables=['original_query', 'hop1_docs_contents'],
    template=(
        "사용자의 복합 질문:\n"
        "{original_query}\n\n"
        "1-hop 검색 결과 문서의 요약/본문:\n"
        "{hop1_docs_contents}\n\n"
        "위 정보를 바탕으로, 복합 질문 해결을 위한 2-hop 서브쿼리를 한 문장으로 작성하세요."
    )
)
output_parser = StrOutputParser()

chat_model = ChatOpenAI(
    model_name=OPENAI_LLM_MODEL,
    openai_api_key=OPENAI_API_KEY,
    temperature=0.0
)

followup_chain = followup_prompt | chat_model | output_parser
print("Follow-up Query 생성 체인 설정 완료")

Follow-up Query 생성 체인 설정 완료라는 출력으로, LLM을 활용한 후속 질의 생성 체인이 무사히 준비되었음을 확인할 수 있습니다.

Follow-up Query 생성 체인 설정 완료

코드 셀 6: 문서 내용 결합 함수 정의

이 셀에서는 Pinecone에 저장된 문서들의 내용을 ID로 찾아 모으는 함수를 정의합니다. get_docs_content_by_ids(doc_ids: list[str]) -> str 함수는 주어진 문서 ID 목록에 대해, 각 ID에 해당하는 문서의 본문 텍스트를 불러와 하나의 문자열로 결합합니다. 구현 세부사항으로, 함수는 vector_store.index.fetch(ids=[did])를 이용해 Pinecone 벡터 스토어에서 해당 ID의 벡터와 메타데이터를 가져옵니다. Pinecone에 저장된 벡터의 메타데이터에는 문서 원문 텍스트(text)가 포함되어 있다고 가정합니다. 각 문서에 대해 메타데이터에서 텍스트를 추출하여 contents 리스트에 추가하고, 모든 문서의 텍스트를 개행 문자(\n)로 연결하여 반환합니다. 이 함수는 1-hop 검색으로 찾은 문서들의 내용을 모아 LLM에 제공함으로써, LLM이 더 정확한 2-hop 질의를 생성할 수 있도록 돕는 역할을 합니다. 마지막 줄의 print("문서 메타에서 텍스트 결합 함수 정의 완료")는 해당 함수 정의가 완료되었음을 알리는 출력입니다.

def get_docs_content_by_ids(doc_ids: list[str]) -> str:
    contents = []
    for did in doc_ids:
        fetch_response = vector_store.index.fetch(ids=[did])
        vector_data = fetch_response.vectors.get(did)
        if vector_data and 'metadata' in vector_data:
            contents.append(vector_data['metadata']['text'])
    return "\n".join(contents)

print("문서 메타에서 텍스트 결합 함수 정의 완료")

출력 메시지 문서 메타에서 텍스트 결합 함수 정의 완료는 문서 내용을 가져와 결합하는 함수가 정상적으로 정의되었음을 알려줍니다.

문서 메타에서 텍스트 결합 함수 정의 완료

코드 셀 7: 평가 함수 정의

이 셀에서는 검색 성능 평가를 위한 함수와 보조 함수를 정의합니다. 먼저 precision_at_k(predicted: list[str], relevant: list[str], k: int = 5) 함수는 검색 결과로 예측된 문서 ID 리스트 predicted의 상위 k개 중 실제 정답 문서(relevant 리스트)에 포함된 것이 얼마나 되는지를 계산합니다. 상위 k개의 예측 결과를 확인하여 정답에 해당하는 문서 개수를 세고, 그 수를 k로 나누어 정밀도(Precision)를 산출합니다. 예를 들어 상위 5개 결과 중 2개가 정답 문서이면 P@5 = 2/5 = 0.4가 됩니다.

또 다른 함수 parse_relevant_ids(relevant_str: str)는 "DOCID=값;..." 형태로 문자열에 나열된 정답 문서 ID들을 파싱하여 ID 목록(List[str])으로 변환합니다. 데이터셋의 정답 문서 목록이 이러한 형식의 문자열로 되어 있기 때문에, 이 함수를 통해 문자열을 다루기 쉬운 리스트로 바꾸는 것입니다. 마지막의 print("평가 함수 정의 완료")로 두 함수 정의가 모두 완료되었음을 알리고 있습니다.

def precision_at_k(predicted: list[str], relevant: list[str], k: int = 5) -> float:
    hits = sum(1 for doc in predicted[:k] if doc in relevant)
    return hits / k if k > 0 else 0.0

def parse_relevant_ids(relevant_str: str) -> list[str]:
    return [pair.split('=')[0] for pair in relevant_str.split(';') if pair]

print("평가 함수 정의 완료")

평가 함수 정의 완료라는 출력이 표시되며, 이는 Precision@5 계산 함수와 정답 ID 파싱 함수가 모두 성공적으로 정의되었음을 의미합니다.

평가 함수 정의 완료

코드 셀 8: 멀티홉 검색 실험 실행

이 셀에서는 앞서 정의한 요소들을 활용하여 멀티홉(2-hop) 검색 실험을 수행합니다. queries_multihop_df에 담긴 각 질의에 대해 루프를 돌면서 다음과 같은 과정을 거칩니다:

  1. 데이터프레임에서 query_id와 원본 질문 텍스트(query_text)를 가져옵니다. 또한 해당 질문과 관련된 1-hop 정답 문서 ID들과 2-hop 정답 문서 ID들을 parse_relevant_ids 함수를 통해 리스트(gt_hop1, gt_hop2)로 변환합니다.
  2. retrieve_1hop(orig_query, k=5)를 호출하여 1-hop 검색(원본 질문으로 검색)을 수행합니다. 그 결과로 상위 5개의 검색된 문서 ID 리스트(hop1_retrieved)와 이 검색에 걸린 시간(t1)을 얻습니다.
  3. 1-hop에서 얻은 문서 ID 리스트를 get_docs_content_by_ids(hop1_retrieved) 함수에 전달하여, 해당 문서들의 본문 내용을 하나의 문자열(hop1_text)로 결합합니다.
  4. 준비된 followup_chain을 사용하여 LLM에 후속 질의 생성을 요청합니다. followup_chain.invoke({...})에 원본 질문과 hop1_text를 넣어 호출하면, LLM이 2-hop 단계에서 사용할 추가 질의문 sub_query를 생성합니다.
  5. 생성된 sub_query로 다시 retrieve_1hop(sub_query, k=5)를 수행하여 2-hop 검색을 합니다. 이로써 두 번째 단계의 상위 5개 문서 ID 리스트(hop2_retrieved)와 검색 시간(t2)를 얻습니다.
  6. 1-hop 검색 결과 리스트와 정답 문서 리스트(gt_hop1)를 비교하여 Precision@5 값을 계산하고 p1에 저장합니다. 마찬가지로 2-hop 결과와 정답(gt_hop2)을 비교하여 Precision@5 값을 계산한 후 p2에 저장합니다.
  7. 위에서 구한 모든 정보를 모아 하나의 딕셔너리로 정리하고, 이를 results 리스트에 추가합니다. 각 딕셔너리에는 질의 ID, 1-hop/2-hop 검색 시간, 원본 질문과 LLM이 생성한 서브쿼리 텍스트, 1-hop/2-hop 검색으로 얻은 문서 ID 리스트, 그리고 두 단계의 P@5 값이 포함됩니다.

모든 질의에 대해 위 절차를 마치고 나면, results 리스트를 기반으로 results_df 데이터프레임을 생성합니다. 이 데이터프레임에는 각 질의별로 1-hop 및 2-hop 검색의 결과와 성능 지표가 행(row) 하나에 정리됩니다. 코드 마지막으로 print('멀티홉 실험 완료')를 실행하여 전체 루프 완료를 알린 뒤, results_df.head()를 호출하여 결과 데이터프레임의 처음 5개 행을 출력합니다. 이를 통해 일부 질의에 대한 검색 결과를 예시로 확인할 수 있습니다.

results = []
for idx, row in queries_multihop_df.iterrows():
    qid = row['query_id']
    orig_query = row['query_text']
    gt_hop1 = parse_relevant_ids(row['hop1_relevant_ids'])
    gt_hop2 = parse_relevant_ids(row['hop2_relevant_ids'])
    hop1_retrieved, t1 = retrieve_1hop(orig_query, k=5)
    hop1_text = get_docs_content_by_ids(hop1_retrieved)
    sub_query = followup_chain.invoke({'original_query': orig_query, 'hop1_docs_contents': hop1_text})
    hop2_retrieved, t2 = retrieve_1hop(sub_query, k=5)
    p1 = precision_at_k(hop1_retrieved, gt_hop1)
    p2 = precision_at_k(hop2_retrieved, gt_hop2)
    results.append({'query_id': qid, 'hop1_time': t1, 'hop2_time': t2,
                    'original_query': orig_query, 'sub_query': sub_query,
                    'retrieved_hop1': hop1_retrieved, 'retrieved_hop2': hop2_retrieved,
                    'P@5_hop1': p1, 'P@5_hop2': p2})
results_df = pd.DataFrame(results)
print('멀티홉 실험 완료')
results_df.head()

위 출력은 결과 데이터프레임 results_df의 일부(첫 5개 질의)에 대한 내용을 보여줍니다. 각 행에는 개별 질의에 대한 1-hop 및 2-hop 검색 결과와 성능 지표가 담겨 있습니다. hop1_time과 hop2_time 열은 해당 질의의 1-hop 검색과 2-hop 검색(두 번째 단계) 각각에 소요된 시간(초)을 나타내며, 질의에 따라 수백 밀리초에서 몇 초까지 걸리는 차이를 볼 수 있습니다. original_query에는 원본 질문 텍스트가, sub_query에는 1-hop 결과를 바탕으로 LLM이 생성한 후속 질의가 들어 있습니다 (긴 문장은 중간에 ...으로 생략되어 표시됩니다). retrieved_hop1와 retrieved_hop2 열은 각각 1-hop과 2-hop 단계에서 검색된 상위 5개 문서의 ID 목록을 보여줍니다.

가장 오른쪽의 P@5_hop1와 P@5_hop2 열은 Precision@5 점수를 의미하며, 상위 5개의 결과 중 실제 정답 문서가 차지하는 비율을 나타냅니다. 값이 0.0이면 정답 문서를 하나도 찾지 못한 것이고, 0.2이면 5개 중 1개를 맞혔음을 의미합니다 (20% 정확도). 결과를 살펴보면, 예시로 출력된 첫 5개 질의 중 Q01, Q02, Q04, Q05의 경우 1-hop과 2-hop 모두 P@5 값이 0.0으로 상위 5개 결과 내에 정답이 없었습니다. 반면 Q03 질의는 1-hop에서는 P@5가 0.0이었으나, 2-hop 단계에서는 P@5가 0.2로 향상되어 상위 5개 결과 중 1개 문서를 맞힌 것을 볼 수 있습니다. 이처럼 LLM을 통한 후속 질의가 일부 케이스에서는 검색 성능을 높여주지만, 대부분의 질의에서는 여전히 정답을 상위 5위 내에 찾지 못했음을 알 수 있습니다.

멀티홉 실험 완료
  query_id  hop1_time  hop2_time                      original_query  \
0      Q01   3.558921   0.578442  저자 김민수이 쓴 문서의 카테고리와 동일한 다른 문서를 찾아줘   
1      Q02   0.534903   0.484699  저자 이영희이 쓴 문서의 카테고리와 동일한 다른 문서를 찾아줘   
2      Q03   0.541996   1.813387  저자 박지훈이 쓴 문서의 카테고리와 동일한 다른 문서를 찾아줘   
3      Q04   0.517297   0.827806  저자 최수정이 쓴 문서의 카테고리와 동일한 다른 문서를 찾아줘   
4      Q05   0.617960   0.523706  저자 정우성이 쓴 문서의 카테고리와 동일한 다른 문서를 찾아줘   

                                           sub_query  \
0       "김민수가 쓴 문서와 동일한 카테고리에 속하는 다른 저자의 문서를 찾아주세요."   
1      "이영희 저자의 문서와 동일한 카테고리에 속하는 다른 저자의 문서를 찾아주세요."   
2  "박지훈이 쓴 문서의 카테고리를 기반으로 해당 카테고리에 속하는 다른 저자의 문서를...   
3  "최수정이 쓴 문서의 카테고리를 기반으로 해당 카테고리에 속하는 다른 저자의 문서를...   
4         "정우성이 쓴 문서의 카테고리와 동일한 다른 문서의 제목과 요약을 찾아줘."   

              retrieved_hop1             retrieved_hop2  P@5_hop1  P@5_hop2  
0  [D29, D28, D17, D19, D30]  [D29, D28, D30, D17, D19]       0.0       0.0  
1   [D17, D29, D30, D28, D4]  [D17, D30, D29, D10, D28]       0.0       0.0  
2  [D30, D29, D28, D18, D17]  [D30, D29, D17, D28, D18]       0.0       0.2  
3  [D29, D28, D30, D20, D17]  [D29, D30, D28, D17, D19]       0.0       0.0  
4  [D29, D28, D17, D30, D18]  [D29, D28, D17, D30, D18]       0.0       0.0  

코드 셀 9: 검색 성능 요약 계산

이 셀에서는 전체 질의에 대한 1-hop과 2-hop 검색의 평균적인 성능을 요약하여 비교합니다. 우선 results_df로부터 1-hop 검색 시간(hop1_time 열)과 2-hop 검색 시간(hop2_time 열)의 평균값을 계산하여 avg_hop1_time과 avg_hop2_time에 저장합니다. 마찬가지로 Precision@5 성능 지표인 P@5_hop1과 P@5_hop2 열의 평균을 구해 avg_p1과 avg_p2에 저장합니다. 그런 다음 이러한 평균 값들을 이용해 요약용 데이터프레임 summary를 생성합니다. summary 데이터프레임은 '단계' 열에 '1-hop'과 '2-hop'을 두 행으로 갖고, 각 단계에 대해 '평균 지연(초)'(검색 소요 시간 평균)와 '평균 P@5'(Precision@5 평균)를 표시합니다. 마지막 줄에서 이 summary 데이터프레임을 출력하여, 단일 단계 검색과 두 단계 검색의 성능을 한눈에 비교할 수 있도록 합니다.

avg_hop1_time = results_df['hop1_time'].mean()
avg_hop2_time = results_df['hop2_time'].mean()
avg_p1 = results_df['P@5_hop1'].mean()
avg_p2 = results_df['P@5_hop2'].mean()

summary = pd.DataFrame({'단계': ['1-hop', '2-hop'],
                        '평균 지연(초)': [avg_hop1_time, avg_hop2_time],
                        '평균 P@5': [avg_p1, avg_p2]})
summary

요약 결과 표를 통해 전체 질의 집합에 대한 1-hop vs 2-hop 검색의 평균 성능을 비교할 수 있습니다. 1-hop의 '평균 지연(초)'은 약 1.166초로 나타났는데, 이는 한 번의 벡터 검색에 걸리는 평균 시간입니다. 2-hop의 '평균 지연(초)'은 약 0.622초로 더 짧게 나왔지만, 이 값은 2-hop 과정 중 두 번째 벡터 검색만을 측정한 것이며 LLM을 통한 질의 생성 시간은 포함되지 않았을 수 있습니다.

'평균 P@5' 항목을 보면 1-hop 검색의 평균 Precision@5는 약 0.2267 (22.67%)로, 다섯 개의 검색 결과 중 평균적으로 한 개 이상은 정답을 포함하고 있음을 의미합니다. 반면 2-hop 검색의 평균 P@5는 0.18 (18%)로 1-hop보다 오히려 약간 낮아졌습니다. 이는 전체적으로 볼 때, 본 실험에서 멀티홉 방식이 단일 홉 검색 대비 상위 5개 결과 내 정답 포함율을 향상시키지 못했다는 것을 보여줍니다. 요약하면, 멀티홉 검색은 일부 개별 질의에선 효과가 있었지만, 전반적인 평균 성능은 1-hop보다 떨어지는 결과를 보였습니다.

      단계  평균 지연(초)    평균 P@5
0  1-hop   1.166084  0.226667
1  2-hop   0.621878  0.180000