Зачем мучить 10 000 книг на слабом VPS? Потому что можно
Представьте себе задачу: у вас есть коллекция из 10 000 книг - техническая документация, художественная литература, научные труды. Вы хотите задавать вопросы по этому корпусу и получать точные, обоснованные ответы. Облачные API съедят ваш бюджет за неделю, а аренда GPU-сервера тянет на отдельную зарплату. Остается вариант, который все обходят стороной: дешевый CPU VPS с 4-8 ядрами и 16 гигабайтами оперативки.
Звучит как издевательство. Но именно здесь начинается настоящая инженерия. Не бросать железо на задачу, а заставить задачу подчиниться железу. Если вы думаете, что RAG - это только про большие модели и терабайты памяти, вы ошибаетесь. Это про эффективность. Про то, как выжать из каждого мегагерца и каждого мегабайта максимум.
Главный миф: для обработки 10k книг нужен GPU. Неправда. Эмбеддинги и поиск отлично работают на CPU. Проблема не в вычислениях, а в организации данных и памяти. 16 ГБ ОЗУ - это много, если не тратить их впустую.
Архитектура, которая не сломается под нагрузкой
Классический RAG развалится на таком железе. Загрузите все эмбеддинги в память - и 16 ГБ закончатся после первой тысячи книг. Используйте тяжелую модель для эмбеддингов - запрос будет обрабатываться 10 секунд. Забудьте про re-ranking - точность упадет на 30-40%.
Нужна система, где каждый компонент выбран за его эффективность, а не просто потому, что он популярен на GitHub.
| Компонент | Наш выбор (2026) | Почему именно он | Потребление RAM |
|---|---|---|---|
| Модель эмбеддингов | BGE-M3 (light версия) | Поддержка 8192 токена, мультиязычность, оптимизирована для CPU. all-MiniLM-L6-v2 уже устарела для таких объемов. | ~500 МБ |
| Векторная БД | Qdrant 1.9.x с mmap | Единственная, которая умеет хранить вектора на диске с mmap, почти не занимая RAM. Альтернативы (FAISS, Chroma) сожрут всю память. | ~50-100 МБ (зависит от запросов) |
| Реранкер | BGE-Reranker-v3-Mini | Специально облегченная версия 2025 года. Точность падает на 2-3% против большой, но скорость в 5 раз выше на CPU. | ~300 МБ |
| LLM для ответов | Llama 3.2 3B (4-битная квантизация) | Может работать в 4 ГБ ОЗУ, выдает связные ответы. Для экспертного ассистента не нужна 70B модель, нужна точная информация из контекста. | ~4 ГБ |
Общий подсчет: 500 МБ + 100 МБ + 300 МБ + 4 ГБ = примерно 5 ГБ. Остается 11 ГБ на операционную систему, кэши и буферы. Это реально. Если вы возьмете VPS с 16 ГБ ОЗУ у провайдера вроде Timeweb Cloud, этого хватит с запасом.
1 Подготовка данных: как резать 10 000 книг без потери смысла
Самая скучная и самая важная часть. Если нарезать текст на равные куски по 256 токенов, вы потеряете контекст. Глава книги, разбитая посередине, сделает эмбеддинги бесполезными.
Правило: чанковать по структурным элементам. Главы, разделы, подразделы. Если структуры нет (сплошной текст), используйте алгоритмическое чанкование с перекрытием.
# Пример чанкинга с помощью актуальной библиотеки langchain-text-splitters 2026
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Не делайте так (потеря контекста):
# splitter = RecursiveCharacterTextSplitter(chunk_size=256, chunk_overlap=50)
# Делайте так (сохраняем структуру):
splitter = RecursiveCharacterTextSplitter(
separators=["\n\n\n", "\n\n", ". ", "? ", "! ", " ", ""], # Приоритет разделителей
chunk_size=1024, # Большие чанки для книг
chunk_overlap=200, # Значительное перекрытие
length_function=len,
is_separator_regex=False,
)
chunks = splitter.split_text(book_text)
# Каждому чанку добавьте метаданные: название книги, автор, глава, страница
2 Индексирование: как не сжечь CPU и не ждать неделю
10 000 книг - это примерно 5-6 миллионов чанков. Создание эмбеддингов для каждого - задача на дни, если делать в один поток.
Решение: батчинг и асинхронность. Модель BGE-M3 на CPU обрабатывает примерно 100-200 чанков в секунду (зависит от длины). Вам нужно распараллелить процесс на все ядра VPS.
from sentence_transformers import SentenceTransformer
import numpy as np
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
import asyncio
from concurrent.futures import ProcessPoolExecutor
model = SentenceTransformer('BAAI/bge-m3-light', device='cpu')
# Создаем батчи
batch_size = 128 # Не больше! Иначе упретесь в память.
chunks = [...] # Ваш список чанков
points = []
for i in range(0, len(chunks), batch_size):
batch = chunks[i:i + batch_size]
# Кодируем батч
embeddings = model.encode(batch, normalize_embeddings=True, show_progress_bar=False)
for j, (chunk, embedding) in enumerate(zip(batch, embeddings)):
points.append(
PointStruct(
id=i + j,
vector=embedding.tolist(),
payload={
"text": chunk.text,
"book_id": chunk.metadata["book_id"],
"title": chunk.metadata["title"],
"author": chunk.metadata["author"],
"chapter": chunk.metadata.get("chapter", ""),
}
)
)
# Каждые 10 000 точек сбрасываем в Qdrant, чтобы не держать в памяти
if len(points) > 10000:
client.upsert(collection_name="books", points=points)
points = []
print(f"Indexed {i + batch_size} chunks")
Это займет время. На 8-ядерном VPS процесс индексирования 5 миллионов чанков растянется на 10-15 часов. Смиритесь. Или используйте гибридный подход: построение индекса на GPU и обслуживание на CPU, если есть временный доступ к мощной машине.
3 Настройка Qdrant: магия mmap и как не дать БД съесть всю память
Qdrant с настройками по умолчанию загрузит все вектора в оперативку. Для 5 миллионов векторов по 768 измерений это примерно 15 ГБ. Прощай, 16 ГБ RAM.
Секрет в режиме mmap. Он позволяет Qdrant читать вектора прямо с диска, почти не используя RAM. Скорость падает, но для RAG, где запросов в секунду не тысячи, это приемлемо.
# config.yaml для Qdrant 1.9.x
storage:
# Включаем mmap для векторов
vectors:
mmap: true
# Кэш эмбеддингов в памяти для часто запрашиваемых данных (опционально)
cache:
size: 1000 # Количество векторов в кэше
service:
# Лимит памяти для сервиса
memory_limit: 2048 # Не больше 2 ГБ для самого Qdrant
performance:
# Увеличиваем количество потоков для поиска
max_search_threads: 4
Запускаем Qdrant:
docker run -d \
-p 6333:6333 \
-v ./qdrant_storage:/qdrant/storage \
-v ./config.yaml:/qdrant/config/production.yaml \
qdrant/qdrant:latest
Важно: mmap работает хорошо только с быстрым диском (SSD NVMe). Если ваш VPS использует HDD, поиск будет мучительно медленным. При выборе VPS обращайте внимание на тип диска. На Timeweb Cloud можно выбрать конфигурации с NVMe.
4 Сборка пайплайна запроса: реранкинг не для бедных
Стандартный пайплайн: запрос -> эмбеддинг -> поиск по векторам -> топ-10 результатов -> реранкинг -> топ-3 -> подача в LLM. На CPU каждый этап добавляет задержку. Наша цель - уложиться в 3-5 секунд на ответ.
Хитрость в том, чтобы не отправлять в реранкер все 10 результатов. Сначала делаем гибридный поиск: векторный + лексический (BM25). Это повышает recall. Затем реранкеру отдаем только топ-7. Подробнее про гибридный поиск я писал в отдельном гайде.
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue, SearchRequest
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
import numpy as np
client = QdrantClient(host="localhost", port=6333)
# Реранкер
reranker_tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-reranker-v3-mini")
reranker_model = AutoModelForSequenceClassification.from_pretrained("BAAI/bge-reranker-v3-mini")
reranker_model.eval()
def hybrid_search(query, limit=10):
# 1. Векторный поиск
query_embedding = embedder.encode(query, normalize_embeddings=True)
vector_results = client.search(
collection_name="books",
query_vector=query_embedding,
limit=limit * 2, # Берем больше для гибридности
with_payload=True
)
# 2. Лексический поиск (через Qdrant, если настроен, или через отдельный индекс BM25)
# Для простоты опустим, но это критически важно для точности
# 3. Объединение и ранжирование (простейший способ)
combined = vector_results # Здесь должна быть реальная логика слияния
return combined[:limit] # Возвращаем топ-10 для реранкинга
def rerank(query, passages):
# passages - список текстов чанков
pairs = [[query, passage] for passage in passages]
with torch.no_grad():
inputs = reranker_tokenizer(pairs, padding=True, truncation=True, return_tensors='pt', max_length=512)
scores = reranker_model(**inputs, return_dict=True).logits.view(-1,).float()
# Сортируем по убыванию score
ranked_indices = np.argsort(scores.numpy())[::-1]
return [passages[i] for i in ranked_indices[:3]] # Топ-3 для LLM
Реранкер - узкое место. Модель v3-Mini быстрая, но все равно добавляет 0.5-1 секунду. Если это слишком много, можно увеличить количество исходных результатов поиска до 20, а в реранкер отдавать только топ-5 по векторному сходству. Теряете в точности, выигрываете в скорости.
Ошибки, которые убьют вашу систему
- Игнорирование метаданных. Без фильтрации по книге, автору, году, каждый запрос будет искать по всем 10k книг. Это медленно и неточно. Всегда добавляйте фильтры в поиск, если пользователь уточняет.
- Чанкование по таймеру или фиксированному размеру. Вы получите обрывы предложений и потерянные смыслы. Инвестируйте время в парсинг структуры книг (оглавление, разделы).
- Запуск всего в одном процессе. Если ваше приложение - один скрипт, который делает эмбеддинг, поиск, реранкинг и генерацию, он будет падать под нагрузкой. Разделите на микросервисы или хотя бы отдельные процессы с очередями.
- Отказ от кэширования. Повторяющиеся запросы должны кэшироваться. Кэшируйте эмбеддинги запросов и топовые результаты. Redis на том же VPS съест память, но файловый кэш на SSD может спасти.
Что в итоге? Система, которая работает
Вы получите ассистента, который отвечает на вопросы по 10 000 книг за 3-5 секунд, потребляя 10-12 ГБ ОЗУ на пике. Это не магия, это инженерия. Каждый компонент выбран потому, что он решает конкретную проблему в условиях ограничений.
Самое сложное - не написать код, а принять решения. Использовать ли гибридный поиск? Какой размер чанка оптимален для книг? Стоит ли вообще использовать реранкинг на CPU? Ответы зависят от ваших данных и запросов. Начните с простого пайплайна (эмбеддинг -> поиск -> LLM), измеряйте точность, а затем добавляйте сложность.
И помните: RAG на CPU - это не удел бедных. Это демонстрация того, что эффективность важнее мощности. Когда все вокруг бросают на задачи GPU за $10 000, ваша система на $20 VPS будет делать то же самое, просто немного медленнее. А иногда - и точнее, потому что вы были вынуждены думать над каждой деталью.