> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

PixelRAG: Why This Project Screenshots Wikipedia Instead of Parsing It

[ View on GitHub ]

PixelRAG: Why This Project Screenshots Wikipedia Instead of Parsing It

Hook

What if the best way to search documentation wasn't to parse it at all, but to treat every page like a photograph? PixelRAG bets 217GB that screenshots beat text extraction.

Context

Traditional RAG systems follow a predictable pattern: crawl HTML, extract text, chunk by tokens, embed with a text model, stuff into a vector database. This works beautifully for prose but falls apart when semantic meaning lives in layout. Try parsing a financial statement where rows and columns encode relationships, a scientific paper where chart legends explain methodology, or a dashboard where spatial arrangement conveys hierarchy. Your chunker splits tables mid-row. Your text extractor loses chart data entirely. You bolt on OCR as a fallback, maybe add table detection as a preprocessing step, and end up with a Rube Goldberg pipeline that still can't answer "show me pages with bar charts comparing Q3 revenue."

PixelRAG inverts this entirely. Instead of treating visual rendering as a fallback for broken parsing, it makes screenshots the primary artifact. Web pages and PDFs get sliced into overlapping tiles, each tile becomes an image embedding via a fine-tuned vision-language model, and retrieval happens in visual space. No HTML parsing. No text extraction. No special-casing for tables or charts. The system treats a Wikipedia article the same way a human does: as a visual arrangement of information where layout carries meaning. This is a research bet on whether abandoning decades of web parsing infrastructure can unlock retrieval quality that hybrid text-vision approaches fundamentally can't reach.

Technical Insight

Decoupled Stages

1024x1024 tiles

with overlap

Visual embeddings

768-dim vectors

Query vector

Top-k similar tiles

orchestrates

orchestrates

orchestrates

Document URL/PDF

Pixelshot Renderer

Playwright + CDP

Screenshot Tiles

Qwen3-VL-Embedding-2B

LoRA fine-tuned

FAISS Index

User Query

Query Embedding

Same VL model

FastAPI Server

Retrieved Screenshots

YAML Pipeline Config

System architecture — auto-generated

The architecture splits into three independently executable stages, orchestrated through YAML configs or run standalone. First, pixelshot handles rendering. It's a Playwright-based CLI that launches headless Chrome, navigates to a URL, and captures overlapping screenshot tiles using Chrome DevTools Protocol. The overlap is deliberate—tiles share a border so tables or paragraphs split across boundaries appear complete in at least one tile. For a typical web page, you get 4-8 tiles at 1024x1024 resolution. The tool works offline for PDFs by rendering them in a headless browser, treating each page spread as a web view.

# Simplified rendering logic (actual implementation uses CDP)
from playwright.sync_api import sync_playwright

def capture_tiles(url, tile_size=1024, overlap=128):
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page(viewport={"width": tile_size, "height": tile_size})
        page.goto(url)
        
        # Get full page dimensions
        full_height = page.evaluate("document.body.scrollHeight")
        
        tiles = []
        y_offset = 0
        while y_offset < full_height:
            page.evaluate(f"window.scrollTo(0, {y_offset})")
            screenshot = page.screenshot()
            tiles.append({"image": screenshot, "offset": y_offset})
            y_offset += tile_size - overlap
        
        return tiles

The second stage runs these tiles through a LoRA-fine-tuned Qwen3-VL-Embedding-2B model. The base model was trained on natural images—photos, artwork, everyday scenes—not web screenshots. PixelRAG addresses this domain gap with a custom dataset: 8.28 million Wikipedia pages rendered as screenshot tiles, paired with synthetic queries generated by prompting an LLM to create search questions answerable by each page's visual content. The fine-tuning teaches the model that a screenshot of a bar chart about GDP should embed near queries like "economic growth visualization" even though there's no text saying those exact words.

# Embedding pipeline (pseudocode based on train/ structure)
from transformers import Qwen2VLForConditionalGeneration
from peft import PeftModel

# Load base model + LoRA adapters
base_model = Qwen2VLForConditionalGeneration.from_pretrained(
    "Qwen/Qwen2-VL-2B-Instruct"
)
model = PeftModel.from_pretrained(base_model, "StarTrail-org/pixelrag-qwen3-vl")

def embed_tile(image_path):
    # Vision encoder processes screenshot
    inputs = processor(images=Image.open(image_path), return_tensors="pt")
    with torch.no_grad():
        embedding = model.get_image_features(**inputs)
    return embedding.cpu().numpy()

The third stage is deliberately minimal: FAISS indexing with cosine similarity search. No hybrid retrieval combining text and vision. No re-ranking with cross-encoders. No query expansion or pseudo-relevance feedback. The engineering philosophy is that retrieval quality should come from embedding space geometry, not from piling on retrieval tricks. The production index uses FAISS IVF (Inverted File Index) with 16,384 centroids for 8.28M Wikipedia tiles, which fits in 217GB on disk. Search executes in ~100ms on CPU for top-10 retrieval, though the repository documentation emphasizes GPU inference for the embedding model.

The serve component wraps this in FastAPI with a single /search endpoint accepting either text queries (embedded via the same VLM's text encoder) or image queries (embedded via the vision encoder). This architectural symmetry—queries and documents live in the same multimodal embedding space—means you can search with a screenshot of a chart and retrieve pages containing visually similar charts, something impossible with text-only RAG.

The most clever distribution hack is the Claude Code plugin. Instead of running an MCP server or building a custom integration, pixelbrowse is just a bash script that shells out to pixelshot, captures page screenshots, and feeds them to Claude's vision API via the standard Anthropic SDK. This gives any LLM conversation visual perception of web pages without infrastructure—Claude sees the rendered page, not HTML soup. For developers already using Claude for coding tasks, this is zero-setup multimodal web browsing.

# Example pipeline config (actual format from repo)
pipeline:
  - stage: render
    tool: pixelshot
    input: urls.txt
    output: tiles/
  
  - stage: embed
    model: StarTrail-org/pixelrag-qwen3-vl
    input: tiles/
    output: embeddings.npy
  
  - stage: index
    backend: faiss
    index_type: IVF16384
    embeddings: embeddings.npy
    output: wikipedia.index

Gotcha

The 217GB index size is both a feature and a failure mode. Yes, you get all of Wikipedia ready to search out of the box. But self-hosting this requires enterprise hardware or cloud instances with 256GB+ RAM (FAISS loads indices into memory for fast search). The repository includes the index as a Hugging Face dataset, which downloads as sharded parquet files that you reconstruct locally. There's no documentation on horizontal scaling—if you want to search faster or handle more QPS, your options are "buy a bigger GPU" or "rewrite the serve endpoint yourself."

Incremental indexing is conspicuously absent. The pipeline is built for batch processing: render all documents, embed all tiles, build the index. If you want to add ten new PDFs tomorrow, you can't append them—you have to regenerate embeddings and rebuild the FAISS index from scratch. For a research prototype exploring retrieval quality, this is fine. For production systems where documents change daily, this is a dealbreaker. The repository issues have feature requests for incremental updates, but they're marked as "future work."

The training infrastructure is brittle. The train/ directory pins torch==2.9.1+cu129 and cuDNN 9.20, which only exists on specific CUDA 12.9 installations. If your GPU cluster runs CUDA 11.8 or 12.1, you're rewriting dependency specs and hoping nothing breaks. The training script expects a specific directory structure with preprocessed Wikipedia dumps in parquet format, and there's no data loader abstraction—it's hardcoded paths everywhere. This isn't unusual for research code, but it means reproducing the training run or fine-tuning on your own corpus requires more archaeology than engineering.

Finally, the Qwen3-VL-Embedding-2B model is small by modern standards. The paper (linked in the repo) doesn't include ablations against larger vision backbones like Qwen-VL-7B or comparisons to hybrid approaches that combine text retrieval with vision re-ranking. The implicit claim is that pixel-native retrieval beats text-first pipelines, but the evidence provided is limited to a single model size and architecture. If you're betting production infrastructure on this approach, you'll want to run your own evals before committing.

Verdict

Use if: You're building search over visually rich documents where layout encodes meaning (financial reports, scientific papers, dashboards), you have the infrastructure to host 200GB+ indices and run GPU inference for embedding queries, you're okay with batch-processing document corpora rather than incremental updates, or you want to quickly add visual web perception to Claude workflows without standing up MCP servers. This is compelling for research prototypes exploring whether screenshot-based retrieval unlocks new capabilities, and the pre-built Wikipedia index makes experimentation frictionless. Skip if: You need production-grade infrastructure with incremental indexing and horizontal scaling, your documents are mostly plain text where HTML parsing works fine, you're already happy with text-based RAG augmented with OCR fallbacks, or you don't have the hardware budget for 256GB+ RAM and GPU inference. The operational complexity only pays off when visual structure genuinely carries semantic weight that text extraction loses—if you're not sure whether that's true for your use case, stick with traditional RAG until you prove the hypothesis.