OSINT на минималках, или Как подружить ClickHouse, Neo4j, Qdrant и локальную LLM без штата дата-сайентистов
Как собрать локальный OSINT-комбайн для Due Diligence и внутреннего аудита без Palantir и огромных бюджетов. Разбираем гибридный конвейер на базе ClickHouse, Neo4j, Qdrant и локальной LLM.
Содержание
- Layer 0 :: Мы берём откуда то данные -> складываем в SSoT(S3)
- Layer 1 :: парсим, подготавливаем к дальнейшему поиску(Simple NER) и складываем в DDS(ClickHouse)
- Layer 2 :: Вытаскиваем оттуда фокус-кластер данных по целевому запросу и складываем во временные таблицы (опять ClickHouse). Fuzzy-search мы делаем на стороне аналитики, одновременно с этим, сохраняем таблицу поисковых сущностей
- Layer 3 :: Вытаскиваем связи первого рода для поисковых сущностей(через co-occurance в ClickHouse), складываем в локальный граф(Neo4j) и в векторную базу выбранные куски(Qdrant)
- Layer 4 :: дедупликация локального графа через векторы(BERT) с созданием зонтичных узлов
- Layer 5 :: обогащаем наш граф поиска(Global Neo4j)
- Layer 6 :: при необходимости повторяем с Layer 2, предварительно уничтожая все временные сущности(фокус-кластер, локальный граф)
- Layer 7 :: поисковые алгоритмы(GDS) и получение ответа на целевой поиск(JSON)
- Layer 8 :: Генерация human-readable report(LLM)
- Layer 9 :: Коррекция отчёта, при необходимости(Man-in-the-Middle)
- Экономика системы: Считаем косты на железо
- FAQ
Введение
Путь был долог и тернист... Если кратко: долго писал эту статью, не один раз что-то переделывал-добавлял. Порой казалось, что она стала каким-то Франкенштейном, а чаще -- воспринималась как "чемодан без ручек". Вроде бы и бросить пора, ибо надоело. А с другой стороны, постоянно казалось "вот-вот, ещё чуть-чуть и всё сойдётся воедино". Не знаю, насколько хорошо получилось в итоге, если смотреть со стороны, ибо мне всё внутри кажется логичным и правильным, но допускаю, что я где-то жёстко просчитался. Что ж, время покажет.
Сама идея этой статьи зрела в голове уже давно. Думаю, это знакомо всем, кто имеет отношение к ИТ. В какой-то момент времени всё начинает казаться "простым": люди до тебя уже столько всего сделали, показали путь, как можно на ровном месте всё реализовать для всего. И ты начинаешь задумываться, а почему бы, собственно, и нет? Задача автоматизации обработки данных сейчас (да и всегда) очень востребована, человечество само по себе ленивое существо, вперёд! И... да и всё. Ходишь, крутишь всё в голове, "как бы сделать волшебную кнопку, нажал на неё и сразу получил результат". Прямо как с джинном.
А когда стали массово появляться 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. Поэтому всегда будем держать в голове необходимость метаданных, со ссылкой на него, где это необходимо.
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:
Узлы не удаляются, чтобы избежать потери форензик-информации. Создаётся зонтичный узел 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.
Модель избавлена от длинного контекста, что устраняет эффект Lost in the Middle и экономит VRAM под KV-Cache.
Вот один из вариантов промпта:
После чего аналитик получает готовый 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 рублей. За эти деньги средний бизнес получает автономную песочницу, способную пакетно перемалывать умеренные массивы данных без внешних подписок и риска утечки информации.
Небольшой обещанный FAQ
Когда собирал статью в один монолит(да и в процессе самого написания тоже), в голове возникали периодически всякие вопросы, я их старательно записывал. А потом решил написать тут их отдельно. Потому как мог что то упустить под конец или не совсем понятно объяснить свои мысли.
Почему не реализован механизм идемоптентности в SSoT?
Это сделано сознательно, причин я вижу несколько. Первая -- излишнее усложнение самой конструкции, каждый слой делает ровно то, для чего он создан. В данном случае, речь идёт о Layer 0, который занят ровно одним: собрать откуда то данные и положить их в локальное хранилище. Вторая -- уже логическая. Иные данные важны в динамике, как например, у Иванова был долг на одну дату, а в последствии он был полностью закрыт. В этом случае, сам факт погашения долга тоже является информацией. А так как данные изначально разнородные, мы не можем с уверенностью сказать, какие именно данные важны на этапе их сбора.
А давайте запихнём вообще всё в Qdrant? Смысл нам дёргать его бесконечно(да и скорее всего могут быть пересечения кусков по co-occurences)?
Была такая мысль. Кроме того, что это дорого(по времени), это будет ещё и бестолково(по результату). Первое я думаю понятно, на средних по объёму данных простая обработка в виде "засунуть всё в векторную базу" займёт около 12-14 часов работы. Ну и что, может придти в голову, сам же пишешь что время -- не проблема. Да, но только не надо уж совсем на него плевать. Ибо мы всё таки планируем соблюдать баланс целесообразности. Кроме того, мы не должны забывать про вторую часть -- а именно, что будет если мы сразу засунем, к примеру, всю книгу в векторную базу? контекст будет довольно сильно размыт при точечном поиске информации, что опять таки нас приводит к возможным искажениям фактов, что недопустимо.
Ты пишешь "не надо писать парсеры на все случае жизни", приводя в пример сайт со сложной капчей и небольшой выборкой, так может вообще стоит отказаться от парсеров и вообще всего(включая эту архитектуру)?
А вот это напрямую зависит от объёмов исходных данных. Если их и правда немного -- то смысла нет. Я всё таки старался придерживаться подобия гибридного подхода -- не уходить в чистую сферическую архитектуру в вакууме, но и не закапываться в код, а просто как то автоматизировать труд аналитического отдела. Да, тут много "а вот если", ибо если прописывать всё жестко, это надо брать конкретную задачу, которая в любом случае будет содержать в себе некий элемент уникальности, где то проще обойтись ручным вводом, а где то -- парсерами. Получается, что это не совсем архитектура в чистом виде, а лишь набор принципов-идеологии с архитектурным ядром. Некий гибрид архитектуры с алгоритмической точки зрения, программного кода и самой философии подхода к решению задач по обработки данных
Так а всё таки, на кого она, архитектура, ориентирована?
Я вижу несколько применений -- самый я думаю востребованный, это DD. Там всегда крутятся деньги, всегда кто то кого то обманывает. Но так как это модульная архитектура, и нам никто не мешает делать на ней например, факт-чекинг через поиск скрытых бенефициаров. Да, её лучше применять когда подобные сценарии проверок нужны регулярно. В этом случае выгодна будет существенной -- на железо тратимся один раз и один раз на построение самой архитектуры. А дальше -- мы меняем каждый модуль по необходимости. Появилась необходимость парсить дополнительно список транзакций блокчейна -- пожалуйста, мы его пишем отдельно, а осталльным слоям комбайна вообще не важно что и как ты им подсовываешь, главное что бы xml парсер был такой, какой надо. То есть, ядро меняться не будет или незначительно, в виде дописывания каких то новых графовых алгоритмов. Но это не коренная перестройка
А не слишком ли сложно? Накрутить столько сущностей, ради чего? "Слетать на космическом корабле за хлебом?"
Любая архитектура призвана решать какие то конкретные задачи(или класс задач). Глупо требовать универсальности. Если предполагается разовое использование и нет жёстких коммерческих тайн -- видимо, да, стоит рассмотреть просто о разовых запросах в специализированные конторы. Но вообще, суммарная стоимость железа правда невелика, основная задача видимо придётся на программиста и аналитика. Если это суммарно выйдет дороже, чем разово заказать проверку -- заморачиваться не стоит, насильно мил не будешь
А не слишком ли она "универсальная"? И не слишком ли много trade-off? Получается что то вроде "мы делаем всё, при этом не делая ничего". Парсеры -- пишите сами. Какие алгоритмы крутить -- выбирайте опять таки сами. Суть то архитектуры тогда в чём?
Опять таки. Это не ноу-хау, это просто попытка структурировать свои собственные мысли под конкретные задачи, в частности, DueDiligence. И да, я полагаю, что это всё таки будет дешевле чем аналитику лопатить руками горы логов и всего прочего.
А вот как раз про "кучу логов". Неужели там всё так серьёзно? Кажде расследование будет содержать в себе тысячи файлов, десятки терабайт информации, которую надо как то обрабатывать?
Ну во-первых, это видимо проблема класса задач, которые она должна решать. Во-вторых, BigData подразумевает(неявно так), что данных -- не две записи в реестре ОГРНИП и им подобных. А связка и анализ разнородных источников.
А не упадём ли мы в full-scan при первичном фильтре DDS?
На самом первом проходе -- почти наверняка. Опять таки, хочу напомнить, что цель -- пакетная обработка, не реалтайм. Это важно понимать, потому что "работает комбайн минуту" или "работает комбайн 30 минут" -- не должно играть значительной роли. Его цель -- автоматизировать работу аналитика, которому в противном случае, придётся все эти десятки-сотни гигабайт обрабатывать руками. Теперь давайте немного посмотрим со стороны -- что такое наш SSoT? Это набор данных, которые нам стали доступны в рамках нашего расследования. То есть, поступает запрос(допустим, от гендиректора) -- надо проверить компанию "Рога и Копыта", дальше уже начинается сбор данных. Комбайн не инетересует конкретно откуда эти данные, нас интересует что это за данные. А теперь следим за руками. Могут ли быть в SSoT данные, которые вообще никакого отношения к ООО "Рога и копыта" не имеют? Конечно. Может ли их там быть много? Чисто в теории, да. Но -- это будет означать, что нам вместо таргетированной выборки зачем то накидали просто мусора. По логике, там должна быть очень высокая кардинальность данных с ORG:"Рога и копыта". А что это значит? Как мы все знаем, если кардинальность >10% то индекс скорее всего использоваться при выборке не будет. Но мы вообще ожидаем что встречаться будет хотя бы в 90% материалов. Поэтому при первом проходе по DDS мы должны будем выбрать почти всё, за исключением действительно "левых" документов, которые случайно попали туда. А вот уже после первого прохода, когда мы начнём фокусироваться на связях первого рода и проходить всю цепочку заново -- вот там мы ожидаем, что индексы включатся на полную мощность, ибо SSoT содержит целевые данные о "Рогах и копытах", а не о том, как звали кошку главного бухгалтера
Так это ж очень-очень похоже на то, что использует тот же Palantir, то есть ты ты хвастаешься что изобрёл велосипед(и колёса не совсем круглые при этом)? Может, тебе стоит ещё "All You Need Is Attention" пересказать и гордиться тем, что Transformer придумал?
Нет. Тут нет никакого хвастовства, ровно как нет и "посмотрите, разве я не гений? придумать такое...". Я не претендую на какое то know-how. Но во-первых, ценность этой статьи для меня в том, что я до этого дошёл сам, не опираясь на "передовой опыт". И вот так вот, накручивая одно на одно, понемногу, прыгая то вверх, то вниз, я попытался собрать это всё воедино. Ну и во-вторых, опять же, одно из главных отличий, я вижу в том, какие бюджеты на это можно выделить. Если принять что бюджет неограничен(или очень большой), а данных много и даже полчаса ожидания могут быть критическими -- эта архитектура явно проиграет Elastic и им подобным. Но я не зря статью назвал именно так -- мне хотелось посмотреть, что можно реально сделать при относительно небольшом бюджете



