> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pinecone.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-tenant RAG

> Sample Next.js multi-tenant RAG app that uses Pinecone namespaces to isolate document context per workspace.

export const ArrowNE = () => {
  return <svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21" fill="none">
            <path d="M4.92299 17.1668L3.8335 16.0773L14.5209 5.38992H4.71547V3.8335H17.1668V16.2849H15.6104V6.47941L4.92299 17.1668Z" fill="var(--text-primary)" />
        </svg>;
};

export const GithubIcon = () => {
  return <svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21" fill="none">
            <path fill-rule="evenodd" clip-rule="evenodd" d="M9.70928 4.05007C8.31793 4.21017 6.96422 4.84637 5.94277 5.82016C4.24747 7.43636 3.52414 9.77037 4.00655 12.0678C4.46605 14.2561 6.02227 16.0844 8.11272 16.892C8.44405 17.0199 8.60214 17.0277 8.72515 16.9219L8.81336 16.8461L8.82292 16.1332L8.83247 15.4202L8.73955 15.4406C8.50614 15.4919 8.10282 15.5127 7.86284 15.4858C7.40903 15.435 7.01153 15.2388 6.79946 14.9611C6.73983 14.883 6.61481 14.6617 6.52166 14.4695C6.33116 14.0762 6.15107 13.8536 5.86758 13.6611C5.5353 13.4355 5.54995 13.2709 5.90191 13.2757C6.03736 13.2776 6.13771 13.3031 6.28618 13.3737C6.55388 13.5008 6.72474 13.6554 6.91734 13.9445C7.19338 14.3588 7.39605 14.5261 7.74282 14.6258C7.98116 14.6943 8.38716 14.6732 8.66027 14.5782L8.85297 14.5111L8.88852 14.332C8.93357 14.1048 9.04413 13.8664 9.17182 13.721C9.2859 13.591 9.28559 13.5908 8.97604 13.5556C8.70198 13.5243 8.21426 13.399 7.95469 13.2931C7.5852 13.1423 7.38567 13.008 7.08345 12.7068C6.57847 12.2034 6.3554 11.644 6.28281 10.6989C6.22063 9.88945 6.37347 9.29167 6.78619 8.73013L6.93636 8.5258L6.87447 8.29003C6.79346 7.98149 6.80996 7.41637 6.91027 7.06404C6.99339 6.77209 7.03036 6.74053 7.2572 6.76816C7.62556 6.81299 8.05296 6.98479 8.54616 7.28629L8.81336 7.44963L9.14344 7.38346C9.73562 7.26472 10.0045 7.24354 10.6681 7.26331C11.2515 7.28069 11.4102 7.29927 11.9297 7.4109L12.1225 7.45233L12.3541 7.30892C12.8263 7.01647 13.2498 6.83572 13.5901 6.78137C13.9054 6.73098 13.9339 6.74657 14.0112 7.01148C14.1466 7.47534 14.1575 8.05323 14.0375 8.40003C13.9956 8.52102 13.9958 8.52159 14.1525 8.72787C14.3401 8.97477 14.5238 9.35728 14.5959 9.65114C14.7536 10.2933 14.6496 11.354 14.3681 11.9748C14.0175 12.7481 13.4125 13.2118 12.4285 13.4617C12.2729 13.5012 12.0607 13.5432 11.9569 13.555C11.6405 13.5909 11.6372 13.5933 11.7462 13.708C11.848 13.8152 11.973 14.0466 12.0373 14.2471C12.0657 14.336 12.0829 14.7354 12.0959 15.6095L12.1141 16.8461L12.2023 16.9219C12.3253 17.0277 12.4834 17.0199 12.8148 16.892C15.1496 15.99 16.7734 13.8552 17.0364 11.3417C17.0826 10.9001 17.0576 9.99571 16.9878 9.5789C16.6755 7.71598 15.6173 6.10421 14.016 5.05281C13.2185 4.52921 12.1702 4.15856 11.171 4.0469C10.8383 4.00971 10.0449 4.01144 9.70928 4.05007Z" fill="var(--text-primary)" />
        </svg>;
};

export const InlineCode = ({copyCode, displayCode, copy}) => {
  const copyToClipboard = async e => {
    await navigator.clipboard.writeText(copyCode);
    const button = e.target.closest("button");
    button.innerHTML = `<svg  width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform: translateY(-3px)"><path d="M14.7813 1.21873C15.0751 1.51248 15.0751 1.98748 14.7813 2.2781L6.53135 10.5312C6.2376 10.825 5.7626 10.825 5.47197 10.5312L1.21885 6.28123C0.925098 5.98748 0.925098 5.51248 1.21885 5.22185C1.5126 4.93123 1.9876 4.9281 2.27822 5.22185L5.99697 8.9406L13.7188 1.21873C14.0126 0.924976 14.4876 0.924976 14.7782 1.21873H14.7813Z" fill="var(--brand-blue)"></path></svg>`;
    setTimeout(() => {
      button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none">
      <path d="M14.2502 0.833496H4.25016C3.3335 0.833496 2.5835 1.5835 2.5835 2.50016V14.1668H4.25016V2.50016H14.2502V0.833496ZM16.7502 4.16683H7.5835C6.66683 4.16683 5.91683 4.91683 5.91683 5.8335V17.5002C5.91683 18.4168 6.66683 19.1668 7.5835 19.1668H16.7502C17.6668 19.1668 18.4168 18.4168 18.4168 17.5002V5.8335C18.4168 4.91683 17.6668 4.16683 16.7502 4.16683ZM16.7502 17.5002H7.5835V5.8335H16.7502V17.5002Z" fill="var(--brand-blue)" fill-opacity="0.38"/>
      </svg>`;
    }, 2000);
  };
  return <div className="relative">
      <code className="inline-flex gap-2 items-center py-2 pl-3 pr-10 custom-code">
        {displayCode}
      </code>

      {copy && <div className="absolute group right-3 top-1/2 -translate-y-1/2" style={{
    width: "1.375rem",
    height: "1.375rem"
  }}>
          <button onClick={e => copyToClipboard(e)}>
            <svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none">
              <path d="M14.2502 0.833496H4.25016C3.3335 0.833496 2.5835 1.5835 2.5835 2.50016V14.1668H4.25016V2.50016H14.2502V0.833496ZM16.7502 4.16683H7.5835C6.66683 4.16683 5.91683 4.91683 5.91683 5.8335V17.5002C5.91683 18.4168 6.66683 19.1668 7.5835 19.1668H16.7502C17.6668 19.1668 18.4168 18.4168 18.4168 17.5002V5.8335C18.4168 4.91683 17.6668 4.16683 16.7502 4.16683ZM16.7502 17.5002H7.5835V5.8335H16.7502V17.5002Z" fill="var(--brand-blue)" className="opacity-40 group-hover:opacity-100 transition-opacity" />
            </svg>
          </button>
        </div>}
    </div>;
};

<div className="sample-app">
  <div className="sample-app-heading">
    <span className="eyebrow">SAMPLE APP</span>

    # Multi-tenant RAG

    A simple multi-tenant RAG app to chat with documents isolated by namespace

    <InlineCode copyCode="npx create-pinecone-app@latest --template namespace-notes" displayCode={<span><span style={{color: "#215CCE"}}>$</span> npx create-pinecone-app@latest --template namespace-notes</span>} copy />
  </div>

  <div className="w-full h-fit rounded-lg " style={{background: "#121142", margin: "4rem 0 3rem"}}>
    <div className="container py-50">
      <iframe id="sample-app-iframe" className=" border-[0.5px] border-gray-300 rounded-lg shadow-xl" src="https://namespace-notes.sample-app.pinecone.io/" width="100%" height="700px" allow="clipboard-write" allowTransparency="true" />
    </div>
  </div>

  <div className="sample-app-split">
    <div className="content no-margin">
      Namespace Notes is a simple multi-tenant RAG app. The application allows users to create workspaces, upload documents to Pinecone, and to feed the workspace’s chatbot with custom context.
      This concept can be used to store anywhere from just a few documents, to many billions of contextual embeddings.
    </div>

    <div className="sidebar">
      <InlineCode copyCode="npx create-pinecone-app --template namespace-notes" displayCode={<span><span style={{color: "#215CCE"}}>$</span> npx create-pinecone-app@latest --template namespace-notes</span>} copy />

      <a href="https://github.com/pinecone-io/sample-apps/tree/main/namespace-notes" target="_blank" className="flex items-center gap-2 no-underline mt-6"><GithubIcon /> Github</a>
      <a href="https://namespace-notes.sample-app.pinecone.io/" target="_blank" className="flex items-center gap-2 no-underline mt-4"><ArrowNE /> Open in a new window</a>
    </div>
  </div>

  ***

  <div className="sample-app-split">
    <div className="content relative mt-8 prose prose-gray dark:prose-invert">
      ## Built with

      * Pinecone Serverless
      * Vercel AI SDK + OpenAI
      * Next.js + tailwind
      * Node version 20 or higher

      ***

      ## Run the sample app

      The fastest way to get started is to use the `create-pinecone-app` CLI tool to get up and running:

      ```bash theme={null}
      npx -y create-pinecone-app@latest --template namespace-notes
      ```

      ### Get your API key

      You need an API key to make API calls to your Pinecone project:

      <div style={{minWidth: '450px', minHeight:'152px'}}>
        <div id="pinecone-connect-widget">
          <div class="connect-widget-skeleton">
            <div class="skeleton-content" />
          </div>
        </div>
      </div>

      Then copy your generated key:

      ```
      PINECONE_API_KEY="{{YOUR_API_KEY}}"
      ```

      Alternatively, follow these steps:

      1. Open the Pinecone console.
      2. Select your project.
      3. Go to **API Keys**.
      4. Copy your API key.

      ### Create a Pinecone serverless index

      Create a Pinecone index for this project.
      The index should have the following properties:

      * **dimension**: `1536`
        You can change this as long as you change the default embedding model.
      * **metric**: `cosine`
      * **region**: `us-east-1`

      You can create the index [in the console](https://app.pinecone.io/organizations/-/projects/-/create-index/serverless),
      or by following the instructions [here](https://docs.pinecone.io/guides/get-started/quickstart#4-create-a-serverless-index).

      ### Start the project

      **Requires Node version 20+**

      To start the project, clone the [sample-apps repo](https://github.com/pinecone-io/sample-apps) and navigate to the `namespace-notes` directory.
      You will need two separate terminal instances, one for running the client and one for the server.

      #### Client setup

      From the project root directory, run the following command:

      ```bash theme={null}
      cd client && npm install
      ```

      Make sure you have populated the client `.env` with relevant keys:

      ```bash theme={null}
      # You must first activate a Billing Account here: https://platform.openai.com/account/billing/overview
      # Then get your OpenAI API Key here: https://platform.openai.com/account/api-keys
      OPENAI_API_KEY="your-api-key-here"

      # The URL of the server (only used for development)
      SERVER_URL="http://localhost:4001"
      ```

      Start the client:

      ```bash theme={null}
      npm run dev
      ```

      #### Server setup

      From the project root directory, run the following command:

      ```bash theme={null}
      cd server && npm install
      ```

      Make sure you have populated the server `.env` with relevant keys:

      ```bash theme={null}
      PINECONE_API_KEY="your_pinecone_api_key_here"
      PINECONE_INDEX_NAME="your_pinecone_index_name_here"
      OPENAI_API_KEY="your_openai_api_key_here"

      # Digital Ocean Spaces (OPTIONAL - for public file hosting)
      DO_SPACES_ACCESS_KEY_ID="your_do_spaces_access_key_id_here"
      DO_SPACES_SECRET_ACCESS_KEY="your_do_spaces_secret_access_key_here"
      DO_SPACES_ENDPOINT="your_do_spaces_endpoint_here"
      DO_SPACES_BUCKET_NAME="your_do_spaces_bucket_name_here"
      ```

      Start the server:

      ```bash theme={null}
      npm run dev
      ```

      <Note> You may notice that Digital Ocean Spaces is available as document storage. Using Digital Ocean Spaces is entirely optional. The project has a class defined to store document files locally on the server for quick project spin-up. </Note>

      ## Project structure

      ![image](https://github.com/pinecone-io/sample-apps/assets/24496327/5f13972f-386d-4a04-84f7-6e89cb95f124)

      In this example we opted to use a simple client/server structure. We separate the frontend from the backend in this manner in case you'd like to swap either out with a stack of your choice.

      **Frontend Client**

      The frontend uses Next.js, tailwind and components from Vercel's AI SDK to power the chatbot experience. It also leverages API routes to make calls to the server to fetch document references and context for both the UI and chatbot LLM.
      The client uses local storage to store workspace information.

      **Backend Server**

      This project uses Node.js and Express to handle file uploads, validation checks, chunking, upsertion, context provision etc. Learn more about the implementation details below.

      ### Simple multi-tenant RAG methodology

      This project uses a basic RAG architecture that achieves multitenancy through the use of namespaces. Files are uploaded to the server where they are chunked, embedded and upserted into Pinecone.

      **Tenant isolation**

      We use namespaces as the mechanism to separate context between workspaces. When we add documents, we check for a namespaceId or generate a new id if the workspace is being created.

      ```typescript theme={null}
      /**
      * Adds a new document.
      * @param req - The request object.
      * @param res - The response object.
      * @returns A promise that resolves to the added document.
      */
      async addDocuments(req: Request, res: Response) {
      // This is effectively the ID of the workspace / tenant
      let namespaceId = req.body.namespaceId;
      //...
      ```

      **Chunking**

      This project uses a basic paragraph chunking approach. We use `pdf-parse` to stream and parse pdf content and leverage a best effort paragraph chunking strategy with a defined `minChunkSize` and `maxChunkSize` to
      account for documents with longer or shorter paragraph sizes. This helps us provide sizable content chunks for our Pinecone record metadata which will later be used by the LLM during retreival.

      ```typescript theme={null}
      /**
       * Splits a given text into chunks of 1 to many paragraphs.
      *
      * @param text - The input text to be chunked.
      * @param maxChunkSize - The maximum size (in characters) allowed for each chunk. Default is 1000.
      * @param minChunkSize - The minimum size (in characters) required for each chunk. Default is 100.
      * @returns An array of chunked text, where each chunk contains 1 or multiple "paragraphs"
      */
      function chunkTextByMultiParagraphs(
      text: string,
      maxChunkSize = 1500,
      minChunkSize = 500
      ): string[] {
      const chunks: string[] = [];
      let currentChunk = "";

      let startIndex = 0;
      while (startIndex < text.length) {
          let endIndex = startIndex + maxChunkSize;
          if (endIndex >= text.length) {
          endIndex = text.length;
          } else {
          // Just using this to find the nearest paragraph boundary
          const paragraphBoundary = text.indexOf("\n\n", endIndex);
          if (paragraphBoundary !== -1) {
              endIndex = paragraphBoundary;
          }
          }

          const chunk = text.slice(startIndex, endIndex).trim();
          if (chunk.length >= minChunkSize) {
          chunks.push(chunk);
          currentChunk = "";
          } else {
          currentChunk += chunk + "\n\n";
          }

          startIndex = endIndex + 1;
      }

      if (currentChunk.length >= minChunkSize) {
          chunks.push(currentChunk.trim());
      } else if (chunks.length > 0) {
          chunks[chunks.length - 1] += "\n\n" + currentChunk.trim();
      } else {
          chunks.push(currentChunk.trim());
      }

      return chunks;
      }
      ```

      **Embedding**

      Once we have our chunks we embed them in batches using [`text-embedding-3-small`](https://www.pinecone.io/models/text-embedding-3-small/)

      ```typescript theme={null}

      /**
       * Embed a piece of text using an embedding model or service.
      * This is a placeholder and needs to be implemented based on your embedding solution.
      *
      * @param text The text to embed.
      * @returns The embedded representation of the text.
      */
      export async function embedChunks(chunks: string[]): Promise<any> {
      // You can use any embedding model or service here.
      // In this example, we use OpenAI's text-embedding-3-small model.
      const openai = new OpenAI({
          apiKey: config.openAiApiKey,
          organization: config.openAiOrganizationId,
      });
      try {
          const response = await openai.embeddings.create({
          model: "text-embedding-3-small",
          input: chunks,
          encoding_format: "float",
          dimensions: 1536,
          });
          return response.data;
      } catch (error) {
          console.error("Error embedding text with OpenAI:", error);
          throw error;
      }
      }
      ```

      **RAG document management**

      In order to store multiple documents within a particular namespace we need a convention that allows us to target the chunks belonging to a particular document.

      We do this through id prefixing. We generate a document Id for each uploaded document, and then before uposertion we assign it as a prefix to the particular chunk id.
      The below example uses the document id with an appended chunk id separated by a '`:`' symbol.

      ```typescript theme={null}
      // Combine the chunks and their corresponding embeddings
      // Construct the id prefix using the documentId and the chunk index
      for (let i = 0; i < chunks.length; i++) {
      document.chunks.push({
          id: `${document.documentId}:${i}`,
          values: embeddings[i].embedding,
          text: chunks[i],
      });
      ```

      This comes in handy for targeted document updates and deletions.

      **Upsertion**

      Lastly, we upsert our embeddings to the Pinecone Namespace associated with the tenant in the form of a `PineconeRecord`.
      This allows us to provide the reference text and url as metadata for use by our retreival system.

      ```typescript theme={null}
      /**
       * Upserts a document into the specified Pinecone namespace.
      * @param document - The document to upsert.
      * @param namespaceId - The ID of the namespace.
      */
      async upsertDocument(document: Document, namespaceId: string) {
          // Adjust to use namespaces if you're organizing data that way
          const namespace = index.namespace(namespaceId);

          const vectors: PineconeRecord<RecordMetadata>[] = document.chunks.map(
          (chunk) => ({
              id: chunk.id,
              values: chunk.values,
              metadata: {
              text: chunk.text,
              referenceURL: document.documentUrl,
              },
          })
          );

          // Batch the upsert operation
          const batchSize = 200;
          for (let i = 0; i < vectors.length; i += batchSize) {
          const batch = vectors.slice(i, i + batchSize);
          await namespace.upsert(batch);
          }
      }
      ```

      **Context**

      When a user asks a question via the frontend chat component, the Vercel AI SDK leverages the `/chat` endpoint for retrieval.
      We then send the `top_k` most similar results back from Pinecone via our context route.

      We populate a `CONTEXT BLOCK` that is wrapped with system prompt instructions for our chosen LLM to take advantage of in the response output.

      It's important to note that different LLMs will have different context windows, so your choice of LLM will influence the `top_k` value you should return from Pinecone and along with the size of your chunks.
      If the context block / prompt is longer than the context window of the LLM, it will not be fully included in generation results.

      ```typescript theme={null}
      import { getContext } from "./context";

      export async function createPrompt(messages: any[], namespaceId: string) {
      try {
          // Get the last message
          const lastMessage = messages[messages.length - 1]["content"];

          // Get the context from the last message
          const context = await getContext(lastMessage, namespaceId);

          const prompt = [
          {
              role: "system",
              content: `AI assistant is a brand new, powerful, human-like artificial intelligence.
              DO NOT SHARE REFERENCE URLS THAT ARE NOT INCLUDED IN THE CONTEXT BLOCK.
              AI assistant will not apologize for previous responses, but instead will indicated new information was gained.
              If user asks about or refers to the current "workspace" AI will refer to the the content after START CONTEXT BLOCK and before END OF CONTEXT BLOCK as the CONTEXT BLOCK. 
              If AI sees a REFERENCE URL in the provided CONTEXT BLOCK, please use reference that URL in your response as a link reference right next to the relevant information in a numbered link format e.g. ([reference number](link))
              If link is a pdf and you are CERTAIN of the page number, please include the page number in the pdf href (e.g. .pdf#page=x ).
              If AI is asked to give quotes, please bias towards providing reference links to the original source of the quote.
              AI assistant will take into account any CONTEXT BLOCK that is provided in a conversation. It will say it does not know if the CONTEXT BLOCK is empty.
              AI assistant will not invent anything that is not drawn directly from the context.
              AI assistant will not answer questions that are not related to the context.
              START CONTEXT BLOCK
              ${context}
              END OF CONTEXT BLOCK
          `,
          },
          ];
          return { prompt };
      } catch (e) {
          throw e;
      }
      }
      ```

      **Document deletion**

      To delete a document from a particular workspace, we need to perform a targeted deletion of the RAG document. Luckily, we can take advantage of the id prefixing strategy we employed earlier to perform a deletion of a specific document.
      We use our `documentId:` to identify all the chunks associated with a particular document and then we perform deletions until we have successfully deleted all document chunks.

      ```typescript theme={null}
      // We retreive a paginated list of chunks from the namespace
      const listResult = await namespace.listPaginated({
      prefix: `${documentId}:`,
      limit: limit,
      paginationToken: paginationToken,
      });

      ...

      // Then we delete each of the chunks based on their ids
      async deleteDocumentChunks(chunkIds: string[], namespaceId: string) {
      console.log("Deleting Document Chunks")
      const namespace = index.namespace(namespaceId);
      await namespace.deleteMany(chunkIds);
      }
      ```

      **Workspace deletion** (offboarding)

      This is even simpler to achieve. If we have the workspace / namespaceId at our disposal, we can call `deleteAll()` on the relevant namespace.

      ```typescript theme={null}
      /**
       * Deletes a Pinecone namespace.
      * 
      * @param namespaceId - The ID of the namespace to delete all records.
      * @returns A Promise that resolves when the namespace is deleted successfully.
      */
      async deletePineconeNamespace(namespaceId: string) {
          console.log("Deleting Workspace")
          const namespace = index.namespace(namespaceId);
          await namespace.deleteAll();
          console.log("Workspace deleted from Pinecone successfully")
      }
      ```

      ***

      ## Further optimizations for the RAG pipeline

      This is a relatively simple RAG pipeline - in practice there are improvements that could be made depending on a particular set of requirements.

      **Using rerankers**

      For example, a reranker could be used in order to provide the most relevant set of retrieved results from Pinecone to the LLM.
      A reranker could allow us to increase the `top_k` requested from Pinecone significantly and then constrain the output to a highly relevant set of records ordered by relevance all while abiding by the context length restrictions of the LLM.

      Follow our [RAG series for more optimizations](https://www.pinecone.io/learn/series/rag/)

      **Optimizing chunking strategy**

      This project uses a paragraph chunker, which can provide good results for some use cases. Often, the quality of a chunk will play a significant role in the quality of the retrieval system as a whole.

      Learn more about various [chunking strategies](https://www.pinecone.io/learn/chunking-strategies/)

      **Enhancing metadata structure**

      The metadata in this project consists simply of a reference url to the original content and the particular text snippet. You could extract richer metadata from the PDFs to provide improved context to the LLM.
      This, of course, assumes a given PDF upload contains additional metadata and that it would be useful (page count, title, author(s), etc).

      Read more about [vectorizing structured text](https://www.pinecone.io/learn/structured-data/).

      ## Troubleshooting

      Experiencing any issues with the sample app?
      [Submit an issue, create a PR](https://github.com/pinecone-io/sample-apps/), or post in our [community forum](https://community.pinecone.io)!
    </div>

    <div className="sidebar toc" />
  </div>
</div>
