Module 7: Production with Pinecone — the migration from "working demo" to "24/7 service"
Capsule 03: Setting up serverless Pinecone — your first production-ready index in 30 minutes
Capsule description
You decided to migrate (capsule 02). Now comes the first concrete step: creating your Pinecone index. It sounds simple — and it is — but there are three decisions you make when creating the index that you can NOT change later without recreating everything: dimensions, distance metric, and region/cloud. This capsule teaches you to make those decisions right the first time.
You'll configure a serverless index (no thinking about infrastructure), validate that the dimensions match your embedding model, and leave everything ready with idempotent scripts that any dev on the team can re-run without breaking state.
By the end of this capsule you'll be able to:
- ✅ Create a serverless Pinecone index with the right dimension and metric
- ✅ Configure API keys and environment variables properly
- ✅ Implement an idempotent setup that can run in any environment (dev, staging, prod)
- ✅ Validate that the index's dimension matches the embedding model before indexing
- ✅ Decide between serverless and pod-based for your case
- ✅ Anticipate migration mistake #1: incompatible dimensions between the index and the embeddings
Estimated time: 30-35 minutes
The three immovable decisions when creating an index
When you create a Pinecone index, three parameters are fixed forever. Changing them requires recreating the index and re-indexing everything:
1. Dimension
pc.create_index(
name="my-rag",
dimension=1536, # ← it gets fixed here
...
)
How to choose: it must match EXACTLY your embedding model's dimension.
| Model | Dimension |
|---|---|
| OpenAI text-embedding-3-small | 1536 |
| OpenAI text-embedding-3-large | 3072 |
| OpenAI text-embedding-ada-002 (legacy) | 1536 |
| Cohere embed-english-v3 | 1024 |
| Cohere embed-multilingual-v3 | 1024 |
| Sentence Transformers all-MiniLM-L6-v2 | 384 |
| Voyage voyage-2 | 1024 |
A common trap: creating an index with dim=1536 (assuming OpenAI), then later deciding to migrate to text-embedding-3-large (dim=3072). The index won't accept the new embeddings — you have to recreate it.
2. Distance metric
pc.create_index(
name="my-rag",
metric="cosine", # ← it gets fixed here
...
)
The options:
cosine— the default for text embeddings from OpenAI, Cohere, Sentence Transformerseuclidean(L2) — some specific cases (image embeddings, recommendations)dotproduct— when the embeddings are already normalized
The recommendation: for 95% of RAG cases with text embeddings, cosine. If your embedding model recommends another metric, use that one.
3. Region and cloud
pc.create_index(
name="my-rag",
spec=ServerlessSpec(
cloud="aws", # aws | gcp | azure
region="us-east-1", # ← choose it by latency to your users
),
)
How to choose the region:
- If your app is in AWS us-east-1, put the index in us-east-1 too.
- Network latency: ~5-15ms in the same region, 50-100ms across regions.
- Pinecone serverless doesn't support automatic multi-region replication (Enterprise does).
Setup, step by step
Step 1: installation and configuration
pip install "pinecone-client>=4.0"
# .env
PINECONE_API_KEY=... # from the Pinecone console
PINECONE_INDEX_NAME=production-rag
PINECONE_ENVIRONMENT=production
Step 2: the client and a check
# pinecone_setup.py
import os
from pinecone import Pinecone, ServerlessSpec
from dotenv import load_dotenv
load_dotenv()
def get_pinecone_client() -> Pinecone:
"""Initializes the Pinecone client with the API key."""
api_key = os.getenv("PINECONE_API_KEY")
if not api_key:
raise EnvironmentError("PINECONE_API_KEY is not configured")
return Pinecone(api_key=api_key)
# A quick smoke test
pc = get_pinecone_client()
print(f"Existing indexes: {pc.list_indexes().names()}")
Step 3: idempotent index creation
def ensure_index(
pc: Pinecone,
name: str,
dimension: int = 1536,
metric: str = "cosine",
cloud: str = "aws",
region: str = "us-east-1",
):
"""
Creates the index if it doesn't exist. If it exists, validates the config
and returns a reference.
Idempotent: it can be run multiple times without breaking anything.
"""
existing_indexes = [idx.name for idx in pc.list_indexes()]
if name in existing_indexes:
# Validate that the existing config matches
index_info = pc.describe_index(name)
if index_info.dimension != dimension:
raise ValueError(
f"Index '{name}' exists with dimension {index_info.dimension}, "
f"expected {dimension}. Recreate it or use another name."
)
if index_info.metric != metric:
raise ValueError(
f"Index '{name}' exists with metric {index_info.metric}, "
f"expected {metric}."
)
print(f"Index '{name}' already exists, config OK")
else:
print(f"Creating index '{name}'...")
pc.create_index(
name=name,
dimension=dimension,
metric=metric,
spec=ServerlessSpec(cloud=cloud, region=region),
)
# Wait until the index is ready (it can take 30-60 seconds)
import time
while True:
status = pc.describe_index(name).status["ready"]
if status:
break
print("Waiting for the index to be ready...")
time.sleep(2)
print(f"Index '{name}' created")
return pc.Index(name)
# Usage
pc = get_pinecone_client()
index = ensure_index(
pc,
name="production-rag",
dimension=1536, # text-embedding-3-small
metric="cosine",
cloud="aws",
region="us-east-1",
)
Step 4: the smoke test
def smoke_test(index):
"""Verifies that the index responds correctly."""
# The index's stats
stats = index.describe_index_stats()
print(f"Total vectors: {stats['total_vector_count']}")
print(f"Dimension: {stats['dimension']}")
print(f"Index fullness: {stats.get('index_fullness', 'N/A')}")
# A test insert + delete
test_vector = [0.1] * 1536 # a trivial vector
index.upsert([("smoke_test_vector", test_vector, {"test": True})])
print("Insert OK")
# Query
results = index.query(vector=test_vector, top_k=1, include_metadata=True)
print(f"Query OK, results: {len(results['matches'])}")
# Cleanup
index.delete(ids=["smoke_test_vector"])
print("Delete OK")
smoke_test(index)
Expected output:
Total vectors: 0
Dimension: 1536
Index fullness: 0
Insert OK
Query OK, results: 1
Delete OK
Validation: the dimension matches the embedding model
def validate_embedding_compatibility(index, embedding_function):
"""
Generates a test embedding and verifies it matches the index's dimension.
Critical before doing a bulk ingest.
"""
test_text = "test"
test_embedding = embedding_function(test_text)
if isinstance(test_embedding, list):
actual_dim = len(test_embedding)
elif hasattr(test_embedding, "shape"):
actual_dim = test_embedding.shape[0]
else:
raise TypeError(f"Embedding type unexpected: {type(test_embedding)}")
index_dim = index.describe_index_stats()["dimension"]
if actual_dim != index_dim:
raise ValueError(
f"Dimension mismatch! The embedding produces dim={actual_dim}, "
f"the index expects dim={index_dim}. Recreate the index or change the model."
)
print(f"OK: the embedding ({actual_dim}d) is compatible with the index ({index_dim}d)")
# Use it before ingesting
from openai import OpenAI
openai_client = OpenAI()
def get_openai_embedding(text):
response = openai_client.embeddings.create(
input=text,
model="text-embedding-3-small",
)
return response.data[0].embedding
validate_embedding_compatibility(index, get_openai_embedding)
Serverless vs pod-based
Pinecone offers two kinds of index:
| Aspect | Serverless | Pod-based |
|---|---|---|
| Cost | Pay per use (~$0.50 per 1M vectors/month + queries) | A fixed cost per pod (~$70/month minimum) |
| Scaling | Automatic, no configuration | Manual, scale horizontally |
| Latency | 30-100ms | 10-50ms (optimized pods) |
| Cases | Getting started, variable traffic | Latency-critical, constant traffic |
The initial recommendation: serverless. No commitments, pay for what you use. If latency or cost grow a lot later, evaluate pod-based.
# Serverless (recommended to start)
pc.create_index(
name="my-rag",
dimension=1536,
metric="cosine",
spec=ServerlessSpec(cloud="aws", region="us-east-1"),
)
# Pod-based (advanced, latency-critical)
from pinecone import PodSpec
pc.create_index(
name="my-rag-pods",
dimension=1536,
metric="cosine",
spec=PodSpec(
environment="us-east-1-aws",
pod_type="p1.x1", # type and size
pods=2, # the number of pods
),
)
Configuration per environment
# config.py
import os
from typing import NamedTuple
class PineconeConfig(NamedTuple):
api_key: str
index_name: str
dimension: int
metric: str
cloud: str
region: str
def get_config(env: str = None) -> PineconeConfig:
"""Configuration separated by environment: dev, staging, prod."""
env = env or os.getenv("APP_ENV", "dev")
base_index = os.getenv("PINECONE_INDEX_BASENAME", "rag")
return PineconeConfig(
api_key=os.getenv("PINECONE_API_KEY"),
index_name=f"{base_index}-{env}", # rag-dev, rag-staging, rag-prod
dimension=int(os.getenv("EMBEDDING_DIMENSION", "1536")),
metric=os.getenv("PINECONE_METRIC", "cosine"),
cloud=os.getenv("PINECONE_CLOUD", "aws"),
region=os.getenv("PINECONE_REGION", "us-east-1"),
)
# Usage
config = get_config() # auto-detects APP_ENV
print(f"Using index: {config.index_name}") # e.g. rag-prod
The benefits:
- Dev/staging/prod completely separated (you don't contaminate the data).
- Switch environments with an env var, without touching the code.
- Versioned configuration (git) with no secrets (those live in .env).
Traps and common mistakes
Trap 1: the wrong dimension, with no validation
The mistake: creating an index with dimension=1536 (assumed) and then using 384-dim embeddings. The inserts fail.
The symptom: PineconeApiException: Vector dimension 384 does not match the dimension of the index 1536.
How to prevent it: run validate_embedding_compatibility before ingesting in bulk.
Trap 2: the wrong metric
The mistake: an index with metric="euclidean" when the embeddings expect cosine.
The symptom: the system works but the retrieval gives strange rankings. Cosine and euclidean produce similar rankings but NOT identical ones.
How to prevent it: use the metric the embedding model recommends (cosine for OpenAI, Cohere, ST).
Trap 3: an API key without sufficient permissions
The mistake: a "read-only" API key used to create indexes.
The symptom: 403 Forbidden on creation.
How to prevent it: create a specific API key with the "Manage Indexes" permission for the setup script. Afterwards you can use a lower-permission key for queries.
Trap 4: forgetting the time.sleep after creating
The mistake:
pc.create_index(...)
index = pc.Index(name)
index.upsert(...) # fail! the index isn't ready yet
The symptom: a 503 error or a timeout.
How to prevent it: poll the status until ready=True before using it (as shown in ensure_index).
Trap 5: an index name with invalid characters
The mistake: pc.create_index(name="my_rag_index") (an underscore).
The symptom: Pinecone only allows lowercase + hyphens. my_rag_index fails.
How to prevent it: always use lowercase + -. my-rag-index ✓
Trap 6: the same account for dev and prod
The mistake: dev and prod use the same API key and the same index.
The symptom: a dev runs a cleanup in dev and wipes production data.
How to prevent it: separate API keys, indexes with different names (rag-dev vs rag-prod).
Applied exercise
Scenario: you're migrating an existing RAG system to Pinecone. The details:
- Current embedding model: OpenAI
text-embedding-3-small(1536 dim) - The team is evaluating moving up to
text-embedding-3-large(3072 dim) in the next 6 months - The app is deployed on GCP us-central1
- 3 environments: dev, staging, prod
- Multi-tenant with 50 customers
Your job:
- Configure the appropriate indexes (one or three? which dimension?).
- Design an idempotent setup script.
- A migration plan for the model change in 6 months.
Solution
1. The index configuration
The decision: 3 separate indexes (dev, staging, prod) with dimension 1536.
The reasons:
- 3 separate environments is non-negotiable. Mixing them = the risk of wiping prod.
- Dimension 1536 (the current model). For the future change to 3072, a separate plan.
- Cloud GCP, region us-central1 (matching the app).
- Metric cosine (recommended for OpenAI).
# config.py
INDICES_CONFIG = {
"dev": {
"name": "rag-dev",
"dimension": 1536,
"metric": "cosine",
"cloud": "gcp",
"region": "us-central1",
},
"staging": {
"name": "rag-staging",
"dimension": 1536,
"metric": "cosine",
"cloud": "gcp",
"region": "us-central1",
},
"prod": {
"name": "rag-prod",
"dimension": 1536,
"metric": "cosine",
"cloud": "gcp",
"region": "us-central1",
},
}
2. The idempotent script
# scripts/setup_pinecone.py
"""
Idempotent setup of the Pinecone indexes.
Usage: APP_ENV=prod python scripts/setup_pinecone.py
"""
import os
import sys
import time
from pinecone import Pinecone, ServerlessSpec
def main():
env = os.getenv("APP_ENV")
if env not in ["dev", "staging", "prod"]:
print("APP_ENV must be dev | staging | prod")
sys.exit(1)
# An extra guard: prod requires confirmation
if env == "prod":
confirm = input("⚠️ Modifying the PRODUCTION index. Continue? (yes/no): ")
if confirm != "yes":
print("Aborted.")
sys.exit(0)
config = INDICES_CONFIG[env]
pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY"))
existing = [idx.name for idx in pc.list_indexes()]
if config["name"] in existing:
info = pc.describe_index(config["name"])
# Validations
assert info.dimension == config["dimension"], (
f"Dim mismatch! Existing={info.dimension}, "
f"config={config['dimension']}"
)
assert info.metric == config["metric"]
print(f"OK: {config['name']} exists with the right config")
else:
print(f"Creating {config['name']}...")
pc.create_index(
name=config["name"],
dimension=config["dimension"],
metric=config["metric"],
spec=ServerlessSpec(cloud=config["cloud"], region=config["region"]),
)
# Wait until ready
while not pc.describe_index(config["name"]).status["ready"]:
print("Waiting for ready...")
time.sleep(3)
print(f"✅ {config['name']} created")
if __name__ == "__main__":
main()
3. The plan for migrating to text-embedding-3-large (3072 dim)
You can't simply change the dimension. The plan:
Phase 1 (1 week): create a new index with dim 3072
pc.create_index(
name="rag-prod-v2", # ← v2 signals the new dimension
dimension=3072,
metric="cosine",
spec=ServerlessSpec(cloud="gcp", region="us-central1"),
)
Phase 2 (2 weeks): dual indexing
- Every new doc gets ingested into BOTH indexes (with a 1536 embedding into the old one, 3072 into the new).
- The extra cost during this phase: ~$200-400/month.
Phase 3 (1 week): backfill the new index
- Re-embed all the old docs with
text-embedding-3-large. - Cost: $50-200 depending on the volume.
- Time: 2-3 days for 5M docs.
Phase 4 (1 week): A/B test
- 10% of queries go to the new index, 90% to the old one.
- Measure recall, precision, latency, costs.
- If quality is +3% or better, it justifies the doubled cost (large is 6x more expensive than small for embedding generation).
Phase 5 (1 week): the full rollout
- 50% → 100% to the new one.
- Monitor for 1 week.
- Decommission the old index.
The protective metrics:
- Recall@5 must not drop (validate with the eval set).
- p95 latency must not rise more than 20%.
- The total cost must not exceed the budget.
Plan B if large doesn't justify itself:
- Keep small. Decommission the new index.
- Document the analysis for future decisions.
Summary and next step
What you learned:
- Three immovable decisions when creating an index: dimension, metric, region/cloud.
- The dimension must match your embedding model EXACTLY.
- Cosine for 95% of RAG with text embeddings; change it only if the model recommends it.
- An idempotent setup: an ensure_index that validates the existing config or creates a new one.
- Pre-ingest validation: confirm the model's dimension matches the index's.
- Serverless is the reasonable default; pod-based for latency-critical or constant-traffic cases.
- Configuration per environment with separate indexes (rag-dev, rag-staging, rag-prod).
- The traps: an API key with insufficient permissions, not waiting for the index to be ready, a name with an underscore, the same index for dev and prod.
Checkpoint: before moving on, you should be able to:
- Create a Pinecone index with the right configuration.
- Validate that the dimension matches the embedding model.
- Implement an idempotent setup that can run in any environment.
Next capsule: 04 — The ChromaDB → Pinecone migration.
You have the index. Now comes the real migration: moving the data from ChromaDB without losing vectors or metadata, with no downtime for production, and with a rollback plan if something goes wrong.
Resources
- Pinecone — Quickstart — The official setup
- Pinecone — Create Index — The complete parameters
- Pinecone — Serverless vs Pods — The decision
- OpenAI Embeddings — Dimensions per model
- Pinecone — API Reference — The SDK in detail
- Pinecone Examples GitHub — Real code
Estimated time: 30-35 minutes Next: 04-migrating-from-chromadb-to-pinecone.md