В два часа ночи в воскресенье, не в силах заснуть и снова раздражённый качеством поисковых систем, разработчик открыл терминал и набросал план. Каждый интересующий его запрос — портфолио, зины, странные арт-проекты, самодельный софт — тонул под слоем корпоративной документации и SEO-мусора.
«Хочу сделать поисковик только для себя. Доменов в мире около 40 миллионов. Можно хранить немного метаданных о каждом. Даже по 1 КБ на домен это 40 ГБ — вполне подъёмно».
К обеду среды в базе накопилось 560 183 каталогизированные домашние страницы, очередь на обработку опустела, и пришло время остановиться. Далее — что было построено, что ломалось, сколько это стоило и что стоит знать тому, кто захочет повторить.
Главный вывод, если читать только один абзац: примерно за $10, одну ночь аренды GPU и несколько часов ручного управления процессом можно получить персональный поисковый индекс на несколько сотен тысяч сайтов, занимающий меньше гигабайта на диске. Дальше — подробности о том, как к этому пришли и где были острые углы.
Полные технические детали доступны в отдельном материале.

Что на самом деле хотелось построить
Не «индексировать весь веб», а находить людей, которые что-то делают — искусство, код, железо, поэзию, маленькие театры — и не утонуть в бесконечных docs.company.com. Персональный однопользовательский инструмент без аккаунтов: краулер, который смотрит только на домашние страницы, небольшая локальная языковая модель, читающая каждую страницу и выдающая название, пару-тройку предложений, категорию и несколько тегов. Сверху — простой поисковый интерфейс с нечётким совпадением, чтобы можно было ввести половину слова и всё равно найти нужный сайт.
Было явно зафиксировано, что не будет реализовано — в основном чтобы AI-агенты, помогавшие с кодом, не начали незаметно «упрощать» проект во что-то более масштабное: никакого сканирования по IP-диапазонам, никакого Redis, никакого хранения полного HTML страниц, никакого планировщика повторных обходов, никакой multi-tenant архитектуры. «Игнорировать» — это просто чекбокс у категории, применяемый на этапе поиска. Краулер всё равно суммировал интернет-магазины, просто их не нужно было потом просматривать.
Прикидка на салфетке: десятки миллионов доменов по примерно 1 КБ метаданных каждый — это буквально ничто для обычного Postgres. Текст самой страницы никогда не задумывался как корпус — только как буфер, который выбрасывается сразу после того, как модель закончила с ним работать.
Устройство системы
Четыре процесса, три из них на собственном ПК, один — арендованный GPU, который никогда не обращается к базе данных напрямую:
- Fetcher. Берёт домен из очереди, пробует HTTPS, затем HTTP, извлекает заголовок, текст страницы и исходящие ссылки простым HTML-парсером без выполнения JavaScript. Помечает строку как «готова».
- Worker. Берёт готовый домен, полностью пропускает обращение к модели, если страница пустая, запаркована или защищена бот-стеной, в остальных случаях делает один структурированный запрос к небольшой локальной модели (Gemma, 4B параметров) и получает название, summary, категорию и теги. Затирает черновой текст, ставит исходящие ссылки в очередь с приоритетом, зависящим от типа страницы-источника.
- Steward. Не трогает основную очередь вообще. Выборочно проверяет домены, от которых поступает подозрительно много страниц, спрашивает модель «блокировать, оставить или неясно» и ведёт список блокировок. Подробнее о причинах — ниже по тексту.
- API и небольшой веб-интерфейс. Поиск с фильтрами, страница для скрытия ненужных категорий и дашборд, чтобы видеть, чем в реальном времени занята «фабрика», а не вглядываться в логи.

Всё, что забирает fetcher, живёт прямо в строке домена в базе данных, пока страница находится «в обработке». После того как модель закончила с ней работать, текст затирается. Это важнее, чем звучит: при реальном масштабе нельзя позволить сырому тексту страниц накапливаться бесконечно. Сорок миллионов строк по несколько килобайт быстро складываются в заметный объём, а текст был нужен всего на несколько секунд, пока его читала модель.
Веб на 90% состоит из корпоративного контента
Первая версия заработала в течение пары часов: указываешь выборку доменов, наблюдаешь, как они суммируются, ищешь по ним. В воскресенье днём стало ясно, что каталогизируется не тот веб — свыше 90% приходилось на корпоративные сайты и документацию. Исходный список посевных доменов был смещён, а у краулера не было никаких «предпочтений» насчёт того, куда двигаться дальше.
Решением стало не блокирование — это казалось соблазнительным, но неверным ходом, поскольку скучная страница корпоративной документации всё же может вести на чей-то личный блог, и терять эту возможность не хотелось. Вместо этого очередь получила веса: страницы, отнесённые к категориям «портфолио», «зин» или «софт», резко поднимают приоритет своих исходящих ссылок, а страницы категорий «корпоративное» или «документация» — понижают. Один текстовый файл, category-priority.txt, стал рулём управления на оставшуюся часть выходных. Значение подкручивалось, затем наблюдался результат за следующий час, и подкручивалось снова.
В тот же день базу пришлось очистить дважды — качество было настолько плохим, что проще было начать заново. Выделялись две проблемы:
У одного сайта поле summary просто содержало «academic-profile» — метку категории, которую модель по ошибке подставила в чужое поле. Исправление: считать подозрительно короткое summary признаком сбоя и повторять запрос со строже сформулированным промптом.
Другой сайт — страница парковки домена на GoDaddy без реального HTML — был суммирован моделью как сайт «для фурри-сообщества». Модель просто увидела слово «furry» в имени хоста при пустом теле страницы и придумала целый фандом-сайт из ничего. Этот случай подсказал реальное правило: доверять видимому тексту больше заголовка, заголовку — больше любых догадок по домену, а если страница почти пуста или явно запаркована — даже не обращаться к модели, просто помечать и двигаться дальше.
Воскресный вечер: Tumblr — не весь интернет
К вечеру у обхода появилась новая проблема. Блоги на Tumblr и Neocities начали появляться в таком количестве, что грозили стать всем индексом целиком. Первым порывом было заблокировать их, но это ощущалось неправильным — Neocities как раз соответствует искомой эстетике. Настоящим решением стал лимит: главный домен разрешён всегда, но после того как корневой домен произвёл более 100 поддоменов, новые из него больше не ставятся в очередь. Один проход этого правила мгновенно удалил свыше 45 000 страниц из очереди. Tumblr всё равно внёс в финальный индекс более 9000 страниц, поскольку лимит действует только на будущее, но перестал быть основным содержимым.
В эту же ночь окончательно прояснилась настоящая цель проекта — не «индексировать всё», а «находить людей, создающих что-то для сообщества». Обход был пересеян примерно десятком осознанно выбранных «дверей»: tilde-сообщества, небольшие независимые блог-платформы, парочка вебринг. Почти весь итоговый индекс восходит к ссылкам, найденным через эти десять точек входа, а не к исходному списку посевных доменов.

Утро понедельника принесло похожую проблему в другом обличье: фермы форумов и китайские b2b-микросайты вендоров катались на буфере приоритета категории «форум» и утягивали обход в чёрную дыру почти идентичных страниц. Тот же урок, другая категория — приоритет «форума» был резко понижен, а для известных «мельниц» контента появился отдельный явный blocklist.
Аренда GPU: сначала плохо, потом хорошо
Собственная видеокарта, обычная потребительская модель, суммировала примерно одну страницу в секунду локально — годится для отладки промпта, но безнадёжно для реального наполнения индекса. Поэтому был арендован облачный GPU для запуска той же небольшой модели с реальной конкурентностью — и именно здесь ушла основная часть времени на отладку, причём вообще не связанную с самим AI.
Короткая версия: первая конфигурация аренды использовала обёртку-библиотеку, которая настойчиво поднимала распределённый вычислительный фреймворк даже для одного GPU, и этот фреймворк боролся с хост-машиной за процессорное время. Оплачивался GPU, а получалось троттлинг из-за конкуренции за CPU, которую никто не заказывал. От этой конфигурации отказались в пользу обычного open source inference-сервера без обёрток. Возникал краш при холодном старте на высокой конкурентности — оказалось, что дело в скачке потребления памяти на первом батче, а не в устойчивой работе; исправлением стал постепенный разогрев конкурентности вместо запуска на полную мощность сразу с холодного старта.
Машина, которая действительно справилась с задачей, — GPU среднего уровня для рабочих станций с полным, не расшаренным набором CPU-ядер. Именно эта деталь — выделенный слайс CPU против впечатляюще названного, но общего — оказалась важнее модели самого GPU. Устойчивая пропускная способность на этой машине составляла около 600 summary в минуту при стоимости аренды порядка 35 центов в час. Это примерно доллар за каталогизацию ста тысяч сайтов.
Прикидочные расчёты по этим ставкам предполагают, что GPU удаётся держать загруженными и масштабирование получается близким к линейному. Аренда двух-трёх GPU для достижения конкретного объёма стоит примерно столько же — просто каждый работает меньшее время в реальных часах; в основном покупаются сэкономленные дни:
| Сайтов | Прим. стоимость | 1 GPU | 2 GPU | 3 GPU |
|---|---|---|---|---|
| 10 тыс. | ~$0.10 | ~17 мин | ~8 мин | ~6 мин |
| 100 тыс. | ~$1 | ~3 ч | ~1.5 ч | ~1 ч |
| 1 млн | ~$10 | ~1.2 дня | ~14 ч | ~9 ч |
| 10 млн | ~$100 | ~12 дней | ~6 дней | ~4 дня |
Эти цифры основаны на лучшей из трёх опробованных конфигураций. Устойчивая пропускная способность выглядела так:

* Прогон на 4090 использовал обёртку Vast.ai / vLLM с упёршимся в потолок CPU — GPU при этом недогружен. Цифры PRO 4500 получены на обычной установке vLLM с выделенным слайсом CPU.
Данные по утилизации GPU/CPU взяты из собственного интерфейса управления серверов, где было заметно, что CPU держится на +90%, пока GPU — в диапазоне 30-50%. Утилизацию GPU/CPU на RTX PRO 4500 отдельно не замеряли, поскольку это был ночной прогон только для очистки приоритетной очереди и завершения работы. Значения пропускной способности по токенам взяты из логов серверов. По часам большая часть понедельника и вторника прошла спокойно, затем на «хорошей» машине держалось около 600 страниц в минуту, пока очередь не опустела:

Второй worker, придуманный в полночь
Поздно в понедельник ночью, наблюдая за ещё работающим обходом, возник вопрос: при таком масштабе невозможно вручную следить за плохими «спиралями» роста — а что если завести второй небольшой процесс, который выбирает пять, потом десять завершённых страниц с любого домена, производящего подозрительно много контента, и спрашивает саму модель, стоит ли его блокировать?
Так появился steward — единственное добавление, позволившее индексу вырасти с примерно 65 000 до 560 000 страниц без ручного контроля. Он работал на той же локальной видеокарте и в том же LM Studio, что использовались для отладки промптов, поэтому его решения никогда не конкурировали за ресурсы с арендованным GPU, наполнявшим индекс. За весь прогон он самостоятельно заблокировал 177 проблемных доменов — почти исключительно «мельницы» отельных и букинговых сайтов, — и корректно оставил в покое университеты и крупные легитимные платформы, у которых просто много поддоменов.
Как выглядели цифры на практике

Наблюдать за этим четыре дня было по-настоящему интересно. В начале блоги и личные сайты составляли больше половины индекса — в основном за счёт наплыва Tumblr и Neocities. К концу их доля упала до примерно 12%. Не из-за того, что личных сайтов стало меньше находиться — их находилось больше, как и всего остального, но обход «созрел» до гораздо более широкого набора: некоммерческие организации, комьюнити-сайты, софтверные проекты, журналы, музеи, подкасты.
Категория некоммерческих организаций в итоге набрала почти 27 000 реальных организаций. Выборочная проверка показала, что там действительно полно продовольственных банков, благотворительных фондов по защите природы и гражданских объединений. Именно такой результат делает потраченные выходные оправданными — категория, о которой почти не думали в начале, превратилась в одну из самых сильных частей индекса.
В какой-то момент приоритетные категории закончились, и краулер начал вычищать backlog с нулевым приоритетом — контент, который стоял в очереди всё это время, но так и не поднимался наверх. Этот backlog — честный срез сырого интернета без какой-либо кураторской правки. Поэтому, как только приоритизировать стало нечего, модель начала каталогизировать волну сайтов для взрослых.
Примерно 15% всего обойдённого контента попало в категорию «пусто» — либо страница представляла собой чистую JavaScript-обёртку, которую простой HTML-парсер не мог пробить, либо это была бот-защита. Резервный механизм с реальным рендерингом JavaScript сознательно не строился — это означало бы запуск полноценного браузера в масштабе, а это уже совсем другой и куда более дорогой проект. Некоторые из самых эстетически подходящих сайтов для этой затеи сейчас сидят именно в этой «пустой» корзине, что немного огорчает, но было правильным компромиссом при бюджете выходных.
Что на самом деле создаёт проблемы при масштабировании
Тем, кто захочет повторить это, обход и аренда GPU покажутся приятной и увлекательной частью. Тихо превращается в беспорядок именно обработка категорий и тегов. Модели было позволено свободно придумывать собственные названия категорий и тегов — в теории это должно было раскрыть таксономию, а не навязать её заранее. Для быстрого старта это было правильным решением. Но итог — 671 уникальная категория, многие использованы ровно один раз, и свыше 121 000 тегов, больше половины из которых встречаются лишь однократно, в основном потому что модель каждый раз слегка по-разному формулирует одно и то же. Пришлось написать инструмент для ручного объединения худших случаев. Такой подход не масштабируется дальше хобби-проекта. При построении чего-то крупнее выходного индекса нужно сразу решить: либо ограничить модель фиксированным списком категорий, либо заложить реальное время на последующую чистку — «пусть модель фристайлит» быстро настигает.
Вторая настоящая проблема масштабирования — управление промптом и направлением обхода как таковое. Ни одно из описанных выше исправлений не появилось из удачного промпта, придуманного за один заход. Все они возникли от наблюдения за реальными продакшн-данными в несколько моментов на выходных и подкручивания весов — один файл, несколько чисел, — исходя из того, что обход реально делал. Этот цикл — посмотреть на реальный результат, подкрутить один рычаг, посмотреть снова — дал больше, чем любое количество предварительного планирования.
Стоит ли строить такой же поисковик
Для тех, кто хочет поисковик, возвращающий только то, что действительно интересно искать, — да, за выходные это вполне реализуемо, и настолько дёшево, что счёт за GPU не станет причиной отказаться от идеи. Рабочая формула: разделить fetching и суммирование, чтобы медленное сетевое соединение никогда не оставляло дорогой GPU простаивать; взвешивать очередь обхода вместо жёсткой блокировки нежелательных пока категорий; ограничивать число поддоменов вместо запрета целых платформ; полностью пропускать обращение к модели на пустых или запаркованных страницах; и построить какой-нибудь автоматический детектор «чёрных дыр» контента, как только объём перерастёт возможность просматривать входящие страницы вручную.
Хостить или публиковать собственную продакшн-базу данных пока не планируется, так что готового набора из 560 000 сайтов не будет. Но код выкладывается как open source, так что каждый может направить собственный обход туда, куда ведёт собственное любопытство.
Репозиторий: Marlin — название в честь персонажа «В поисках Немо», отправившегося в путешествие через океан
