Pinecone 벡터 DB 무료 셋업과 OpenAI 임베딩 연동 실습

FAISS에서 Pinecone으로 넘어간 솔직한 이유

제가 한창 RAG 챗봇을 로컬 FAISS로 돌릴 때 문서가 5만 건 넘어가면서 인덱스 파일 사이즈가 2GB를 훌쩍 넘었어요. 매번 서버 재시작할 때마다 이 2GB짜리 파일을 통째로 메모리에 올리느라 40초를 꼬박 잡아먹었는데, 솔직히 그 40초 동안 손 놓고 있으면 미칠 지경이더라고요. 게다가 그 시간 동안은 챗봇 응답이 아예 멈춰서 사용자 경험이 완전 쓰레기였어요.

그래서 클라우드 벡터 DB를 찾다가 Pinecone 무료 플랜을 발견했는데, 인덱스 1개, 저장 용량 2GB 제한인데도 신용카드 등록도 안 하고 공짜로 쓸 수 있어서 솔직히 반신반의하면서 바로 테스트해봤습니다. OpenAI 임베딩과 연결하는 코드가 딱 30줄이라 너무 간단해 보여서 바로 도전했죠. 셋업부터 검색까지 실제로 돌려본 경험을 최대한 디테일하게 정리할게요.



1. 벡터 DB가 왜 필요한지, Pinecone 무료 플랜의 현실적 한계

사실 저는 일반 DB가 딱 맞는 값을 찾는 데는 최적화돼 있다고 생각해요. 근데 텍스트처럼 의미가 비슷한 걸 찾아야 할 땐 벡터 DB가 필수입니다. 예를 들어 “강아지 사료 추천”이라고 검색하면, “반려견 먹이 종류” 같은 유사 문서를 뽑아내는 게 벡터 검색인데, 텍스트를 1536차원 숫자 벡터(임베딩)로 바꿔서 저장하고, 질문 벡터와 가장 가까운 걸 수학적으로 찾는 방식입니다.

Pinecone 무료 플랜(Starter)의 조건은 다음과 같은데, 솔직히 저는 몇 가지 아쉬움이 컸어요.

  • 인덱스 수: 딱 1개만 만들 수 있어서 여러 프로젝트 동시 운영은 불가능
  • 저장 용량: 약 100만 벡터, 1536차원 기준으로 대략 2GB까지 지원
  • 월 비용: 0원, 신용카드 등록도 안 함
  • 리전: AWS us-east-1 고정. 서울에서 접속하면 100~200ms 정도 지연이 느껴져서 모바일 테스트할 때 답답했습니다

특히 저장 용량 2GB가 한계라 데이터가 조금만 더 늘어나도 바로 막히는데, 이건 진짜 제 프로젝트에서는 치명적이었어요. 그리고 AWS us-east-1 리전 고정이라 서울에서 쓸 때 네트워크 지연이 불가피해서, 모바일에서 메뉴가 꼬이거나 반응이 느려서 30분 넘게 디버깅하다가 결국 리전 문제라는 걸 알게 됐습니다. 짜증나는 경험이었죠.

2. Pinecone 계정 생성과 인덱스 셋업 과정

계정 만들고 API 키 발급받기

Pinecone 공식 사이트(pinecone.io)에서 구글 계정으로 가입하자마자 대시보드가 뜨는데, 저는 가입 후 30초 만에 왼쪽 메뉴 [API Keys]에서 기본 키를 복사했습니다. 그리고 프로젝트 루트에 .env 파일을 만들어 아래처럼 저장했어요.

PINECONE_API_KEY=여기에_발급받은_키
OPENAI_API_KEY=여기에_OpenAI_키

인덱스 생성 — 차원 수 맞추는 게 30분 삽질 막는 핵심

인덱스 만들 때 가장 중요한 게 차원 수(dimension)입니다. OpenAI text-embedding-3-small 모델이 1536차원 벡터를 뱉으니, 인덱스도 1536으로 꼭 맞춰야 해요. 저는 처음에 차원 개념 모르고 1536으로 인덱스 만들었는데, 나중에 3072차원 모델로 바꾸려다 Vector dimension mismatch 에러로 30분 날렸습니다. 차원이 다르면 인덱스는 무조건 새로 만들어야 하니, 이건 꼭 주의하세요.

필요한 라이브러리 설치는 이 커맨드 한 줄이면 끝납니다.

pip install pinecone openai python-dotenv

아래 코드는 인덱스를 만들거나 이미 있으면 재사용하는 코드인데, 제가 매번 서버 재시작할 때마다 인덱스 존재 여부를 꼭 체크해서 불필요한 생성 시도를 막습니다.

import os
from pinecone import Pinecone, ServerlessSpec
from dotenv import load_dotenv

load_dotenv()

pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY"))

INDEX_NAME = "my-rag-index"

# 인덱스가 없으면 새로 생성
if INDEX_NAME not in pc.list_indexes().names():
    pc.create_index(
        name=INDEX_NAME,
        dimension=1536,          # text-embedding-3-small 출력 차원 맞춤
        metric="cosine",         # 텍스트 유사도는 코사인 유사도가 적합하다고 생각함
        spec=ServerlessSpec(
            cloud="aws",
            region="us-east-1"   # 무료 플랜은 US 동부 리전만 지원
        )
    )
    print(f"인덱스 '{INDEX_NAME}' 생성 완료")
else:
    print(f"인덱스 '{INDEX_NAME}' 이미 존재")

index = pc.Index(INDEX_NAME)

3. OpenAI 임베딩 생성부터 Pinecone 저장·검색까지 코드 전격 공개

1단계: 텍스트 벡터로 변환해 Pinecone에 저장하기

문서 리스트를 임베딩으로 변환해 Pinecone에 올릴 때 저는 한 번에 100개씩 배치로 처리했는데, API 호출 횟수도 줄고 훨씬 안정적이더라고요. 아래 코드를 보시면 제가 어떻게 했는지 감 잡히실 겁니다.

from openai import OpenAI

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def get_embedding(text: str) -> list:
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding

# 저장할 문서 예시
documents = [
    {"id": "doc1", "text": "파이썬은 데이터 분석과 AI 개발에 널리 쓰이는 프로그래밍 언어다."},
    {"id": "doc2", "text": "텔레그램 봇은 파이썬으로 30분 만에 만들 수 있다."},
    {"id": "doc3", "text": "Pinecone은 클라우드 기반 벡터 데이터베이스 서비스다."},
    {"id": "doc4", "text": "OpenAI Whisper API는 음성을 텍스트로 변환해주는 STT 모델이다."},
    {"id": "doc5", "text": "GitHub Actions를 활용하면 파이썬 스크립트를 무료로 자동 실행할 수 있다."},
]

# 임베딩 생성 후 Pinecone에 배치 저장
vectors = []
for doc in documents:
    embedding = get_embedding(doc["text"])
    vectors.append({
        "id": doc["id"],
        "values": embedding,
        "metadata": {"text": doc["text"]}  # 원문 텍스트도 함께 저장
    })

index.upsert(vectors=vectors)
print(f"{len(vectors)}개 문서 저장 완료")

2단계: 질문으로 유사 문서 찾아오기

질문을 임베딩으로 바꿔서 Pinecone에 쿼리할 때 top_k로 몇 개까지 받을지 정할 수 있는데, 저는 원문 텍스트까지 꼭 받고 싶어서 include_metadata=True를 넣었어요. 안 넣으면 결과가 너무 빈약해서 답답하더군요.

def search_similar(query: str, top_k: int = 3) -> list:
    query_embedding = get_embedding(query)
    results = index.query(
        vector=query_embedding,
        top_k=top_k,
        include_metadata=True
    )
    return results["matches"]

# 테스트 검색
query = "자동화 스크립트를 서버 없이 실행하는 방법"
matches = search_similar(query)

print(f"n['{query}'] 검색 결과:")
for match in matches:
    score = round(match["score"], 4)
    text = match["metadata"]["text"]
    print(f"  유사도 {score} — {text}")

4. 제가 직접 맞은 에러 두 가지와 해결법

에러 ① dimension mismatch — 차원 불일치

이건 진짜 삽질이었는데, 인덱스를 1536차원으로 만들어놓고 나중에 임베딩 모델을 3072차원인 text-embedding-3-large로 바꾸면서 Vector dimension mismatch 오류가 떴어요. 1시간 이상 헤매다가 결국 인덱스는 차원 변경 불가라 새로 만들어야 한다는 걸 깨달았습니다. 비용 아끼려고 작은 차원 버전으로 통일하는 게 낫겠더군요.

에러 ② upsert 직후 바로 query하면 결과 0건

문서를 업서트하고 바로 검색했는데 결과가 하나도 안 나와서 뭔가 했더니 Pinecone은 업서트 후 인덱싱이 완전히 끝날 때까지 2~3초 지연이 있더라고요. 저는 time.sleep(3) 넣으니까 딱 해결됐어요. 아니면 index.describe_index_stats()로 벡터 개수가 올라갔는지 확인하는 방법도 있습니다.

5. FAISS와 Pinecone, 내 프로젝트에 딱 맞는 선택은?

두 가지를 모두 써본 뒤 정리한 기준이다. 프로젝트 규모와 목적에 따라 선택이 달라진다.

비교 항목 FAISS (로컬) Pinecone (클라우드)
비용 완전 무료 무료 플랜 제공 (100만 벡터)
데이터 규모 수만 건까지 쾌적, 그 이상은 메모리 부담 수백만 건도 안정적 처리
서버 재시작 시 인덱스 파일 전체 메모리 로딩 필요 클라우드 상시 유지, 즉시 접근
적합한 용도 로컬 프로토타입, 소규모 RAG 클라우드 배포, 중대규모 RAG 서비스

로컬에서 빠르게 프로토타입을 만들 때는 FAISS가 낫다. 설치도 없고 API 키도 필요 없다. 하지만 서비스를 클라우드에 올리거나 데이터가 수십만 건을 넘어가면 Pinecone으로 넘어가는 게 맞다. 두 가지 모두 써본 지금은 개발 초기에는 FAISS, 배포 단계에서 Pinecone으로 마이그레이션하는 흐름을 기본 패턴으로 쓰고 있다.