Skip to content
Memory & Knowledge

How to build AI memory with Pinecone and LangChain

3 min read

A working, client-separated retrieval setup: one Pinecone namespace per client, LangChain to load, split, embed and search.


Short answer: create a serverless Pinecone index, give each client its own namespace, use LangChain to load each client's documents, split them into chunks, embed them and upsert them into that client's namespace, then query only that namespace when you work on that client. Pinecone stores each namespace separately, so one client's data is not searched for another.

Steps follow the LangChain RAG tutorial and Pinecone's Implement multitenancy guide as of October 2026. This guide is for people comfortable with Python.

What you need

  • Python 3.10 or later.
  • A Pinecone account and API key, and an embeddings provider key (this example uses OpenAI).
  • Packages: pip install -U langchain langchain-openai langchain-pinecone langchain-text-splitters langchain-community pinecone

Step 1: Create the index

import os
from pinecone import Pinecone, ServerlessSpec

pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
if not pc.has_index("client-memory"):
    pc.create_index(
        name="client-memory",
        dimension=3072,  # matches text-embedding-3-large
        metric="cosine",
        spec=ServerlessSpec(cloud="aws", region="us-east-1"),
    )
index = pc.Index("client-memory")

The dimension must match your embeddings model.

Step 2: Index one client's documents into its namespace

LangChain's tutorial describes indexing in four steps: load, split, embed, store.

from pathlib import Path
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore

embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)

def index_client(client_slug: str, folder: str):
    docs = []
    for path in Path(folder).glob("*.md"):
        for d in TextLoader(str(path)).load():
            d.metadata.update({"client": client_slug, "source": path.name})
            docs.append(d)
    chunks = splitter.split_documents(docs)
    store = PineconeVectorStore(index=index, embedding=embeddings, namespace=client_slug)
    store.add_documents(chunks)

index_client("harbor-dental", "clients/harbor-dental")

Pinecone's guide: create a namespace by naming it on the first upsert, and every read and write targets one namespace.

Step 3: Retrieve for one client only

def ask(client_slug: str, question: str, k: int = 5):
    store = PineconeVectorStore(index=index, embedding=embeddings, namespace=client_slug)
    hits = store.similarity_search(question, k=k)
    context = "\n\n".join(f"[{h.metadata['source']}] {h.page_content}" for h in hits)
    return context  # pass this to your chat model with the question

print(ask("harbor-dental", "What did we decide about the spring campaign?"))

Always pass the namespace. A query without it searches the default namespace, not the client you meant.

Step 4: Keep it current

  • Re-index changed files. Delete the old chunks for a file (by id or by namespace) before you add the new version, or old and new will both be retrieved.
  • Schedule it. LangChain suggests you refresh the index on a schedule when documentation changes.
  • Offboard cleanly. Pinecone says offboarding a tenant is just deleting its namespace: index.delete(delete_all=True, namespace="harbor-dental").

Step 5: Add what retrieval does not do

Retrieval finds chunks that look like your question. For client work you will also want dated decisions that override older ones, a list of open commitments, and a way to record corrections. Those are extra tables and code on top of the index.

Sources

Checked October 9, 2026.

Related: Armbrain vs a DIY vector database · Agentic memory vs RAG · Mem0, Zep, Letta and other memory setups compared · All comparisons