В эпоху, когда большие языковые модели (LLM) стали стандартом для работы с текстом, мы часто забываем, что значительная часть информации в мире представлена не в виде чистого текста, а в виде визуальных форматов. PDF-файлы с сложной версткой, сканы документов, веб-страницы с динамическим контентом и изображения с диаграммами часто теряют свой смысл при попытке извлечь из них текст с помощью традиционных инструментов. Классический подход к Retrieval-Augmented Generation (RAG), основанный на парсинге HTML или извлечении текста через OCR, сталкивается с серьезными ограничениями: он разрушает контекст, игнорирует пространственное расположение элементов и не может «увидеть» структуру документа.
В этой статье мы подробно разберем концепцию Pixel-Native RAG — подхода, при котором документы индексируются не как строки символов, а как наборы изображений (тайлов). Мы построим полноценный пайплайн с нуля, используя современные инструменты: Playwright для рендеринга веб-страниц и PDF, мультимодальные эмбеддинги (SigLIP, CLIP или Qwen3-VL) для создания векторных представлений и FAISS для быстрого поиска. Этот метод позволяет модели «видеть» документ так, как его видит человек, сохраняя визуальный контекст, который часто является ключом к правильному ответу.
01Почему традиционный RAG терпит неудачу с визуальными документами?
Стандартный пайплайн RAG обычно включает три этапа: загрузка документа, разделение на чанки (chunks) и создание эмбеддингов. Проблема возникает уже на первом этапе. Для веб-страниц используются парсеры, которые выдергивают текст из тегов, но игнорируют CSS-стили, позиционирование и визуальные акценты. Для PDF применяются инструменты вроде PyPDF2 или даже OCR, которые часто путают строки в таблицах, не могут различить заголовки и основной текст и полностью теряют смысл графиков.
Рассмотрим пример: документ, где ключевая информация находится в боковой панели или в виде цветовой кодировки в таблице. Текстовый парсер выдаст вам просто набор слов, лишенный структуры. Визуальный же подход, о котором пойдет речь ниже, сохраняет «пиксельную» целостность. Мы рендерим страницу или страницу PDF в изображение, разбиваем его на перекрывающиеся фрагменты (тайлы) и создаем для каждого фрагмента векторное представление. Это позволяет искать не по ключевым словам, а по смыслу и визуальному сходству.
02Архитектура системы: от URL до векторного индекса
Наша система PixelRAG состоит из нескольких четко определенных этапов. Сначала мы загружаем источники данных. Это могут быть URL-адреса веб-страниц или локальные PDF-файлы. Для веб-страниц мы используем Playwright — инструмент для автоматизации браузера, который позволяет рендерить страницы так, как их видит пользователь, включая JavaScript-контент. Для PDF мы используем библиотеку PyMuPDF (fitz) для конвертации страниц в изображения.

Затем происходит процесс сегментации на тайлы. Изображение документа разбивается на квадраты размером 1024x1024 пикселя с перекрытием (overlap) в 128 пикселей. Это перекрытие критически важно: оно гарантирует, что контент, находящийся на границе двух тайлов, не будет потерян или обрезан. Каждый тайл сохраняется как отдельное изображение PNG.
На следующем этапе мы применяем мультимодальные модели эмбеддинга. В примере используется SigLIP (Google) или CLIP, которые преобразуют изображение в вектор высокой размерности. Эти векторы захватывают не только содержание, но и стиль, верстку и визуальные элементы. Полученные векторы сохраняются в индексе FAISS (Facebook AI Similarity Search), который оптимизирован для быстрого поиска ближайших соседей (ANN - Approximate Nearest Neighbor).
Важным дополнением является гибридный поиск. Помимо векторного поиска (dense retrieval), мы можем использовать OCR для извлечения текста из тайлов и применять классический алгоритм BM25 для.sparse-поиска. Результаты обоих методов затем объединяются с помощью Reciprocal Rank Fusion (RRF), что значительно повышает точность выдачи.
03Техническая реализация: рендеринг и подготовка данных
Давайте рассмотрим код, который лежит в основе этого процесса. Первым шагом является настройка окружения и установка зависимостей. Система автоматически проверяет наличие необходимых библиотек, таких как PIL (Pillow), numpy, faiss-cpu, transformers и playwright. Если какие-то пакеты отсутствуют, скрипт пытается установить их автоматически, что делает решение удобным для запуска в средах вроде Google Colab или Jupyter Notebook.

import os
import sys
import io
import re
import json
import time
import math
import shutil
import hashlib
import asyncio
import logging
import argparse
import threading
import subprocess
from pathlib import Path
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional, Tuple
@dataclass
class Config:
urls: List[str] = field(default_factory=lambda: [
"https://en.wikipedia.org/wiki/Retrieval-augmented_generation",
"https://en.wikipedia.org/wiki/Vector_database",
"https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)",
"https://en.wikipedia.org/wiki/Photosynthesis",
"https://en.wikipedia.org/wiki/Delhi"
])
include_synthetic_pdf: bool = True
tile_width: int = 1024
tile_height: int = 1024
tile_overlap: int = 128
device_scale: float = 1.0
max_page_height: int = 24000
max_tiles_per_doc: int = 12
min_tile_height: int = 200
blank_std_threshold: float = 6.0
dedup_hamming: int = 16
nav_timeout_ms: int = 60000
headless_args: List[str] = field(default_factory=lambda: [
"--no-sandbox",
"--disable-dev-shm-usage",
"--hide-scrollbars",
"--disable-gpu",
"--force-color-profile=srgb",
"--font-render-hinting=none"
])
backend: str = "siglip"
model_id: str = "google/siglip-base-patch16-224"
qwen_model_id: str = "Qwen/Qwen3-VL-Embedding-2B"
embed_batch_size: int = 32
embed_image_size: Optional[int] = None
index_dir: str = "./pixel_index"
ivf_threshold: int = 2000
ivf_nprobe: int = 16
top_k_tiles: int = 20
n_docs: int = 5
use_ocr_hybrid: bool = True
rrf_k: int = 60
dense_weight: float = 1.0
sparse_weight: float = 1.0
enable_server: bool = True
server_port: int = 8000
enable_eval: bool = True
enable_adapter_train: bool = True
enable_vlm_answer: bool = False
vlm_model_id: str = "Qwen/Qwen2.5-VL-3B-Instruct"
show_plots: bool = True
work_dir: str = "./pixelrag_work"
seed: int = 42
CFG = Config()
EVAL_QUERIES: List[Tuple[str, str]] = [
("how do plants convert sunlight into chemical energy", "Photosynthesis"),
("chlorophyll light dependent reactions", "Photosynthesis"),
("converting scanned images of text into machine readable characters", "Optical_character"),
("approximate nearest neighbour search over embeddings", "Vector_database"),
("self-attention multi-head architecture", "Transformer"),
("grounding a language model with retrieved documents", "Retrieval-augmented"),
("capital territory of india red fort", "Delhi"),
]
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)-7s | %(message)s",
datefmt="%H:%M:%S"
)
log = logging.getLogger("pixelrag")
for noisy in ["urllib3", "PIL", "matplotlib", "httpx", "asyncio", "uvicorn.error"]:
logging.getLogger(noisy).setLevel(logging.WARNING)
IN_COLAB = "google.colab" in sys.modulesОбратите внимание на класс Config. Он позволяет гибко настраивать параметры системы. Например, tile_width и tile_height определяют размер тайлов, а blank_std_threshold используется для фильтрации пустых или однотонных фрагментов, которые не несут смысловой нагрузки. Это экономит вычислительные ресурсы и улучшает качество индекса.
04Рендеринг веб-страниц и PDF: магия Playwright и PyMuPDF
Одной из самых сложных частей является надежный рендеринг веб-страниц. Простое скачивание HTML-кода недостаточно. Нам нужно получить визуальное представление. Для этого мы используем playwright.async_api. Скрипт запускает безголовый браузер (headless Chromium), загружает страницу, выполняет JavaScript (что важно для динамического контента) и делает скриншоты.
Чтобы обработать длинные страницы, используется алгоритм автоматической прокрутки. Скрипт программно прокручивает страницу вниз, делая скриншоты отдельных фрагментов. Это позволяет обойти ограничения на размер изображения и сохранить высокое качество деталей. Также применяются CSS-стили для очистки страницы от рекламы, баннеров и анимаций, которые могут мешать анализу.
_JS_AUTOSCROLL = """
async () => {
await new Promise((resolve) => {
let y = 0;
const timer = setInterval(() => {
window.scrollBy(0, 800);
y += 800;
if (y >= document.body.scrollHeight || y > 40000) {
clearInterval(timer);
window.scrollTo(0, 0);
setTimeout(resolve, 250);
}
}, 40);
});
"""
_JS_FLATTEN = """
() => {
document.querySelectorAll('*').forEach((el) => {
const s = getComputedStyle(el);
if (s.position === 'fixed' || s.position === 'sticky') el.style.position = 'absolute';
});
document.querySelectorAll('[role="dialog"], .cookie, #cookie-banner, .cc-banner')
.forEach((el) => el.remove());
}
"""
_CSS_CLEANUP = """
* { animation: none !important; transition: none !important;
scroll-behavior: auto !important; }
html { -webkit-font-smoothing: antialiased; }
video, iframe[src*="youtube"] { visibility: hidden !important; }
"""
_UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " \
"Chrome/124.0 Safari/537.36 PixelRAG-Tutorial/1.0"
async def _render_urls_async(urls: List[str], cfg: Config, out_dir: Path) -> List[Tile]:
from playwright.async_api import async_playwright
from PIL import Image
all_tiles: List[Tile] = []
async with async_playwright() as pw:
browser = await pw.chromium.launch(headless=True, args=cfg.headless_args)
ctx = await browser.new_context(
viewport={"width": cfg.tile_width, "height": cfg.tile_height},
device_scale_factor=cfg.device_scale,
user_agent=_UA,
java_script_enabled=True
)
for url in urls:
doc_id = _doc_id_from_source(url)
page = await ctx.new_page()
try:
await page.goto(url, wait_until="domcontentloaded", timeout=cfg.nav_timeout_ms)
try:
await page.wait_for_load_state("networkidle", timeout=12000)
except Exception:
pass
await page.evaluate(_JS_AUTOSCROLL)
await page.add_style_tag(content=_CSS_CLEANUP)
await page.evaluate(_JS_FLATTEN)
title = await page.title() or doc_id
height = await page.evaluate(
"() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)"
)
height = int(min(height, cfg.max_page_height))
step = max(cfg.tile_height, cfg.tile_overlap)
seen: List[int] = []
seq = 0
while height and seq < cfg.max_tiles_per_doc:
min_h = min(cfg.tile_height, height)
if cfg.min_tile_height and seq > 0 and min_h < cfg.min_tile_height:
break
buf = await page.screenshot(full_page=True, type="png", clip={
"x": 0, "y": 0, "width": cfg.tile_width, "height": min_h
})
img = Image.open(io.BytesIO(buf)).convert("RGB")
if img.size != (cfg.tile_width, min_h):
img = img.resize((cfg.tile_width, max(int(cfg.tile_width * img.size[1] / img.size[0]), 1)))
if _is_informative(img, cfg):
hsh = _ahash(img, 32)
if all(_hamming(hsh, s) > cfg.dedup_hamming for s in seen):
seen.append(hsh)
tid = f"{doc_id}__p0__t{seq}"
all_tiles.append(Tile(
tile_id=tid,
doc_id=doc_id,
source=url,
kind="web",
page=0,
seq=seq,
y0=0,
y1=min_h,
title=title,
path=_save_tile(img, out_dir, tid)
))
seq += 1
height -= step
except Exception as exc:
log.warning(f" FAILED {url} ({type(exc).__name__})")
finally:
await page.close()
await ctx.close()
await browser.close()
return all_tilesДля PDF-файлов используется другой подход. Библиотека fitz (PyMuPDF) открывает файл и конвертирует каждую страницу в растровое изображение с заданным DPI (по умолчанию 150). Затем эти изображения также разбиваются на тайлы. Это обеспечивает высокую точность для отсканированных документов и сложных макетов.

05Векторизация и индексация: SigLIP и FAISS
После того как у нас есть набор тайлов, следующий шаг — преобразование изображений в векторы. Мы используем модели типа SigLIP (Google) или CLIP. Эти модели обучены на огромных наборах данных пар «изображение-текст» и умеют сопоставлять визуальные и текстовые концепции в общем векторном пространстве. Это означает, что запрос «как растения преобразуют солнечный свет» может быть найден по изображению страницы, где есть текст об фотосинтезе и, возможно, диаграмма процесса.
Векторы сохраняются в индексе FAISS. FAISS использует алгоритмы приближенного поиска ближайших соседей (ANN), такие как IVF (Inverted File Index), что позволяет быстро находить наиболее релевантные тайлы среди миллионов других. Параметр ivf_nprobe контролирует баланс между скоростью и точностью: чем выше значение, тем точнее поиск, но тем он медленнее.
06Гибридный поиск и объединение результатов
Одним из сильных сторон описанной системы является возможность гибридного поиска. Помимо векторного поиска, система может извлекать текст из тайлов с помощью OCR (Tesseract) и применять алгоритм BM25. BM25 отлично работает с точными совпадениями ключевых слов, тогда как векторный поиск лучше справляется с семантическим сходством.
Результаты обоих методов объединяются с помощью Reciprocal Rank Fusion (RRF). Этот метод ранжирует документы на основе их позиций в обоих списках результатов, что позволяет получить более устойчивые и точные ответы. Весовые коэффициенты dense_weight и sparse_weight позволяют настраивать баланс между семантическим и лексическим поиском.

07Генерация ответов с помощью Vision-Language Models
Финальный этап — генерация ответа. В базовой версии системы ответ может формироваться на основе текста, извлеченного из найденных тайлов. Однако для максимальной точности можно использовать Vision-Language Models (VLM), такие как Qwen2.5-VL. В этом случае найденные изображения тайлов передаются непосредственно в модель вместе с запросом. Модель «видит» изображение и формулирует ответ, опираясь на визуальный контекст. Это особенно полезно для ответов на вопросы, требующие анализа графиков, таблиц или сложных макетов.
08Что это значит на практике
Внедрение Pixel-Native RAG меняет подход к работе с документами. Вместо того чтобы полагаться на неточное извлечение текста, вы получаете систему, которая понимает структуру и визуальный контекст. Это критически важно для:
- Юридических и финансовых документов: где таблицы, подписи и печати несут важную информацию.
- Научных статей: где диаграммы и формулы часто содержат ключевые данные.
- Веб-архивов: где динамический контент и верстка определяют смысл страницы.
Хотя такой подход требует больше вычислительных ресурсов на этапе индексации, он значительно повышает качество поиска и генерации ответов. Использование современных инструментов, таких как Playwright, SigLIP и FAISS, делает эту технологию доступной для реализации даже в небольших проектах. Главное — правильно настроить параметры сегментации и выбрать подходящую модель эмбеддингов для вашей задачи.
Источник: MarkTechPost ↗
