Въведение

За да използваш embeddings, превърни всеки документ (текст, код, описания на продукти) в вектор, запази векторите във vector store (например Postgres + pgvector или специализирана услуга) и при търсене вгради (embed) заявката, намери най-близките вектори и върни най-добрите пасажи/документи. Embeddings са практичният „мост“ между естествен език и математическо търсене по смисъл.

Embeddings са основата на семантичното търсене, препоръки, класификация по сходство, deduplication и Retrieval-Augmented Generation (RAG). Идеята е проста: еднакви по смисъл текстове се оказват близки като вектори, а различните се отдалечават.

Ако ти трябва търсене „какво има предвид човекът“, embeddings почти винаги са по-точни от чисто търсене по ключови думи.

В тази HOW-TO статия ще минем през целия pipeline: подготовка на данни, chunking, избор на модел, съхранение, индексиране, kNN търсене, re-ranking и практики за продукция.

Стъпка 1: Дефинирай задачата и метриката за успех

Преди да пишеш код, фиксирай какво оптимизираш. Най-честите сценарии:

  • Семантично търсене в база знания (FAQ, документи, вътрешни политики)
  • RAG за чатбот (намиране на пасажи + генерация)
  • Откриване на дубликати/почти дубликати (near-duplicate)
  • Препоръки (подобни продукти/статии)
  • Клъстеризация и тематично групиране

Избери метрика за оценка. Практични варианти:

  • Precision@k и Recall@k за търсене
  • MRR (Mean Reciprocal Rank) за „колко високо е правилният резултат“
  • Човешка оценка на релевантност по 3-точкова скала (нерелевантно/частично/релевантно)
  • За RAG: „answer accuracy“ + „groundedness“ (дали отговорът е подкрепен от пасажи)

Събери малък тестов набор: 30-100 реални заявки + очаквани документи. Това ще ти спести дни „тунинг на сляпо“.

Стъпка 2: Подготви съдържанието (cleaning + chunking)

Embeddings работят най-добре, когато входът е чист и последователен. Минимални правила:

  • Премахни навигация, футъри, повторяеми менюта
  • Нормализирай whitespace
  • Запази заглавията и йерархията (H1/H2/H3), защото дават контекст
  • Извади таблици като текст (или отделни „редове“), ако са важни за търсене

Chunking стратегия

„Chunk“ е парче текст, което вграждаш като един вектор. Добрият chunking решава повече проблеми от смяната на модел.

Практични подходи:

  • По параграфи/секции (предпочитано за документация)
  • По изречения с плъзгащ прозорец (ако текстът е неструктуриран)
  • По токени с overlap (например 20-25% припокриване)

Насоки:

  • Ако chunks са твърде дълги, търсенето става „размазано“ (векторът описва твърде много теми)
  • Ако са твърде къси, губиш контекст и получаваш фалшиви съвпадения

Добави метаданни към всеки chunk:

  • doc_id, source, url (ако имаш), section_title, updated_at, language
  • за код: repo, path, symbol, commit

В продукция точността най-често пада заради лош chunking и мръсни данни, не заради „лош модел“.

Стъпка 3: Генерирай embeddings (API и batching)

Най-честият вариант е да използваш готов embedding модел (например през API). Важно е:

  • Вградиш и документите, и заявките със същия модел
  • Да batch-ваш заявки (по-ниска цена и по-висока производителност)
  • Да кешираш embeddings за документи (генерираш при ingest, не при всяко търсене)

Пример (Python, псевдо-практичен)

from openai import OpenAI
client = OpenAI()

def embed_texts(texts):
    # texts: list[str]
    resp = client.embeddings.create(
        model="text-embedding-3-small",
        input=texts,
    )
    return [d.embedding for d in resp.data]

Пример (Node.js)

import OpenAI from "openai";
const client = new OpenAI();

export async function embedTexts(texts) {
  const resp = await client.embeddings.create({
    model: "text-embedding-3-small",
    input: texts,
  });
  return resp.data.map(d => d.embedding);
}

Практика: използвай отделен job за ingest (ETL), който:

  • чете документите
  • прави chunking
  • генерира embeddings
  • записва в базата

Така търсенето става евтино и бързо: при заявка embed-ваш само query-то.

Стъпка 4: Съхрани embeddings във vector store (pgvector вариант)

Ако вече използваш Postgres, pgvector е често най-прагматичният избор: една база, транзакции, лесни join-ове с метаданни.

Минимална схема

CREATE TABLE doc_chunks (
  id bigserial PRIMARY KEY,
  doc_id text NOT NULL,
  chunk_index int NOT NULL,
  content text NOT NULL,
  embedding vector(1536),
  metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
  created_at timestamptz NOT NULL DEFAULT now()
);

-- Индекс за ANN (пример: HNSW)
CREATE INDEX doc_chunks_embedding_hnsw
  ON doc_chunks
  USING hnsw (embedding vector_cosine_ops);

Забележки:

  • Размерът vector(1536) е пример. Провери размерността на избрания модел и я фиксирай в схемата.
  • Избери дистанция: cosine е най-често удобна за текст.
  • Ако имаш много данни, тествай HNSW срещу IVFFlat и настрой параметрите спрямо latency/recall.

Стъпка 5: Семантично търсене (kNN) + филтри + re-ranking

Базово kNN търсене

SELECT
  doc_id,
  chunk_index,
  content,
  1 - (embedding <=> $1) AS score
FROM doc_chunks
WHERE metadata->>'language' = 'bg'
ORDER BY embedding <=> $1
LIMIT 10;

Тук $1 е embedding на заявката.

Филтри по метаданни

Embeddings рядко са достатъчни сами. Добави филтри:

  • само последна версия (updated_at)
  • само конкретен продукт/модул
  • само определен тип документ (например policy, FAQ, tutorial)

Re-ranking (силен ъпгрейд на качеството)

Често работи много добре комбинация:

  1. Vector retrieval за top 20-50
  2. Re-rank с по-силен модел (cross-encoder / LLM scoring) за топ 5-10

Това пази latency разумен и качва точността.

Hybrid search (вектори + ключови думи)

За някои заявки хората очакват точни съвпадения (номера на фактура, кодове, имена). Hybrid подход:

  • BM25/FTS за точни термини
  • vector search за смисъл
  • смесване на резултатите (например линейна комбинация на рангове)

Стъпка 6: Превърни pipeline-а в „продукционно качество“

Embeddings проектите често работят „на лаптоп“, но се чупят в продукция. Минимален production checklist:

  • Версионирай ingestion pipeline (код + конфиг)
  • Версионирай embeddings (model name + дата) в metadata
  • Добави backfill стратегия при смяна на модел (batch job, постепенна миграция)
  • Наблюдавай качество: drift (нови теми), празни/шумни резултати, latency
  • Кеширай query embeddings за повтарящи се заявки (с TTL)
  • За чувствителни данни: криптиране, RLS/ACL, audit лог

Най-важният „трик“ за стабилно качество е да измерваш Recall@k на реални заявки всяка седмица и да следиш регресии след ingest промени.

Съвети за по-добри резултати

  • Започни с малък корпус (например 200-500 документа) и тестов набор от заявки.
  • Използвай overlap при chunking, ако документите имат дълги обяснения.
  • Съхранявай оригиналния текст и заглавията, за да можеш да показваш „цитирания“ в UI.
  • Ако работиш на български, не смесвай езици в един chunk без нужда.
  • Поддържай „stoplist“ за шумни секции (повторяеми legal текстове, менюта).

Чести грешки, които да избягваш

  • Да embed-ваш цели PDF-и като един chunk.
  • Да смениш модела без backfill и без да отбележиш версията.
  • Да тестваш само с 5 „примерни“ заявки вместо реални.
  • Да връщаш директно топ 1 резултат без re-ranking или без да показваш пасажи.
  • Да игнорираш филтрите по метаданни (често те носят най-големия lift).

Често задавани въпроси

1) Какъв vector store да избера: Postgres + pgvector или специализирана услуга?

Ако вече имаш Postgres и данните ти не са огромни, pgvector е отличен старт. Ако имаш много милиони вектори, сложни multi-tenant нужди или искаш управлявано скалиране, специализирана услуга може да е по-лесна.

2) Трябва ли да нормализирам векторите?

При cosine similarity обикновено работиш с нормализирани вектори или с оператори, които го управляват. Важно е да следваш препоръките на конкретния store/индекс.

3) Колко голям да е chunk-ът?

Няма универсално число. Започни със секции/параграфи и тествай с реални заявки. Ако често връщаш „не това“, намали размера; ако губиш контекст, увеличи и добави overlap.

4) Как да подобря качеството без смяна на модел?

Първо оправи cleaning + chunking, после добави метаданни филтри и re-ranking. Това често носи по-голям ефект от смяна на embedding модел.

5) Как да правя миграция към нов embedding модел?

Запази старите вектори, добави нова колона/таблица за новите embeddings и прави dual-retrieval за период. После премини към новите, когато метриките са стабилни.

Актуалност и източници

Цени и наличности на embedding модели и услуги се променят. Провери официалните страници за pricing и документацията на избрания vector store (например OpenAI pricing/model docs и документацията на pgvector) преди да фиксираш архитектурни решения.

Пример: end-to-end embeddings pipeline за вътрешно търсене (semantic search)

Ето работеща рамка, която можеш да приложиш за вътрешна база знания (Confluence/Notion/Wiki), сайт с документация или help center:

  1. Ingest (ежедневно/на час): изтегляш нови и променени страници, чистиш HTML, извличаш само полезното съдържание.
  2. Chunking: разделяш по секции (заглавия + параграфи). За всяко парче добавяш метаданни като продукт, версия, език, URL и дата на последна промяна.
  3. Embedding: генерираш вектори за всеки chunk на партиди и ги записваш (включително model и model_version в metadata).
  4. Indexing: изграждаш ANN индекс (например HNSW) и периодично правиш ANALYZE/поддръжка, ако си на Postgres.
  5. Query: при търсене embed-ваш заявката, филтрираш по език/продукт, връщаш top 20.
  6. Re-rank + UI: преоценяваш топ резултатите (с по-силен модел или правила), показваш 2-3 пасажа с подчертани извадки, а не цял документ.
  7. Feedback loop: събираш „клик/доволство“ или прост бутон „полезно/неполезно“ и го използваш за подобряване на chunking и филтри.

Кеширане и цена

  • Кеширай embeddings за документи завинаги (до следващата промяна в документа).
  • За заявки кеширай най-честите (например 24 часа), защото хората често търсят едни и същи неща.
  • Ако имаш много ingestion, обмисли batch режим (когато доставчикът го предлага) за по-ниска цена на 1M tokens.

Кога embeddings НЕ са правилният инструмент

  • Ако търсиш точни идентификатори (номера, SKU, EGN, кодове), първо използвай keyword/структурирано търсене.
  • Ако данните са 100% структурирани (таблици с ясни колони), класически SQL филтри и индекси често са по-добри.
  • Ако имаш много малко съдържание (под 20-30 документа), правилно написано keyword търсене може да е достатъчно.