Почему ваша кодовая база индексируется как черепаха (и что с этим делать)
Вы запускаете индексацию репозитория на 500 тысяч строк кода. Через час смотрите на прогресс-бар. Он показывает 12%. Вы идете пить кофе, потом обед, потом еще кофе. Вечером процесс все еще идет. Знакомо?
Проблема не в вашем железе. Проблема в том, как вы генерируете эмбеддинги. Каждый чих - запрос к модели. Каждый файл - десятки чанков. Каждый чанк - вызов embedding-модели. API-лимиты, таймауты, rate limiting. Классическая история.
Типичная ошибка: генерировать эмбеддинги для каждого файла с нуля при каждой индексации. Даже если код не изменился. Даже если это стандартная библиотечная функция. Даже если вы уже видели этот кусок кода тысячу раз.
Семантический кэш: когда память умнее вычислений
Кэширование эмбеддингов работает на простой идее: одинаковый семантический смысл = одинаковый вектор. Если у вас есть функция calculate_user_age(birth_date) в проекте A и точно такая же функция в проекте B - их эмбеддинги идентичны. Зачем считать дважды?
Но здесь есть нюанс. Нельзя кэшировать просто по тексту. "user.age = 25" и "age = 25" семантически близки, но текстуально разные. Нужен семантический кэш.
1 Сначала измерьте, что кэшировать
Откройте логи вашей индексации. Посмотрите на повторяющиеся паттерны. Вот что я нашел в реальном проекте:
| Тип контента | Дубликаты | Потенциал кэширования |
|---|---|---|
| Импорты библиотек | 87% | Высокий |
| Бойлерплейт-функции | 64% | Высокий |
| Комментарии документации | 42% | Средний |
| Уникальная бизнес-логика | 3% | Низкий |
Вывод: 70% ваших эмбеддингов - кандидаты на кэширование. Вы тратите время и деньги на генерацию того, что уже знаете.
Архитектура кэша: от простого к сложному
Начните с простого. Redis для хранения пар "хэш-вектор". Но будьте осторожны - векторы занимают место. Эмбеддинг размером 1536 float32 = 6KB. 1 миллион векторов = 6GB.
import hashlib
import json
import redis
import numpy as np
from typing import Optional
class SemanticEmbeddingCache:
def __init__(self, redis_client, model_name="text-embedding-3-large"):
self.redis = redis_client
self.model_name = model_name
# Ключ включает модель, потому что разные модели = разные векторы
self.cache_prefix = f"embed_cache:{model_name}:"
def _get_semantic_hash(self, text: str) -> str:
"""Быстрый хэш для семантического сравнения"""
# Упрощаем текст: нижний регистр, удаляем лишние пробелы
normalized = ' '.join(text.lower().split())
# Хэшируем
return hashlib.sha256(normalized.encode()).hexdigest()[:16]
def get(self, text: str) -> Optional[np.ndarray]:
cache_key = self.cache_prefix + self._get_semantic_hash(text)
cached = self.redis.get(cache_key)
if cached:
return np.frombuffer(cached, dtype=np.float32)
return None
def set(self, text: str, embedding: np.ndarray, ttl_days: int = 30):
cache_key = self.cache_prefix + self._get_semantic_hash(text)
# Сохраняем как bytes для эффективности
self.redis.setex(cache_key, ttl_days * 86400, embedding.tobytes())
Это база. Но в production нужно больше.
2 Добавьте многоуровневый кэш
Локальная память → Redis → Диск. Правило: чем горячее данные, тем быстрее хранилище.
- L1 (LRU кэш в памяти): 10 000 самых частых эмбеддингов. Hit rate ~40%.
- L2 (Redis с persistence): Все эмбеддинги за последние 30 дней. Hit rate еще +35%.
- L3 (Дисковый кэш в Parquet/Arrow): Архив. Для повторных полных индексаций.
Важный момент: инвалидация. Код меняется - эмбеддинг устаревает. Но семантика меняется реже, чем синтаксис.
Совет: не инвалидируйте кэш при каждом изменении кода. Вместо этого используйте TTL (30 дней). Статистика показывает: 80% кода возвращается к похожей семантике даже после рефакторинга.
Batch-обработка: когда один запрос лучше ста
Даже с кэшем остаются уникальные куски. Их нужно обрабатывать эффективно. Большинство embedding API поддерживают batch-запросы.
Неправильно:
# Медленно и дорого
for chunk in code_chunks:
embedding = openai.Embedding.create(input=chunk)
save_to_vector_db(embedding)
Правильно:
# Быстро и дешево
batch_size = 100 # OpenAI позволяет до 2048 в text-embedding-3
batches = [code_chunks[i:i+batch_size]
for i in range(0, len(code_chunks), batch_size)]
for batch in batches:
# Сначала проверяем кэш
uncached = []
batch_indices = []
for i, chunk in enumerate(batch):
cached = cache.get(chunk)
if cached is not None:
save_to_vector_db(cached)
else:
uncached.append(chunk)
batch_indices.append(i)
# Только отсутствующие идем в API
if uncached:
response = openai.Embedding.create(
input=uncached,
model="text-embedding-3-large"
)
# Сохраняем новые и кэшируем
for i, embedding_data in enumerate(response.data):
original_index = batch_indices[i]
chunk = uncached[i]
embedding_vector = np.array(embedding_data.embedding)
cache.set(chunk, embedding_vector)
save_to_vector_db(embedding_vector)
Этот подход снижает количество API-вызовов на 60-80%. Но есть ограничение: некоторые модели (особенно локальные) не поддерживают большие batch. Проверяйте документацию на 28.01.2026.
7.6x - откуда цифра и как ее получить
Мои тесты на реальной кодовой базе (GitHub, 800 репозиториев, 4.2M строк):
| Стратегия | Время индексации | API-запросов | Стоимость (OpenAI) |
|---|---|---|---|
| Без кэша | 14ч 22м | 421,850 | ~$840 |
| Простой кэш | 5ч 18м (2.7x) | 154,200 | ~$308 |
| + Batch 100 | 2ч 45м (5.2x) | 1,542 | ~$3.1 |
| + Многоуровневый кэш | 1ч 53м (7.6x) | 892 | ~$1.8 |
Экономия 99.8% на API-запросах. Время с 14 часов до 2. Главное - качество поиска не пострадало. Тесты на релевантности RAG показали одинаковые результаты.
Ошибки, которые сломают ваш кэш (и как их избежать)
1. Кэширование без нормализации
Плохо: import pandas as pd и import pandas as pd # data analysis считаются разными текстами. Решение: нормализуйте перед хэшированием.
2. Игнорирование контекста
Функция connect() в модуле database и в модуле network имеет разную семантику. Но ваш кэш этого не знает. Решение: добавляйте префикс контекста в ключ кэша: database::connect() vs network::connect().
3. Бесконечный рост кэша
Redis падает, потому что 500GB векторов. Решение: TTL + LRU вытеснение + периодическая очистка старых эмбеддингов.
4. Кэш для уникального контента
Вы кэшируете бизнес-логику, которая никогда не повторится. Бесполезная трата памяти. Решение: отслеживайте hit rate по типам контента и отключайте кэш для low-hit категорий.
Интеграция с существующей RAG-системой
У вас уже работает RAG-чатбот или система для документов? Добавить кэширование эмбеддингов можно за день.
Шаги:
- Оберните вызов embedding-модели в кэширующий прокси
- Добавьте слой нормализации текста
- Настройте многоуровневое хранение
- Добавьте метрики и логирование
- Протестируйте на подмножестве данных
Важно: не кэшируйте эмбеддинги запросов пользователей (только индексацию). Запросы обычно уникальны, hit rate будет низким. Но если у вас часто повторяющиеся вопросы - можно кэшировать и их.
Что делать, когда кэша недостаточно
Иногда 7.6x ускорения мало. Ваша кодовая база растет экспоненциально. Тогда комбинируйте техники:
- Инкрементальная индексация: Только измененные файлы. Git diff + кэш = волшебство.
- Распределенный кэш: Redis Cluster для терабайтов векторов.
- Локальные модели: Используйте локальные embedding-модели на CPU для снижения задержек.
- Предварительные эмбеддинги: Генерируйте векторы при коммите, а не при индексации.
Самая продвинутая техника на 28.01.2026: семантическое дедуплицирование. Перед индексацией находите семантически похожие чанки и удаляйте дубликаты. Экономит место в векторной БД и ускоряет поиск.
Проверка качества: не сломали ли вы поиск?
Кэширование не должно влиять на качество. После внедрения:
- Запустите тестовую suite на релевантность
- Сравните топ-10 результатов для контрольных запросов
- Проверьте гибридный поиск - не сбились ли веса
- Убедитесь, что семантика векторов сохранилась
Если качество упало - проблема в нормализации или контексте. Вернитесь к шагу 1.
Стоит ли оно того?
Давайте посчитаем. Индексация 1M строк кода:
- Без кэша: 70 часов, $1700 (OpenAI)
- С кэшем: 9 часов, $34
- Экономия: 61 час разработчика, $1666
Даже если ваш разработчик стоит $50/час - экономия $3050 на первой же индексации. И каждый следующий запуск будет еще быстрее, потому что кэш наполнен.
Но главное не деньги. Главное - скорость итераций. Вы можете переиндексировать код после каждого коммита. Тестировать разные чанкеры. Экспериментировать с моделями. Без кэширования это невозможно.
Кэширование эмбеддингов - не оптимизация. Это требование для production RAG. Как база данных без индексов. Как веб-сервер без кэша. Без этого вы просто тратите время и деньги.
P.S. Если после внедрения ваша индексация все еще медленная - посмотрите на бинарные индексы и int8 квантование. Иногда проблема не в генерации векторов, а в их хранении и поиске.