Локальный RAG с FAISS и BM25 для гуманитариев: пошаговый гайд 2026 | AiManual
AiManual Logo Ai / Manual.
15 Фев 2026 Гайд

Гуманитарий против кода: как я заставил компьютер понимать философию без единой строчки Python

История филолога, который собрал RAG-систему для анализа PDF с E5-multilingual, FAISS и BM25. Без программирования, только логика.

Предисловие: зачем философу векторная база

Я пишу диссертацию по средневековой герменевтике. Моя жизнь - это стопки PDF на пяти языках. Искать в них что-то конкретное - все равно что искать иголку в стоге сена, где каждая соломинка написана на латыни. ChatGPT забывает контекст после трех вопросов. Классический поиск по словам не находит "истолкование", когда в тексте стоит "экзегеза". Я устал. Я решил построить свою поисковую систему.

Проблема в том, что я не программист. Я знаю, что такое цикл for, только потому, что читал про вечное возвращение у Ницше. Но у меня есть преимущество: гуманитарии умеют видеть системы там, где технари видят код. Я расскажу, как собрал RAG (Retrieval-Augmented Generation) систему, которая понимает смысл, а не просто слова. Полностью локально. Без облаков. Без ежемесячных подписок.

Важно: это не технический мануал. Это рассказ о том, как человек без технического бэкграунда заставил машину работать на свои гуманитарные задачи. Все инструменты, о которых пойдет речь, актуальны на февраль 2026 года.

Архитектура без архитектора: как я понял, что мне нужно

Сначала я потратил неделю, пытаясь разобраться в терминах. Эмбеддинги, индексы, ранжирование. Потом понял: мне не нужна теория. Мне нужна простая схема работы.

💡
RAG-система работает по принципу библиотекаря. Вы задаете вопрос ("Найди все про символику яблока в кельтских мифах"). Система ищет в своей "картотеке" (векторной базе) самые релевантные отрывки. Затем передает эти отрывки и ваш вопрос языковой модели, которая формулирует связный ответ. Весь процесс происходит у вас на компьютере.

Моя система состоит из трех ключевых компонентов:

  • Парсер 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-ключей, никаких лимитов.

💡
E5-multilingual-v3 создает эмбеддинги размерностью 1024. Каждое число в этом 1024-мерном пространстве кодирует какой-то аспект смысла. Когда вы ищете "любовь", система ищет векторы, которые находятся "близко" к вектору вашего запроса в этом многомерном пространстве.

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 обработаны. Завтра я добавлю еще сто. Послезавтра - научю ее работать с рукописями на старославянском. Это бесконечный процесс, как и любое исследование.

Но теперь у меня есть союзник. Не искусственный интеллект, а интеллектуальный протез. И это, пожалуй, самое человечное применение технологии из всех возможных.