OSINT на минималках, или Как подружить ClickHouse, Neo4j, Qdrant и локальную LLM без штата дата-сайентистов

Содержание

  1. Layer 0 :: Мы берём откуда то данные -> складываем в SSoT(S3)
  2. Layer 1 :: парсим, подготавливаем к дальнейшему поиску(Simple NER) и складываем в DDS(ClickHouse)
  3. Layer 2 :: Вытаскиваем оттуда фокус-кластер данных по целевому запросу и складываем во временные таблицы (опять ClickHouse). Fuzzy-search мы делаем на стороне аналитики, одновременно с этим, сохраняем таблицу поисковых сущностей
  4. Layer 3 :: Вытаскиваем связи первого рода для поисковых сущностей(через co-occurance в ClickHouse), складываем в локальный граф(Neo4j) и в векторную базу выбранные куски(Qdrant)
  5. Layer 4 :: дедупликация локального графа через векторы(BERT) с созданием зонтичных узлов
  6. Layer 5 :: обогащаем наш граф поиска(Global Neo4j)
  7. Layer 6 :: при необходимости повторяем с Layer 2, предварительно уничтожая все временные сущности(фокус-кластер, локальный граф)
  8. Layer 7 :: поисковые алгоритмы(GDS) и получение ответа на целевой поиск(JSON)
  9. Layer 8 :: Генерация human-readable report(LLM)
  10. Layer 9 :: Коррекция отчёта, при необходимости(Man-in-the-Middle)
  11. Экономика системы: Считаем косты на железо

Введение

Путь был долог и тернист... Если кратко: долго писал эту статью, не один раз что-то переделывал-добавлял. Порой казалось, что она стала каким-то Франкенштейном, а чаще -- воспринималась как "чемодан без ручек". Вроде бы и бросить пора, ибо надоело. А с другой стороны, постоянно казалось "вот-вот, ещё чуть-чуть и всё сойдётся воедино". Не знаю, насколько хорошо получилось в итоге, если смотреть со стороны, ибо мне всё внутри кажется логичным и правильным, но допускаю, что я где-то жёстко просчитался. Что ж, время покажет.

Сама идея этой статьи зрела в голове уже давно. Думаю, это знакомо всем, кто имеет отношение к ИТ. В какой-то момент времени всё начинает казаться "простым": люди до тебя уже столько всего сделали, показали путь, как можно на ровном месте всё реализовать для всего. И ты начинаешь задумываться, а почему бы, собственно, и нет? Задача автоматизации обработки данных сейчас (да и всегда) очень востребована, человечество само по себе ленивое существо, вперёд! И... да и всё. Ходишь, крутишь всё в голове, "как бы сделать волшебную кнопку, нажал на неё и сразу получил результат". Прямо как с джинном.

А когда стали массово появляться open-source LLM, вообще начался бум! Всемогущий LLM, смотрите — он меня на теннис записал! А мне билеты дешёвые нашёл! А мне так вообще помог за меня написать приложение! И возникает соблазн: "А раз он такой всемогущий, может, и на этой задаче сгодится? А что, напишу несколько вдумчивых промптов, он всё сделает, а кто молодец? Я молодец!" (Спойлер: нет, так это не работает)

Виной тому несколько причин. Первая -- анализировать десятки, а то и сотни терабайт сырых логов -- это непозволительная роскошь почти для любой компании (особенно если это делать регулярно) из-за инфраструктурных затрат. Вторая, хотя на самом деле, самая главная -- это галлюцинации моделей. Любая, даже самая продвинутая и насквозь "флагманская" модель не избавлена от этого, ибо в основе их всех лежит вероятностная природа. Не думаю, что сотрудникам ИБ или директорам компаний было бы приятно читать отчёты, в которых половина данных выдумана или написаны несуществующие компании, выдуманные только что, по ассоциативным цепочкам. Вечное "допиливание" промптов с попытками жёстко задавать рамки всё равно не гарантирует от того, что где-то на дальней дистанции всё не сломается. Не буду приводить тут примеры, я думаю, все сталкивались с подобным поведением и не раз. Квен уходит в китайский язык, лама -- в бесконечную рекурсию и т.д. И нет, даже чудо-флагманы с их миллионными окнами не помогут. Длинный контекст удерживают они внезапно тоже не очень из-за эффекта Lost in the Middle и колоссального разрастания KV-Cache в ОЗУ видеокарты. ждём Суцкевера, вдруг он что то новое придумает.

А ещё заметим, что, учитывая специфику, модели должны быть локальными, дабы не допустить компрометацию самих данных и доступа к ним третьих лиц. А запуск enterprise-решений на своей локальной архитектуре потребует колоссальных затрат на неё. Да и вообще, именно флагманы никто и не даст так развернуть, а "мелкие" модели "всего" на 14B параметров как раз и страдают тем, что я описал выше. Ну и они в целом не очень быстрые для больших объёмов.

Пара нюансов: по ходу дела, я буду тут неоднократно использовать, что именно скорость решающей роли не играет, это не онлайн-погоня за преступниками, а методичная, кропотливая работа. В которой роль нашей архитектуры отводится именно как вспомогательная. Ещё стоит помнить про роль trade-off, к которому мы будем неоднократно возвращаться(ибо тут речь не про сферическую обработку данных в вакууме). И напоследок -- роль человека здесь будет далеко не последней (не только в плане написания кода). По сути, стоит рассматривать всё изложенное здесь как описание некоего гибрида, когда все элементы системы ускоряют работу человека, но никак не призваны его заменить. То есть они не "хитрее" человека, а быстрее его. Поэтому при написании я часто задавал себе вопрос: "А как бы делал человек, если был просто очень быстрым?" и "Что логично в этой ситуации с точки зрения человека?"

Отсюда и родился такой стек: ClickHouse + Neo4j + Qdrant + GDS + LLM. О каждом этапе мы поговорим отдельно и посмотрим, где и какие сильные стороны есть у каждого компонента. Отдельно лишь хочется отметить несколько вещей: а) я не претендую на всеобъемлемость; б) trade-off, которые я изложил в статье, я считаю оправданными и буду стараться сразу давать пояснения почему; в) статья поэтому так и называется "...на минималках...", потому что моя цель — показать, что даже для условно "средней" компании, не содержащей большой ИТ-отдел, создать подобный комбайн вполне под силу, затраты будут достаточно скромные. Поэтому, к примеру, я сознательно отказался от Elastic и его "автоматов Левенштейна", ибо всё это очень дорого и громоздко. А для "минималок" вполне подойдёт trade-off со снятым ограничением по времени работы, зато способным запускаться "на коленке" и масштабироваться горизонтально при необходимости.

Итак, как я вижу, кому может быть интересна эта статья:

  • среднего размера компаниям, которые предоставляют услуги по анализу рисков в сфере DD(DueDiligence), фактчекинга, мониторинга;
  • крупным компаниям, которые вполне могут содержать собственный штат для внутренних расследований, не выходящих за пределы сферы интересов самой компании. То есть у одних нет бюджета на Palantir, а у других нет необходимости в таких масштабах, и оба варианта подходят к решению подобных задач с точки зрения цена/качество. Поэтому и "на минималках", который обеспечит приемлемый уровень решения поставленных задач. Приблизительные расчёты цены инфраструктуры я привожу в самом конце.

Layer 0 :: Schema Drift - Парсеры - SSoT

Каждый, кто хоть раз занимался сбором данных, может легко представить по крайней мере две главные проблемы, о которые спотыкаются "не только лишь все". Первая — это многообразие источников. Которая порождает за собой почти мгновенное желание сделать супер-универсальный парсер на все случаи жизни. Ибо сперва ты пишешь парсер для задачи, скажем, разбора web-страницы через какой-нибудь newspaper4k, дабы вытащить оттуда интересующий текст о конкретной фирме. Потом тебе требуется достать данные из тг-канала в формате JSON через telethon. А ведь кроме этого есть ещё и другие форматы данных — сырые дампы баз, pdf-сканы и прочее.

И возникает соблазн объединить их все под одно крыло, сделать универсальный механизм, который будет "что-то (разнородное)" принимать на вход, а выдавать уже "другое (однородное)". Спойлер: идея не очень.

Почему? Потому что данные могут быть действительно абсолютно разные. И попытки объединить всё в одной сущности завязнут на этапе проектирования/разработки такого "супер-парсера". Решение получится "хрустальным", очень уязвимым как к добавлению новых источников, так и к изменению данных уже существующих (Schema Drift). Ибо никто с вами не договаривался, что формат всегда будет один.

Поэтому тут мне пришла в голову мысль вспомнить ТРИЗ с его ММЧ (Моделирование Маленькими Человечками), суть которого в том, что парсеры (человечки) собираются вместе для работы, а потом опять разбегаются по своим делам до следующего раза. Ну или букву I из SOLID, кому как нравится. Поэтому я предлагаю тут сознательно отказаться от монолита и перейти к "микросервисам". То есть для каждого типа источника мы будем использовать свой отдельный парсер. Да, вполне возможно переиспользование, к примеру, если у нас несколько новостных лент используют одинаковый формат, или нам нужно вытащить данные с 10 тг-каналов. А для того, чтобы "человечки" собирались вместе для работы, логичнее всего использовать оркестраторы, такие как Apache Airflow, Prefect и им подобные. А для самих парсеров/скраперов логично использовать Go с его мощью goroutines, которые можно одновременно выполнять на весь доступный ресурс процессора. И ограничены они будут разве что RateLimiter'ами на отдающей стороне. Ну или Rust, это непринципиально.

То есть, тут мы просто занимаемся сбором данных и складыванием их в одно централизованное место. Оно будет играть роль единственного источника (Single Source of Truth, SSoT) для всего комбайна. Данные там пока опять-таки будут абсолютно разнородные -- JSON-файлы, HTML-страницы, csv-дампы, pdf-сканы. Но они будут лежать у нас локально. И мы больше не будем зависеть от того, что кто-то решил закрыть доступ. В качестве такого источника удобнее всего использовать S3-хранилища, хотя в принципе подойдёт и обычный выделенный SSD.

На этом мы заканчиваем работу нашего Layer 0. Результат: [External Sources] -> [SSoT(S3)]

Layer 1 :: Parsing - NER - DDS

Теперь понемногу начинаем заниматься тем, ради чего мы тут все собрались. А именно -- готовим наши материалы для удобного поиска и обработки. Уже на этом этапе мы приводим их к условно-единому формату. И вот как мы это будем делать:

  • Как только новые данные попадают в SSoT, мы через Event Notifications узнаём об этом и отправляем задачу на обработку в Kafka. Использование Kafka здесь обусловлено отчасти тем, что наша обработка будет воспроизводимой в случае каких-то непредвиденных обстоятельств.
  • Каждая задача будет характеризоваться парой (источник, тип данных), посредством которой мы будем назначать необходимый парсер. По возможности задачи на обработку можно будет распараллеливать. Именно тут заканчивается "разнородность" данных, ибо наша главная цель — привести всё к единому формату raw_text + metadata.
  • Именно тут "живут" все возможные обработчики — JSON-парсеры, запросы к LLM для парсинга pdf-сканов через OCR и тому подобное. Сразу может возникнуть резонный вопрос — а не проще, чтобы Layer 0 также принимал посильное участие в первичной обработке сырых байт? Например, сразу куда-то складывать текст новости, прогнанный через newspaper4k. Я считаю, что нет, ибо это породит хаос, потому как часть источников можно обработать на лету, а для кого-то нужно время (как для OCR). И кроме того, нет гарантии от сбоя во время работы самих сборщиков. Нам придётся делать дополнительную логику "всё ли мы скачали и обработали", что нарушает принцип единственной ответственности. Поэтому Layer 1 будет самостоятельным и будет работать только "оффлайн", не запрашивая ничего из внешнего мира.
  • Из построенных выше шагов следует, что мы абсолютно не зависим от Layer 0 и можем запускаться одновременно с ним, как только станут поступать первые данные. Главное архитектурное правило этого слоя: мы жестко делим входной поток на Наратив (тексты, судебные иски, статьи) и Логи (таблицы, выгрузки CSV). Наратив мы прогоняем через легкий локальный NER (Natasha), вытаскивая массивы entities_per (люди), entities_org (организации) и entities_loc (адреса). А для Логов никакой NER не запускается -- пусть парсер просто маппит ФИО и ИНН из колонок напрямую в массивы сущностей ClickHouse. Покупки и операции («купил зимний костюм») остаются лежать внутри поля raw_text как текст, не забивая поисковые индексы лишним мусором. Далее полученные данные мы записываем в базу, которая нам будет служить DDS (Data-Derived Storage), суть которой -- удобное представление информации из SSoT, не более. Отдельно стоит отметить один момент -- мы будем предполагать, что для всех последующих слоёв, именно DDS будет SoT. Поэтому всегда будем держать в голове необходимость метаданных, со ссылкой на него, где это необходимо.

Чтобы не перегружать статью техническим кодом, просто отметим -- всё это после пред-обработки пишется в плоскую таблицу ClickHouse, с инвертированными индексами на Array(String). Где строки и есть те самые сущности, которые мы извлекли.

Layer 2 :: Выделение фокус-кластера и контролируемый Fuzzy-Search

Итак, на этом этапе мы занимаемся магией нечёткого поиска (fuzzy search). Как звучит наша цель на этом этапе? Найти все данные, которые содержат строки, похожие на то, что мы ищем. То есть мы вполне допускаем, что в DDS будут, к примеру, "ивонов" вместо "иванов" (опечатка OCR) или "ксрябин" вместо "скрябин" (опечатка перестановкой при наборе). И эти данные для нас так же важны. То есть мы не можем просто искать по точному совпадению строк, мы можем пропустить много важных данных.

Де-факто стандартом при таком поиске можно считать ограничение по расстоянию Дамерау-Левенштейна, который как раз и будет ограничивать, насколько допустимо считать одинаковыми подобные строки. После долгих размышлений мною было принято решение перенести подобный поиск на сторону клиента (аналитика).

Каким образом мы это делаем? Вместо того чтобы для каждого вхождения считать это расстояние на стороне базы, мы переносим всю генерацию возможных искажений на скриптовую часть аналитика. Скрипт генерирует список допустимых мутаций на основе таблиц OCR-ошибок и карты опечаток клавиатуры, что полностью отсекает комбинаторный взрыв. Для 7-буквенного слова мы получаем компактный список из 700-800 строк. А затем мы просто делаем один поисковый запрос в ClickHouse через быстрый hasAny.

Почему это круто? Во-первых, мы избавляем базу от тяжелых вычислений матриц динамического программирования. Во-вторых, мы избавляемся от false positive, так как ищем только то, что аналитик посчитал допустимым. В-третьих, аналитик сам тонко управляет поиском (задает точное совпадение, 1 перестановку и т.д.), не изменяя при этом DDS.

А ещё логично добавить сюда переключатель режимов. Если мы ищем объект в разнородных данных («Иголка в стогу»), тоhasAny за секунды вырезает из базы компактный фокус-кластер. Если же мы работаем в режиме «Аудит» (выгрузка баз внутреннего аудита, где объект сидит в каждом документе, а сами данные однородны и структурированы, как к примеру, в csv), система видит низкую селективность и на следующих слоях отключает нарезку текста, переходя к прямой потоковой агрегации массивов ClickHouse.

Layer 3 :: Экстракция контекста, локальный граф (Neo4j) и векторный кэш (Qdrant)

Теперь, у нас есть набор таблиц, в которых лежат только интересующие нас документы фокус-кластера. Теперь предстоит прикладная задача -- вытащить связи первого рода, а именно с кем и чем наш "Иванов" упоминается рядом (co-occurrences).

В режиме «Иголка в стогу» мы забираем данные из temp_focus_cluster в Python. Чтобы не крутить тяжелые вложенные циклы под каждую из 800 мутаций, мы компилируем список вариантов в один регулярный паттерн. Скрипт находит точные индексы вхождений в raw_text за один проход:

Чтобы связи были достоверными, сопутствующие сущности (ООО «Ромашка») вытаскиваются из текста строго внутри этого окна по их координатам. Из ClickHouse для этого забираются массивы сущностей вместе с их позициями в тексте, сформированными на Layer 1. По умолчанию создается техническая связь MENTIONED_WITH. В режиме «Аудит», когда Иванов везде и одинаковых строк миллионы, мы полностью отключаем регулярки в Python, чтобы не лечь на full-scan текста, и заставляем ClickHouse собрать граф агрегацией по колоночным массивам:

Данные отправляются в локальную песочницу Neo4j, развернутую под текущую итерацию расследования. Узлы -- сущности, ребра -- совместные упоминания со сквозным хэшем связи (edge_hash). Если Иванов и организация встретились в 50 разных документах, вес ребра становится равен 50.

Параллельно вырезанный текстовый контекст (только для значимых связей) прогоняется через модель эмбеддингов DeepPavlov/rubert-base-cased-sentence на видеокарте, и вектор пишется в локальный Qdrant. В payload сохраняются doc_id и edge_hash. Сам текст документов и фрагменты лежат в ClickHouse в таблице доказательств edge_evidence, разгружая граф.

Layer 4 :: Дедупликация локального графа (BERT + Жаккар)

В локальном графе Neo4j образовался избыточный массив узлов с опечатками. Нам нужно слить дубли узлов через два алгоритма: коэффициент Жаккара и семантику BERT. Процесс идет строго от жесткого к мягкому: если у двух узлов (Иванов и Ивонов) есть жесткие маркеры (ИНН или Паспорт) и они совпадают -- склейка разрешена(вполне логично, ибо таких супер-уникальных идентификаторов суть несколько всего на все случаи жизни). Если они различаются -- автоматическая склейка жестко блокируется, какими бы похожими ни были тексты. Далее, для узлов без ИНН мы берем векторы контекстов из Qdrant. Сложность O(N^2) обходится за счет пространственного индекса HNSW: мы делаем векторный поиск search в радиусе косинусного сходства > 0.85 только среди соседей по фокус-кластеру.После чего, сначала стягиваются очевидные дубли организаций. Как только их окружения пересеклись, мы вычисляем коэффициент Жаккара по структуре связей в Neo4j на Cypher(для начала можно ставить "стандартный" порог в 0.4)

Узлы не удаляются, чтобы избежать потери форензик-информации. Создаётся зонтичный узел UmbrellaNode со связями SIMILAR_TO к мутациям. Каноническим именем выбирается самая полная строка. После валидации аналитиком все внешние связи первого рода пересчитываются на этот зонтичный узел.

Layer 5 :: Слияние с глобальным графом и запуск GDS

Мы никогда не льем в глобальный граф расследований сырые строки. Переносятся только каноничные зонтичные узлы, их проверенные entity_id, агрегированные веса связей и уникальные edge_hash.

Если локально «Иванов» упомянут с «ООО Ромашка» в 15 новых документах, в глобальный граф уходит одна связь с инкрементом веса, зажатая проверкой уникальности пакета, чтобы исключить двойной учет при повторном запуске воркера:

На глобальный граф натравливаются алгоритмы пакета Neo4j Graph Data Science (GDS). Они запускаются в режиме write, записывая результаты в свойства узлов:PageRank / Degree Centrality: Подсвечивают структурно значимые узлы-посредники (массовые адреса, номиналы), через которые проходит аномально много транзитных связей. И Louvain Community Detection: Нарезает граф на изолированные сообщества. Если наш объект попал в один плотный кластер с фирмами-однодневками, система вешает маркер для проверки аналитиком. Алгоритмы не являются детекторами вины автоматически, они лишь группируют топологию для человека.

Layer 6 :: Интерфейс аналитика и Human-in-the-Loop (HITL)

Робот может ошибаться, поэтому аналитик-расследователь -- это высший валидатор системы. Интерфейс показывает ему предложенные системой UmbrellaNodes и подсвечивает их пунктиром со скором сходства (путь к примеру, score: 0.87).

Далее, варианты взаимодействия аналитика с графом могут быть такие: а)Утверждение (Approve): Аналитик подтверждает связь, статус меняется на VERIFIED_BY_HUMAN. Робот больше никогда не трогает этот узел. б)Разделение (Split): Если система ошибочно склеила полных тезок, аналитик разрывает связь. В базу летит жесткий запрет NEVER_LINK между этими ID, блокирующий Жаккар и BERT на будущих итерациях. в)Добавление ручной связи: Аналитик сам рисует ребро MANUAL_LINK на основе оперативных данных. Таким образом, ещё раз -- главный таки человек остаётся, а суть комбайна -- максимально(и детерминированно) ускорить его работу. На каждое такое действие человека делаем запись в таблице ClickHouse human_feedback_events. На следующих итерациях Layers 3 и 4 первыми читают эту таблицу фидбека: если человек принял решение, тяжелые алгоритмы BERT для этих пар просто выключаются, экономя ресурсы GPU. После закрытия сессии расследования фокус-кластера локальный граф в Neo4j и временная коллекция в Qdrant полностью уничтожаются (DROP DATABASE / delete_collection).

Layer 7 :: Сборка контекстного JSON-подграфа

Итак, плавно движемся к финалу, а именно, будем считать, что все данные каким то образом обработаны и теперь аналитик нажимает кнопку «Сформировать досье по объекту Х». Так что наша задача тут -- собрать вокруг объекта жестко сфокусированное смысловое окружение глубиной в 2–3 шага (hops) от цели.

Мы упаковываем этот подграф в древовидный JSON. Из глобального Neo4j мы выгружаем сущности, типы связей (через нативную функцию type(r)) и сохраненные edge_hash. Python-скрипт берет эти хэши, делает точечный быстрый запрос по первичному ключу к таблице edge_evidence в ClickHouse и забирает оттуда реальные ID документов и сохраненные куски текстового «мякиша».

Собственно, в результате мы получим сфокусированную структуру данных, где вес связей означает количество уникальных документов совместного упоминания, и для каждого ребра прикреплены ссылки на первоисточники.

Layer 8 :: Инференс локальной LLM и синтез финального отчета

И вот наконец мы дошли до того, что будем использовать LLM(например, Qwen-2.5-14B-Instruct или Llama-3-8B-Instruct в квантовании Q8), развернутая в изолированном контуре на бытовой видеокарте.

И вот тут важно то, о чём мы говорили в самом начале -- мы будем использовать её исключительно как профессионального копирайтера-оформителя. Мы полностью лишаем её возможности «думать» и самостоятельно искать связи. Ей на вход отдаём жесткий структурированный JSON из Layer 7 и точечные текстовые вырезки из ClickHouse по edge_evidence.

{ "target_object": "Иванов Игорь Иванович", "risk_metrics": { "community_id": "cluster_12", "review_status": "requires_human_review" }, "direct_links": [ { "entity": "ООО Ромашка", "weight": 54, "evidence_documents_count": 54, "evidence_docs_sample": ["doc-001", "doc-002"] }, { "entity": "Кипрский Офшор Ltd", "weight": 12, "evidence_documents_count": 12, "evidence_docs_sample": ["doc-003"] } ], "hidden_patterns": [ { "path": "Иванов -> ООО Ромашка -> Кипрский Офшор Ltd", "evidence_documents_count": 5, "evidence_docs_sample": ["doc-004", "doc-005"] } ] }

Модель избавлена от длинного контекста, что устраняет эффект Lost in the Middle и экономит VRAM под KV-Cache. Вот один из вариантов промпта:

System: Ты — ведущий аналитик департамента корпоративной безопасности. Твоя задача — составить строгое аналитическое заключение на основе предоставленного структурированного графа связей (JSON) и верифицированных текстовых вырезок. ПРАВИЛА: 1. Пиши только в официально-деловом стиле. 2. Используй исключительно факты из предоставленного JSON. 3. Если в JSON указано весовое значение 54, напиши "зафиксировано совместное упоминание в 54 уникальных документах". Не называй это доказанными фактами аффилированности. 4. КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО выдумывать фамилии, названия компаний, даты и ИНН, которых нет в контексте. 5. Выдели в отчете три блока: "Резюме для руководства (Executive Summary)", "Выявленные аномалии и схемы", "Список подтверждающих документов".

После чего аналитик получает готовый doc/md, где каждое слово засинонизировано с конкретным doc_id в ClickHouse(DDS).

Экономика системы: Считаем косты на железо

В отличие от Palantir или Elastic, наш комбайн спроектирован под работу на одной локальной рабочей станции, которая покупается компанией один раз. Теперь немного прикинем что и во сколько может обойтись:

  • GPU (Видеокарта): NVIDIA GeForce RTX 3090 / 4090 (24 GB VRAM). На б/у или розничном рынке это база для локального инференса 14B-моделей в Q8 и обсчета эмбеддингов. Начиная с 2025-2026 годов флагманским выбором становится RTX 5090 с 32 GB GDDR7, разгружающая KV-Cache. Стоимость: ~140 000 – 250 000 руб.
  • CPU (Процессор): AMD Ryzen 9 7950X (16 ядер / 32 потока) под многопоточную регулярку Python и движок ClickHouse. Стоимость: ~60 000 руб.
  • RAM (Оперативная память): 128 GB DDR5 под графовые проекции GDS. Стоимость: ~45 000 руб.
  • Хранилище (Диски): Быстрый NVMe SSD на 4TB (под ClickHouse/Qdrant) + HDD на 16TB (под S3-архив SSoT). Стоимость: ~65 000 руб.
  • Обвесы (MB, БП 1000W+, охлаждение и прочая и прочая): ~50 000 руб. Сложим и получим примерный бюджет на железо: ~360 000 – 470 000 рублей. За эти деньги средний бизнес получает автономную песочницу, способную пакетно перемалывать умеренные массивы данных без внешних подписок и риска утечки информации.

P.S. Проект сделан в концепте HLD. Дабы не забивать ленту vc.ru портянками кода, все рабочие SQL-схемы таблиц ClickHouse, Python-скрипты с регулярками и Cypher-запросы батчинга для Neo4j, а так же FAQ с вопросами, которые могли возникнуть, я выложил в полной технической версии статьи на iXBT