Spring AI: Создание базы знаний и ETL-конвейеры для LLM

Практический курс по реализации архитектуры RAG с использованием Spring AI. Вы изучите процесс преобразования документов в векторные представления, настройку абстракций и построение эффективных конвейеров загрузки данных.

1. Введение в Spring AI и RAG: роль Embeddings и векторных представлений

Введение в Spring AI и RAG: роль Embeddings и векторных представлений

Добро пожаловать на курс Spring AI: Создание базы знаний и ETL-конвейеры для LLM. В этой первой статье мы заложим фундамент для понимания того, как современные Java-приложения могут взаимодействовать с большими языковыми моделями (LLM), не ограничиваясь только теми знаниями, на которых эти модели были обучены.

Мы разберем концепцию RAG (Retrieval Augmented Generation), узнаем, почему ключевые слова больше не работают для поиска смысла, и погрузимся в математику векторных представлений, которая делает магию «понимания» текста возможной.

Проблема контекста и галлюцинаций

Большие языковые модели, такие как GPT-4 или Claude, обладают невероятными способностями к генерации текста. Однако у них есть два существенных недостатка при использовании в корпоративной среде:

  • Устаревшие знания: Модель знает мир только до момента окончания своего обучения (training cutoff).
  • Отсутствие доступа к приватным данным: Модель ничего не знает о вашей внутренней документации, базе знаний Confluence или истории переписки с клиентами.
  • Если вы спросите модель о политике отпусков вашей компании, она либо честно скажет, что не знает, либо, что хуже, придумает правдоподобный, но ложный ответ. Это явление называется галлюцинацией.

    Чтобы решить эту проблему, мы используем архитектурный паттерн RAG.

    Что такое RAG (Retrieval Augmented Generation)?

    RAG — это методика, которая объединяет поиск информации (Retrieval) с генерацией текста (Generation). Представьте, что вы сдаете экзамен. Обычная LLM — это студент, который пытается ответить по памяти. RAG — это студент, которому разрешили пользоваться учебником (вашей базой знаний).

    Процесс выглядит следующим образом:

  • Пользователь задает вопрос.
  • Система ищет релевантную информацию в вашей базе данных.
  • Найденная информация «приклеивается» к вопросу пользователя в виде контекста.
  • Весь этот пакет отправляется в LLM с инструкцией: «Используя предоставленный контекст, ответь на вопрос».
  • !Диаграмма потока данных в архитектуре RAG: от запроса пользователя через поиск контекста к генерации ответа.

    Главный вызов здесь — шаг №2. Как найти именно ту информацию, которая нужна, если пользователь формулирует вопрос не так, как написано в документе?

    Ограничения поиска по ключевым словам

    Традиционный полнотекстовый поиск (например, в SQL базах данных или Elasticsearch) работает на основе совпадения слов. Если пользователь ищет «проблемы с питанием устройства», а в документации написано «дефекты аккумулятора», классический поиск может ничего не найти, так как слова не совпадают.

    Здесь на сцену выходят Embeddings (эмбеддинги) или векторные представления.

    Embeddings: Превращаем смысл в числа

    Embedding — это процесс преобразования текста (слова, предложения или целого документа) в вектор чисел с плавающей точкой. Этот вектор представляет собой координаты текста в многомерном семантическом пространстве.

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

    Математическая интуиция

    Представьте упрощенное двумерное пространство, где ось X — это «Королевский статус», а ось Y — «Пол». В таком пространстве:

    * Слово «Король» может иметь координаты * Слово «Королева» — * Слово «Мужчина» — * Слово «Женщина» —

    В реальности модели используют тысячи измерений (например, 1536 измерений для модели text-embedding-3-small от OpenAI), что позволяет улавливать тончайшие нюансы смысла.

    !Визуализация семантической близости слов в векторном пространстве.

    Косинусное сходство (Cosine Similarity)

    Чтобы понять, насколько два текста близки по смыслу, мы измеряем угол между их векторами. Для этого используется формула косинусного сходства:

    Где: * — мера сходства между вектором и вектором (чем ближе к 1, тем больше сходство). * — угол между векторами и . * — скалярное произведение векторов (сумма произведений соответствующих координат). * — евклидова норма (длина) вектора . * — евклидова норма (длина) вектора .

    В Spring AI и векторных базах данных этот расчет происходит молниеносно, позволяя находить наиболее релевантные куски текста для контекста RAG.

    Spring AI: Абстракции для работы с данными

    Spring AI предоставляет удобный API для работы с этими концепциями, скрывая сложность взаимодействия с различными провайдерами (OpenAI, Ollama, PostgresML и др.).

    1. Document

    Класс Document — это базовая единица информации в Spring AI. Он содержит: * Текст: Сам контент (например, абзац из инструкции). * Метаданные: Map<String, Object> (источник, автор, дата создания). * ID: Уникальный идентификатор.

    Именно объекты Document мы будем сохранять в векторную базу данных.

    2. EmbeddingModel (ранее EmbeddingClient)

    Это интерфейс, который отвечает за преобразование текста в вектор. Вы просто вызываете метод embed(), передаете текст, и получаете List<Double>. Spring AI автоматически настраивает клиента в зависимости от конфигурации (например, отправляет REST-запрос в OpenAI или вызывает локальную модель через Ollama).

    3. VectorStore

    VectorStore — это абстракция над векторной базой данных (например, PGVector, Weaviate, Neo4j). Она позволяет: * Сохранять документы (при этом автоматически вычисляя их эмбеддинги). * Выполнять поиск похожих документов по текстовому запросу.

    ETL-конвейер: Ingestion Pipeline

    Прежде чем мы сможем искать информацию, нам нужно наполнить нашу базу знаний. Этот процесс называется Ingestion (поглощение) и представляет собой классический ETL-процесс (Extract, Transform, Load).

    В контексте Spring AI этот конвейер выглядит так:

  • Reader (Extract): Чтение данных из источника. Spring AI предоставляет JsonReader, TextReader, TikaDocumentReader (для PDF, Word) и другие.
  • Transformer (Transform): Обработка данных. Самый важный этап здесь — TokenTextSplitter. Мы не можем превратить в вектор целую книгу сразу (она потеряет точность смысла, и мы упремся в лимиты токенов). Мы должны разбить текст на небольшие смысловые куски (чанки).
  • Writer (Load): Запись в VectorStore. На этом этапе происходит вычисление векторов и их сохранение в БД.
  • !Схема ETL-конвейера: от сырого документа до векторов в базе данных.

    Пример кода (концептуальный)

    Вот как выглядит простейший процесс загрузки данных с использованием Spring AI:

    Заключение

    В этой статье мы разобрали теоретическую основу RAG. Мы выяснили, что для качественного поиска нам нужно перейти от сравнения слов к сравнению смыслов, используя Embeddings и косинусное сходство. Spring AI предоставляет нам мощные абстракции — Document, EmbeddingModel и VectorStore — чтобы строить такие системы на Java просто и элегантно.

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

    2. Document Abstraction: работа с интерфейсом Document, метаданными и загрузчиками (Readers)

    Document Abstraction: работа с интерфейсом Document, метаданными и загрузчиками (Readers)

    Добро пожаловать во вторую часть курса Spring AI: Создание базы знаний и ETL-конвейеры для LLM. В предыдущей статье мы обсудили теоретические основы RAG и векторных представлений. Мы узнали, что текст нужно превратить в числа, чтобы машина могла понять его смысл. Но прежде чем мы сможем вычислить вектор, нам нужно получить сам текст из реального мира.

    В корпоративной среде данные редко лежат в виде аккуратных текстовых строк. Они спрятаны в PDF-отчетах, Excel-таблицах, JSON-выгрузках или на веб-страницах. Сегодня мы разберем этап Extract (извлечение) нашего ETL-конвейера. Мы изучим, как Spring AI унифицирует работу с разнородными данными через абстракцию Document и как использовать DocumentReader для загрузки информации.

    Центральная абстракция: Класс Document

    В мире Spring AI всё вращается вокруг класса org.springframework.ai.document.Document. Это атом нашей информационной системы. Неважно, откуда пришли данные — из Telegram-чата, базы данных PostgreSQL или скана бумажного документа 1990 года — в вашем коде они должны превратиться в объект Document.

    Анатомия Document

    Объект Document состоит из трех основных частей:

  • Content (Контент): Это непосредственно текстовое содержимое. На данный момент LLM работают преимущественно с текстом, поэтому любой входной формат должен быть преобразован в String.
  • Metadata (Метаданные): Это словарь Map<String, Object>, содержащий контекстную информацию о документе.
  • ID: Уникальный идентификатор документа (обычно UUID).
  • Вот как это выглядит в коде:

    !Структура объекта Document в Spring AI, объединяющая текст, метаданные и идентификатор.

    Сила метаданных

    Новички часто игнорируют метаданные, просто загружая текст. Это критическая ошибка. Метаданные играют решающую роль в качестве RAG-системы по двум причинам:

  • Прослеживаемость (Traceability): Когда LLM выдает ответ, вы должны знать, на основе какого документа он был сгенерирован. Ссылка на источник (например, URL страницы Confluence или имя файла) должна храниться в метаданных.
  • Фильтрация (Metadata Filtering): Это самый мощный инструмент оптимизации поиска. Представьте, что у вас есть база знаний за 10 лет. Пользователь спрашивает: «Какие изменения в политике отпусков были в 2024 году?».
  • Без метаданных векторный поиск будет искать похожие фразы по всей базе, включая документы за 2015 год. С метаданными вы можете выполнить гибридный поиск: сначала отфильтровать документы, где year == 2024, и только потом искать векторы внутри этого подмножества.

    Загрузчики данных: Document Readers

    Вручную создавать Document (как в примере выше) приходится редко. Обычно мы используем Document Readers. В Spring AI это компоненты, реализующие интерфейс DocumentReader (или Supplier<List<Document>>). Их задача — прочитать внешний ресурс и вернуть список документов.

    Стандартные Reader-ы

    Spring AI предоставляет несколько готовых реализаций для простых форматов:

    * TextReader: Читает простые текстовые файлы (.txt). * JsonReader: Читает JSON-файлы. Здесь интересно то, что вы можете указать, какие поля JSON считать контентом, а какие — метаданными. * PagePdfDocumentReader: Читает PDF, создавая отдельный документ для каждой страницы (или диапазона страниц).

    Пример использования TextReader:

    Работа со сложными форматами: Apache Tika

    В реальном бизнесе информация живет в офисных документах: Word (.docx), Excel (.xlsx), PowerPoint (.pptx) и сложных PDF. Писать парсеры для каждого формата — задача неблагодарная.

    Spring AI интегрируется с библиотекой Apache Tika — стандартом де-факто для извлечения контента в мире Java. Tika умеет определять тип файла и извлекать из него текст и метаданные.

    Для использования вам понадобится добавить зависимость (если вы не используете стартер, который включает её):

    Использование TikaDocumentReader:

    !Процесс унификации данных: различные форматы файлов проходят через Reader и превращаются в стандартные объекты Document.

    Паттерн ETL: Extract

    В контексте нашего курса мы сейчас рассматриваем первую букву аббревиатуры ETL — Extract.

  • Extract (Извлечение): DocumentReader загружает сырые байты и превращает их в текст и метаданные.
  • Transform (Трансформация): (Тема следующей статьи) Разбиение текста на части (chunking).
  • Load (Загрузка): Сохранение векторов в базу данных.
  • Важно понимать, что DocumentReader обычно возвращает один объект Document на один файл (или один на страницу в случае PDF). Это создает проблему: если вы загрузите книгу «Война и мир» как один Document, она не поместится в контекстное окно LLM. Именно поэтому этап трансформации, который мы рассмотрим далее, так важен.

    Создание собственного Reader-а

    Иногда данные лежат не в файлах, а приходят по API или лежат в специфической базе данных. В этом случае вы можете реализовать интерфейс DocumentReader самостоятельно.

    Лучшие практики при загрузке данных

  • Очистка на входе: Если вы знаете, что в ваших документах есть «мусор» (например, дисклеймеры в футере каждой страницы PDF), лучше удалить их на этапе чтения или сразу после него, до векторизации.
  • Богатые метаданные: Всегда добавляйте максимум контекста в метаданные. Имя файла, дата загрузки, категория документа, права доступа — всё это пригодится при поиске.
  • Обработка ошибок: При массовой загрузке тысяч файлов один битый PDF не должен останавливать весь процесс. Оборачивайте чтение каждого файла в try-catch.
  • Заключение

    Мы разобрали фундамент работы с данными в Spring AI. Класс Document служит универсальным контейнером, а DocumentReader и его реализации (особенно на базе Apache Tika) позволяют легко извлекать текст из большинства офисных форматов.

    Однако, полученные нами документы часто слишком велики для прямой обработки нейросетями. В следующей статье мы перейдем к этапу Transform и изучим Splitters (разделители) — инструменты, которые грамотно нарезают текст на смысловые фрагменты для создания качественных эмбеддингов.

    Теперь давайте проверим, насколько хорошо вы усвоили материал о Document и Readers.

    3. Трансформация данных: стратегии разбиения текста и использование TokenTextSplitter

    Трансформация данных: стратегии разбиения текста и использование TokenTextSplitter

    Добро пожаловать в третью часть курса Spring AI: Создание базы знаний и ETL-конвейеры для LLM. В предыдущих статьях мы заложили фундамент, разобравшись с векторными представлениями, и научились извлекать «сырые» данные из файлов с помощью DocumentReader. Мы успешно выполнили этап Extract (Извлечение).

    Теперь мы переходим к самому интеллектуальному этапу ETL-конвейера — Transform (Трансформация). Если вы просто загрузите «Войну и мир» в векторную базу данных одним куском, ваша RAG-система будет работать плохо. В этой статье мы разберем, почему так происходит, что такое «чанкинг» (chunking), зачем нужно перекрытие (overlap) и как использовать TokenTextSplitter в Spring AI для подготовки идеальных данных.

    Проблема больших текстов

    Представьте, что вы загрузили в базу данных целый учебник по Java как один документ. Когда пользователь спросит: «Как работает Garbage Collector?», векторный поиск найдет этот учебник. Но когда этот огромный текст будет передан в LLM (например, GPT-4), возникнут две критические проблемы:

  • Лимит контекстного окна (Context Window Limit): У каждой модели есть предел памяти. Если текст превышает этот лимит (например, 8000 или 128000 токенов), модель просто «обрежет» конец или выдаст ошибку.
  • Размытие смысла (Semantic Dilution): Это более тонкая проблема. Вектор документа — это усредненное значение смысла всего текста. Вектор целой книги — это «средняя температура по больнице». Он не будет достаточно близок к вектору конкретного вопроса про Garbage Collector, потому что в книге также написано про потоки, коллекции, синтаксис и историю языка. Точность поиска (Retrieval Accuracy) будет низкой.
  • Решение — Chunking (разбиение на части). Мы должны нарезать большой документ на маленькие, смысловые фрагменты.

    !Визуализация процесса разбиения длинного документа на отдельные фрагменты (чанки) для последующей обработки.

    Стратегии разбиения: Символы против Токенов

    Самый простой способ разбить строку — посчитать символы. Например, резать каждые 1000 символов. Но LLM не «читают» по буквам, они оперируют токенами.

    Что такое токен?

    Токен — это базовая единица текста для нейросети. Это может быть часть слова, целое слово или даже пробел. Грубая оценка для английского языка: 1000 токенов 750 слов. Для русского языка, из-за особенностей кириллицы и морфологии, один токен часто кодирует меньше символов, чем в английском.

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

    Именно поэтому в Spring AI основным инструментом является TokenTextSplitter.

    TokenTextSplitter: Умное разбиение

    Класс TokenTextSplitter разбивает текст, ориентируясь на количество токенов, а не символов. Это гарантирует, что каждый чанк оптимально заполнит контекстное окно модели, не превышая лимитов.

    Ключевые параметры конфигурации

    При настройке сплиттера мы оперируем тремя главными параметрами:

  • Chunk Size (Размер чанка): Целевое количество токенов в одном фрагменте. Обычно выбирают значения от 500 до 2000 токенов, в зависимости от задачи.
  • Min Chunk Size (Минимальный размер): Если остаток текста слишком мал, его не стоит сохранять как отдельный документ.
  • Chunk Overlap (Перекрытие): Количество токенов, которые дублируются в конце одного чанка и в начале следующего.
  • Зачем нужно перекрытие (Overlap)?

    Это критически важный концепт. Представьте, что важное предложение разорвано ровно посередине между двумя чанками:

    Чанк 1: «...ключ к успеху — это постоянное»* Чанк 2: «обучение и практика...»*

    Ни один из этих чанков не содержит полной мысли. Векторный поиск не найдет ни один из них по запросу «Что является ключом к успеху?». Перекрытие решает эту проблему, создавая «скользящее окно».

    !Схема перекрытия чанков: дублирование части текста на стыке блоков для сохранения контекста.

    Математика разбиения

    Мы можем оценить количество получаемых чанков () по следующей формуле:

    Где: * — примерное количество результирующих чанков. * — функция округления вверх до ближайшего целого числа. * — общее количество токенов в исходном тексте. * — размер перекрытия (overlap) в токенах. * — целевой размер чанка (chunk size) в токенах.

    Эта формула показывает, что чем больше перекрытие (), тем больше чанков мы получим, так как «шаг» нашего окна () становится меньше.

    Использование в Spring AI

    В Spring AI интерфейс TextSplitter наследуется от Function<List<Document>, List<Document>>. Это означает, что он принимает список документов и возвращает (обычно более длинный) список документов.

    Пример кода

    Давайте посмотрим, как настроить и использовать TokenTextSplitter.

    Что происходит с метаданными?

    Это один из самых приятных моментов в Spring AI. Когда TokenTextSplitter разбивает один Document на десять маленьких, он копирует метаданные исходного документа в каждый из дочерних чанков.

    Если у исходного файла было поле "filename": "report.pdf", то у всех его кусочков тоже будет это поле. Это критически важно для того, чтобы потом, найдя маленький кусочек текста, вы могли дать ссылку на полный исходный файл.

    Лучшие практики (Best Practices)

  • Подбирайте размер под модель: Если вы используете модель эмбеддингов text-embedding-3-small, она отлично работает с небольшими чанками (500-1000 токенов). Слишком большие чанки «размывают» смысл, слишком маленькие — теряют контекст.
  • Overlap обязателен: Никогда не ставьте overlap в 0. Рекомендуемое значение — 10-15% от размера чанка (например, для чанка 800 токенов overlap может быть 100).
  • Чистите данные ДО сплиттера: Если в тексте есть повторяющиеся хедеры, футеры или HTML-теги, лучше удалить их на этапе Reader или отдельным трансформером перед сплиттингом. Сплиттер просто режет то, что ему дали.
  • Заключение

    В этой статье мы разобрали этап Transform. Мы узнали, что:

    * LLM имеют ограничения по контексту, поэтому текст нужно разбивать. * Векторный поиск работает точнее на небольших фрагментах, посвященных одной теме. * TokenTextSplitter — это предпочтительный способ разбиения, так как он учитывает «мышление» нейросети токенами. * Перекрытие (Overlap) спасает смысл на границах чанков.

    Теперь у нас есть список аккуратных, оптимизированных объектов Document. В следующей статье мы перейдем к финальному этапу Load: мы подключим векторную базу данных, сгенерируем эмбеддинги и сохраним наши знания для вечного использования.

    4. Генерация Embeddings и интеграция с векторными хранилищами через интерфейс VectorStore

    Генерация Embeddings и интеграция с векторными хранилищами через интерфейс VectorStore

    Добро пожаловать в четвертую часть курса Spring AI: Создание базы знаний и ETL-конвейеры для LLM. Мы прошли долгий путь: научились читать файлы (Extract) и разбивать их на смысловые фрагменты (Transform). Теперь перед нами лежит финальная и самая технически насыщенная задача этапа ETL — Load (Загрузка).

    У нас есть список объектов Document, содержащих текст и метаданные. Но компьютер не понимает текст. Чтобы наша RAG-система заработала, нам нужно превратить эти документы в векторы и сохранить их в специальную базу данных, которая умеет искать не по совпадению букв, а по смыслу.

    В этой статье мы разберем два ключевых компонента Spring AI: EmbeddingModel и VectorStore.

    Магия превращения: EmbeddingModel

    Как мы обсуждали в первой лекции, эмбеддинг — это массив чисел с плавающей точкой. В Spring AI за этот процесс отвечает интерфейс EmbeddingModel (в ранних версиях он назывался EmbeddingClient).

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

    * OpenAI: Модели серии text-embedding-3. * Ollama: Локальные модели (например, nomic-embed-text или llama3). * PostgresML, Azure OpenAI, Vertex AI и многие другие.

    Как это работает в коде?

    Spring AI делает работу с эмбеддингами тривиальной. Если вы подключили стартер (например, spring-ai-openai-spring-boot-starter), бин EmbeddingModel уже настроен и готов к работе.

    Важно понимать концепцию размерности (dimensions). Вектор — это не просто случайный набор чисел, это координаты в -мерном пространстве.

    !Процесс преобразования текстового документа в числовой вектор через модель эмбеддинга.

    Хранилище смыслов: VectorStore

    Получить вектор — это полдела. Нам нужно где-то хранить миллионы таких векторов и, что самое главное, быстро находить среди них те, которые похожи на вектор запроса пользователя.

    Обычные базы данных (PostgreSQL, MySQL) без специальных расширений не умеют эффективно сравнивать векторы. Для этого существуют Векторные базы данных (Vector Databases).

    В Spring AI интерфейс VectorStore предоставляет унифицированный API для работы с такими базами. Он скрывает особенности конкретной реализации, будь то:

    * PGVector: Расширение для PostgreSQL (очень популярно в Java-мире). * Redis: Использует модуль Redis Search. * Weaviate, Milvus, Qdrant: Специализированные векторные БД. * SimpleVectorStore: Простая реализация в памяти (In-Memory), удобная для тестов.

    Основные операции VectorStore

    Интерфейс VectorStore выполняет две главные функции:

  • Добавление (Add): Принимает список документов, автоматически вычисляет для них эмбеддинги (используя внедренный EmbeddingModel) и сохраняет результат в БД.
  • Поиск (Search): Принимает текстовый запрос, превращает его в вектор и находит ближайшие документы в базе.
  • Реализация этапа Load

    Давайте соберем наш ETL-конвейер полностью. Представим, что у нас уже есть список чанков List<Document> chunks из прошлой лекции.

    Это выглядит обманчиво просто. Но «под капотом» происходит сложная математика.

    Математика поиска: как найти похожее?

    Когда пользователь задает вопрос, VectorStore должен найти векторы, которые находятся «близко» к вектору вопроса. Но что значит «близко» в 1536-мерном пространстве?

    Чаще всего используется метрика Косинусного сходства (Cosine Similarity), о которой мы говорили в первой статье. Однако, для оптимизации вычислений многие базы данных используют Скалярное произведение (Dot Product) или Евклидово расстояние.

    Рассмотрим формулу скалярного произведения для двух векторов и размерности :

    Где: * — скалярное произведение векторов (число). * — размерность пространства (количество чисел в векторе). * — -й компонент вектора (координата). * — -й компонент вектора (координата). * — знак суммирования.

    Если векторы нормализованы (их длина равна 1), то скалярное произведение равно косинусному сходству. Чем больше это число, тем больше похожи тексты.

    Поиск информации (Retrieval)

    Теперь, когда данные загружены, мы можем выполнять поиск. В Spring AI для этого используется метод similaritySearch.

    Вы можете передать просто строку запроса, но для тонкой настройки лучше использовать класс SearchRequest.

    Параметры поиска

  • Top-K: Ограничивает количество результатов. Мы не хотим отправлять в LLM 50 документов, нам нужны только самые релевантные. Обычно выбирают от 3 до 5.
  • Similarity Threshold: Порог отсечения. Значение варьируется от 0 до 1. Если документ имеет сходство 0.2, он, скорее всего, не релевантен. Установка порога (например, 0.7) позволяет отфильтровать мусор, если в базе нет ничего похожего на запрос.
  • Фильтрация по метаданным

    В статье про Document мы говорили о важности метаданных. VectorStore позволяет использовать их при поиске. Это называется Metadata Filtering.

    В Spring AI используется переносимый язык выражений фильтрации (Expression Language). Это позволяет вам написать фильтр один раз, и он будет работать и в PGVector, и в Neo4j, и в Redis.

    Такой подход значительно повышает точность RAG-системы, сужая область поиска до логически верного подмножества документов.

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

    Заключение

    Мы завершили построение ETL-конвейера. Теперь наша система умеет:

  • Extract: Читать данные из файлов.
  • Transform: Разбивать их на чанки.
  • Load: Генерировать эмбеддинги и сохранять их в VectorStore.
  • Retrieve: Искать релевантную информацию по смыслу и метаданным.
  • В следующей статье мы перейдем к самой захватывающей части — Generation. Мы подключим ChatClient, передадим ему найденные документы и заставим LLM отвечать на вопросы, используя нашу базу знаний. Это и будет полноценная реализация RAG.

    5. Построение Ingestion Pipeline: реализация полного цикла ETL для базы знаний

    Построение Ingestion Pipeline: реализация полного цикла ETL для базы знаний

    Добро пожаловать в пятую часть курса Spring AI: Создание базы знаний и ETL-конвейеры для LLM. Мы проделали огромную работу: изучили теорию векторных представлений, научились читать документы с помощью DocumentReader (Extract), разбивать их на части с помощью TokenTextSplitter (Transform) и сохранять в векторные базы данных через VectorStore (Load).

    До этого момента мы рассматривали эти компоненты изолированно. Теперь пришло время собрать их вместе в единый, автоматизированный механизм. Этот механизм называется Ingestion Pipeline (Конвейер загрузки данных).

    Сегодня мы напишем сервис, который берет папку с документами и превращает её в готовую к использованию базу знаний, решая при этом проблемы дубликатов и управления транзакциями.

    Что такое Ingestion Pipeline?

    В мире обработки данных существует концепция ETL: Extract, Transform, Load. В контексте RAG (Retrieval Augmented Generation) этот процесс часто называют Ingestion (поглощение/загрузка).

    Наша цель — создать сервис, который выполняет следующую последовательность действий:

  • Extract: Находит новые файлы в источнике (например, в локальной папке или S3 бакете).
  • Transform: Читает содержимое, очищает его и разбивает на чанки (фрагменты).
  • Load: Вычисляет эмбеддинги и сохраняет их в векторную базу данных.
  • !Визуализация потока данных от исходных файлов до векторного хранилища.

    Архитектура сервиса IngestionService

    Давайте создадим Spring-сервис IngestionService. Он будет объединять компоненты, которые мы изучили ранее. Нам понадобятся зависимости VectorStore и DocumentReader.

    1. Подготовка компонентов

    Предположим, мы используем SimpleVectorStore (для тестов) или PgVectorStore (для продакшена) и TokenTextSplitter. Spring AI позволяет внедрить их через конструктор.

    2. Реализация этапа Extract и Transform

    Для чтения документов мы будем использовать TikaDocumentReader, так как он универсален и поддерживает большинство форматов (PDF, DOCX, TXT).

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

    Проблема идемпотентности и дубликатов

    Идемпотентность — это свойство операции давать одинаковый результат при многократном применении. Наш пайплайн должен быть идемпотентным.

    Самая простая стратегия обновления базы знаний — Delete-then-Insert (Удалить, затем вставить). Перед загрузкой новых чанков файла мы должны удалить все старые чанки, связанные с этим файлом.

    Для этого нам критически важны метаданные. В предыдущем шаге мы добавили filename в метаданные. Теперь мы можем использовать это для очистки.

    Обновим наш метод ingest:

    Теперь, сколько бы раз вы ни запускали загрузку report-2024.pdf, в базе всегда будет актуальная версия без дубликатов.

    Оценка объема хранилища (Математика векторов)

    При проектировании Ingestion Pipeline важно понимать, сколько места займет ваша база знаний. Векторы занимают значительно больше места, чем исходный текст.

    Размер одного вектора в байтах можно рассчитать по формуле:

    Где: * — размер одного вектора в байтах. * — размерность вектора (Dimensions). Для модели text-embedding-3-small от OpenAI это 1536. * — размер типа данных float. Обычно это 4 байта (32 бита).

    Таким образом, один вектор занимает байт (около 6 КБ).

    Теперь оценим общий объем базы данных ():

    Где: * — общий объем хранилища. * — количество чанков (фрагментов текста). * — размер вектора (мы вычислили выше). * — средний размер метаданных и самого текста чанка в байтах.

    Пример: У вас есть 1000 PDF-файлов. Каждый разбит на 50 чанков. Итого 50,000 чанков. Размер векторов: байт МБ. Плюс текст и индексы базы данных. Для 1000 документов это немного, но для миллионов документов вам потребуются серьезные ресурсы.

    Автоматизация запуска

    Чтобы наш Pipeline запускался автоматически при старте приложения (или по расписанию), мы можем использовать интерфейс CommandLineRunner в Spring Boot.

    Продвинутые техники Ingestion

    В реальных высоконагруженных системах простой цикл for может не подойти. Вот что используют профессионалы:

  • Параллельная обработка: Использование CompletableFuture или параллельных стримов для одновременной обработки нескольких файлов. Но будьте осторожны с Rate Limits (лимитами запросов) от OpenAI!
  • Spring Batch: Если документов миллионы, лучше использовать Spring Batch. Это фреймворк для пакетной обработки, который умеет делать повторные попытки (retry), управлять транзакциями и сохранять прогресс.
  • Отслеживание изменений (Change Data Capture): Вместо полной перезагрузки файлов, система следит за датой изменения файла и обрабатывает только те, которые обновились.
  • Заключение

    Мы построили полноценный Ingestion Pipeline. Теперь ваше приложение умеет:

  • Брать сырые файлы.
  • Превращать их в объекты Document.
  • Разбивать их на оптимальные чанки.
  • Управлять дубликатами через метаданные.
  • Сохранять векторы в базу знаний.
  • Это «тело» нашей системы. В следующей, заключительной статье теоретического блока, мы вдохнем в него «душу» — мы реализуем слой Generation, подключим ChatClient и научим LLM отвечать на вопросы, используя данные, которые мы только что загрузили.