В нашей компании AnyMaint, которая занимается разработкой софта для управления техническим обслуживанием и ремонтом (CMMS) промышленного оборудования, одной из главных задач является нормализация имён тулов (инструментов). Под «тулом» мы подразумеваем любой промышленный актив: машины, станки, приборы, оборудование и т.д.

Зачем это нужно?

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

Проблема нормализации тулов

Нормализация — непростая задача. Одно и то же оборудование могут называть по-разному в разных компаниях (и даже в рамках одной):

  • Разные языки.

  • Разные коды производителя.

  • Разные системы категорий.

  • Аббревиатуры против полных названий.

Имя тула

Перевод

Normalized name

מזגן חדר חומר גלם

Air conditioner raw material room

air conditioner

מזגן מזכירה

Secretary’s Air conditioner

air conditioner

מזגן אוויר צח (1)

Clear air conditioner

air conditioner

מזגנים כללי

General air conditioners

air conditioner

Eppendorf Centrifuge D456 XXX-123

centrifuge

Sorvall Super T21 centrifuge XYZ-333

centrifuge

צנטריפוגה Z 33UK GH-098

centrifuge

centrifuge

Больше года для этой задачи мы использовали систему RAG (Retrieval-Augmented Generation), построенную на AWS Knowledge Base. Наш воркфлоу был таким:

  1. Когда в системе появляется новый тул, мы ищем похожие в RAG.

  2. Если совпадения найдены (с достаточно высоким скором), берем их нормализованное имя.

  3. Если совпадений нет, мы передаем всю доступную инфу о туле в LLM вместе со списком наиболее вероятных нормализованных названий.

  4. LLM принимает финальное решение о нормализации.

Почему мы решили уйти с AWS Knowledge Base

Система работала неплохо, но под капотом AWS Knowledge Base использует AWS OpenSearch — он быстрый, но дорогой. Наш процесс нормализации асинхронный, и нам не нужен сверхбыстрый отклик. Платить за эту инфраструктуру нам не хотелось

В нашей базе около 20 000 тулов с верифицированной нормализацией. Задачи нормализации запускаются в Lambda-функциях по событиям EventBridge.

Когда AWS анонсировала S3 Vector Search (на момент написания статьи — еще в бете), мы решили его попробовать. Потенциальная экономия была слишком привлекательной.

Новая архитектура с S3 Vector Search

Схема работы

Вот как работает наша новая схема:

  1. Генерация эмбеддингов: Когда появляется новый тул, мы создаем его эмбеддинг, используя локальную LLM-модель.

  2. Векторный поиск: Ищем в S3 Vector store, где хранятся пре-индексированные эмбеддинги существующих тулов.

  3. Сопоставление: Если distance ниже порога, используем нормализованное имя из найденного тула.

  4. Fallback: Если хорошее совпадение не найдено, окончательное решение принимает Claude Sonnet 3.5.

Выбор модели эмбеддингов

Мы попробовали несколько моделей и остановились на Xenova/bge-m3:

  • Многоязыковая поддержка (критично для нас).

  • Доступна как на Python, так и на TypeScript.

  • Хороший баланс между качеством и размером.

  • Эмбеддинги на 1024 измерения.

Docker-образ для Лямбды

Одной из самых больших проблем была производительность холодного старта. Загрузка тяжелой ML-модели в лямбде занимает время. Мы решили это так:

  1. Создали Docker-образ, который уже содержит загруженную модель Xenova/bge-m3.

  2. Модель скачивается во время Docker build, а не в рантайме.

  3. Лямбда грузит пре-кэшированную модель из файловой системы образа.

Пример Dockerfile:

# Используем AWS Lambda Node.js 20 в качестве базового образа
FROM public.ecr.aws/lambda/nodejs:20

# Устанавливаем рабочую директорию
WORKDIR ${LAMBDA_TASK_ROOT}

# Копируем package.json и package-lock.json для лучшего кэширования
COPY package*.json ./

# Устанавливаем зависимости
RUN npm install

# Копируем остальной исходный код
COPY . .

RUN npm run build

# Прогреваем модель, загружая ее во время сборки
# Это снижает cold start time Lambda, предварительно загружая модель transformers
RUN echo "Starting model warmup..." && \
  node dist/src/scripts/warmup-model.js

# Устанавливаем переменные окружения для Lambda
ENV NODE_ENV=production
ENV AWS_NODEJS_CONNECTION_REUSE_ENABLED=1

# Обработчик функции Lambda
CMD ["src/lambda_functions/get-tools-chunk/index.handler"]

Скрипт для загрузки модели (warmup-model.js):

/**
 * Скрипт для прогрева модели, который предварительно скачивает модель transformers во время Docker build.
 */
import { Embeddings } from '@/helpers/Embeddings';

async function warmupModel() {
  console.log('Warming up S3VectorStorage model in Docker build...');
  try {
    // Создаем инстанс
    const vectorStorage = new Embeddings({
      modelConfigKey: 'bge-m3',
    });
    // Тестируем создание эмбеддинга, чтобы триггернуть скачивание модели
    console.log('Creating test embedding to download model...');
    const embedding = await vectorStorage.createEmbedding(
      'Warmup text for model download'
    );
    console.log('Model downloaded and embedding created successfully');
    console.log('Embedding length:', embedding.length);
    console.log('First few values:', embedding.slice(0, 3));
    console.log('Model warmup completed successfully!');
  } catch (error) {
    const errorMessage =
      error instanceof Error ? error.message : String(error);
    console.error('Model warmup failed:', errorMessage);
    // Не выходим с ошибкой, просто логируем проблему.
  }
}

// Запускаем прогрев
warmupModel()
  .then(() => {
    console.log('Model warmup process finished.');
    process.exit(0);
  })
  .catch(error => {
    const errorMessage =
      error instanceof Error ? error.message : String(error);
    console.error('Model warmup failed:', errorMessage);
    // Не фейлим Docker build, просто ворнинг.
    process.exit(0);
  });

Текущий размер нашего Docker-образа — 778 МБ. Лямбда настроена с 4 ГБ памяти. Как результат:

  • Загрузка модели занимает 5–6 секунд.

  • Первый холодный старт иногда вылетает по таймауту.

  • Последующие запуски быстрые (модель остается в памяти).

Для нашей асинхронной задачи старт за 5–6 секунд вполне приемлем.

S3 Vector Search Integration

В отличие от AWS Knowledge Base, где требовался один файл на тул, S3 Vector работает иначе — вы просто используете API для сохранения документов и их эмбеддингов. Сервис сам занимается индексацией.

Фрагмент класса S3VectorStorage (Python):

class S3VectorStorage:
    def __init__(self,
                 bucket_name: str,
                 index_name: str,
                 region_name: str = 'us-east-1',
                 ):
        """
        Инициализирует S3VectorStorage с поддержкой конфигурируемой модели.
        """
        self.region_name = region_name
        self.s3_vectors = boto3.client('s3vectors', region_name=region_name)
        self.bucket_name = bucket_name
        self.index_name = index_name
        logger.info(f"S3VectorStorage initialized with index: {self.index_name}")

    def store_document(self, document_id: str, embedding: List[float], content: str,
                       additional_attributes: Dict[str, Any] = None) -> str:
        """
        Сохранить документ с его вектором и атрибутами.
        """
        try:
            # Подготовка атрибутов метаданных
            attributes = {
                'content': content
            }
            if additional_attributes:
                attributes.update(additional_attributes)

            # Сохраняем векторный документ
            logger.info(f"Storing document: {document_id}")
            self.s3_vectors.put_vectors(
                vectorBucketName=self.bucket_name,
                indexName=self.index_name,
                vectors=[
                    {
                        "key": document_id,
                        "data": {"float32": embedding},
                        "metadata": attributes
                    }
                ]
            )
            return document_id
        except Exception as e:
            logger.error(f"Failed to store document {document_id}: {e}")
            raise

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

  • AWS Knowledge Base возвращает Similarity Score (чем выше, тем лучше).

  • S3 Vector возвращает Distance (чем ниже, тем лучше).

В Knowledge Base мы использовали порог score >= 0.64. С метрикой расстояния S3 Vector мы до сих пор экспериментируем, чтобы найти оптимальный порог.

Метод поиска search_documents:

def search_documents(self, query_embedding: List[float], query_filter: Dict[str, Any] = None,
                     max_results: int = 10) -> List[Dict]:
    """
    Ищет похожие документы на основе эмбеддинга запроса.
    """
    try:
        # Подготовка параметров поиска
        search_params = {
            'vectorBucketName': self.bucket_name,
            'indexName': self.index_name,
            'queryVector': {'float32': query_embedding},
            'topK': max_results,
            'returnMetadata': True,
            'returnDistance': True
        }
        if query_filter:
            # Создаем фильтр с оператором $eq для каждой пары ключ-значение
            filter_conditions = []
            for key, value in query_filter.items():
                filter_conditions.append({key: {'$eq': value}})

            # Используем оператор $and, если условий несколько
            if len(filter_conditions) == 1:
                search_params['filter'] = filter_conditions[0]
            else:
                search_params['filter'] = {'$and': filter_conditions}

        # Выполняем поиск
        response = self.s3_vectors.query_vectors(**search_params)
        
        results = []
        for match in response.get('vectors', []):
            result = {
                'key': match['key'],
                'distance': match['distance'],
                'metadata': match.get('metadata', {})
            }
            results.append(result)
        logger.info(f"Found {len(results)} matching documents")
        return results
    except Exception as e:
        logger.error(f"Search failed: {e}")
        raise

Сравнение производительности

Latency

  • AWS Knowledge Base: около 1 секунды для векторного поиска.

  • S3 Vector: около 7–8 секунд (включая генерацию эмбеддингов + поиск).

  • С LLM fallback: плюс время на вызов Claude API.

Более высокий latency в S3 Vector в основном из-за локальной генерации эмбеддингов (5–6 секунд на загрузку модели). Если использовать эмбеддинги Bedrock, задержка будет ближе к Knowledge Base.

Cost

Мы пока не перешли в продакшн, поэтому не могу дать точных цифр. Но ожидаемая экономия значительна:

  • Нет OpenSearch кластера, который нужно саппортить.

  • Хранилище S3 сильно дешевле, чем OpenSearch.

  • Платим только за фактические вызовы API, а не за простаивающую инфраструктуру.

Выводы

  • Cold starts (холодные старты) решают: Даже 5–6 секунд — проблема, если у вас строгие SLA. Можно использовать Provisioned Concurrency, переключиться на Bedrock или пре-прогревать лямбды.

  • Distance vs Score: Будьте внимательны при миграции между системами. Distance и Similarity Score — инверсные метрики. Тщательно тестируйте свои пороги.

  • Трейд-оффы размера модели: Крупные модели точнее, но медленнее грузятся. Xenova/bge-m3 стал для нас хорошим компромиссом.

Комментарии (0)