본문 바로가기
AI System/Agentic RAG: Agent 시스템을 위한 Vector RAG 설계

d01 - 3. Pinecone

by Toddler_AD 2026. 6. 15.

S2 · 벡터 검색과 Pinecone 기초

Agentic RAG 실전 (14시간) · Day 1

S1에서 RAG의 첫 단계가 '의미가 가까운 문서 검색'이라 했다. 이 교시는 그 검색을 가능하게 하는 원리(임베딩·유사도)와 도구(Pinecone)를 다룬다.

목표

  • 임베딩과 벡터 유사도, 의미 기반 검색이 되는 이유를 이해한다.
  • 벡터 데이터베이스가 필요한 이유를 안다.
  • Pinecone Serverless 인덱스를 만들고 벡터를 upsert/query 한다.
 

1. 임베딩과 유사도

임베딩(embedding) 은 텍스트를 의미를 담은 고차원 벡터로 변환한 것이다. 의미가 가까운 텍스트는 벡터 공간에서 가깝게 위치한다. 벡터를 이루는 숫자의 개수가 차원(dimension) 이며, 임베딩 모델이 결정한다.

  • multilingual-e5-large(한국어) → 1024차원
  • OpenAI text-embedding-3-small → 1536차원

가까움은 보통 코사인 유사도로 잰다(방향이 같을수록 1에 가깝고 크기는 무시). 검색은 질문을 임베딩한 뒤 저장된 벡터 중 코사인 유사도가 높은 것을 꺼내는 방식이다.

차원 일치 원칙: 임베딩 차원과 인덱스 dimension 이 같아야 한다. 모델을 바꾸면 인덱스도 그 차원으로 다시 만든다.

 

2. 벡터 데이터베이스와 Pinecone

문서가 수만~수백만 조각이면 매 질의마다 전수 비교는 느리다. 벡터 데이터베이스는 벡터를 미리 색인해 가장 가까운 것들을 빠르게(근사) 찾는다. 이 과정에서는 Pinecone을 쓴다.

  • 인덱스(index): 벡터가 저장되는 단위(테이블에 해당).
  • Serverless: 용량 사전 할당 없이 사용량 기반 과금.
  • 네임스페이스(namespace): 한 인덱스 내부의 논리적 칸막이(테넌트·실험 분리).
  • 메타데이터(metadata): 벡터에 함께 저장하는 부가 정보(원문·출처). 검색 후 표시나 필터에 사용.
 

3. 실행 준비

S0에서 구성한 프로젝트의 .venv 커널로 실행한다. 추가 설치는 없다. 아래 셀로 .env 의 키만 로드한다.

import os
from getpass import getpass

# S0 프로젝트 루트의 .env 에서 키 로드 (없으면 입력으로 폴백)
try:
    from dotenv import load_dotenv
    load_dotenv()
except ImportError:
    pass
for name in ["OPENAI_API_KEY", "PINECONE_API_KEY"]:
    if not os.environ.get(name):
        os.environ[name] = getpass(f"{name}: ")
print("키:", {k: bool(os.environ.get(k)) for k in ["OPENAI_API_KEY", "PINECONE_API_KEY"]})
키: {'OPENAI_API_KEY': True, 'PINECONE_API_KEY': True}
 

4. Pinecone 연결

Pinecone 객체가 모든 작업의 진입점이다. API 키로 생성한 뒤 기존 인덱스 목록을 조회한다.

import os                                       # 환경변수에서 키를 읽기 위해
from pinecone import Pinecone, ServerlessSpec     # Pinecone 클라이언트와 Serverless 설정 클래스

pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])   # API 키로 클라이언트 생성
print("기존 인덱스:", [ix["name"] for ix in pc.list_indexes()])  # 계정에 있는 인덱스 이름 목록
기존 인덱스: []
 

5. Serverless 인덱스 생성

차원은 임베딩 모델에 맞춘다(multilingual-e5-large → 1024). metric 은 cosine, spec 은 서버리스(AWS 리전)로 둔다. 차원 불일치는 색인 실패로 이어지므로 모델 변경 시 인덱스를 재생성한다.

INDEX_NAME = "agentic-rag-lab"     # 이 과정 전체에서 재사용할 인덱스 이름
EMBED_DIM  = 1024                  # multilingual-e5-large 의 임베딩 차원

existing = [ix["name"] for ix in pc.list_indexes()]   # 현재 인덱스 목록을 다시 조회
if INDEX_NAME not in existing:                         # 같은 이름이 없을 때만 생성
    pc.create_index(
        name=INDEX_NAME,                               # 인덱스 이름
        dimension=EMBED_DIM,                           # 벡터 차원 (임베딩과 반드시 일치)
        metric="cosine",                               # 유사도 측정 방식: 코사인
        spec=ServerlessSpec(cloud="aws", region="us-east-1"),  # 서버리스: 클라우드/리전 지정
    )
    print(f"생성 완료: {INDEX_NAME} (dim={EMBED_DIM})")
else:
    print(f"이미 존재: {INDEX_NAME}")

index = pc.Index(INDEX_NAME)        # 인덱스 핸들 획득 (이후 upsert/query에 사용)
print(index.describe_index_stats()) # 벡터 수·차원 등 현재 상태 확인
생성 완료: agentic-rag-lab (dim=1024)
{'dimension': 1024,
 'index_fullness': 0.0,
 'metric': 'cosine',
 'namespaces': {},
 'total_vector_count': 0,
 'vector_type': 'dense'}
 

6. 저수준 API — 직접 임베딩·upsert

LangChain 추상화(S3) 이전에 DB에 무엇이 저장되는지 직접 확인한다. Pinecone 호스팅 임베딩으로 문장을 1024차원 벡터로 만들어, id·원문(metadata)과 함께 demo 네임스페이스에 upsert 한다.

  • input_type: 저장 문서는 passage, 검색 질의는 query(e5 계열에서 품질에 영향).
# Pinecone 호스팅 임베딩으로 텍스트 → 벡터 변환 (외부 임베딩 서버 불필요)
sentences = [
    "연차 휴가는 15일입니다.",
    "재택근무는 주 3일까지 가능합니다.",
    "점심 메뉴로 김치찌개를 먹었습니다.",
]

# inference.embed: 문장 리스트를 임베딩 벡터 리스트로 변환
emb = pc.inference.embed(
    model="multilingual-e5-large",            # 한국어 지원 임베딩 모델 (1024차원)
    inputs=sentences,                          # 임베딩할 입력 문장들
    parameters={"input_type": "passage"},      # 'passage'=저장용 문서, 'query'=검색용 질의
)
vectors = [
    {
        "id": f"s{i}",                         # 각 벡터의 고유 ID
        "values": emb[i].values,               # 1024개 실수로 이루어진 임베딩 값
        "metadata": {"text": sentences[i]},    # 원문을 메타데이터로 함께 저장 (검색 후 사람이 읽기 위함)
    }
    for i in range(len(sentences))
]

index.upsert(vectors=vectors, namespace="demo")   # 'demo' 네임스페이스에 벡터 저장(upsert=삽입/갱신)
print("upsert 완료:", len(vectors), "개")
upsert 완료: 3 개
 

7. 검색(query)

질문도 같은 모델로 임베딩(query)한 뒤 가장 가까운 top_k 개를 받는다. include_metadata=True 로 원문도 함께 받는다.

question = "휴가가 며칠인지 궁금해요"

# 질의는 input_type='query' 로 임베딩 (저장용 passage와 구분)
q_emb = pc.inference.embed(
    model="multilingual-e5-large",
    inputs=[question],
    parameters={"input_type": "query"},
)

res = index.query(
    namespace="demo",                 # 검색할 네임스페이스
    vector=q_emb[0].values,           # 질의 임베딩 벡터
    top_k=3,                          # 가장 가까운 3개를 반환
    include_metadata=True,            # 저장해 둔 원문(metadata)도 함께 받기
)

for match in res["matches"]:          # 결과를 점수 높은 순으로 순회
    print(round(match["score"], 4), "|", match["metadata"]["text"])  # 유사도 점수 + 원문
0.8324 | 연차 휴가는 15일입니다.
0.8185 | 재택근무는 주 3일까지 가능합니다.
0.7654 | 점심 메뉴로 김치찌개를 먹었습니다.
 

🔍 관찰 포인트

  • "연차 휴가는 15일" 이 "휴가가 며칠" 질의에 상위로 잡힌다 — 의미 기반 검색의 특성.
  • "점심 메뉴" 문장은 점수가 낮다.
  • passage/query 구분이 검색 품질에 영향을 준다.
 

✏️ 미니 실습

  1. top_k 를 1·5로 바꿔 결과 변화를 본다.
  2. 새 네임스페이스에 다른 문장을 넣고 demo 와 분리되는지 확인한다.
  3. 질의를 영어로 바꿔도 한국어 문장이 검색되는지(다국어 임베딩) 확인한다.
# 미니 실습 공통 준비
# 아래 함수는 세 실습에서 공통으로 사용합니다.
# 검색 결과를 항상 점수(score) 높은 순으로 정렬해 보여 주기 위한 출력 함수입니다.

def print_matches(title, matches):
    # 제목을 먼저 출력해 어떤 실험 결과인지 구분하기 쉽게 만듭니다.
    print(f"\n{title}")

    # 반환 결과를 score 기준 내림차순으로 다시 정렬해 보여 줍니다.
    sorted_matches = sorted(matches, key=lambda item: item["score"], reverse=True)

    # 정렬된 결과를 하나씩 출력합니다.
    for match in sorted_matches:
        # score 는 유사도 점수이고, metadata['text'] 는 저장해 둔 원문입니다.
        print(round(match["score"], 4), "|", match["metadata"]["text"])
# 미니 실습 1. top_k 값 비교
# 같은 질의 벡터를 사용하되, 반환 개수(top_k)만 바꿔서 검색합니다.
for top_k in [1, 5]:
    res = index.query(
        namespace="demo",              # 기존에 저장한 demo 네임스페이스에서 검색합니다.
        vector=q_emb[0].values,        # 바로 위 셀에서 만든 질의 임베딩 벡터를 재사용합니다.
        top_k=top_k,                   # 이번 반복에서 사용할 반환 개수입니다.
        include_metadata=True,         # 사람이 읽을 수 있도록 원문 메타데이터를 함께 받습니다.
    )

    # top_k=5 로 요청해도 현재 demo 안에 문장이 3개뿐이면 3개만 반환됩니다.
    print_matches(f"[top_k={top_k}] 검색 결과", res["matches"])
[top_k=1] 검색 결과
0.8324 | 연차 휴가는 15일입니다.

[top_k=5] 검색 결과
0.8324 | 연차 휴가는 15일입니다.
0.8185 | 재택근무는 주 3일까지 가능합니다.
0.7654 | 점심 메뉴로 김치찌개를 먹었습니다.
 

실습 1 해설

  • top_k 는 "몇 개까지 보여 줄지"를 정하는 값입니다.
  • top_k=1 에서는 가장 관련도 높은 문장 1개만 보입니다.
  • top_k=5 로 늘려도 현재 demo 네임스페이스 안에는 문장이 3개뿐이므로 최대 3개까지만 반환됩니다.
  • 즉, top_k 는 "원하는 최대 개수"이지 "항상 정확히 그 개수를 보장하는 값"은 아닙니다.
# 미니 실습 2. 네임스페이스 분리 확인
# 기존 demo 와 구분할 새 네임스페이스 이름입니다.
alt_namespace = "demo-alt"

# demo 와 다른 주제의 문장을 새 네임스페이스에 저장해 분리 여부를 확인합니다.
alt_sentences = [
    "여름 세일은 다음 주 월요일부터 시작됩니다.",
    "신규 회원은 첫 주문에 10퍼센트 할인을 받을 수 있습니다.",
    "배송은 보통 이틀 안에 완료됩니다.",
]

# 새 문장도 저장용 문서이므로 input_type='passage' 로 임베딩합니다.
alt_emb = pc.inference.embed(
    model="multilingual-e5-large",
    inputs=alt_sentences,
    parameters={"input_type": "passage"},
)

# Pinecone upsert 형식에 맞춰 id, values, metadata 를 묶습니다.
alt_vectors = [
    {
        "id": f"alt-{i}",                 # demo 쪽 id 와 겹치지 않도록 접두어를 붙입니다.
        "values": alt_emb[i].values,      # 새 문장의 임베딩 벡터입니다.
        "metadata": {"text": alt_sentences[i]},  # 사람이 읽을 원문도 함께 저장합니다.
    }
    for i in range(len(alt_sentences))
]

# 새 벡터들을 demo-alt 네임스페이스에 저장합니다.
index.upsert(vectors=alt_vectors, namespace=alt_namespace)

# 네임스페이스 분리를 더 분명히 보기 위해 alt 문장과 관련 있는 새 질의를 만듭니다.
alt_question = "할인 혜택이 어떻게 되나요?"

# 이 질의는 검색용이므로 input_type='query' 로 임베딩합니다.
alt_q_emb = pc.inference.embed(
    model="multilingual-e5-large",
    inputs=[alt_question],
    parameters={"input_type": "query"},
)

# 같은 질의를 demo 와 demo-alt 에 각각 보내면, 각 네임스페이스 안의 데이터만 검색됩니다.
for namespace in ["demo", alt_namespace]:
    res = index.query(
        namespace=namespace,
        vector=alt_q_emb[0].values,
        top_k=3,
        include_metadata=True,
    )

    print_matches(f"[namespace={namespace}] '{alt_question}' 검색 결과", res["matches"])

# 필요하면 실습 후 아래 코드를 실행해 demo-alt 네임스페이스만 정리할 수 있습니다.
# index.delete(delete_all=True, namespace=alt_namespace)
 
[namespace=demo] '할인 혜택이 어떻게 되나요?' 검색 결과
0.7573 | 재택근무는 주 3일까지 가능합니다.
0.7424 | 연차 휴가는 15일입니다.
0.7277 | 점심 메뉴로 김치찌개를 먹었습니다.

[namespace=demo-alt] '할인 혜택이 어떻게 되나요?' 검색 결과
0.7985 | 신규 회원은 첫 주문에 10퍼센트 할인을 받을 수 있습니다.
0.7703 | 여름 세일은 다음 주 월요일부터 시작됩니다.
0.7555 | 배송은 보통 이틀 안에 완료됩니다.
 

실습 2 해설

  • 네임스페이스(namespace)는 한 인덱스 안에서 데이터를 논리적으로 분리하는 칸막이 역할을 합니다.
  • 같은 인덱스를 쓰더라도 demo  demo-alt 는 서로 다른 검색 공간처럼 동작합니다.
  • 그래서 할인 관련 질의를 보내면 demo 에서는 원래 문장들만, demo-alt 에서는 새로 넣은 할인/세일 문장들만 검색됩니다.
  • 실제 서비스에서는 사용자별 데이터 분리, 실험용 데이터 분리, 버전별 데이터 분리에 이 방식을 많이 사용합니다.
# 미니 실습 3. 영어 질의로 한국어 문장 검색
english_question = "How many vacation days do employees get?"

# 영어 질의도 같은 다국어(multilingual) 임베딩 모델로 query 형태로 변환합니다.
english_q_emb = pc.inference.embed(
    model="multilingual-e5-large",
    inputs=[english_question],
    parameters={"input_type": "query"},
)

# 검색 대상은 다시 원래의 demo 네임스페이스로 지정합니다.
english_res = index.query(
    namespace="demo",
    vector=english_q_emb[0].values,
    top_k=3,
    include_metadata=True,
)

# 한국어 문장이 상위에 나온다면 다국어 임베딩이 작동하는 좋은 예시입니다.
print_matches(f"[영어 질의] '{english_question}' 검색 결과", english_res["matches"])
[영어 질의] 'How many vacation days do employees get?' 검색 결과
0.7999 | 연차 휴가는 15일입니다.
0.7684 | 재택근무는 주 3일까지 가능합니다.
0.6975 | 점심 메뉴로 김치찌개를 먹었습니다.
 

실습 3 해설

  • multilingual-e5-large 는 다국어 임베딩 모델이므로 영어 질의와 한국어 문장을 같은 의미 공간에서 비교할 수 있습니다.
  • 그래서 질문은 영어여도, 의미가 가장 가까운 한국어 문장이 상위에 검색될 수 있습니다.
  • 이것이 다국어 검색의 핵심이며, 문서 언어와 사용자 질문 언어가 다를 때 특히 유용합니다.
  • 다만 실제 서비스에서는 언어 혼합 데이터셋의 품질을 따로 점검해야 합니다.
# 실습 정리용 셀: demo-alt 네임스페이스 삭제
# 실습 2에서 만든 demo-alt 데이터만 지우고, 원래 demo 데이터는 그대로 남겨 둡니다.

index.delete(delete_all=True, namespace="demo-alt")
print("정리 완료: demo-alt 네임스페이스의 벡터를 삭제했습니다.")
정리 완료: demo-alt 네임스페이스의 벡터를 삭제했습니다.
 

정리

  • 임베딩 → 유사도 → 벡터 DB 검색의 원리를 저수준 API로 확인했다.
  • 차원 일치·네임스페이스·메타데이터·input_type 의 역할을 짚었다.
  • 다음(S3): 이 과정을 LangChain으로 추상화하고, 긴 문서를 청킹해 색인하며 청크 크기 영향을 실험한다.

데모 벡터 삭제: index.delete(delete_all=True, namespace="demo")

'AI System > Agentic RAG: Agent 시스템을 위한 Vector RAG 설계' 카테고리의 다른 글

d01 - 6. AgenticRAG 실습  (0) 2026.06.15
d01 - 5. LangGraph  (0) 2026.06.15
d01 - 4. RAG 파이프라인  (0) 2026.06.15
d01 - 2. AgenticRAG  (0) 2026.06.15
d01 - 1. 개발환경 구축  (0) 2026.06.15