В нашей компании 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. Наш воркфлоу был таким:
Когда в системе появляется новый тул, мы ищем похожие в RAG.
Если совпадения найдены (с достаточно высоким скором), берем их нормализованное имя.
Если совпадений нет, мы передаем всю доступную инфу о туле в LLM вместе со списком наиболее вероятных нормализованных названий.
LLM принимает финальное решение о нормализации.
Почему мы решили уйти с AWS Knowledge Base
Система работала неплохо, но под капотом AWS Knowledge Base использует AWS OpenSearch — он быстрый, но дорогой. Наш процесс нормализации асинхронный, и нам не нужен сверхбыстрый отклик. Платить за эту инфраструктуру нам не хотелось
В нашей базе около 20 000 тулов с верифицированной нормализацией. Задачи нормализации запускаются в Lambda-функциях по событиям EventBridge.
Когда AWS анонсировала S3 Vector Search (на момент написания статьи — еще в бете), мы решили его попробовать. Потенциальная экономия была слишком привлекательной.
Новая архитектура с S3 Vector Search
Схема работы
Вот как работает наша новая схема:
Генерация эмбеддингов: Когда появляется новый тул, мы создаем его эмбеддинг, используя локальную LLM-модель.
Векторный поиск: Ищем в S3 Vector store, где хранятся пре-индексированные эмбеддинги существующих тулов.
Сопоставление: Если distance ниже порога, используем нормализованное имя из найденного тула.
Fallback: Если хорошее совпадение не найдено, окончательное решение принимает Claude Sonnet 3.5.
Выбор модели эмбеддингов
Мы попробовали несколько моделей и остановились на Xenova/bge-m3:
Многоязыковая поддержка (критично для нас).
Доступна как на Python, так и на TypeScript.
Хороший баланс между качеством и размером.
Эмбеддинги на 1024 измерения.
Docker-образ для Лямбды
Одной из самых больших проблем была производительность холодного старта. Загрузка тяжелой ML-модели в лямбде занимает время. Мы решили это так:
Создали Docker-образ, который уже содержит загруженную модель
Xenova/bge-m3.Модель скачивается во время Docker build, а не в рантайме.
Лямбда грузит пре-кэшированную модель из файловой системы образа.
Пример 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стал для нас хорошим компромиссом.