Pinecone Vector Database — Complete Tutorial for LLM Apps in 2025

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

Pinecone is the most widely adopted managed vector database, powering retrieval layers in thousands of production AI applications. Its serverless architecture means you can go from zero to a scalable vector search API in minutes — no infrastructure management, no capacity planning, no index tuning required.

For teams building RAG systems, semantic search, recommendation engines, or image similarity search, Pinecone removes the operational burden so you can focus on application logic. With a generous free tier supporting up to 100K vectors and serverless pay-per-query pricing, it is the default choice for startups and the preferred managed option for enterprise teams that do not want to run their own vector infrastructure.

Understanding Pinecone deeply — indexes, namespaces, metadata filters, hybrid search, and the difference between serverless and pod-based deployments — is essential for building cost-effective, high-performance retrieval systems.

Account Setup and Installation

  1. Create a free account at pinecone.io
  2. Generate an API key from the console
  3. Install the Python client:
pip install pinecone openai python-dotenv
import os
from pinecone import Pinecone
 
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
print(pc.list_indexes().names())  # verify connection

Creating Indexes

Pinecone offers two deployment types:

Serverless (recommended for most use cases — pay per query, auto-scale, no pods):

from pinecone import Pinecone, ServerlessSpec
 
pc = Pinecone(api_key="your-key")
 
pc.create_index(
    name="rag-knowledge-base",
    dimension=1536,           # must match your embedding model dimension
    metric="cosine",          # cosine | euclidean | dotproduct
    spec=ServerlessSpec(
        cloud="aws",
        region="us-east-1"
    )
)
 
index = pc.Index("rag-knowledge-base")
print(index.describe_index_stats())

Embedding model dimensions:

  • text-embedding-3-small: 1536 dimensions
  • text-embedding-3-large: 3072 dimensions
  • text-embedding-ada-002: 1536 dimensions
  • BAAI/bge-large-en-v1.5: 1024 dimensions

Upserting Vectors

Upsert (insert or update) vectors with IDs and metadata:

from openai import OpenAI
from pinecone import Pinecone
 
client = OpenAI()
pc = Pinecone(api_key="your-key")
index = pc.Index("rag-knowledge-base")
 
def get_embeddings(texts: list[str]) -> list[list[float]]:
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=texts
    )
    return [item.embedding for item in response.data]
 
# Prepare documents with metadata
documents = [
    {
        "id": "doc-001",
        "text": "Our refund policy allows returns within 30 days of purchase.",
        "metadata": {"source": "handbook", "section": "returns", "page": 12, "year": 2025}
    },
    {
        "id": "doc-002",
        "text": "Enterprise customers receive a dedicated account manager.",
        "metadata": {"source": "contracts", "section": "enterprise", "tier": "enterprise"}
    },
    {
        "id": "doc-003",
        "text": "API rate limits are 1000 requests per minute on the Pro plan.",
        "metadata": {"source": "api-docs", "section": "limits", "plan": "pro"}
    },
]
 
# Batch upsert (more efficient than one-by-one)
texts = [doc["text"] for doc in documents]
embeddings = get_embeddings(texts)
 
vectors = [
    (doc["id"], emb, doc["metadata"])
    for doc, emb in zip(documents, embeddings)
]
 
index.upsert(vectors=vectors, batch_size=100)

Querying with Metadata Filters

def embed_query(query: str) -> list[float]:
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=query
    )
    return response.data[0].embedding
 
# Basic semantic search
query_vector = embed_query("what is the return window?")
 
results = index.query(
    vector=query_vector,
    top_k=3,
    include_metadata=True
)
 
# With metadata filtering — pre-filters before ANN search
results_filtered = index.query(
    vector=query_vector,
    top_k=3,
    include_metadata=True,
    filter={
        "source": {"$eq": "handbook"},
        "year": {"$gte": 2024}
    }
)
 
for match in results_filtered["matches"]:
    print(f"Score: {match['score']:.4f}")
    print(f"ID: {match['id']}")
    print(f"Metadata: {match['metadata']}")
    print()

Supported filter operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin

Namespaces for Multi-Tenancy

Namespaces provide logical separation within a single index — perfect for multi-tenant SaaS apps:

# Upsert to a specific namespace
index.upsert(
    vectors=[("doc-1", embedding, {"content": "..."})],
    namespace="tenant-acme-corp"
)
 
index.upsert(
    vectors=[("doc-1", embedding, {"content": "..."})],
    namespace="tenant-globex-inc"
)
 
# Query only within a namespace — tenants never see each other's data
results = index.query(
    vector=query_vector,
    top_k=5,
    namespace="tenant-acme-corp",
    include_metadata=True
)
 
# Delete all vectors for a specific tenant
index.delete(delete_all=True, namespace="tenant-acme-corp")

Full RAG Pipeline with Pinecone

from openai import OpenAI
from pinecone import Pinecone
import textwrap
 
openai_client = OpenAI()
pc = Pinecone(api_key="your-pinecone-key")
index = pc.Index("rag-knowledge-base")
 
def embed(text: str) -> list[float]:
    return openai_client.embeddings.create(
        model="text-embedding-3-small", input=text
    ).data[0].embedding
 
def retrieve(query: str, top_k: int = 4, filter: dict = None) -> list[dict]:
    results = index.query(
        vector=embed(query),
        top_k=top_k,
        include_metadata=True,
        filter=filter
    )
    return results["matches"]
 
def generate_answer(query: str, matches: list[dict]) -> str:
    context_parts = []
    for i, m in enumerate(matches):
        text = m["metadata"].get("text", "")
        source = m["metadata"].get("source", "unknown")
        context_parts.append(f"[{i+1}] (source: {source})\n{text}")
 
    context = "\n\n".join(context_parts)
 
    response = openai_client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "Answer using only the provided context. Cite sources with [1], [2], etc."
            },
            {
                "role": "user",
                "content": f"Context:\n{context}\n\nQuestion: {query}"
            }
        ]
    )
    return response.choices[0].message.content
 
# End-to-end query
question = "What is our refund policy?"
matches = retrieve(question, top_k=4, filter={"source": {"$eq": "handbook"}})
answer = generate_answer(question, matches)
print(answer)

Managing Indexes

# Update metadata on existing vectors (upsert overwrites)
index.update(id="doc-001", set_metadata={"reviewed": True, "updated_at": "2025-03-01"})
 
# Delete specific vectors
index.delete(ids=["doc-001", "doc-002"])
 
# Delete by metadata filter
index.delete(filter={"source": {"$eq": "outdated-docs"}})
 
# Index statistics
stats = index.describe_index_stats()
print(f"Total vectors: {stats['total_vector_count']}")
print(f"Namespaces: {stats['namespaces']}")
 
# List all indexes
print(pc.list_indexes().names())
 
# Delete an index
pc.delete_index("old-index-name")

Common Mistakes / Pitfalls

  • Creating a new index every deployment — create once, reuse; deletion is destructive
  • Not batching upserts — sending one vector at a time is slow and hits rate limits
  • Using the wrong metric — use cosine for normalized embeddings (text models), dotproduct for unnormalized
  • Storing full document text in metadata — Pinecone metadata has a 40KB limit; store chunk text separately if needed
  • Not using namespaces for multi-tenant apps — sharing an index without namespaces means your tenants can see each other's data if filters fail

Best Practices

  • Batch upserts in groups of 100 vectors for optimal throughput
  • Use serverless for development and low-throughput production; pod-based for high-throughput predictable latency
  • Always include the original text in metadata ({"text": chunk_text}) to avoid a separate document lookup
  • Monitor the describe_index_stats() response to track vector count and namespace sizes
  • Implement a re-indexing pipeline for when you change embedding models — you must re-embed everything

Key Takeaways

  • Pinecone is a fully managed serverless vector database — no infrastructure to operate, auto-scaling, pay-per-query
  • Serverless indexes are the default choice for most teams; pod-based indexes are for high-throughput predictable workloads
  • Vector dimension must exactly match the embedding model used — change models means re-creating the index
  • Metadata filters ($eq, $in, $gte, etc.) pre-filter before ANN search, improving both relevance and speed
  • Namespaces provide tenant isolation within a single index — essential for multi-tenant SaaS applications
  • Batch upserts in groups of 100 for optimal throughput — single vector upserts are rate-limited
  • The free tier supports 100K vectors and 5 serverless indexes — sufficient for prototyping and small production apps
  • Store chunk text in metadata to enable single-call retrieve-and-read without additional database lookups

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading