300 lexime
300 lexime

Si të ndërtoni një dokumentacion të zgjuar - Bazuar në Embeddings OpenAI (Chunking, Indexing, and Searching)

nga Aymeric PINEAU13m2025/05/01
Read on Terminal Reader

Shume gjate; Te lexosh

Ideja kryesore është të indeksoni dokumentacionin duke i ndarë ato në copa të menaxhueshme, duke gjeneruar embeddings me OpenAI, dhe duke kryer një kërkim të ngjashmërisë për të gjetur dhe kthyer informacionin më të rëndësishëm në pyetjen e një përdoruesi.
featured image - Si të ndërtoni një dokumentacion të zgjuar - Bazuar në Embeddings OpenAI (Chunking, Indexing, and Searching)
Aymeric PINEAU HackerNoon profile picture
0-item

Unë doja të ndaja qasjen time në krijimin e një "dokumentacioni inteligjent" chatbot për një projekt që unë jam duke punuar në.I’m not an AI expert, so any suggestions or improvements are more than welcome!


Qëllimi i këtij postimi nuk është të krijojë një tutorial tjetër për ndërtimin e një chatbot bazuar në OpenAI. Ka tashmë shumë përmbajtje në këtë temë.index documentationduke i ndarë në të menaxhueshmechunkstë gjenerojëembeddingsme të hapur, dheperforming a similarity searchpër të gjetur dhe kthyer informacionin më të rëndësishëm në pyetjen e një përdoruesi.


Në rastin tim, dokumentacioni do të jetë skedarët Markdown, por mund të jetë çdo formë e tekstit, objekti i bazës së të dhënave, etj.

Përse ?

Për shkak se ndonjëherë mund të jetë e vështirë për të gjetur informacionin që ju nevojitet, unë doja të krijoja një chatbot që mund të përgjigjet në pyetje në lidhje me një temë të veçantë dhe të sigurojë kontekstin e duhur nga dokumentacioni.


Ky asistent mund të përdoret në mënyra të ndryshme, të tilla si:

  • Përgjigje të shpejta për pyetjet më të shpeshta
  • Kërkimi i një doc/faqe si Algolia bën
  • Ndihmon përdoruesit të gjejnë informacionin që kanë nevojë në një dokument të veçantë
  • Kërkimi i shqetësimeve / pyetjeve të përdoruesve duke ruajtur pyetjet e bëra

përmbledhje

Më poshtë, unë do të përshkruaj tre pjesët kryesore të zgjidhjes sime:

  1. Leximi i dosjeve të dokumentacionit
  2. Indeksimi i dokumentacionit (chunking, overlap, dhe embedding)
  3. Kërkimi i dokumentacionit (dhe lidhja e tij deri në një chatbot)

Arkivi i pemës

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

1 Leximi i dosjeve të dokumentacionit

Në vend që të kodoni tekstin e dokumentit, mund të skanoni një dosje për.mdPërdorimi i mjeteve të tilla siglob.

// 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;
};

Si alternativë, natyrisht që mund të merrni dokumentacionin tuaj nga baza juaj e të dhënave ose CMS etj.

Si alternativë, natyrisht që mund të merrni dokumentacionin tuaj nga baza juaj e të dhënave ose CMS etj.


Indeksimi i dokumentacionit

Për të krijuar motorin tonë të kërkimit, ne do të përdorim OpenAIVektorë Embedings APIpër të ndërtuar ndërtesat tona.


Embeddings vektor janë një mënyrë për të përfaqësuar të dhënat në një format numerik, të cilat mund të përdoren për të kryer kërkime ngjashmërie (në rastin tonë, midis pyetjes së përdoruesit dhe seksioneve të dokumentacionit tonë).


Ky vektor, i përbërë nga një listë e numrave të pikave fluturuese, do të përdoret për të llogaritur ngjashmërinë duke përdorur një formulë matematikore.

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

Bazuar në këtë koncept, u krijua Vector Database. Si rezultat, në vend të përdorimit të OpenAI API, është e mundur të përdorni një bazë të dhënash vektorike si Chroma, Qdrant ose Pinecone.

Bazuar në këtë koncept, u krijua Vector Database. Si rezultat, në vend të përdorimit të OpenAI API, është e mundur të përdorni një bazë të dhënash vektorike si Chroma, Qdrant ose Pinecone.

2.1 Chunk Çdo File & Overlap

Blloqe të mëdha të tekstit mund të tejkalojnë kufijtë e kontekstit të modelit ose të shkaktojnë goditje më pak relevante, kështu që rekomandohet t'i ndajmë ato në copa për ta bërë kërkimin më të synuar. megjithatë, për të ruajtur disa vazhdimësi midis copave, ne i mbivendosim ato me një numër të caktuar tokenësh (ose karaktereve). Në këtë mënyrë, kufijtë e copave janë më pak të prirur për të prerë mesfushën relevante të kontekstit.

Shembuj të Chunking

Në këtë shembull, kemi një tekst të gjatë që duam ta ndajmë në copa më të vogla.Në këtë rast, duam të krijojmë copa prej 100 karaktereve dhe t’i mbulojmë ato me 50 karaktere.


Full Text (406 characters):

Në zemër të qytetit të gjallë, qëndronte një bibliotekë e vjetër që shumë njerëz e kishin harruar. Sheshet e saj kulmuese ishin të mbushura me libra nga çdo zhanër i imagjinueshëm, secila duke pëshpëritur histori aventurash, mistere dhe urtësi të përjetshme. Çdo mbrëmje, një bibliotekar i përkushtuar do të hapte dyert e tij, duke mirëpritur mendjet kurioze të etura për të eksploruar njohuritë e gjera brenda.


  • 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.

Kodi i shkurtër

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;
};

Për të mësuar më shumë në lidhje me grumbullimin, dhe ndikimin e madhësisë në embedding, ju mund të shikoni këtë artikull.

Për të mësuar më shumë në lidhje me grumbullimin, dhe ndikimin e madhësisë në embedding, ju mund të shikoni këtë artikull.

2.2 Gjenerata e ndërtuar

Pasi një skedar është copëtuar, ne gjenerojmë embeddings vektor për çdo copë duke përdorur API OpenAI (p.sh.,text-embedding-3-large) të

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 Krijimi dhe ruajtja e embeddings për të gjithë dosjen

Për të shmangur rigjenerimin e embeddings çdo herë, ne do të ruajmë embeddings. ajo mund të ruhet në një bazë të dhënash. por në këtë rast, ne thjesht do ta ruajmë atë në një skedar JSON në vend.


Kodi i mëposhtëm thjesht:

  1. përcjell çdo dokument,
  2. Shpërndaje dokumentin në copa,
  3. për çdo shtresë, për çdo shtresë,
  4. Shkarkoni të dhënat në një skedar JSON.
  5. Plotësoni vektorStore me futjet që do të përdoren në kërkim.
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) Kërkimi i dokumentacionit

3.1 Vektorë të ngjashëm

Për t'iu përgjigjur pyetjes së një përdoruesi, ne së pari gjenerojmë një embedding përuser's questiondhe pastaj llogarisim ngjashmërinë cosine midis futjes së pyetjes dhe futjes së secilit copë. Ne filtrojmë çdo gjë nën një kufi të caktuar të ngjashmërisë dhe mbajmë vetëm ndeshjet më të larta X.

/**
 * 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 Prompting OpenAI me Chunks përkatëse

Pas tërmetit, ne ushqejmëtopKjo do të thotë se ChatGPT i sheh seksionet më të rëndësishme të dokumenteve tuaja sikur t’i kishit shkruar ato në bisedë.

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;
};

Implementimi i OpenAI API për Chatbot duke përdorur Express

Për të ekzekutuar sistemin tonë, ne do të përdorim një server Express.js. Këtu është një shembull i një pika të vogël Express.js për të trajtuar pyetjen:

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: Krijimi i një ndërfaqe chatbot

Në frontend, kam ndërtuar një komponent të vogël React me një ndërfaqe të ngjashme me chat. Ajo dërgon mesazhe në backend-in tim Express dhe tregon përgjigjet.


Kodi i tempullit

Unë bëra njëKodi i tempullitpër ju që të përdorni si një pikë fillestare për chatbot tuaj.

Demo të gjalla

Nëse doni të testoni zbatimin përfundimtar të këtij chatbot, kontrolloni këtëFaqe demo.

Faqe demo

Kodi i Demo

  • Fjalë kyçe askDocQuestion.ts
  • Frontend: Komponentët ChatBot

Shkoni më tej

Në YouTube, shikoni këtëFjalë kyçe Adrien TwarogPërdorimi i OpenAI Embeddings dhe Vector Databases


Edhe unë rashë mbiAsistentët e kërkimit të skedarëve të OpenAI, e cila mund të jetë interesante nëse doni një qasje alternative.


Konkludimi

Unë shpresoj se kjo ju jep një ide se si të merren me indeksimin e dokumentacionit për një chatbot:

  • Duke përdorur chunking + overlap në mënyrë që të gjendet konteksti i duhur,
  • Gjenerimi i embeddings dhe ruajtja e tyre për kërkime të shpejta të ngjashmërisë së vektorëve,
  • Së fundi, ia dorëzova atë ChatGPT me kontekstin përkatës.


Unë nuk jam një ekspert i inteligjencës artificiale; kjo është vetëm një zgjidhje që kam gjetur që punon mirë për nevojat e mia.please let me knowUnë do të doja të dëgjoja reagime në lidhje me zgjidhjet e ruajtjes së vektorëve, strategjitë e grumbullimit ose këshilla të tjera të performancës.


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

VARUR TAGS

KY ARTIKU U PARAQIT NË...

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks