← Code Recipes
Embeddings & Search

Pinecone Free code examples in C#, Python, Java and JavaScript

Managed vector database — no cluster to operate.

C# Python Java JavaScript

WHAT THIS PAGE COVERS

Namespaces and metadata filters, which is how multi-tenant RAG stays correct.

Model text-embedding-3-small
Package pinecone
Checked

Model ids change. If a sample returns a 404 on the model, that is the first thing to check against the official docs.

Install

THE OFFICIAL PACKAGE IS Pinecone.Client. `Pinecone.NET` is a community library with a similar name and a different API — methods without the Async suffix, MetadataMap instead of Metadata, and an environment argument that no longer exists.

dotnet add package Pinecone.Client

setx PINECONE_API_KEY "pcsk_your-key"     # Windows, then open a new shell
export PINECONE_API_KEY="pcsk_your-key"   # macOS / Linux
Connect and describe

The client is cheap; the index handle is what you hold. DescribeIndexStatsAsync is the fastest way to confirm the key, the index name and the dimension all line up.

using Pinecone;

var apiKey = Environment.GetEnvironmentVariable("PINECONE_API_KEY")
    ?? throw new InvalidOperationException("PINECONE_API_KEY is not set.");

var pinecone = new PineconeClient(apiKey);
var index = pinecone.Index("documents");

var stats = await index.DescribeIndexStatsAsync(new DescribeIndexStatsRequest());

// Expect 1536 here if the index was built for text-embedding-3-small.
Console.WriteLine($"dimension {stats.Dimension}, {stats.TotalVectorCount} vectors");
foreach (var (name, summary) in stats.Namespaces ?? [])
{
    Console.WriteLine($"  {name}: {summary.VectorCount}");
}
Upsert into a namespace

Namespace is a field on the request, not a separate client. Metadata values are typed, so a page number stays a number and stays filterable as one.

var vectors = new List<Vector>
{
    new()
    {
        Id = "doc-1#chunk-0",
        Values = embedding,          // float[] of length 1536
        Metadata = new Metadata
        {
            ["doc_id"] = "doc-1",
            ["source"] = "handbook.pdf",
            ["page"] = 4,
            ["text"] = "The first eighty characters or so, for display.",
        },
    },
};

await index.UpsertAsync(new UpsertRequest
{
    Vectors = vectors,
    Namespace = "tenant-acme",
});
Querying one tenant, filtered

IncludeMetadata is false by default. The namespace is what keeps one customer's query away from another's data — a tenant id in the filter is one forgotten clause from a breach.

var response = await index.QueryAsync(new QueryRequest
{
    Namespace = "tenant-acme",       // hard partition
    Vector = queryVector,
    TopK = 5,
    Filter = new Metadata
    {
        ["source"] = new Metadata { ["$eq"] = "handbook.pdf" },
    },
    IncludeMetadata = true,          // off by default
});

foreach (var match in response.Matches ?? [])
{
    Console.WriteLine($"{match.Score:F3}  {match.Metadata?["text"]}");
}
Install

The gRPC client is worth the extra dependency for bulk upserts — the REST one is fine for queries and noticeably slower for ingestion.

pip install "pinecone[grpc]"

export PINECONE_API_KEY="pcsk_your-key"   # macOS / Linux
setx PINECONE_API_KEY "pcsk_your-key"     # Windows, then open a new shell
Create an index

THE DIMENSION IS FIXED FOR THE LIFE OF THE INDEX. It must match the embedding model exactly — 1536 for text-embedding-3-small — and changing model later means a new index and a full re-embed.

import os
from pinecone import Pinecone, ServerlessSpec

pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])

if not pc.has_index("documents"):
    pc.create_index(
        name="documents",
        dimension=1536,              # exactly text-embedding-3-small
        metric="cosine",
        spec=ServerlessSpec(cloud="aws", region="us-east-1"),
    )

index = pc.Index("documents")
Upsert into a namespace

The namespace is a partition, not a filter. Nothing in one namespace is visible from another, so a per-tenant namespace makes cross-tenant leakage impossible rather than merely unlikely.

index.upsert(
    vectors=[
        {
            "id": "doc-1#chunk-0",
            "values": vector,                  # 1536 floats
            "metadata": {
                "doc_id": "doc-1",
                "source": "handbook.pdf",
                "page": 4,
                "text": "The first eighty characters or so, for display.",
            },
        },
    ],
    namespace="tenant-acme",
)
Querying one tenant, filtered

The namespace scopes the search; the filter narrows it. include_metadata is off by default, and forgetting it returns ids with no way to show the user what was found.

results = index.query(
    namespace="tenant-acme",           # hard partition
    vector=query_vector,
    top_k=5,
    filter={                           # narrowing within the tenant
        "source": {"$eq": "handbook.pdf"},
        "page": {"$gte": 1},
    },
    include_metadata=True,             # off by default
)

for match in results["matches"]:
    print(round(match["score"], 3), match["metadata"]["text"])
Deleting a document without a scan

Chunk ids prefixed with the document id make deletion a list-and-delete rather than a metadata scan. Getting this wrong means stale chunks that keep being retrieved after the source is gone.

# Ids were written as "doc-1#chunk-0", "doc-1#chunk-1", ...
for page in index.list(prefix="doc-1#", namespace="tenant-acme"):
    index.delete(ids=page, namespace="tenant-acme")

# Deleting a whole tenant is one call, because the namespace is real.
index.delete(delete_all=True, namespace="tenant-acme")
Install

The Java client wraps gRPC, so the index connection is a resource with a lifetime — build it once and close it on shutdown.

# Gradle
implementation("io.pinecone:pinecone-client:5.1.0")

export PINECONE_API_KEY="pcsk_your-key"
Connect to an index

getIndexConnection holds a gRPC channel. Opening one per request exhausts connections under load — hold it for the life of the process.

import io.pinecone.clients.Index;
import io.pinecone.clients.Pinecone;

public class Search {
    public static void main(String[] args) {
        String apiKey = System.getenv("PINECONE_API_KEY");
        if (apiKey == null) throw new IllegalStateException("PINECONE_API_KEY is not set.");

        Pinecone pinecone = new Pinecone.Builder(apiKey).build();

        // One connection for the life of the process.
        Index index = pinecone.getIndexConnection("documents");

        System.out.println(index.describeIndexStats());
    }
}
Upsert into a namespace

Vectors are built with buildUpsertVectorWithUnsignedIndices — id, values, sparse indices, sparse values, metadata — and the NAMESPACE is the second argument to upsert. Leave it off and everything lands in the default namespace, which then answers every tenant's queries.

import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import io.pinecone.proto.UpsertResponse;
import io.pinecone.unsigned_indices_model.VectorWithUnsignedIndices;

import java.util.Arrays;
import java.util.List;

import static io.pinecone.commons.IndexInterface.buildUpsertVectorWithUnsignedIndices;

Struct metadata = Struct.newBuilder()
    .putFields("doc_id", Value.newBuilder().setStringValue("doc-1").build())
    .putFields("source", Value.newBuilder().setStringValue("handbook.pdf").build())
    .putFields("page", Value.newBuilder().setNumberValue(4).build())
    .build();

// 1536 floats — the width of text-embedding-3-small.
List<Float> values = Arrays.asList(embedding);

List<VectorWithUnsignedIndices> vectors = List.of(
    buildUpsertVectorWithUnsignedIndices(
        "doc-1#chunk-0", values, null, null, metadata));

UpsertResponse response = index.upsert(vectors, "tenant-acme");
Reading one tenant back

queryByVectorId scopes to the namespace, which is the boundary that matters — nothing in one namespace is reachable from another. The vector-plus-filter overload exists too; check its signature in the SDK reference rather than adapting the Python shape by eye.

import io.pinecone.unsigned_indices_model.QueryResponseWithUnsignedIndices;
import io.pinecone.unsigned_indices_model.ScoredVectorWithUnsignedIndices;

QueryResponseWithUnsignedIndices response = index.queryByVectorId(
    5,                      // topK
    "doc-1#chunk-0",
    "tenant-acme");         // the namespace — the hard partition

for (ScoredVectorWithUnsignedIndices match : response.getMatchesList()) {
    System.out.printf("%.3f  %s%n", match.getScore(), match.getMetadata());
}

// Deleting a whole tenant is one call, because the namespace is real
// rather than a metadata field every query has to remember.
index.deleteAll("tenant-acme");
Install

The npm package is scoped. The client reads PINECONE_API_KEY if you pass no arguments.

npm install @pinecone-database/pinecone

export PINECONE_API_KEY="pcsk_your-key"
Create an index

waitUntilReady matters: a serverless index takes a few seconds to come up, and upserting into one that is still initialising fails in a way that reads like a permissions error.

import { Pinecone } from "@pinecone-database/pinecone";

const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY });

const existing = await pc.listIndexes();
if (!existing.indexes?.some((index) => index.name === "documents")) {
  await pc.createIndex({
    name: "documents",
    dimension: 1536,              // exactly text-embedding-3-small
    metric: "cosine",
    spec: { serverless: { cloud: "aws", region: "us-east-1" } },
    waitUntilReady: true,
  });
}

const index = pc.index("documents");
Upsert into a namespace

namespace() returns a scoped handle. Holding one per tenant is how the tenant id stops being something every call site has to remember.

const tenant = index.namespace("tenant-acme");

await tenant.upsert([
  {
    id: "doc-1#chunk-0",
    values: vector,                 // 1536 floats
    metadata: {
      doc_id: "doc-1",
      source: "handbook.pdf",
      page: 4,
      text: "The first eighty characters or so, for display.",
    },
  },
]);
Querying one tenant, filtered

The namespace is the security boundary and the filter is the relevance boundary. Conflating the two — a tenant id in metadata — is the bug that leaks documents between customers.

const results = await tenant.query({
  vector: queryVector,
  topK: 5,
  filter: {
    source: { $eq: "handbook.pdf" },
    page: { $gte: 1 },
  },
  includeMetadata: true,            // off by default
});

for (const match of results.matches) {
  console.log(match.score.toFixed(3), match.metadata.text);
}
Deleting a document without a scan

Prefixing chunk ids with the document id turns deletion into a list. Without it, removing one source document means scanning metadata across the namespace.

// Ids written as "doc-1#chunk-0", "doc-1#chunk-1", ...
const stale = await tenant.listPaginated({ prefix: "doc-1#" });

if (stale.vectors?.length) {
  await tenant.deleteMany(stale.vectors.map((vector) => vector.id));
}

// Removing a tenant entirely is one call.
await tenant.deleteAll();

QUESTIONS

Which languages are the Pinecone examples in?

C#, Python, Java and JavaScript. Every sample runs as pasted and reads its credentials from an environment variable — no key is ever written into the code.

Which package do I install for Pinecone?

The samples use pinecone. The install step on each language tab gives the exact command.

Which Pinecone model do these examples use?

text-embedding-3-small, checked against the official documentation on 26 September 2026. Model ids change often; if a call returns a model-not-found error, check the current list in the official docs.

Where is the official Pinecone documentation?

https://docs.pinecone.io/ — it is linked at the bottom of this page, and it is the authority whenever it disagrees with a sample here.

ALSO IN EMBEDDINGS & SEARCH

The authority

Official Pinecone documentation

Model names, pricing and rate limits change faster than any page here can. When this page and the official docs disagree, the official docs are right.

Read the docs ↗

https://docs.pinecone.io/

New drops, every week

Free tools & Claude templates, landing regularly.

We keep shipping new free tools, code recipes and Claude Code templates. Follow @freeyantra on Instagram or join the newsletter so you never miss one.

Follow @freeyantra

or get them by email

Double opt-in. Unsubscribe any time. Privacy