cd /news/ai-tools/build-a-q-a-bot-over-your-company-do… Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-163520] src=dev.to β†— pub= topic=ai-tools verified=true sentiment=Β· neutral

Build a Q&A Bot over Your Company Docs with Pinecone + Claude (Python, SDK v10)

A developer published a walkthrough for building a command-line retrieval-augmented generation Q&A bot over internal company documents using Pinecone's Python SDK v10.3.0 and Anthropic's anthropic 2.4.0 with the claude-sonnet-5-5 model. The pipeline chunks documents, embeds and stores them in a Pinecone index via hosted llama-text-embed-v2, retrieves and reranks results with bge-reranker-v2-m3, then prompts Claude to answer only from retrieved chunks with numbered citations, and it maps deprecated v5–v7 SDK calls to their v10 equivalents.

by read19 min views2 publishedNov 4, 2026

AI disclosure: This article was written with the help of AI and reviewed by me. Every API call in it was checked against the SDK source of pinecone==10.3.0 and anthropic==2.4.0, and prices and limits were taken from the official pages linked inline (as of November 2026). Treat the code as a starting point: run it against your own keys before relying on it.

Once a company has enough internal docs, the same problems show up: new hires can't find the leave policy, nobody remembers which version of the expense rules is current, and wiki search only works if you guess the exact words the author used ("PTO" won't find the page that says "annual leave").

Retrieval-augmented generation (RAG) is the usual fix: split documents into chunks, embed them into a vector database, retrieve the most relevant chunks for each question, and have an LLM answer only from those chunks, with citations.

This walkthrough builds a command-line Q&A bot with Pinecone (managed vector database with hosted embedding and rerank models) and Claude (Anthropic's LLM, for answer generation):

Versions used (latest on PyPI in November 2026):

Component Version
Python 3.10+ (Pinecone SDK v10 supports 3.10–3.14)
pinecone (Pinecone Python SDK) 10.3.0, API version 2026-07
anthropic (Anthropic Python SDK) 2.4.0
Claude model claude-sonnet-5-5

The Pinecone SDK changed a lot in v10. Many tutorials still use the v5–v7 style (create_index_for_model, upsert_records, ServerlessSpec), which may fail or emit deprecation warnings. The pitfalls section maps old calls to new ones.

         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Offline ingest ─────────────────┐
 .md/.txt ──> chunk ──> documents.upsert ──> Pinecone index
                              β”‚              (llama-text-embed-v2
                              β”‚               embeds on write)
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Online ask ─────────────────────┐
 question ──> documents.search (embed scoring, recall 12)
          ──> inference.rerank (bge-reranker-v2-m3, keep 4)
          ──> build prompt ──> Claude Messages API ──> answer with [n] citations
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Embedding, storage and reranking all happen on Pinecone's side, so you don't install PyTorch or host an embedding model. The trade-off: your text is processed by Pinecone's hosted models (more on that under data residency).

Create an account at pinecone.io, then create a key on the API Keys page of the console (https://app.pinecone.io). The free Starter plan is enough for everything here, but note its limits (as of November 2026, see Pricing and Rate limits):

us-east-1`` bge-reranker-v2-m3: Create a key in the Claude Console (https://platform.claude.com). Check Anthropic's supported countries list for your region first.

requirements.txt:

pinecone==10.3.0
anthropic==2.4.0
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

The package is pinecone, not the old pinecone-client.

Variable Required Notes
PINECONE_API_KEY yes From the Pinecone console
ANTHROPIC_API_KEY yes From the Claude Console; anthropic.Anthropic() reads it by default
PINECONE_INDEX no Index name, default company-docs
PINECONE_NAMESPACE no Namespace, default default-tenant
USE_RERANK no 1 to rerank (default),0 to skip and save Starter rerank quota
export PINECONE_API_KEY="your-pinecone-key"
export ANTHROPIC_API_KEY="your-anthropic-key"

Don't hard-code keys or commit them to Git.

Chunking is the first thing that makes or breaks RAG quality. Chunks that are too large mix several topics, so the embedding gets "averaged out" and you waste prompt tokens. Chunks that are too small lose context and the model gets half a sentence.

A simple strategy that works for Markdown or plain-text policy docs:

def split_text(text: str, size: int = 500, overlap: int = 80) -> list[str]:
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
    chunks, buf = [], ""
    for p in paragraphs:
        if len(p) > size:
            if buf:
                chunks.append(buf)
                buf = ""
            step = size - overlap
            chunks.extend(p[i:i + size] for i in range(0, len(p), step))
            continue
        if len(buf) + len(p) + 1 <= size:
            buf = f"{buf}\n{p}" if buf else p
        else:
            chunks.append(buf)
            buf = (buf[-overlap:] + "\n" + p) if overlap else p
    if buf:
        chunks.append(buf)
    return chunks

This counts characters, not tokens, to avoid depending on a tokenizer. 500 characters of English is roughly 100–150 tokens, well under both limits that matter here:

llama-text-embed-v2: max 2048 tokens per input, truncated by default beyond that ( For finer strategies (by sentence, by token, by heading), see Pinecone's Chunking Strategies.

Each chunk carries metadata: source (relative path), category (top-level folder such as hr/ or finance/) and chunk_index. category is used for filtering, source for citations and for cleaning up on re-ingest.

Anthropic doesn't offer an embedding model (Embeddings), so you need one from somewhere else. Three common options:

Option How Pros Cons
A. Pinecone integrated embedding (used here) Declare embed on a text field in the index schema; write and query plain text Least code, no dimension mismatches Limited to Pinecone-hosted models; text is processed by those models
B. Pinecone Inference, called separately pc.inference.embed(...) returns vectors you write to adense_vector field You own the vectors and can reuse them You must keep the model and dimension consistent between write and query
C. Third-party or self-hosted model Voyage AI (one of the providers Anthropic's docs point to), another cloud API, or an open-source model Any model, can be fully private Another service to run; you align dimensions and normalization yourself

Pinecone-hosted dense embedding models (docs as of November 2026):

Model Dimensions Max input Notes
llama-text-embed-v2 1024 (default) / 2048 / 768 / 512 / 384 2048 tokens Model page lists 26 supported languages
multilingual-e5-large 1024 507 tokens Multilingual; good for short queries + medium passages
text-embedding-3-small /-large up to 1536 / 3072, configurable 8192 tokens OpenAI models processed via Microsoft Azure; document indexes only
gemini-embedding-2 128–3072 8192 tokens Google model; not available on Starter

On the Standard plan, llama-text-embed-v2 is listed at $0.16 per million tokens and multilingual-e5-large at $0.08 (Pricing).

This walkthrough uses llama-text-embed-v2: multilingual, a roomier input limit than e5, and available on Starter. If your docs are mostly short Q&A pairs, try multilingual-e5-large: change the model name in the schema and rebuild the index.

In SDK v10 the recommended index type is the document index. You declare the scoring fields with SchemaBuilder; every other field you write is stored as metadata and indexed for filtering automatically. Adding an embed config to a string field makes Pinecone embed that field's text on write, and embed the query text at search time (Integrated embedding).

import os
from pinecone import Pinecone, SchemaBuilder

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

schema = (
    SchemaBuilder()
    .add_string_field(
        name="chunk_text",
        embed={"model": "llama-text-embed-v2", "dimension": 1024},
    )
    .build()
)

if not pc.indexes.exists(name="company-docs"):
    pc.indexes.create(
        name="company-docs",
        schema=schema,
        deployment={"deployment_type": "managed", "cloud": "aws", "region": "us-east-1"},
    )

Notes:

deployment_type: "managed" means serverless.spec=ServerlessSpec(...) is deprecated (the SDK source says it's translated into deployment). If you omit pc.indexes.create timeout=-1 to return immediately.source or category in it returns a 400 at creation.embed field also stores the original text and enables full-text search by default (store_text defaults to true), so search results can return chunk_text. Set store_text=False if you don't want the raw text stored in Pinecone, but then you'll have to look it up by ID in your own storage.

index = pc.index(name="company-docs")

documents = [
    {
        "_id": "3be9998a6b0d7b83#0",          # ASCII only
        "chunk_text": "Employees get 5 days of paid annual leave after one year of service...",
        "source": "hr/annual-leave.md",       # metadata, filterable automatically
        "category": "hr",
        "chunk_index": 0,
    },
]

for i in range(0, len(documents), 96):
    index.documents.upsert(namespace="default-tenant", documents=documents[i:i + 96])

Key points:

_id must be 1–512 ASCII characters.usage.embed_total_tokens in the response tells you how many embedding tokens the write used, which helps reconcile costs. Re-ingesting: if a file shrinks and produces fewer chunks, the old extra chunks linger. The script calls documents.delete(filter=...) by source before writing, to clear a file's old chunks.

resp = index.documents.search(
    namespace="default-tenant",
    top_k=12,
    score_by=[{"type": "embed", "fields": ["chunk_text"], "query": "How much annual leave do I get?"}],
    include_fields=["chunk_text", "source", "chunk_index"],
    filter={"category": {"$eq": "hr"}},     # optional metadata filter
)
for m in resp.matches:
    print(m.id, m.score, m.get("source"))

include_fields you only get _score. This is a classic beginner trap: the search "works" but returns no text. ["*"] returns every field.score_by; it can't be combined with a BM25 text clause. For hybrid search, the docs suggest running both searches and merging with RRF ( Vector recall uses a bi-encoder: question and documents are embedded separately and compared by distance. It's fast but coarse. A rerank model (cross-encoder) scores the question and each document together, which is more accurate but slower. The usual pattern is to over-recall with vectors (12 here) and rerank down to the best few (4 here) for the prompt.

ranked = pc.inference.rerank(
    model="bge-reranker-v2-m3",
    query="How much annual leave do I get?",
    documents=[{"id": m.id, "chunk_text": m.get("chunk_text", "")} for m in resp.matches],
    rank_fields=["chunk_text"],
    top_n=4,
    return_documents=False,
    parameters={"truncate": "END"},
)
for r in ranked.data:
    print(r.index, r.score)   # r.index is the position in the documents you passed in

Model choice: Pinecone currently hosts three rerank models: bge-reranker-v2-m3 (multilingual, available on Starter), cohere-rerank-4-fast (Standard and above, processed via Azure, possibly outside the US), and pinecone-rerank-v0 (deprecated; don't use it in new projects).

bge-reranker-v2-m3 defaults to truncate: NONE, which errors on over-long input, so END is set explicitly.

Older tutorials often do recall + rerank in one call via index.search(..., rerank={...}). That's for legacy integrated indexes (created with create_index_for_model, using the Records API). With document indexes, rerank is a separate pc.inference.rerank call.

Wrap each retrieved chunk in numbered XML tags so the model has anchors to cite:

SYSTEM_PROMPT = """You are an internal knowledge-base assistant. Answer employees' questions using ONLY the material inside <documents>.
Rules:
1. If the answer isn't in the material, say "I couldn't find this in the knowledge base." Don't make anything up.
2. After each key statement, cite the source as [n], where n matches <document index="n">.
3. If sources conflict, list the conflict instead of picking one.
4. Answer concisely."""

def build_user_message(question, hits):
    blocks = "\n".join(
        f'<document index="{i}" source="{h["source"]}">\n{h["text"]}\n</document>'
        for i, h in enumerate(hits, start=1)
    )
    return f"<documents>\n{blocks}\n</documents>\n\nQuestion: {question}"

Calling the Messages API:

import anthropic

client = anthropic.Anthropic()   # reads ANTHROPIC_API_KEY

response = client.beta.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=16000,
    system=SYSTEM_PROMPT,
    messages=[{"role": "user", "content": build_user_message(question, hits)}],
    output_config={"effort": "medium"},
    fallbacks="default",
    betas=["server-side-fallback-2026-07-01"],
)

if response.stop_reason == "refusal":
    print("The model declined to answer.")
else:
    print("".join(b.text for b in response.content if b.type == "text"))

What's different from older code:

response.content is a list of blocksthinking blocks. Filter on type == "text" instead of reading content[0].text. output_config.effort``low / medium / high / xhigh / max. Sonnet 5.5 defaults to temperature. 2026-10-21, the default in Anthropic SDK 2.x, rejects any request containing top_p or top_k with a 400. Delete the temperature=0 that old code often has.stop_reason == "refusal"`` fallbacks="default" plus the beta header server-side-fallback-2026-07-01 lets the server retry a refused request on a recommended fallback model (client.beta.messages.create. To skip the beta, drop the last two arguments and use client.messages.create; keep the refusal check either way.max_tokens Save as rag_bot.py:

"""
rag_bot.py - company-docs Q&A bot on Pinecone (integrated embedding + rerank) + Claude

Usage:
    python rag_bot.py ingest ./docs                        # chunk and upsert
    python rag_bot.py ask "How much annual leave do I get?"
    python rag_bot.py ask "expense process" --category finance   # search one category only

Versions at time of writing: pinecone==10.3.0, anthropic==2.4.0, python>=3.10
"""

import argparse
import hashlib
import os
import sys
import time
from pathlib import Path

import anthropic
from pinecone import Pinecone, SchemaBuilder
from pinecone.errors import NotFoundError

INDEX_NAME = os.environ.get("PINECONE_INDEX", "company-docs")
NAMESPACE = os.environ.get("PINECONE_NAMESPACE", "default-tenant")
EMBED_MODEL = "llama-text-embed-v2"      # Pinecone-hosted embedding model
EMBED_DIM = 1024                          # this model supports 384/512/768/1024/2048
RERANK_MODEL = "bge-reranker-v2-m3"      # Pinecone-hosted rerank model (multilingual)
USE_RERANK = os.environ.get("USE_RERANK", "1") == "1"
CLAUDE_MODEL = "claude-sonnet-5-5"

CHUNK_SIZE = 500        # target characters per chunk
CHUNK_OVERLAP = 80      # overlap between adjacent chunks
UPSERT_BATCH = 96       # max docs per upsert for indexes with an embed field
RECALL_TOP_K = 12       # vector recall size
FINAL_TOP_N = 4         # chunks sent to the prompt after rerank

pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
claude = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

def split_text(text: str, size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP) -> list[str]:
    """Split on blank lines, pack paragraphs up to `size`, hard-split long paragraphs with overlap."""
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
    chunks, buf = [], ""
    for p in paragraphs:
        if len(p) > size:                       # long paragraph: flush buffer, then sliding window
            if buf:
                chunks.append(buf)
                buf = ""
            step = size - overlap
            chunks.extend(p[i:i + size] for i in range(0, len(p), step))
            continue
        if len(buf) + len(p) + 1 <= size:
            buf = f"{buf}\n{p}" if buf else p
        else:
            chunks.append(buf)
            buf = (buf[-overlap:] + "\n" + p) if overlap else p
    if buf:
        chunks.append(buf)
    return chunks

def load_documents(root: Path) -> list[dict]:
    """Read .md / .txt files under root and build the documents to upsert."""
    docs = []
    for path in sorted(root.rglob("*")):
        if path.suffix.lower() not in {".md", ".txt"} or not path.is_file():
            continue
        rel = path.relative_to(root).as_posix()
        text = path.read_text(encoding="utf-8")
        file_key = hashlib.sha1(rel.encode("utf-8")).hexdigest()[:16]
        category = rel.split("/")[0] if "/" in rel else "general"
        for i, chunk in enumerate(split_text(text)):
            docs.append({
                "_id": f"{file_key}#{i}",
                "chunk_text": chunk,        # Pinecone embeds this field
                "source": rel,              # the rest is metadata, filterable automatically
                "category": category,
                "chunk_index": i,
            })
    return docs

def ensure_index() -> None:
    if pc.indexes.exists(name=INDEX_NAME):
        return
    schema = (
        SchemaBuilder()
        .add_string_field(
            name="chunk_text",
            embed={"model": EMBED_MODEL, "dimension": EMBED_DIM},
        )
        .build()
    )
    print(f"Creating index {INDEX_NAME} ...")
    pc.indexes.create(
        name=INDEX_NAME,
        schema=schema,
        deployment={"deployment_type": "managed", "cloud": "aws", "region": "us-east-1"},
    )  # blocks until the index is ready by default

def ingest(root: Path) -> None:
    ensure_index()
    index = pc.index(name=INDEX_NAME)
    docs = load_documents(root)
    if not docs:
        print("No .md / .txt files found")
        return

    for source in sorted({d["source"] for d in docs}):
        try:
            index.documents.delete(namespace=NAMESPACE, filter={"source": {"$eq": source}})
        except NotFoundError:
            pass  # namespace doesn't exist yet (first ingest)

    for i in range(0, len(docs), UPSERT_BATCH):
        batch = docs[i:i + UPSERT_BATCH]
        resp = index.documents.upsert(namespace=NAMESPACE, documents=batch)
        tokens = resp.usage.embed_total_tokens if resp.usage else "?"
        print(f"Upserted {i + len(batch)}/{len(docs)} chunks, embedding tokens this batch: {tokens}")

    time.sleep(5)  # indexing is asynchronous; wait a moment before querying
    print("Ingest complete")

def retrieve(question: str, category: str | None = None) -> list[dict]:
    index = pc.index(name=INDEX_NAME)
    resp = index.documents.search(
        namespace=NAMESPACE,
        top_k=RECALL_TOP_K,
        score_by=[{"type": "embed", "fields": ["chunk_text"], "query": question}],
        include_fields=["chunk_text", "source", "chunk_index"],
        filter={"category": {"$eq": category}} if category else None,
    )
    hits = [
        {
            "id": m.id,
            "score": m.score,
            "text": m.get("chunk_text", ""),
            "source": m.get("source", ""),
        }
        for m in resp.matches
    ]
    if not hits or not USE_RERANK:
        return hits[:FINAL_TOP_N]

    ranked = pc.inference.rerank(
        model=RERANK_MODEL,
        query=question,
        documents=[{"id": h["id"], "chunk_text": h["text"]} for h in hits],
        rank_fields=["chunk_text"],
        top_n=FINAL_TOP_N,
        return_documents=False,
        parameters={"truncate": "END"},
    )
    return [dict(hits[r.index], score=r.score) for r in ranked.data]

SYSTEM_PROMPT = """You are an internal knowledge-base assistant. Answer employees' questions using ONLY the material inside <documents>.
Rules:
1. If the answer isn't in the material, say "I couldn't find this in the knowledge base." Don't make anything up.
2. After each key statement, cite the source as [n], where n matches <document index="n">.
3. If sources conflict, list the conflict instead of picking one.
4. Answer concisely."""

def build_user_message(question: str, hits: list[dict]) -> str:
    blocks = "\n".join(
        f'<document index="{i}" source="{h["source"]}">\n{h["text"]}\n</document>'
        for i, h in enumerate(hits, start=1)
    )
    return f"<documents>\n{blocks}\n</documents>\n\nQuestion: {question}"

def answer(question: str, category: str | None = None) -> None:
    hits = retrieve(question, category)
    if not hits:
        print("Nothing was retrieved from the knowledge base.")
        return

    response = claude.beta.messages.create(
        model=CLAUDE_MODEL,
        max_tokens=16000,
        system=SYSTEM_PROMPT,
        messages=[{"role": "user", "content": build_user_message(question, hits)}],
        output_config={"effort": "medium"},
        fallbacks="default",
        betas=["server-side-fallback-2026-07-01"],
    )

    if response.stop_reason == "refusal":
        print("The model declined to answer.")
        return

    text = "".join(b.text for b in response.content if b.type == "text")
    print(text)
    print("\n---- Sources ----")
    for i, h in enumerate(hits, start=1):
        score = f"{h['score']:.4f}" if h["score"] is not None else "n/a"
        print(f"[{i}] {h['source']}  (score={score})")
    u = response.usage
    print(f"\n(model={response.model}, input_tokens={u.input_tokens}, output_tokens={u.output_tokens})")

def main() -> None:
    parser = argparse.ArgumentParser(description="Pinecone + Claude docs Q&A")
    sub = parser.add_subparsers(dest="cmd", required=True)
    p_ingest = sub.add_parser("ingest")
    p_ingest.add_argument("path", type=Path)
    p_ask = sub.add_parser("ask")
    p_ask.add_argument("question")
    p_ask.add_argument("--category", default=None)
    args = parser.parse_args()

    if args.cmd == "ingest":
        ingest(args.path)
    else:
        answer(args.question, args.category)

if __name__ == "__main__":
    try:
        main()
    except anthropic.APIStatusError as e:
        sys.exit(f"Claude API error {e.status_code}: {e.message}")

Prepare a folder whose top-level subfolders act as categories:

docs/
β”œβ”€β”€ hr/
β”‚   β”œβ”€β”€ annual-leave.md
β”‚   └── onboarding.md
└── finance/
    └── expenses.md
python rag_bot.py ingest ./docs
python rag_bot.py ask "Do I get annual leave in my first year?"
python rag_bot.py ask "What's the hotel limit on business trips?" --category finance
USE_RERANK=0 python rag_bot.py ask "How far ahead do I need to request leave?"   # no rerank quota used

The output shows the answer, the source file behind each [n] with its score, and the token usage for the call so you can check it against your bill.

These are list-price estimates, not a measured bill; your console usage is the source of truth.

Pinecone (as of November 2026, Pricing):

Plan Price Notes
Starter Free Limits in 2.1; over the limit requests get 429, never billed
Builder $20/month flat Higher limits; overages are also blocked, not billed
Standard $50/month minimum, then usage Storage $0.33/GB/month; reads $16–18 per million RU; writes $4–4.5 per million WU (varies by cloud/region); llama-text-embed-v2 $0.16 per million tokens;bge-reranker-v2-m3 $2 per 1,000 requests; egress $0.10/GB (100 GB/month included)
Enterprise $500/month minimum 99.95% uptime SLA

An internal knowledge base of a few hundred policy docs usually fits within Starter's 2 GB and 5M embedding tokens a month. The first limit you'll hit is usually 500 reranks a month. Either disable rerank on Starter, call it only when recall scores are too close to separate, or upgrade. Read/write unit accounting is detailed in Understanding cost.

Claude (as of November 2026, Pricing): claude-sonnet-5-5 is $2 per million input tokens, $10 per million output tokens, and $0.10 per million cache-hit tokens.

Rough math for one question with these settings: four chunks plus system prompt and question is a few thousand input tokens; the answer is a few hundred. Input cost is well under a cent; output cost depends on answer length, and thinking tokens are billed as output, which is why effort is worth tuning. For real numbers, multiply the input_tokens / output_tokens the script prints by the unit prices.

Ways to save:

FINAL_TOP_N aren't automatically better: 10 chunks instead of 4 is more than double the input tokens. If you generate vectors yourself (options B/C), the index dimension must match the model output. Writing with model A and querying with model B gives essentially random results even if the dimensions happen to match. Using an embed field with embed scoring avoids this structurally, because Pinecone uses the model fixed at creation for both. Remember: the schema and embedding config can't change after creation; a new model means a new index and a full re-ingest.

With option B, the quickstart pattern is pc.inference.embed(model=..., inputs=[...], parameters={"input_type": "passage"}), written into a field declared with add_dense_vector_field, and input_type="query" at search time. Omitting or swapping input_type won't error, but retrieval quality drops.

If the write and query namespaces differ, you get no error, just empty results. The script keeps the namespace in one env var. Namespaces are good for multi-tenant isolation (one per department or customer), but Starter caps an index at 100 namespaces, so many tenants need a higher plan or metadata filtering instead.

$eq, $in, $gte and friends.$match_phrase / $match_all / $match_any. chunk_index always an integer. Metadata filters are not access control. If finance policies should only be visible to finance staff, the server must add the filter based on the logged-in user; never trust a category sent from the client. For stricter isolation, use separate namespaces or even separate indexes.

truncate=END), so the tail never makes it into the vector. Indexing is asynchronous, so there's a short eventual-consistency window. A few seconds of sleep is enough in an ingest script; if your app needs read-after-write, handle it with a retry or a UI hint.

What old tutorials show In v10
pip install pinecone-client Package renamed to pinecone
pc.create_index(..., spec=ServerlessSpec(...)) spec deprecated; usepc.indexes.create(..., deployment={...})
pc.create_index_for_model(...) +index.upsert_records(...) +index.search(..., rerank=...) Creates a "legacy integrated index" on the old Records API; still works, but no text updates or import
from pinecone import ...preview... pinecone.preview removed in v10; document indexes andSchemaBuilder import frompinecone directly
Testing locally with Pinecone Local Pinecone Local reached end of life in 2026 ( Release notes )

messages to steer formatting; current models don't support prefill and return 400. Use output_config.format for structured output.stop_reason == "max_tokens"). Check these before rolling this out inside a company:

ap-southeast-1 and Europe) need a paid plan (global, possibly outside the US ( If your docs contain personal data or sensitive business information, get legal and security review first. If data can't leave your environment, this managed stack isn't the right fit; look at self-hostable options like pgvector or Milvus with a model you can run or contract for in-region.

string field of a document index, write plain text, let Pinecone embed it with documents.search with pc.inference.rerank with <documents> tags; Claude Sonnet 5.5 answers only from them, with citations. The upside is very little code and no models or databases to operate, which makes it a good way to test whether "Q&A over company docs" is worth building. The downsides are just as clear: Pinecone is a closed managed service and your data lives in its cloud; Starter's rerank and region limits are tight; and costs grow linearly with data and query volume, so reconcile your bill regularly.

Where to go next:

CHUNK_SIZE, effort. Pinecone and Anthropic iterate quickly; check versions before running, and where this article and the official docs disagree, trust the docs.

── more in #ai-tools 4 stories Β· sorted by recency
── more on @pinecone 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/build-a-q-a-bot-over…] indexed:0 read:19min 2026-11-04 Β· β€”