teachyou.ai academy
← All posts
Vector DatabasesPineconeRAGEmbeddingsSemantic Search

A Practical Guide to Pinecone

Pramod Dutta · Jun 20, 2026 · 12 min read

This pinecone guide is for engineers who need to store and search embeddings at scale without running their own vector search cluster. Pinecone is a managed vector database: you send it vectors (plus optional metadata), it indexes them, and you query it with a vector to get the nearest neighbors back in milliseconds. If you are building retrieval-augmented generation (RAG), semantic search, recommendation systems, or deduplication on top of embeddings, Pinecone removes the operational work of running something like a self-hosted HNSW index at scale.

The rest of this guide walks through everything you need to go from an empty account to a production-ready index: picking an index type, upserting vectors correctly, querying with metadata filters, organizing data with namespaces, wiring it into a RAG pipeline, and the failure modes that catch people who skip the docs.

What Pinecone Actually Stores and Returns

A Pinecone index holds records. Each record has three parts:

  • An id: a string you choose, usually a stable identifier from your source system (a document chunk ID, a product SKU, a user ID).
  • A values array: the embedding vector itself, a fixed-length list of floats produced by an embedding model.
  • Optional metadata: a JSON object of scalar fields and string lists (tenant ID, source URL, timestamps, tags) that you can filter on at query time.

When you query, you send a vector and Pinecone returns the k nearest records by similarity, each with its id, similarity score, and (if requested) its metadata and values. Pinecone does not compute embeddings for you unless you explicitly use one of its integrated embedding models; in most production setups you generate embeddings yourself with a model you control (OpenAI, Cohere, Voyage, or an open-weights model you host) and just hand Pinecone the resulting vectors.

Why This Pinecone Guide Starts With Index Types

Before writing any code, decide on an index type, because it is the one choice that is annoying to change later. Pinecone offers two index modes:

  • Serverless indexes: you specify a cloud and region, and Pinecone manages capacity automatically, scaling storage and compute with usage. This is the default recommendation for new projects because you are not pre-provisioning pods, and cost tracks actual usage.
  • Pod-based indexes: you provision a fixed number of pods of a given size and type, which gives you predictable, dedicated capacity. This mode still exists for workloads that want guaranteed throughput independent of noisy-neighbor effects, but most new projects should start serverless and only move to pod-based if there is a specific latency or throughput requirement serverless cannot meet.

The second decision is the distance metric: cosine, dot product, or Euclidean. If your embedding model was trained with normalized vectors (most modern text embedding models are), cosine and dot product give equivalent rankings, and cosine is the safer default because it is scale-invariant. Use Euclidean only if your embeddings specifically encode magnitude as meaningful signal, which is rare for text.

The third decision is dimension, and it must match your embedding model exactly. A 1536-dimension OpenAI embedding cannot go into a 768-dimension index. Mixing embedding models in one index is a common mistake: if you switch embedding providers later, you need a new index, because the vector spaces are not comparable.

Setting Up Your First Index

Install the Python SDK:

pip install pinecone

Create a serverless index:

from pinecone import Pinecone, ServerlessSpec

pc = Pinecone(api_key="YOUR_API_KEY")

pc.create_index(
    name="docs-index",
    dimension=1536,
    metric="cosine",
    spec=ServerlessSpec(cloud="aws", region="us-east-1"),
)

# Wait until the index is ready before writing to it
import time
while not pc.describe_index("docs-index").status["ready"]:
    time.sleep(1)

index = pc.Index("docs-index")

That index object is what you use for every read and write from here on. Keep the API key out of source control; load it from an environment variable in real code (os.environ["PINECONE_API_KEY"]), not hardcoded like the example above.

Upserting Vectors Correctly

"Upsert" means insert-or-update: if the id already exists, Pinecone overwrites the record; if not, it creates one. This makes upserts idempotent, which matters a lot for pipelines that reprocess documents.

vectors = [
    {
        "id": "doc-42-chunk-0",
        "values": embedding_0,  # list[float], length 1536
        "metadata": {
            "source": "handbook.pdf",
            "chunk_index": 0,
            "tenant_id": "acme-corp",
            "published_year": 2026,
        },
    },
    {
        "id": "doc-42-chunk-1",
        "values": embedding_1,
        "metadata": {
            "source": "handbook.pdf",
            "chunk_index": 1,
            "tenant_id": "acme-corp",
            "published_year": 2026,
        },
    },
]

index.upsert(vectors=vectors, namespace="acme-corp")

Batch your upserts. Sending one vector per request works but is slow and burns request overhead; batch 100 to 200 records per call for a good balance of throughput and payload size. For large backfills, parallelize batches across a thread pool or async client rather than looping serially:

import concurrent.futures

def upsert_batch(batch):
    index.upsert(vectors=batch, namespace="acme-corp")

batches = [vectors[i:i+100] for i in range(0, len(vectors), 100)]
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
    list(pool.map(upsert_batch, batches))

Give upserted IDs a structure you can reason about later ({document_id}-chunk-{n}), because you will need to delete or update all chunks belonging to one source document when it changes, and a predictable ID scheme makes that trivial.

A Pinecone Guide to Metadata Filtering and Namespaces

Two features do most of the work of making a shared index usable in production: metadata filters and namespaces.

Metadata filtering lets you narrow the candidate set before or during the nearest-neighbor search, using a Mongo-style filter syntax:

results = index.query(
    vector=query_embedding,
    top_k=5,
    namespace="acme-corp",
    filter={
        "published_year": {"$gte": 2024},
        "source": {"$in": ["handbook.pdf", "policy.pdf"]},
    },
    include_metadata=True,
)

for match in results["matches"]:
    print(match["id"], match["score"], match["metadata"])

Supported operators include $eq, $ne, $gt, $gte, $lt, $lte, $in, and $nin. Filters apply on indexed metadata, so keep metadata values simple scalars and string lists; deeply nested objects are not filterable.

Namespaces partition an index into isolated sub-collections that share the same dimension and metric but are queried separately. The most common pattern is one namespace per tenant in a multi-tenant SaaS product:

index.upsert(vectors=tenant_a_vectors, namespace="tenant-a")
index.upsert(vectors=tenant_b_vectors, namespace="tenant-b")

# Tenant A's queries only ever see tenant A's vectors
index.query(vector=q, top_k=10, namespace="tenant-a")

This is preferable to a tenant_id metadata filter alone for two reasons: it is a hard isolation boundary (a query without a namespace argument returns nothing from other namespaces, so a missing filter cannot leak data across tenants), and it keeps per-tenant index statistics separate, which makes it easier to see how much space and traffic any one tenant consumes.

Use namespaces for hard boundaries (tenant, environment, language) and metadata filters for softer, cross-cutting filters (date ranges, categories, source types) within a namespace.

Building a RAG Pipeline with Pinecone

A minimal RAG retrieval step looks like this. Assume you already have a function embed(text) that calls your embedding model and returns a vector.

def retrieve(question, tenant_id, k=5):
    query_vector = embed(question)
    results = index.query(
        vector=query_vector,
        top_k=k,
        namespace=tenant_id,
        include_metadata=True,
    )
    chunks = []
    for match in results["matches"]:
        chunks.append({
            "text": match["metadata"]["text"],
            "source": match["metadata"]["source"],
            "score": match["score"],
        })
    return chunks

def build_prompt(question, chunks):
    context = "\n\n".join(
        f"[{c['source']}]\n{c['text']}" for c in chunks
    )
    return (
        "Answer the question using only the context below. "
        "If the context does not contain the answer, say so.\n\n"
        f"Context:\n{context}\n\nQuestion: {question}"
    )

Two details that separate a working RAG demo from a reliable one:

  • Store the chunk text in metadata, not just a pointer to it. Pinecone metadata has a size limit per record (well under a megabyte), so this works fine for typical chunk sizes (a few hundred to a couple thousand tokens), but if you have very large chunks, store a short excerpt in metadata and fetch the full text from your primary document store using the id.
  • Set a similarity score threshold and refuse to answer, or fall back to a broader search, when the top match score is too low. A top_k=5 query always returns five results even if none of them are actually relevant, and an LLM asked to answer from irrelevant context will often hallucinate a confident-sounding answer anyway.
MIN_SCORE = 0.75

def retrieve_with_guard(question, tenant_id, k=5):
    chunks = retrieve(question, tenant_id, k)
    relevant = [c for c in chunks if c["score"] >= MIN_SCORE]
    return relevant

Tune MIN_SCORE empirically against your own embedding model and metric; cosine similarity thresholds that work well for one embedding model do not transfer to another.

Handling Updates and Deletes

Documents change, and stale vectors in a RAG index quietly degrade answer quality without throwing any errors. Handle deletes explicitly rather than letting an index grow forever.

Delete by ID when you know exactly which chunks are stale:

index.delete(ids=["doc-42-chunk-0", "doc-42-chunk-1"], namespace="acme-corp")

Delete by metadata filter when a whole category needs to go (a filter-based delete on serverless indexes deletes every record matching the filter):

index.delete(filter={"source": "handbook.pdf"}, namespace="acme-corp")

Delete an entire namespace when offboarding a tenant:

index.delete(delete_all=True, namespace="tenant-a")

For a document that gets re-processed (new content, re-chunked), the simplest correct pattern is: delete by the old document's metadata filter or known ID prefix, then upsert the new chunks. Because upsert is idempotent on id, if your chunk IDs are deterministic ({document_id}-chunk-{n}) and the new document produces fewer chunks than the old one, a plain re-upsert will leave orphaned trailing chunks behind, which is why the delete-then-upsert pattern is safer than upsert-only for re-processing.

Monitoring and Cost Control

Check index stats regularly, either through the console or programmatically:

stats = index.describe_index_stats()
print(stats["total_vector_count"])
print(stats["namespaces"])

This tells you vector counts per namespace, which is the fastest way to catch a bug where a background job is upserting duplicate records instead of updating them (a sign your ID scheme is not actually stable), or a tenant that never got cleaned up after offboarding.

For serverless indexes, cost scales with stored vector volume and query volume, so the practical levers you control are: keep chunk counts sane (do not over-chunk documents into hundreds of tiny fragments when a few well-sized chunks retrieve just as well), delete data you no longer need instead of leaving it to accumulate, and avoid top_k values far larger than what your application actually consumes downstream, since larger result sets cost more to score and return.

Pinecone vs Self-Hosted Alternatives

Pinecone is worth paying for when you want zero infrastructure ownership: no cluster sizing, no HNSW parameter tuning, no replication story to design yourself, and a query API that scales without you thinking about it. The tradeoff is that your vector data lives outside your own infrastructure, and very large deployments can cost more than a self-managed alternative at scale.

Self-hosted options like pgvector (a Postgres extension), Qdrant, Weaviate, or Milvus make sense when you already run Postgres and query volume is modest (pgvector piggybacks on infrastructure you already operate), when data residency requirements mean vectors cannot leave your own cluster, or when you have the operational capacity to tune and scale a vector search system yourself and want to avoid a per-query managed-service cost.

A reasonable default: start with Pinecone serverless for anything you are building today, because the time saved on infrastructure work outweighs the cost difference until you are operating at a scale where that math flips, and you will know you have hit that point because you will be looking directly at a large, specific Pinecone bill rather than guessing.

Common Mistakes

  • Mixing embedding models in one index. Every vector in an index must come from the same model and the same dimension; a query embedded with a different model than the stored vectors will return meaningless nearest neighbors that still look like valid results (no error is thrown).
  • Not normalizing similarity assumptions. Confirm which metric your embedding model expects (most sentence-embedding models are trained for cosine similarity) and set the index metric to match, not to whatever the example code you copied used.
  • Treating metadata as unlimited. Metadata has a per-record size limit; storing full raw documents instead of chunk-sized text will hit it. Chunk before you store.
  • Skipping the score threshold in RAG. Always evaluate whether the top match score clears a bar before feeding it to an LLM as ground truth; low-relevance context silently produces confidently wrong answers.
  • No namespace strategy from day one. Retrofitting namespace isolation into an index that already mixes tenants means re-upserting everything. Decide on your namespace boundary before you write your first vector.
  • Forgetting to delete stale vectors. An index that only grows accumulates outdated chunks that compete with fresh ones for the same top_k slots, quietly lowering retrieval quality over time.

FAQ

Do I need to use Pinecone's own embedding models, or can I bring my own? You can bring your own. Pinecone offers integrated embedding models as a convenience, but the common production pattern is generating embeddings yourself with whichever model you have evaluated for your domain, then upserting the resulting vectors. Just make sure the index dimension matches your model's output dimension.

What happens if I query with a vector from a different embedding model than what I stored? Pinecone will still return results, ranked by whatever distance calculation you configured, but the rankings will not be meaningful, because the two vector spaces are not comparable. There is no built-in check for this; it is on you to keep embedding models consistent per index.

How many vectors can one namespace hold? Namespaces scale with the underlying serverless index and are designed to comfortably hold millions of vectors each; the practical limit you will hit first is usually cost and query latency at very high top_k values, not a hard namespace cap. If you are unsure, check current limits in the Pinecone documentation for your plan before committing to an architecture.

Should I re-chunk my documents if retrieval quality is poor? Often yes, before touching the index configuration. Chunk size and overlap have an outsized effect on retrieval quality compared to metric choice or index type. Start by trying a few chunk sizes (roughly a few hundred tokens, with 10-20% overlap between chunks) and measure retrieval precision on a held-out set of real questions before assuming the problem is the vector database.

Is Pinecone suitable for real-time recommendation systems, not just RAG? Yes. Nearest-neighbor lookups on serverless or pod-based indexes are designed for low-latency query paths, and the same upsert/query/filter primitives used for RAG apply directly to recommendation use cases: embed items and users, upsert item vectors, and query with the user vector plus metadata filters for eligibility rules (in stock, correct region, and so on).

Can I run Pinecone entirely locally for development? Pinecone is a managed cloud service, so local development still talks to a real (usually a small, disposable dev-tier) index over the network rather than an embedded local database. If fully offline development is a hard requirement, an in-process alternative like a local pgvector instance or an embedded vector library is a better fit for that specific workflow, with Pinecone reserved for staging and production.

A Practical Guide to Pinecone · TeachYou Academy