Home / AI & GenAI / Vector Databases

AI & GenAICheat sheetBeginner

Pinecone cheat sheet: metadata filters, query, fetch, update and delete with the Python SDK

The Pinecone operations you use every day on one page - connecting by host, upsert, query with metadata filters and every filter operator, fetch, partial updates, deletes, stats and the metadata rules that cause most filter bugs.

Deepak Bhardwaj4 min read

Applies toPinecone serverless indexes, Python SDK (pinecone package, v5+); operator syntax also applies to the REST API

Connect

from pinecone import Pinecone, ServerlessSpec

pc = Pinecone(api_key=API_KEY)                       # or PINECONE_API_KEY env var

pc.list_indexes().names()
desc = pc.describe_index("kb-prod")                  # dimension, metric, host, status
index = pc.Index(host=desc.host)                     # cache the host; avoid a lookup per request

Create a serverless index:

pc.create_index(
    name="kb-prod",
    dimension=1536,
    metric="cosine",                                 # cosine | dotproduct | euclidean
    spec=ServerlessSpec(cloud="aws", region="us-east-1"),
    deletion_protection="enabled",
)

Dimension and metric can't be changed later. A new embedding model with a different dimension needs a new index.

Upsert

index.upsert(
    vectors=[
        {"id": "doc42#0", "values": emb0, "metadata": {"doc_id": "doc42", "source": "confluence",
                                                      "year": 2026, "tags": ["vpn", "network"]}},
        {"id": "doc42#1", "values": emb1, "metadata": {"doc_id": "doc42", "source": "confluence",
                                                      "year": 2026, "tags": ["vpn"]}},
    ],
    namespace="kb-prod",
)

Same ID = overwrite (values and full metadata replaced).

Query

res = index.query(
    vector=qvec,
    top_k=10,
    namespace="kb-prod",
    filter={"source": {"$eq": "confluence"}, "year": {"$gte": 2025}},
    include_metadata=True,
    include_values=False,                            # values make responses much larger
)
for m in res.matches:
    print(m.id, round(m.score, 4), m.metadata.get("doc_id"))

Query by an existing record's vector: index.query(id="doc42#0", top_k=5, namespace="kb-prod").

Filter operators

Operator Example Matches
$eq {"source": {"$eq": "jira"}} Equal (also {"source": "jira"})
$ne {"status": {"$ne": "archived"}} Not equal
$gt, $gte, $lt, $lte {"year": {"$gte": 2025}} Numbers only
$in {"source": {"$in": ["jira", "confluence"]}} Any of the values
$nin {"team": {"$nin": ["hr"]}} None of the values
$exists {"reviewed": {"$exists": True}} Field present
$and {"$and": [{"year": {"$gte": 2025}}, {"source": "jira"}]} Both
$or {"$or": [{"source": "jira"}, {"priority": {"$lte": 2}}]} Either

Several fields at the top level are an implicit $and.

List fields: for a metadata value that's a list of strings ("tags": ["vpn","network"]), {"tags": {"$in": ["vpn"]}} matches if any element matches, and {"tags": "vpn"} matches records whose list contains vpn.

Metadata rules that cause filter bugs

Rule Consequence
Values: string, number, boolean, list of strings Nested objects aren't filterable; flatten them (owner_team, not owner.team)
Numbers are compared as numbers Store dates as epoch seconds or YYYYMMDD integers, not ISO strings, if you need ranges
null values aren't supported Omit the key instead; filter with $exists
40 KB of metadata per record Large chunk text in metadata eats the budget; store a pointer if needed
Case-sensitive strings "Jira" ≠ "jira"; normalise at ingestion
Range operators on strings don't work Use numeric fields

Fetch, update, list

index.fetch(ids=["doc42#0", "doc42#1"], namespace="kb-prod")

# Partial metadata update: merges keys, leaves the vector untouched
index.update(id="doc42#0", set_metadata={"reviewed": True}, namespace="kb-prod")

# Replace the vector only
index.update(id="doc42#0", values=new_emb, namespace="kb-prod")

# List IDs by prefix (serverless) - useful with doc_id#chunk ID schemes
for ids in index.list(prefix="doc42#", namespace="kb-prod"):
    print(ids)

The doc_id#chunk ID convention makes "all chunks of a document" a cheap prefix listing.

Delete

index.delete(ids=["doc42#0", "doc42#1"], namespace="kb-prod")              # up to 1,000 IDs per call
index.delete(filter={"doc_id": {"$eq": "doc42"}}, namespace="kb-prod")    # by metadata
index.delete(delete_all=True, namespace="kb-prod")                         # empty the namespace

Delete-by-filter has its own, lower rate limits than delete-by-ID. For bulk cleanup, list IDs by prefix and delete by ID in batches.

Caution

delete_all=True without the right namespace empties the default namespace. Wrap deletes in a helper that refuses an empty namespace.

Stats and health

stats = index.describe_index_stats()
stats.dimension, stats.total_vector_count
stats.namespaces["kb-prod"].vector_count

Counts are eventually consistent; they can trail just-written data.

Common errors

Error Meaning
Vector dimension 3072 does not match the dimension of the index 1536 Wrong embedding model or index
400 ... metadata size exceeds the limit Metadata over 40 KB for a record
404 on index Wrong host, or index name in another project
429 Rate limit; back off and batch
401/403 Wrong key or key from another project

Prevent mistakes next time

  • Define a metadata schema (field, type, allowed values) and validate before upsert.
  • Store dates as integers for ranges.
  • Use doc_id#chunk_no IDs.
  • Keep include_values=False in production queries.