בניית pipeline לחיפוש וקטורי

שלב 1: חלוקת מסמכים לקטעים (Chunking)

לפני יצירת embeddings, מפצלים מסמכים ארוכים לקטעים (chunks) בגודל סביר — כמה משפטים עד פסקה. זו החלטת עיצוב מעשית ולא רק פרט טכני: קטעים גדולים מדי מייצרים embedding "מטושטש" שמנסה לייצג יותר מדי רעיונות שונים בוקטור אחד (הבעיה שהוזכרה בפרק העוסק ב-embeddings), וקטעים קטנים מדי מאבדים הקשר סביבתי חשוב שקיים במסמך המקורי.

פתרון נפוץ לאיזון בין השניים הוא חפיפה (overlap) בין קטעים סמוכים — כל קטע חדש מתחיל קצת לפני שהקטע הקודם הסתיים, כך שמידע שנמצא בדיוק על הגבול בין שני קטעים לא הולך לאיבוד לגמרי מאף אחד מהם.

תרשים המציג מסמך ארוך כתיבה מלבנית עם שורות טקסט בחלקו העליון, עם חץ יורד לפס אופקי ארוך המחולק לקטעים חופפים לסירוגין בכחול ובכתום — אזורי החפיפה בין קטעים סמוכים מודגשים בגוון כהה יותר, ממחיש שכל קטע חדש מתחיל קצת לפני שהקודם הסתיים.
חלוקת המסמך לקטעים חופפים (chunks) — אזורי החפיפה שומרים על מידע שנמצא בדיוק על הגבול בין קטעים

שלב 2: יצירת Embeddings לכל קטע

כל קטע עובר במודל embedding ומקבל וקטור. בשלב הזה בונים בדרך כלל תור עבודה שמריץ את כל הקטעים במודל (לרוב באמצעות API חיצוני), ואוסף את הוקטורים שמתקבלים לצד מזהה הקטע המקורי.

TypeScript
async function embed(text: string): Promise<number[]> {
  const response = await embeddingClient.create({
    model: "text-embedding-model",
    input: text,
  });
  return response.embedding;
}

const chunks = chunkDocument(document, { size: 500, overlap: 50 });
const embeddedChunks = await Promise.all(
  chunks.map(async (chunk) => ({
    id: chunk.id,
    vector: await embed(chunk.text),
    metadata: { source: document.id, ...chunk.metadata },
  }))
);

שלב 3: שמירה באינדקס וקטורי (Upsert)

הוקטורים, יחד עם מטא-דאטה רלוונטי (מזהה מקור, תאריך, קטגוריה וכד'), נשמרים באינדקס הוקטורי — בין אם זה מסד נתונים וקטורי ייעודי, תוסף וקטורי למסד קיים, או ספרייה מוטמעת (כפי שנסקר בפרק הקודם). פעולת השמירה נקראת בדרך כלל upsert — הוספה אם המזהה חדש, עדכון אם הוא כבר קיים.

TypeScript
await vectorIndex.upsert(
  embeddedChunks.map((chunk) => ({
    id: chunk.id,
    values: chunk.vector,
    metadata: chunk.metadata,
  }))
);

שלב 4: שאילתה — Embedding וחיפוש Top-k

בזמן שאילתה, טקסט החיפוש עצמו עובר באותו מודל embedding בדיוק (חשוב: אותו מודל שיצר את הוקטורים המאונדקסים — לא ניתן לערבב מודלים שונים, כי המרחבים הווקטוריים שלהם אינם ניתנים להשוואה זה לזה), ומתקבל וקטור שאילתה. הוקטור הזה מוזן לאינדקס, שמחזיר את k הפריטים הקרובים ביותר.

TypeScript
const queryVector = await embed(userQuestion);

const results = await vectorIndex.query({
  vector: queryVector,
  topK: 5,
});

שלב 5: שילוב סינון לפי מטא-דאטה

רוב מערכות הפרודקשן לא מסתפקות בחיפוש וקטורי "נקי" — הן משלבות אותו עם סינון קשיח לפי שדות מבניים, בהתאם לצורך שהוזכר בפרק הקודם ("פריטים דומים, אבל רק כאלה שבמלאי"). הסינון מצטרף לשאילתה עצמה, ומצמצם את מרחב החיפוש עוד לפני או תוך כדי חישוב הדמיון הוקטורי.

TypeScript
const filteredResults = await vectorIndex.query({
  vector: queryVector,
  topK: 5,
  filter: { category: "docs", updatedAfter: "2026-01-01" },
});