Класичні RAG-системи працюють лише з текстом — але що робити, коли потрібно шукати по сайтах із каталогами товарів, інфографіками чи медичними знімками? Цей туторіал покаже, як зібрати повноцінний мультимодальний RAG-пайплайн: Crawlee краулить сайт, витягує зображення та текст, OpenAI CLIP перетворює їх на ембедінги, а Qdrant зберігає та шукає по векторах. На все знадобиться 2–3 години і базові знання Python. Переконайся, що в тебе є Node.js 20+, Python 3.11+ і Docker.
🛠️ Що знадобиться
- Crawlee (Node.js) — відкритий веб-краулер від Apify для збору сторінок та зображень; безкоштовний, open-source
- Python 3.11+ із бібліотеками — обробка зображень, генерація ембедінгів, оркестрація пайплайну; безкоштовно
- OpenAI CLIP (openai/clip-vit-base-patch32) — мультимодальна модель для ембедінгів тексту і зображень одночасно; безкоштовна, запускається локально через HuggingFace
- Qdrant — векторна база даних для зберігання та семантичного пошуку; безкоштовний self-hosted варіант через Docker
- Docker Desktop 4.x — для запуску Qdrant контейнера; безкоштовний
- OpenAI API або Ollama — для фінального генеративного кроку (відповідь на запит); OpenAI платний, Ollama — безкоштовна локальна альтернатива
📋 Покрокова інструкція
Крок 1: Налаштування Crawlee та збір даних із сайту
Відкрий термінал і виконай npm create crawlee@latest my-crawler — обери шаблон Playwright crawler та мову TypeScript. Перейди до папки cd my-crawler і відкрий файл src/main.ts. Замість дефолтного прикладу встав такий код: у requestHandler додай витяг всіх зображень через page.$$eval('img', imgs => imgs.map(i => i.src)) і збережи результат разом із URL сторінки та текстом (document.body.innerText) у Dataset.pushData(). Запусти краулер командою npm start -- --url https://твій-сайт.com — після завершення дані з’являться у папці storage/datasets/default/ у форматі JSONL. Перевір, що кожен запис містить поля url, text та images (масив URL-адрес зображень).

Крок 2: Завантаження зображень і підготовка датасету
Створи Python-скрипт download_images.py. Спочатку встанови залежності: pip install httpx pillow tqdm. У скрипті зчитай JSONL-файл із storage/datasets/default/, пройдись по кожному запису і завантаж кожне зображення через httpx.get(url, follow_redirects=True) — зберігай у папку data/images/ з іменем файлу у форматі {page_hash}_{img_index}.jpg. Важливо: пропускай зображення менші за 100×100 пікселів (логотипи, іконки) — вони зашумлять пошук. Для фільтрації відкрий завантажений файл через PIL.Image.open() і перевір image.size. На виході отримаєш папку з очищеними зображеннями та оновлений JSON-маніфест, де кожен запис пов’язує локальний шлях до зображення з текстом сторінки та оригінальним URL.
Крок 3: Генерація мультимодальних ембедінгів через CLIP
Встанови залежності: pip install transformers torch pillow numpy. Створи скрипт generate_embeddings.py. На початку завантаж модель: from transformers import CLIPProcessor, CLIPModel, потім model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32") і processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32"). Для кожного зображення зі свого маніфесту виклич processor(images=image, return_tensors="pt") і отримай вектор через model.get_image_features(**inputs).detach().numpy() — розмірність буде 512. Окремо генеруй текстовий ембедінг для тексту сторінки через model.get_text_features() з тими самими 512 вимірами. Збережи обидва вектори разом із метаданими (url, шлях до зображення, перші 300 символів тексту) у файл embeddings.jsonl — один рядок на одне зображення.
Крок 4: Запуск Qdrant та індексація ембедінгів
Запусти Qdrant через Docker: docker run -p 6333:6333 -v qdrant_storage:/qdrant/storage qdrant/qdrant:v1.9.0. Відкрий браузер і перейди на http://localhost:6333/dashboard — переконайся, що дашборд відкривається. Встанови клієнт: pip install qdrant-client. Створи скрипт index_to_qdrant.py: спочатку створи колекцію командою client.create_collection(collection_name="rag_images", vectors_config=VectorParams(size=512, distance=Distance.COSINE)). Далі читай embeddings.jsonl батчами по 100 записів і завантажуй через client.upsert(collection_name="rag_images", points=[PointStruct(id=i, vector=embedding, payload=metadata)]). Після завершення відкрий дашборд Qdrant, знайди колекцію rag_images і перевір, що кількість точок відповідає кількості рядків у твоєму JSONL-файлі.
Крок 5: Побудова RAG-пайплайну та пошук по зображеннях
Створи фінальний скрипт rag_search.py. Для текстового запиту користувача (наприклад, “червона сукня з мереживом”) згенеруй CLIP-ембедінг тексту через той самий model.get_text_features() — CLIP спеціально навчений так, що текстові та візуальні вектори живуть в одному просторі. Виконай пошук: results = client.search(collection_name="rag_images", query_vector=text_embedding, limit=5) — отримаєш топ-5 найрелевантніших зображень із метаданими. Передай знайдені тексти сторінок як контекст до LLM: якщо використовуєш Ollama, виклич ollama.chat(model='llama3.2', messages=[{"role":"user","content":f"Контекст: {context}\n\nЗапит: {query}"}]). Фінальний результат — відповідь LLM із посиланнями на релевантні зображення та сторінки. Для тесту запроси “product with green packaging” і переконайся, що система повертає візуально схожі результати.
⚠️ Типові помилки та як їх уникнути
- Crawlee блокується сайтом (403/429) — встанови у
main.tsпараметрmaxConcurrency: 2і додай затримкуawait sleep(randomInterval(1000, 3000))між запитами; для складних сайтів підключиstealthплагін черезnpm install @crawlee/playwright - CLIP повертає однакові ембедінги для всіх зображень — це трапляється, якщо не нормалізувати вектори; після
get_image_features()додайembedding = embedding / np.linalg.norm(embedding), інакше косинусна схожість у Qdrant працює некоректно - Qdrant падає з помилкою розмірності — якщо колекцію вже створено з іншим
size, видали її через дашборд (кнопка Delete Collection) і створи заново; перевір, що всюди використовуєш однакову модель CLIP - Пошук повертає нерелевантні результати — найчастіше через сміттєві зображення (банери, кнопки соцмереж); додай фільтр за aspect ratio у кроці 2: пропускай зображення з відношенням сторін більше ніж 5:1 або менше ніж 1:5
💡 Поради для кращого результату
По-перше, об’єднуй текстовий та візуальний ембедінги перед індексацією: зроби конкатенацію або зважену суму (0.7 × image_emb + 0.3 × text_emb) — це суттєво покращує релевантність для продуктових каталогів. По-друге, використовуй payload filters у Qdrant: під час пошуку додавай фільтр за категорією (filter=Filter(must=[FieldCondition(key="category", match=MatchValue(value="electronics"))])) — швидкість зростає в 3–5 разів на великих колекціях. По-третє, зберігай у payload мініатюри зображень у форматі base64 (resize до 64×64 перед кодуванням) — тоді фронтенд може одразу показувати превью без додаткових запитів. По-четверте, для продакшну запусти Crawlee через Apify Cloud з розкладом — сайт оновлюється, а ти отримуєш свіжі дані автоматично без змін у коді.
❓ Часті запитання (FAQ)
1. Чи можна використовувати інші моделі замість CLIP?
Так, для кращої якості спробуй laion/CLIP-ViT-H-14-laion2B-s32B-b79K — вона точніша, але потребує більше RAM. Для зовсім локального запуску без GPU підійде sentence-transformers/clip-ViT-B-32 через бібліотеку sentence-transformers із простішим API.

2. Скільки зображень може обробити ця система?
Qdrant без проблем тримає мільйони векторів на одній машині з 16 ГБ RAM. Crawlee здатний зібрати десятки тисяч сторінок за годину — основне обмеження це генерація ембедінгів: на CPU без GPU очікуй приблизно 2–5 зображень на секунду.
3. Як оновлювати індекс при зміні контенту сайту?
Використовуй upsert замість insert у Qdrant — він оновить існуючі точки за тим самим ID. Генеруй детерміністичний ID як hashlib.md5(image_url.encode()).hexdigest(), тоді повторний краул автоматично оновить змінені зображення.
4. Чи працює пошук на українськомовних запитах?
Базова модель CLIP навчена переважно на англійському тексті, тому запити українською дадуть гіршу точність. Перекладай запити через googletrans або використовуй мультилінгвальну модель M-CLIP від FreddeFrallan — вона підтримує понад 100 мов включно з українською.
5. Як захистити систему, якщо краулити чужий сайт?
Завжди перевіряй robots.txt перед краулінгом — Crawlee читає його автоматично, якщо встановити respectRobotsTxt: true у налаштуваннях. Використовуй зібрані дані лише у некомерційних або дослідницьких цілях відповідно до умов використання цільового сайту.
🏁 Підсумок
Ти побудував повноцінний мультимодальний RAG-пайплайн: Crawlee автоматично збирає зображення та текст із будь-якого сайту, CLIP перетворює їх на уніфіковані вектори, Qdrant зберігає та миттєво шукає по них, а LLM формує зрозумілу відповідь із контекстом — все це працює повністю локально без щомісячних API-рахунків.
Прямо зараз відкрий термінал і виконай перші дві команди: npm create crawlee@latest my-crawler та docker run -p 6333:6333 qdrant/qdrant:v1.9.0 — поки вони завантажуються, обери сайт для тестового краулінгу і вже через дві години матимеш працюючий пошук по зображеннях.
РОЗСИЛКА
📬 Щотижневий AI-дайджест
Найкращі статті про ШІ та автоматизацію — без спаму, лише суть
Без спаму · Відписатись будь-коли

