Предисловие: зачем философу векторная база
Я пишу диссертацию по средневековой герменевтике. Моя жизнь - это стопки PDF на пяти языках. Искать в них что-то конкретное - все равно что искать иголку в стоге сена, где каждая соломинка написана на латыни. ChatGPT забывает контекст после трех вопросов. Классический поиск по словам не находит "истолкование", когда в тексте стоит "экзегеза". Я устал. Я решил построить свою поисковую систему.
Проблема в том, что я не программист. Я знаю, что такое цикл for, только потому, что читал про вечное возвращение у Ницше. Но у меня есть преимущество: гуманитарии умеют видеть системы там, где технари видят код. Я расскажу, как собрал RAG (Retrieval-Augmented Generation) систему, которая понимает смысл, а не просто слова. Полностью локально. Без облаков. Без ежемесячных подписок.
Важно: это не технический мануал. Это рассказ о том, как человек без технического бэкграунда заставил машину работать на свои гуманитарные задачи. Все инструменты, о которых пойдет речь, актуальны на февраль 2026 года.
Архитектура без архитектора: как я понял, что мне нужно
Сначала я потратил неделю, пытаясь разобраться в терминах. Эмбеддинги, индексы, ранжирование. Потом понял: мне не нужна теория. Мне нужна простая схема работы.
Моя система состоит из трех ключевых компонентов:
- Парсер PDF - вытаскивает текст из научных статей, сохраняя структуру (заголовки, сноски)
- Модель эмбеддингов E5-multilingual-v3 - превращает текст в "цифровые отпечатки", которые сохраняют смысл
- Гибридный поиск (FAISS + BM25) - ищет одновременно по смыслу (FAISS) и по ключевым словам (BM25)
Почему именно эта связка? Потому что она работает. FAISS от Facebook (теперь Meta) - это библиотека для быстрого поиска похожих векторов. BM25 - классический алгоритм поиска по ключевым словам, который отлично справляется с точными совпадениями. Вместе они покрывают 95% моих поисковых потребностей.
1 Подготовка поля боя: что ставить на компьютер
Я работаю на MacBook Air M2 с 16 ГБ оперативной памяти. Для Windows процесс будет аналогичным, только команды установки другие.
Первое, что нужно - Python. Не пугайтесь. Вам не придется писать на нем. Мы будем использовать готовые скрипты. Устанавливаем Python 3.11 или новее с официального сайта. При установке обязательно отмечаем галочку "Add Python to PATH".
Далее открываем терминал (Command Prompt в Windows) и устанавливаем необходимые библиотеки одной командой:
pip install sentence-transformers faiss-cpu rank-bm25 pypdf langchain langchain-community chromadb
Внимание: если у вас Windows и возникают ошибки с установкой faiss-cpu, попробуйте установку через conda или используйте альтернативу - библиотеку Qdrant, которую можно запустить в Docker. Но FAISS быстрее на CPU.
2 Парсинг PDF: как вытащить смысл из графических файлов
Здесь первая ловушка. Большинство научных PDF - это сканы с OCR или криво сконвертированные документы. Простой парсер их не понимает.
Я создал папку `documents` и скопировал туда все свои PDF. Затем написал простой скрипт на Python (скопировал из интернета и адаптировал):
import os
from pypdf import PdfReader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Указываем путь к папке с документами
doc_path = "documents/"
all_texts = []
# Проходим по всем PDF файлам
for filename in os.listdir(doc_path):
if filename.endswith('.pdf'):
print(f"Обрабатываю: {filename}")
reader = PdfReader(os.path.join(doc_path, filename))
text = ""
for page in reader.pages:
text += page.extract_text() + "\n"
# Разбиваем текст на чанки (фрагменты)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
length_function=len
)
chunks = text_splitter.split_text(text)
all_texts.extend(chunks)
print(f"Получено {len(all_texts)} текстовых фрагментов")
Почему чанки по 500 символов? Потому что модели эмбеддингов имеют ограничение на длину входного текста. E5-multilingual работает с 512 токенами. Чанкинг - это искусство. Слишком мелкие фрагменты теряют контекст, слишком крупные - содержат шум. 500 символов с перекрытием в 50 - золотая середина для академических текстов.
3 Волшебство эмбеддингов: как текст становится математикой
Эмбеддинг - это представление текста в виде вектора (списка чисел). Похожие по смыслу тексты имеют похожие векторы. Модель E5-multilingual-v3 (актуальна на 2026 год) специально обучена для многоязычного поиска. Она понимает, что "dog" и "chien" (французский) - это одно и то же.
Устанавливаем и запускаем:
from sentence_transformers import SentenceTransformer
import pickle
# Загружаем модель (скачивается при первом запуске)
model = SentenceTransformer('intfloat/e5-multilingual-v3')
# Создаем эмбеддинги для всех текстовых фрагментов
print("Создаю эмбеддинги... Это может занять время.")
embeddings = model.encode(all_texts, normalize_embeddings=True)
# Сохраняем результаты
with open('embeddings.pkl', 'wb') as f:
pickle.dump({'texts': all_texts, 'embeddings': embeddings}, f)
print(f"Создано {len(embeddings)} эмбеддингов размерностью {embeddings.shape[1]}")
Первый запуск займет время - модель весит около 2 ГБ. Но она скачается один раз и будет храниться локально. Никаких API-ключей, никаких лимитов.
4 Двойной удар: гибридный поиск с FAISS и BM25
Теперь самое интересное. FAISS ищет по смыслу, BM25 - по ключевым словам. Вместе они дают точность, которой нет у каждого по отдельности.
Сначала настроим BM25:
from rank_bm25 import BM25Okapi
import nltk
from nltk.tokenize import word_tokenize
# Скачиваем токенизатор для русского/английского
nltk.download('punkt')
# Токенизируем тексты для BM25
tokenized_texts = [word_tokenize(text.lower()) for text in all_texts]
bm25 = BM25Okapi(tokenized_texts)
Теперь FAISS:
import faiss
import numpy as np
# Создаем индекс FAISS
dimension = embeddings.shape[1]
index = faiss.IndexFlatIP(dimension) # IndexFlatIP для косинусного сходства
faiss.normalize_L2(embeddings) # Нормализуем векторы
index.add(embeddings)
# Сохраняем индекс на диск
faiss.write_index(index, "faiss_index.bin")
А вот как выглядит гибридный поиск:
def hybrid_search(query, top_k=5, alpha=0.5):
"""Гибридный поиск: комбинация семантического (FAISS) и лексического (BM25)"""
# 1. Семантический поиск (FAISS)
query_embedding = model.encode([query], normalize_embeddings=True)
faiss_scores, faiss_indices = index.search(query_embedding, top_k*2)
# 2. Лексический поиск (BM25)
tokenized_query = word_tokenize(query.lower())
bm25_scores = bm25.get_scores(tokenized_query)
bm25_indices = np.argsort(bm25_scores)[::-1][:top_k*2]
# 3. Объединяем результаты
combined_scores = {}
# Нормализуем FAISS scores
if len(faiss_scores[0]) > 0:
faiss_max = np.max(faiss_scores[0])
faiss_scores_norm = faiss_scores[0] / faiss_max if faiss_max > 0 else faiss_scores[0]
# Нормализуем BM25 scores
if len(bm25_scores) > 0:
bm25_max = np.max(bm25_scores)
bm25_scores_norm = bm25_scores / bm25_max if bm25_max > 0 else bm25_scores
# Объединяем с весом alpha
for i, idx in enumerate(faiss_indices[0]):
combined_scores[int(idx)] = alpha * faiss_scores_norm[i]
for idx in bm25_indices:
if idx in combined_scores:
combined_scores[idx] += (1 - alpha) * bm25_scores_norm[idx]
else:
combined_scores[idx] = (1 - alpha) * bm25_scores_norm[idx]
# Сортируем по комбинированному score
sorted_indices = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)[:top_k]
# Возвращаем результаты
results = []
for idx, score in sorted_indices:
results.append({
'text': all_texts[idx],
'score': score,
'source': f"Документ {idx//100 + 1}" # Упрощенно
})
return results
Параметр `alpha=0.5` - это баланс между семантическим и лексическим поиском. Для философских текстов, где важны точные термины, я использую `alpha=0.3` (больший вес BM25). Для литературоведения, где важны смысловые связи - `alpha=0.7`.
5 Связующее звено: как заставить LLM говорить правду
Найденные фрагменты - это еще не ответ. Нужно передать их языковой модели. Я использую Llama 3.2 3B - она отлично работает на моем MacBook и понимает контекст.
Установка через Ollama (самый простой способ):
# Устанавливаем Ollama
curl -fsSL https://ollama.ai/install.sh | sh
# Скачиваем модель Llama 3.2 3B (самая новая стабильная версия на 2026 год)
ollama pull llama3.2:3b
Теперь создаем финальный скрипт, который объединяет все компоненты:
import requests
import json
def ask_llama(query, context_texts):
"""Отправляем запрос локальной LLM с контекстом"""
# Формируем промпт с контекстом
context = "\n\n".join([f"[Контекст {i+1}]: {text}" for i, text in enumerate(context_texts)])
prompt = f"""Ты - помощник-исследователь. Ответь на вопрос на основе предоставленного контекста.
Если в контексте нет информации для ответа, скажи "В предоставленных материалах этого нет."
Контекст:
{context}
Вопрос: {query}
Ответ:"""
# Отправляем запрос в Ollama
response = requests.post('http://localhost:11434/api/generate',
json={
'model': 'llama3.2:3b',
'prompt': prompt,
'stream': False,
'options': {'temperature': 0.1} # Низкая температура для точных ответов
}
)
if response.status_code == 200:
return response.json()['response']
else:
return "Ошибка при обращении к модели"
# Пример использования
query = "Какие существуют интерпретации символа зеркала в работах Лакана?"
search_results = hybrid_search(query, top_k=3)
contexts = [r['text'] for r in search_results]
answer = ask_llama(query, contexts)
print(answer)
Где я споткнулся: ошибки, которые стоит избегать
За месяц работы с системой я наступил на все возможные грабли. Вот главные:
| Ошибка | Решение | Почему это важно |
|---|---|---|
| Чанки слишком большие (1000+ символов) | Уменьшить до 400-600 символов с overlap 50 | Большие чанки содержат шум, модель теряет фокус |
| Использование только FAISS | Добавить BM25 для гибридного поиска | Точные термины (имена, даты) плохо ищутся по смыслу |
| Нет нормализации эмбеддингов | Всегда использовать normalize_embeddings=True | Косинусное сходство работает только с нормализованными векторами |
| Слишком высокая температура у LLM | Установить temperature=0.1-0.3 | Иначе модель начинает "выдумывать" цитаты |
Самая опасная ошибка - когда LLM начинает галлюцинировать, приписывая мысли авторам, которые об этом не писали. Всегда проверяйте цитаты по оригинальным источникам. RAG - не замена чтению, а инструмент навигации.
Что дальше? Эволюция системы
Сейчас моя система работает с 500 PDF (около 2 ГБ текста). Поиск занимает 0.2-0.5 секунды. Но есть куда расти:
- Мультимодальность - добавление поиска по изображениям из статей (диаграммы, рукописи)
- Временные метки - учет хронологии: что писал автор в 1920-е vs 1950-е
- Перекрестные ссылки - автоматическое построение графа связей между концепциями
Если вы хотите пойти дальше простого поиска, посмотрите мой гайд про Agentic RAG системы, где модель не просто отвечает на вопросы, а планирует исследовательские задачи.
Важный нюанс 2026 года: современные модели эмбеддингов стали значительно лучше понимать контекст. E5-multilingual-v3 по сравнению с версией 2024 года на 15% лучше справляется с полисемией (многозначностью слов). Это значит, что она различает "банк" финансовый и "банк" речной без дополнительных указаний.
Философский итог: почему это работает
Я начал этот проект от отчаяния. Закончил - с пониманием, что искусственный интеллект в гуманитарных науках это не про замену исследователя, а про усиление его возможностей. Моя система не "думает" за меня. Она выстраивает мост между моим вопросом и тысячами страниц, которые я физически не могу перечитать за человеческую жизнь.
FAISS и BM25 - это не просто алгоритмы. Это метафора двух способов мышления: ассоциативного (похожие смыслы) и аналитического (точные совпадения). Вместе они создают то, что в герменевтике называют "герменевтическим кругом" - движение между частью и целым, между словом и смыслом.
Система готова. 500 PDF обработаны. Завтра я добавлю еще сто. Послезавтра - научю ее работать с рукописями на старославянском. Это бесконечный процесс, как и любое исследование.
Но теперь у меня есть союзник. Не искусственный интеллект, а интеллектуальный протез. И это, пожалуй, самое человечное применение технологии из всех возможных.