Обычный поиск по сайту работает просто: он ищет точное совпадение слов. Если вы ищете «настройка SSL», а в статье написано «установка сертификата Let’s Encrypt», поиск ничего не найдёт. Это раздражает пользователей и снижает ценность вашего контента.
Векторные эмбеддинги и RAG (Retrieval Augmented Generation) решают эту проблему. Они позволяют искать по смыслу, а не по буквам. А если добавить к этому языковую модель, можно получить умного ассистента, который отвечает на вопросы пользователей на основе вашего контента.
В этой статье мы подробно разберём:
- Что такое векторные эмбеддинги и как они работают.
- Что такое RAG и зачем он нужен.
- Архитектуру системы умного поиска.
- Пошаговую реализацию: от разбивки контента до поиска по смыслу.
- Выбор инструментов: локальные и облачные эмбеддинги, векторные базы.
- Интеграцию с AI-ассистентом для ответов на вопросы.
- Практические примеры на PHP и Python.
- Оптимизацию и частые ошибки.
1. Проблема обычного поиска
Представьте, что у вас есть сайт с 500 статьями. Пользователь хочет найти информацию о том, как защитить сервер. Он вводит запрос «безопасность сервера».
Что делает обычный поиск (LIKE ‘%безопасность%’):
- Ищет точное вхождение слова «безопасность» в заголовках и тексте.
- Не найдёт статью «Защита SSH от брутфорса», потому что там нет слова «безопасность».
- Не найдёт статью «Настройка фаервола», хотя она прямо отвечает на запрос.
Результат: пользователь уходит, не найдя ответ.
Что делает умный поиск (векторный):
- Понимает, что «безопасность сервера», «защита SSH» и «настройка фаервола» — это близкие по смыслу понятия.
- Находит все три статьи и сортирует их по релевантности.
- Пользователь получает нужную информацию.
Разница — в понимании смысла.
2. Что такое векторные эмбеддинги
Векторный эмбеддинг — это числовое представление текста в виде массива чисел (вектора). Например, предложение «Защита сервера» может быть представлено как [0.12, -0.45, 0.78, ...] — массив из 768 или 1536 чисел.
Главная идея: если два текста близки по смыслу, их векторы будут близки в многомерном пространстве. Если далеки по смыслу — векторы будут далеко.
Как это работает:
- Модель эмбеддингов (например,
text-embedding-3-smallот OpenAI илиnomic-embed-textот Ollama) обучена на огромном количестве текстов. - Она преобразует текст в вектор так, что семантически близкие тексты получают близкие векторы.
- Мы можем сравнивать векторы с помощью косинусного сходства (cosine similarity) — чем ближе значение к 1, тем более похожи тексты по смыслу.
Пример:
| Текст | Вектор (упрощённо) |
|---|---|
| «Защита сервера» | [0.8, 0.2, -0.1] |
| «Безопасность SSH» | [0.7, 0.3, -0.2] |
| «Рецепт борща» | [-0.9, 0.1, 0.5] |
Косинусное сходство между первыми двумя — высокое (например, 0.95). Между первым и третьим — низкое (например, 0.1).
Что это даёт:
- Поиск по смыслу, а не по словам.
- Работа с синонимами, перефразированиями, опечатками.
- Возможность искать на разных языках (если модель многоязычная).
3. Что такое RAG
RAG (Retrieval Augmented Generation) — это подход, при котором языковая модель (LLM) генерирует ответ, опираясь на найденные в базе знаний документы.
Зачем это нужно:
- LLM обучена на общих данных и не знает специфики вашего сайта.
- Если просто спросить LLM «Как настроить SSL на Nginx?», она ответит в общем виде, но не учтёт ваши статьи.
- RAG позволяет: сначала найти релевантные документы в вашей базе, затем передать их LLM вместе с вопросом, и LLM сгенерирует ответ на основе вашего контента.
Схема RAG:
Вопрос пользователя
│
▼
┌─────────────────┐
│ Поиск по базе │ ← векторный поиск
│ (эмбеддинги) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Найденные │ ← топ-5 релевантных чанков
│ документы │
└────────┬────────┘
│
▼
┌─────────────────┐
│ LLM + промпт │ ← «Ответь на вопрос, используя эти документы»
│ (генерация) │
└────────┬────────┘
│
▼
Ответ пользователюЧто это даёт:
- Ответы на основе вашего контента, а не общих знаний модели.
- Актуальность (вы обновляете статьи — обновляется и база знаний).
- Ссылки на источники (можно указать, из какой статьи взят ответ).
- Снижение галлюцинаций (модель опирается на реальные документы).
4. Архитектура системы умного поиска
Система состоит из нескольких компонентов:
- Контент — статьи, страницы, документация на вашем сайте.
- Чанкинг — разбивка контента на небольшие фрагменты (чанки).
- Модель эмбеддингов — преобразует текст в векторы.
- Векторная база — хранилище векторов с возможностью быстрого поиска.
- Поисковый запрос — преобразуется в вектор той же моделью.
- Поиск — находит ближайшие векторы (по косинусному сходству).
- LLM — генерирует ответ на основе найденных чанков.
- Интерфейс — форма поиска или чат на сайте.
Этапы работы:
Индексация (один раз или при обновлении контента):
- Взять все статьи.
- Разбить на чанки (например, по 500–1000 символов).
- Сгенерировать эмбеддинги для каждого чанка.
- Сохранить в векторную базу.
Поиск (при каждом запросе):
- Пользователь вводит вопрос.
- Сгенерировать эмбеддинг для вопроса.
- Найти топ-N ближайших чанков в векторной базе.
- Вернуть их пользователю (обычный поиск) или передать LLM для генерации ответа (RAG).
5. Выбор инструментов
5.1 Модели эмбеддингов
| Модель | Тип | Размерность | Особенности |
|---|---|---|---|
| OpenAI text-embedding-3-small | Облачная | 1536 | Высокое качество, платно |
| OpenAI text-embedding-3-large | Облачная | 3072 | Максимальное качество |
| Cohere Embed | Облачная | 1024 | Хорошо для мультиязычности |
| nomic-embed-text | Локальная | 768 | Бесплатно, через Ollama |
| all-MiniLM-L6-v2 | Локальная | 384 | Лёгкая, быстрая |
| multilingual-e5-large | Локальная | 1024 | Хорошо для русского |
Для русского языка: рекомендуется использовать многоязычные модели, например multilingual-e5-large или nomic-embed-text (поддерживает русский). ВАЖНО! Обратите внимание, что multilingual-e5-large — тяжёлая модель (около 2 ГБ), и при первом запуске она будет долго скачиваться. Для быстрого старта можно использовать nomic-embed-text (через Ollama, легче) или all-MiniLM-L6-v2 (если качество не критично).
Для простоты: можно начать с облачных эмбеддингов (OpenAI, Cohere), если бюджет позволяет. Для приватности — локальные через Ollama.
5.2 Векторные базы
| База | Тип | Особенности |
|---|---|---|
| ChromaDB | Локальная | Простая, встраиваемая, Python |
| Qdrant | Локальная/облачная | Мощная, быстрая, поддержка фильтров |
| Weaviate | Локальная/облачная | Гибкая, с гибридным поиском |
| Milvus | Локальная/облачная | Для больших объёмов |
| Pinecone | Облачная | Управляемая, платная |
| pgvector | Расширение PostgreSQL | Если уже используете PostgreSQL |
| Redis Vector | Расширение Redis | Если уже используете Redis |
Для начала: ChromaDB — самый простой вариант. Работает локально, не требует установки сервера, легко интегрируется с Python.
Для PHP-проектов: можно использовать Qdrant или pgvector (если есть PostgreSQL).
5.3 LLM для генерации ответов
- Облачные: OpenAI GPT-4, Claude, YandexGPT.
- Локальные: Ollama с моделями Mistral, Llama 3, DeepSeek.
Для RAG важно, чтобы модель хорошо следовала инструкциям и не галлюцинировала. Подойдут как облачные, так и локальные модели.
6. Пошаговая реализация
Рассмотрим пример на Python (для индексации) и PHP (для поиска на сайте).
6.1 Шаг 1. Установка зависимостей
Python (для индексации):
pip install chromadb sentence-transformersPHP (для поиска):
composer require guzzlehttp/guzzle6.2 Шаг 2. Разбивка контента на чанки
chunking.py:
import os
import re
def split_into_chunks(text, chunk_size=800, overlap=100):
"""
Разбивает текст на чанки по chunk_size символов с перекрытием overlap.
"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
chunks.append(chunk)
start = end - overlap
return chunks
def load_articles(directory):
"""
Загружает все .md или .txt файлы из директории.
"""
articles = []
for filename in os.listdir(directory):
if filename.endswith('.md') or filename.endswith('.txt'):
with open(os.path.join(directory, filename), 'r', encoding='utf-8') as f:
content = f.read()
articles.append({
'title': filename,
'content': content
})
return articles
if __name__ == '__main__':
articles = load_articles('/var/www/codestack/articles')
all_chunks = []
for article in articles:
chunks = split_into_chunks(article['content'])
for i, chunk in enumerate(chunks):
all_chunks.append({
'article': article['title'],
'chunk_index': i,
'text': chunk
})
print(f"Всего чанков: {len(all_chunks)}")6.3 Шаг 3. Генерация эмбеддингов и сохранение в ChromaDB
index.py:
import chromadb
from chromadb.utils import embedding_functions
from chunking import load_articles, split_into_chunks
# Инициализация ChromaDB (локальная, сохраняется в папку ./chroma_db)
client = chromadb.PersistentClient(path="./chroma_db")
# Используем модель эмбеддингов по умолчанию (all-MiniLM-L6-v2)
# Для русского языка лучше указать multilingual модель
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="intfloat/multilingual-e5-large"
)
# Создание коллекции
collection = client.get_or_create_collection(
name="articles",
embedding_function=embedding_fn
)
# Загрузка статей и разбивка на чанки
articles = load_articles('/var/www/codestack/articles')
# Добавление чанков в коллекцию
for article in articles:
chunks = split_into_chunks(article['content'])
for i, chunk in enumerate(chunks):
collection.add(
documents=[chunk],
metadatas=[{
'article': article['title'],
'chunk_index': i
}],
ids=[f"{article['title']}_{i}"]
)
print(f"Проиндексировано {collection.count()} чанков")Запуск индексации:
python index.py6.4 Шаг 4. Поиск по смыслу
search.py:
import chromadb
from chromadb.utils import embedding_functions
client = chromadb.PersistentClient(path="./chroma_db")
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="intfloat/multilingual-e5-large"
)
collection = client.get_or_create_collection(
name="articles",
embedding_function=embedding_fn,
metadata={"hnsw:space": "cosine"}
)
def search(query, n_results=5):
results = collection.query(
query_texts=[query],
n_results=n_results
)
return results
if __name__ == '__main__':
query = "как защитить сервер от взлома"
results = search(query)
for i, (doc, meta, dist) in enumerate(zip(
results['documents'][0],
results['metadatas'][0],
results['distances'][0]
)):
print(f"--- Результат {i+1} ---")
print(f"Статья: {meta['article']}")
print(f"Релевантность: {1 - dist:.4f}")
print(f"Текст: {doc[:200]}...")
print()Пример вывода:
--- Результат 1 ---
Статья: security.md
Релевантность: 0.92
Текст: Для защиты сервера от взлома необходимо настроить SSH...
--- Результат 2 ---
Статья: firewall.md
Релевантность: 0.87
Текст: Фаервол — это первая линия обороны...6.5 Шаг 5. Интеграция с PHP
Для PHP-сайта можно создать API-эндпоинт, который вызывает Python-скрипт или обращается к векторной базе напрямую.
Вариант 1: PHP вызывает Python через shell_exec
<?php
function searchArticles($query, $n = 5) {
$queryEscaped = escapeshellarg($query);
$output = shell_exec("python3 /path/to/search.py $queryEscaped 2>&1");
return json_decode($output, true);
}
$results = searchArticles("как защитить сервер");
foreach ($results as $result) {
echo "<h3>{$result['article']}</h3>";
echo "<p>{$result['text']}</p>";
}
?>ВАЖНО! Это псевдокод и для реального использования нужен API-эндпоинт (FastAPI/Flask).
Вариант 2: Использование Qdrant через HTTP API
Qdrant предоставляет REST API, который можно вызывать из PHP.
<?php
function searchQdrant($query, $n = 5) {
$client = new GuzzleHttp\Client();
// Сначала получаем эмбеддинг для запроса (через API эмбеддингов)
$embeddingResponse = $client->post('http://localhost:11434/api/embeddings', [
'json' => [
'model' => 'nomic-embed-text',
'prompt' => $query
]
]);
$embedding = json_decode($embeddingResponse->getBody(), true)['embedding'];
// Затем ищем в Qdrant
$response = $client->post('http://localhost:6333/collections/articles/points/search', [
'json' => [
'vector' => $embedding,
'limit' => $n,
'with_payload' => true
]
]);
return json_decode($response->getBody(), true)['result'];
}
?>ВАЖНО! Эндпоинт /points/search устарел в новых версиях Qdrant — заменён на /points/query или /search. Проверьте версию Qdrant в документации!
6.6 Шаг 6. RAG: генерация ответов с помощью LLM
Теперь добавим LLM, чтобы генерировать ответы на основе найденных чанков.
rag.py:
import chromadb
from chromadb.utils import embedding_functions
import requests
client = chromadb.PersistentClient(path="./chroma_db")
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="intfloat/multilingual-e5-large"
)
collection = client.get_collection(name="articles", embedding_function=embedding_fn)
def ask_llm(prompt, context):
"""Отправляет запрос к локальной LLM через Ollama."""
full_prompt = f"""Ответь на вопрос пользователя, используя только предоставленный контекст.
Если в контексте нет ответа, скажи "Я не знаю".
Контекст:
{context}
Вопрос: {prompt}
Ответ:"""
response = requests.post('http://localhost:11434/api/generate', json={
'model': 'mistral',
'prompt': full_prompt,
'stream': False
})
return response.json()['response']
def rag_query(question, n_results=3):
# 1. Поиск релевантных чанков
results = collection.query(query_texts=[question], n_results=n_results)
# 2. Формирование контекста
context = "\n\n".join(results['documents'][0])
sources = [meta['article'] for meta in results['metadatas'][0]]
# 3. Генерация ответа
answer = ask_llm(question, context)
return {
'answer': answer,
'sources': sources
}
if __name__ == '__main__':
result = rag_query("Как защитить сервер от взлома?")
print("Ответ:", result['answer'])
print("Источники:", result['sources'])Пример ответа:
Ответ: Для защиты сервера от взлома необходимо:
1. Настроить SSH: сменить порт, отключить вход root, использовать ключи.
2. Установить и настроить фаервол (UFW или firewalld).
3. Настроить Fail2ban для защиты от брутфорса.
4. Регулярно обновлять ПО.
5. Использовать HTTPS для всех соединений.
Источники: ['security.md', 'ssh.md', 'firewall.md']7. Оптимизация и лучшие практики
7.1 Чанкинг
- Размер чанка: 500–1000 символов. Слишком маленькие — теряется контекст, слишком большие — снижается точность поиска.
- Перекрытие: 10–20% (100–200 символов), чтобы не терять смысл на границах.
- Разбивка по абзацам: лучше сохранять логические блоки, а не резать по символам.
- Метаданные: сохраняйте заголовок статьи, URL, дату — это поможет в фильтрации и отображении.
7.2 Выбор модели эмбеддингов
- Для русского языка используйте многоязычные модели (
multilingual-e5,nomic-embed-text). - Для английского подойдут
all-MiniLM-L6-v2(быстрая, лёгкая) илиtext-embedding-3-small(качественная). - Тестируйте на своих данных: одна модель может давать лучшие результаты, чем другая.
7.3 Гибридный поиск
Комбинируйте векторный поиск с обычным (BM25). Это повышает точность: векторный ищет по смыслу, BM25 — по точным словам.
Пример с Qdrant: поддерживает гибридный поиск «из коробки».
7.4 Кеширование
- Кешируйте эмбеддинги для часто задаваемых вопросов.
- Кешируйте ответы LLM (если вопрос повторяется).
- Используйте Redis или Memcached.
7.5 Обновление индекса
- При добавлении новой статьи — генерируйте эмбеддинги и добавляйте в базу.
- При обновлении статьи — удаляйте старые чанки и добавляйте новые.
- Автоматизируйте через cron или webhook.
7.6 Оценка качества
- Собирайте обратную связь от пользователей (полезно/не полезно).
- Логируйте запросы и результаты.
- Периодически проверяйте, находит ли поиск то, что нужно.
8. Частые ошибки
| Ошибка | Последствия | Решение |
|---|---|---|
| Слишком маленькие чанки | Потеря контекста | Увеличьте размер до 500–1000 символов. |
| Слишком большие чанки | Снижение точности | Уменьшите или используйте перекрытие. |
| Одна модель для всего | Плохое качество на русском | Используйте многоязычные модели. |
| Нет метаданных | Нельзя фильтровать и показывать источники | Сохраняйте заголовок, URL, дату. |
| Индекс не обновляется | Поиск не находит новые статьи | Автоматизируйте обновление. |
| LLM галлюцинирует | Ответы не соответствуют контенту | Используйте строгий промпт и RAG. |
| Нет кеширования | Медленно и дорого | Кешируйте эмбеддинги и ответы. |
| Игнорирование обратной связи | Качество не улучшается | Собирайте фидбек и анализируйте. |
9. Заключение
Умный поиск по сайту с использованием векторных эмбеддингов и RAG — это мощный инструмент, который:
- Понимает смысл запросов, а не только точные слова.
- Находит релевантные статьи, даже если они не совпадают по формулировкам.
- Позволяет создать AI-ассистента, который отвечает на вопросы на основе вашего контента.
- Повышает вовлечённость пользователей и снижает нагрузку на поддержку.
