Към съдържанието
wannacode

Търсене по смисъл

Накратко: Векторното търсене превръща текста във вектор от числа (embedding) и намира най-близките по смисъл записи чрез косинусово сходство.

Проблем

„Нещо за закуска“

Клиент пише в търсачката на магазина „нещо за закуска“. В имената на продуктите думата я няма:

SQLбаза: магазин

Пълнотекстовото търсене от предишния урок намира само отзива „Пълнозърнестият хляб е чудесен за закуска“. А мляко, кифла, кафе и баница са точно това, което клиентът иска. Той търси смисъл, а не дума. Синоними, преводи и „нещо за…“ не се хващат с думи.

Идеята

Текстът като точка

Езиков модел (embedding model) превръща текст във вектор: списък от числа. Обучен е така, че текстове с близък смисъл дават близки вектори. „Мляко“, „кифла“ и „нещо за закуска“ са близо, а „прах за пране“ е далеч.

Истинските модели дават от няколкостотин до няколко хиляди числа, и никое от тях няма име. Тук, за да се вижда какво става, векторите са измислени и имат само четири измерения с имена: закуска, сладко, готвене и чистене. Таблицата product_vectors пази вектора на всеки продукт като JSON масив:

SQLбаза: магазин

Кифлата е много „закуска“ и малко „сладко“. Картофите са само „готвене“, а прахът за пране само „чистене“.

По-точно, за любопитните

Векторът се смята веднъж, когато продуктът се добави или описанието му се смени, и се пази в базата. Въпросът на клиента се превръща във вектор със същия модел при всяко търсене. Двата вектора трябва да са от един и същ модел: вектори от различни модели не могат да се сравняват. Модели има отворени (например от семейството sentence-transformers, които вървят и на обикновен компютър) и платени услуги.

Идеята

Колко близо: косинус

Два вектора са близки, когато сочат в една и съща посока. Мярката е косинусовото сходство: 1 значи една посока, 0 значи нищо общо. Смята се така: умножаваш числата на едни и същи места и ги събираш (скаларно произведение), после делиш на дължините на двата вектора.

json_each разгъва JSON масив в редове: key е мястото, а value е числото. Сирене срещу въпроса „нещо за закуска“ ([1, 0.2, 0.1, 0], както би го дал моделът):

SQLбаза: магазин

Сборът на последната колона е 0.66. Делен на дължините (0.849 на сиренето и 1.025 на въпроса), дава 0.759. Същото за всички продукти, от най-близкия надолу:

SQLбаза: магазин

WITH q(v) дава вектора на въпроса. Двата json_each се свързват по мястото, а GROUP BY събира произведенията за всеки продукт. Нито един от тези продукти няма думата „закуска“ в името си.

Пробвай

Сладко за закуска

Моделът е превърнал въпроса „нещо сладко за закуска“ във вектора [0.7, 0.9, 0, 0]. Намери трите най-близки продукта (колони name и similarity, закръглено до 0.001), от най-близкия надолу.

SQLбаза: магазин

Идеята

Филтър преди подреждането

Често търсенето по смисъл има и обикновени условия: под някаква цена, само в наличност, само в категория. Условието е в WHERE, тоест преди GROUP BY, ORDER BY и LIMIT. Ако първо вземеш най-близките три и после филтрираш, може да не остане нищо: те са кафе за 14,90 €, баница за 2,50 € и мляко за 1,60 €.

По-точно, за любопитните

Векторните индекси (следващата карта) имат точно този проблем. Приблизителното търсене връща например най-близките 100, и ако филтърът е след него, а евтините продукти не са сред тези 100, резултатът е празен. Затова векторните бази имат филтри, които се прилагат по време на търсенето, а pgvector има настройки като hnsw.ef_search и итеративно сканиране (hnsw.iterative_scan).

Упражнение

Евтино и подобно

1. Клиентът е избрал „до 1,50 €“.

Намери трите продукта, най-близки до „нещо за закуска“ ([1, 0.2, 0.1, 0]), с цена под 1,50 € (колони name и similarity), от най-близкия надолу.

SQLбаза: магазин

2. Страницата на продукта показва „подобни продукти“. Векторът на въпроса е векторът на самия продукт.

Намери трите продукта, най-подобни на сиренето (продукт 7), без самото сирене (колони name и similarity), от най-близкия надолу.

SQLбаза: магазин

Идеята

Милиони вектори: приблизителен индекс

Заявката сравнява въпроса с всеки продукт. Планът показва SCAN p:

SQLбаза: магазин

При 23 продукта с по 4 числа това е нищо. При 10 милиона документа с по 1536 числа всяко търсене са 15 милиарда умножения. Обикновеният индекс не помага: той подрежда по една стойност, а „близо“ във вектори е в стотици посоки наведнъж.

Затова векторните бази ползват приблизителни индекси (ANN, approximate nearest neighbours). Най-разпространеният е HNSW: граф, в който всеки вектор е свързан с близките си, на няколко нива. Търсенето започва от горното, рядко ниво и слиза, като върви все към по-близки съседи. Намира най-близките за милисекунди, но понякога изпуска някой от тях. Делът на истинските най-близки, които индексът намира, е recall, и се настройва срещу скоростта.

Идеята

Къде живеят векторите

Най-простото е векторите да стоят до данните. pgvector е разширение на PostgreSQL:

CREATE EXTENSION vector;
CREATE TABLE product_vectors (
product_id integer PRIMARY KEY REFERENCES products(id),
embedding vector(384)
);
CREATE INDEX ON product_vectors USING hnsw (embedding vector_cosine_ops);
-- <=> е косинусовото разстояние (1 минус сходството)
SELECT p.name
FROM product_vectors pv JOIN products p ON p.id = pv.product_id
WHERE p.price < 1.5
ORDER BY pv.embedding <=> $1
LIMIT 3;

Отделни векторни бази (Qdrant, Weaviate, Milvus, Pinecone) си струват при стотици милиони вектори или когато търсенето по смисъл е главната работа. Elasticsearch, OpenSearch и Redis също имат векторно търсене.

Най-честата употреба днес е RAG (retrieval-augmented generation): чатботът на магазина превръща въпроса във вектор, намира най-близките откъси от описания, отзиви и правила и ги дава на езиковия модел, за да отговори по тях, а не по памет. Често търсенето по смисъл се смесва с пълнотекстовото: думите хващат точните имена и кодове, а векторите смисъла.

Тест

Провери се

1. Какво е embedding?
2. Косинусовото сходство на два вектора е 1. Какво значи?
3. Търсиш трите най-близки продукта под 1,50 €. Защо филтърът по цена не се прилага след LIMIT 3?
4. Какво дава HNSW индексът във векторна база?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·