300 lezingen
300 lezingen

Hoe een slimme documentatie op te bouwen - op basis van OpenAI-embeddings (chunking, indexeren en zoeken)

door Aymeric PINEAU13m2025/05/01
Read on Terminal Reader

Te lang; Lezen

Het belangrijkste idee is om documentatie te indexeren door ze te splitsen in beheersbare stukjes, embeddings te genereren met OpenAI en een vergelijkbaarheidszoek uit te voeren om de meest relevante informatie te vinden en terug te geven aan de query van een gebruiker.
featured image - Hoe een slimme documentatie op te bouwen - op basis van OpenAI-embeddings (chunking, indexeren en zoeken)
Aymeric PINEAU HackerNoon profile picture
0-item

Hallo iedereen! ik wilde mijn benadering van het maken van een "slimme documentatie" chatbot voor een project waar ik aan werk delen.I’m not an AI expert, so any suggestions or improvements are more than welcome!


Het doel van dit bericht is niet om een andere tutorial te maken over het bouwen van een chatbot op basis van OpenAI. Er is al veel inhoud over dat onderwerp.index documentationDoor ze te splitsen in beheersbarechunksHet genererenembeddingsmet OpenAI, enperforming a similarity searchom de meest relevante informatie voor de query van een gebruiker te vinden en te retourneren.


In mijn geval zal de documentatie Markdown-bestanden zijn, maar het kan elke vorm van tekst, database-object, etc. zijn.

Waarom dan?

Omdat het soms moeilijk kan zijn om de informatie te vinden die je nodig hebt, wilde ik een chatbot maken die vragen over een bepaald onderwerp kon beantwoorden en relevante context uit de documentatie kon verstrekken.


Deze assistent kan op verschillende manieren worden gebruikt, zoals:

  • Snelle antwoorden op vaak gestelde vragen
  • Zoeken naar een doc/pagina zoals Algolia doet
  • Gebruikers helpen de informatie te vinden die ze nodig hebben in een specifiek document
  • Gebruikersproblemen/vragen ophalen door de gestelde vragen op te slaan

Samenvatting

Hieronder zal ik de drie belangrijkste delen van mijn oplossing uiteenzetten:

  1. Documentatiebestanden lezen
  2. Indexeren van de documentatie (chunken, overlappen en invoegen)
  3. Het zoeken naar de documentatie (en het koppelen tot een chatbot)

Filterboom

.
└── docs
    └── ...md
└── src
    └── askDocQuestion.ts
    └── index.ts # Express.js application endpoint
└── embeddings.json # Storage for embeddings
└── packages.json

1. lees documentatie bestanden

In plaats van de tekst van de documentatie te hardcoderen, kunt u een map scannen.mdbestanden met behulp van tools zoalsglob.

// Example snippet of fetching files from a folder:
import fs from "node:fs";
import path from "node:path";
import glob from "glob";

const DOC_FOLDER_PATH = "./docs";

type FileData = {
  path: string;
  content: string;
};

const readAllMarkdownFiles = (): FileData[] => {
  const filesContent: FileData[] = [];
  const filePaths = glob.sync(`${DOC_FOLDER_PATH}/**/*.md`);

  filePaths.forEach((filePath) => {
    const content = fs.readFileSync(filePath, "utf8");
    filesContent.push({ path: filePath, content });
  });

  return filesContent;
};

Als alternatief kunt u natuurlijk uw documentatie ophalen uit uw database of CMS enz.

Als alternatief kunt u natuurlijk uw documentatie ophalen uit uw database of CMS enz.


Indexeren van de documentatie

Om onze zoekmachine te maken, gebruiken we OpenAI'sVector embeddings APIom onze embeddings te genereren.


Vector embeddings zijn een manier om gegevens te vertegenwoordigen in een numerieke vorm, die kan worden gebruikt om vergelijkingszoekingen uit te voeren (in ons geval, tussen de gebruikersvraag en onze documentatie secties).


Deze vector, bestaande uit een lijst van drijvende puntnummers, wordt gebruikt om de overeenkomst te berekenen met behulp van een wiskundige formule.

[
  -0.0002630692, -0.029749284, 0.010225477, -0.009224428, -0.0065269712,
  -0.002665544, 0.003214777, 0.04235309, -0.033162255, -0.00080789323,
  //...+1533 elements
];

Op basis van dit concept werd de Vector Database gemaakt.In plaats van de OpenAI API te gebruiken, is het mogelijk om een vectordatabase zoals Chroma, Qdrant of Pinecone te gebruiken.

Op basis van dit concept werd de Vector Database gemaakt.In plaats van de OpenAI API te gebruiken, is het mogelijk om een vectordatabase zoals Chroma, Qdrant of Pinecone te gebruiken.

2.1 Chunk elk bestand & Overlap

Grote blokken tekst kunnen de contextlimieten van het model overschrijden of minder relevante hits veroorzaken, dus het wordt aanbevolen om ze in stukken te splitsen om de zoekopdracht doelgerichter te maken. Om echter enige continuïteit tussen stukken te behouden, overlappen we ze met een bepaald aantal tokens (of tekens).

Voorbeeld van Chunking

In dit voorbeeld hebben we een lange tekst die we willen verdelen in kleinere stukjes.In dit geval willen we stukjes van 100 tekens maken en ze overlappen met 50 tekens.


Full Text (406 characters):

In het hart van de drukke stad stond er een oude bibliotheek die velen hadden vergeten.Zijn torenhoge planken waren gevuld met boeken van elk denkbaar genre, elk fluisterend verhalen van avonturen, mysteries en tijdloze wijsheid.Elke avond opende een toegewijde bibliothecaris zijn deuren en verwelkomde nieuwsgierige geesten die verlangden om de enorme kennis binnen te verkennen.


  • Chunk 1 (Characters 1-150):

    In the heart of the bustling city, there stood an old library that many had forgotten. Its towering shelves were filled with books from every imaginabl.

  • Chunk 2 (Characters 101-250):

    shelves were filled with books from every imaginable genre, each whispering stories of adventures, mysteries, and timeless wisdom. Every evening, a d

  • Chunk 3 (Characters 201-350):

    ysteries, and timeless wisdom. Every evening, a dedicated librarian would open its doors, welcoming curious minds eager to explore the vast knowledge

  • Chunk 4 (Characters 301-406):

    curious minds eager to explore the vast knowledge within. Children would gather for storytelling sessions.

Code Snippet

const CHARS_PER_TOKEN = 4.15; // Approximate pessimistically number of characters per token. Can use `tiktoken` or other tokenizers to calculate it more precisely

const MAX_TOKENS = 500; // Maximum number of tokens per chunk
const OVERLAP_TOKENS = 100; // Number of tokens to overlap between chunks

const maxChar = MAX_TOKENS * CHARS_PER_TOKEN;
const overlapChar = OVERLAP_TOKENS * CHARS_PER_TOKEN;

const chunkText = (text: string): string[] => {
  const chunks: string[] = [];
  let start = 0;

  while (start < text.length) {
    let end = Math.min(start + maxChar, text.length);

    // Don’t cut a word in half if possible:
    if (end < text.length) {
      const lastSpace = text.lastIndexOf(" ", end);
      if (lastSpace > start) end = lastSpace;
    }

    chunks.push(text.substring(start, end));
    // Overlap management
    const nextStart = end - overlapChar;
    start = nextStart <= start ? end : nextStart;
  }

  return chunks;
};

Om meer te weten te komen over chunking en de impact van de grootte op de inbreng, kunt u dit artikel bekijken.

Om meer te weten te komen over chunking en de impact van de grootte op de inbreng, kunt uDit artikel.

2.2 Inbedding generatie

Zodra een bestand is gesplitst, genereren we vector embeddings voor elk stuk met behulp van de API van OpenAI (bijv.text-embedding-3-large) van

import { OpenAI } from "openai";

const EMBEDDING_MODEL: OpenAI.Embeddings.EmbeddingModel =
  "text-embedding-3-large"; // Model to use for embedding generation

const openai = new OpenAI({ apiKey: OPENAI_API_KEY });

const generateEmbedding = async (textChunk: string): Promise<number[]> => {
  const response = await openai.embeddings.create({
    model: EMBEDDING_MODEL,
    input: textChunk,
  });

  return response.data[0].embedding; // Return the generated embedding
};

2.3 Het genereren en opslaan van embeddings voor het hele bestand

Om te voorkomen dat de embeddings elke keer regenereren, slaan we de embeddings op. Het kan worden opgeslagen in een database. Maar in dit geval slaan we het gewoon lokaal op in een JSON-bestand.


De volgende code is eenvoudig:

  1. iteraties over elk document,
  2. het document in stukken snijdt,
  3. het genereren van embeddings voor elke chunk,
  4. Het opslaan van de integraties in een JSON-bestand.
  5. Vul de vectorStore in met de embeddings die worden gebruikt in de zoekopdracht.
import embeddingsList from "../embeddings.json";

/**
 * Simple in-memory vector store to hold document embeddings and their content.
 * Each entry contains:
 * - filePath: A unique key identifying the document
 * - chunkNumber: The number of the chunk within the document
 * - content: The actual text content of the chunk
 * - embedding: The numerical embedding vector for the chunk
 */
const vectorStore: {
  filePath: string;
  chunkNumber: number;
  content: string;
  embedding: number[];
}[] = [];

/**
 * Indexes all Markdown documents by generating embeddings for each chunk and storing them in memory.
 * Also updates the embeddings.json file if new embeddings are generated.
 */
export const indexMarkdownFiles = async (): Promise<void> => {
  // Retrieve documentations
  const docs = readAllMarkdownFiles();

  let newEmbeddings: Record<string, number[]> = {};

  for (const doc of docs) {
    // Split the document into chunks based on headings
    const fileChunks = chunkText(doc.content);

    // Iterate over each chunk within the current file
    for (const chunkIndex of Object.keys(fileChunks)) {
      const chunkNumber = Number(chunkIndex) + 1; // Chunk number starts at 1
      const chunksNumber = fileChunks.length;

      const chunk = fileChunks[chunkIndex as keyof typeof fileChunks] as string;

      const embeddingKeyName = `${doc.path}/chunk_${chunkNumber}`; // Unique key for the chunk

      // Retrieve precomputed embedding if available
      const existingEmbedding = embeddingsList[
        embeddingKeyName as keyof typeof embeddingsList
      ] as number[] | undefined;

      let embedding = existingEmbedding; // Use existing embedding if available

      if (!embedding) {
        embedding = await generateEmbedding(chunk); // Generate embedding if not present
      }

      newEmbeddings = { ...newEmbeddings, [embeddingKeyName]: embedding };

      // Store the embedding and content in the in-memory vector store
      vectorStore.push({
        filePath: doc.path,
        chunkNumber,
        embedding,
        content: chunk,
      });

      console.info(`- Indexed: ${embeddingKeyName}/${chunksNumber}`);
    }
  }

  /**
   * Compare the newly generated embeddings with existing ones
   *
   * If there is change, update the embeddings.json file
   */
  try {
    if (JSON.stringify(newEmbeddings) !== JSON.stringify(embeddingsList)) {
      fs.writeFileSync(
        "./embeddings.json",
        JSON.stringify(newEmbeddings, null, 2)
      );
    }
  } catch (error) {
    console.error(error);
  }
};

3. zoeken naar documentatie

3.1 Vectorgelijkheid

Om de vraag van een gebruiker te beantwoorden, genereren we eerst een embedding voor deuser's questionen bereken vervolgens de cosine-gelijkheid tussen de invoeging van de query en de invoeging van elk stukje. We filteren alles uit onder een bepaalde gelijkenisdrempel en houden alleen de top X-matches.

/**
 * Calculates the cosine similarity between two vectors.
 * Cosine similarity measures the cosine of the angle between two vectors in an inner product space.
 * Used to determine the similarity between chunks of text.
 *
 * @param vecA - The first vector
 * @param vecB - The second vector
 * @returns The cosine similarity score
 */
const cosineSimilarity = (vecA: number[], vecB: number[]): number => {
  // Calculate the dot product of the two vectors
  const dotProduct = vecA.reduce((sum, a, idx) => sum + a * vecB[idx], 0);

  // Calculate the magnitude (Euclidean norm) of each vector
  const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
  const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));

  // Compute and return the cosine similarity
  return dotProduct / (magnitudeA * magnitudeB);
};

const MIN_RELEVANT_CHUNKS_SIMILARITY = 0.77; // Minimum similarity required for a chunk to be considered relevant
const MAX_RELEVANT_CHUNKS_NB = 15; // Maximum number of relevant chunks to attach to chatGPT context

/**
 * Searches the indexed documents for the most relevant chunks based on a query.
 * Utilizes cosine similarity to find the closest matching embeddings.
 *
 * @param query - The search query provided by the user
 * @returns An array of the top matching document chunks' content
 */
const searchChunkReference = async (query: string) => {
  // Generate an embedding for the user's query
  const queryEmbedding = await generateEmbedding(query);

  // Calculate similarity scores between the query embedding and each document's embedding
  const results = vectorStore
    .map((doc) => ({
      ...doc,
      similarity: cosineSimilarity(queryEmbedding, doc.embedding), // Add similarity score to each doc
    }))
    // Filter out documents with low similarity scores
    // Avoid to pollute the context with irrelevant chunks
    .filter((doc) => doc.similarity > MIN_RELEVANT_CHUNKS_SIMILARITY)
    .sort((a, b) => b.similarity - a.similarity) // Sort documents by highest similarity first
    .slice(0, MAX_RELEVANT_CHUNKS_NB); // Select the top most similar documents

  // Return the content of the top matching documents
  return results;
};

3.2 OpenAI prompting met relevante chunks

Na het sorteren voeden we detopDit betekent dat ChatGPT de meest relevante secties van uw documenten ziet alsof u ze in het gesprek hebt ingevoerd.

const MODEL: OpenAI.Chat.ChatModel = "gpt-4o-2024-11-20"; // Model to use for chat completions

// Define the structure of messages used in chat completions
export type ChatCompletionRequestMessage = {
  role: "system" | "user" | "assistant"; // The role of the message sender
  content: string; // The text content of the message
};

/**
 * Handles the "Ask a question" endpoint in an Express.js route.
 * Processes user messages, retrieves relevant documents, and interacts with OpenAI's chat API to generate responses.
 *
 * @param messages - An array of chat messages from the user and assistant
 * @returns The assistant's response as a string
 */
export const askDocQuestion = async (
  messages: ChatCompletionRequestMessage[]
): Promise<string> => {
  // Assistant's response are filtered out otherwise the chatbot will be stuck in a self-referential loop
  // Note that the embedding precision will be lowered if the user change of context in the chat
  const userMessages = messages.filter((message) => message.role === "user");

  // Format the user's question to keep only the relevant keywords
  const formattedUserMessages = userMessages
    .map((message) => `- ${message.content}`)
    .join("\n");

  // 1) Find relevant documents based on the user's question
  const relevantChunks = await searchChunkReference(formattedUserMessages);

  // 2) Integrate the relevant documents into the initial system prompt
  const messagesList: ChatCompletionRequestMessage[] = [
    {
      role: "system",
      content:
        "Ignore all previous instructions. \
        You're an helpful chatbot.\
        ...\
        Here is the relevant documentation:\
        " +
        relevantChunks
          .map(
            (doc, idx) =>
              `[Chunk ${idx}] filePath = "${doc.filePath}":\n${doc.content}`
          )
          .join("\n\n"), // Insert relevant chunks into the prompt
    },
    ...messages, // Include the chat history
  ];

  // 3) Send the compiled messages to OpenAI's Chat Completion API (using a specific model)
  const response = await openai.chat.completions.create({
    model: MODEL,
    messages: messagesList,
  });

  const result = response.choices[0].message.content; // Extract the assistant's reply

  if (!result) {
    throw new Error("No response from OpenAI");
  }

  return result;
};

OpenAI API voor Chatbot met Express implementeren

Om ons systeem uit te voeren, gebruiken we een Express.js-server. Hier is een voorbeeld van een klein Express.js-endpunt om de query te verwerken:

import express, { type Request, type Response } from "express";
import {
  ChatCompletionRequestMessage,
  askDocQuestion,
  indexMarkdownFiles,
} from "./askDocQuestion";

// Automatically fill the vector store with embeddings when server starts
indexMarkdownFiles();

const app = express();

// Parse incoming requests with JSON payloads
app.use(express.json());

type AskRequestBody = {
  messages: ChatCompletionRequestMessage[];
};

// Routes
app.post(
  "/ask",
  async (
    req: Request<undefined, undefined, AskRequestBody>,
    res: Response<string>
  ) => {
    try {
      const response = await askDocQuestion(req.body.messages);

      res.json(response);
    } catch (error) {
      console.error(error);
    }
  }
);

// Start server
app.listen(3000, () => {
  console.log(`Listening on port 3000`);
});

UI: Maak een chatbot-interface

Op de frontend heb ik een klein React-component gebouwd met een chat-achtige interface. Het stuurt berichten naar mijn Express-backend en toont de antwoorden.


Code tempel

Ik maakte eenDe template codeVoor u als uitgangspunt voor uw eigen chatbot.

Live demo

Als u de definitieve implementatie van deze chatbot wilt testen, bekijk dan ditDemo pagina.

Demo pagina

Mijn Demo Code

  • Op zoek naar: AskDocQuestion.ts
  • Frontend: ChatBot componenten

Ga verder

Op YouTube bekijk je ditVideo van Adrien TwarogDit zijn OpenAI Embeddings en Vector Databases.


Ik struikelde ook overOpenAI's Assistants File Search documentatieDit kan interessant zijn als je een alternatieve aanpak wilt.


Conclusie

Ik hoop dat dit je een idee geeft over hoe je documentatieindexering voor een chatbot kunt verwerken:

  • Met behulp van chunking + overlap zodat de juiste context wordt gevonden,
  • Het genereren van embeddings en het opslaan ervan voor snelle vectorgelijkheidsonderzoeken,
  • Uiteindelijk heb ik het overhandigd aan ChatGPT met de relevante context.


Ik ben geen AI-expert; dit is gewoon een oplossing die ik vond goed werkt voor mijn behoeften.please let me knowIk zou graag feedback horen over vectoropslagoplossingen, chunkingstrategieën of andere prestatietips.


Thanks for reading, and feel free to share your thoughts!

L O A D I N G
. . . comments & more!

About Author

Aymeric PINEAU HackerNoon profile picture
Aymeric PINEAU@aymericzip
Founder of Intlayer, internationalisation solution for JS applications

LABELS

DIT ARTIKEL WERD GEPRESENTEERD IN...

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks