Реализация AI-поиска по контенту сайта (Semantic Search)

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация AI-поиска по контенту сайта (Semantic Search)
Сложная
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация AI-поиска по контенту сайта (Semantic Search)

Обычный полнотекстовый поиск (PostgreSQL tsvector, Elasticsearch) ищет по совпадению слов. Пользователь ищет «как оплатить» — находит статьи со словом «оплатить», но не находит статью «способы расчёта» или «пополнение баланса». Семантический поиск работает иначе: сравниваются смысловые векторы, а не строки.

Как работает векторный поиск

Текст преобразуется в вектор (embedding) — числовой массив из 768–3072 измерений, где близкие по смыслу тексты имеют похожие векторы. Расстояние между векторами (косинусное или евклидово) = семантическая близость.

"способы оплаты"  → [0.12, -0.87, 0.34, ...]
"как заплатить"   → [0.11, -0.85, 0.31, ...]  ← близко
"рецепт борща"    → [0.91,  0.23, -0.67, ...] ← далеко

Выбор embedding-модели

Модель Размер вектора Контекст Скорость Стоимость
OpenAI text-embedding-3-small 1536 8K токенов Быстро $0.02/1M токенов
OpenAI text-embedding-3-large 3072 8K токенов Медленнее $0.13/1M токенов
Cohere embed-multilingual-v3 1024 512 токенов Быстро $0.10/1M токенов
BGE-M3 (self-hosted) 1024 8K токенов Зависит от GPU Бесплатно
nomic-embed-text (Ollama) 768 8K токенов CPU-возможно Бесплатно

Для русскоязычного контента text-embedding-3-large или Cohere multilingual дают лучший результат.

Векторная база данных

pgvector — расширение PostgreSQL. Идеален если уже используется Postgres:

CREATE EXTENSION vector;

CREATE TABLE content_chunks (
  id         BIGSERIAL PRIMARY KEY,
  content_id BIGINT REFERENCES content(id),
  chunk_text TEXT NOT NULL,
  chunk_index INT,
  embedding  vector(1536),
  metadata   JSONB
);

-- Индекс для ANN-поиска (approximate nearest neighbor)
CREATE INDEX ON content_chunks USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);
-- Или HNSW (лучше для большинства случаев):
CREATE INDEX ON content_chunks USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

Qdrant — специализированная векторная БД с фильтрацией:

const qdrant = new QdrantClient({ url: 'http://localhost:6333' });

await qdrant.createCollection('content', {
  vectors: { size: 1536, distance: 'Cosine' },
  optimizers_config: { indexing_threshold: 20000 },
  hnsw_config: { m: 16, ef_construct: 100 },
});

Индексирование контента

import OpenAI from 'openai';

const openai = new OpenAI();

function chunkText(text, options = { maxTokens: 400, overlap: 50 }) {
  // Разбиваем по абзацам, затем объединяем до maxTokens
  const paragraphs = text.split(/\n{2,}/);
  const chunks = [];
  let current = '';
  let currentTokens = 0;

  for (const para of paragraphs) {
    const paraTokens = estimateTokens(para);

    if (currentTokens + paraTokens > options.maxTokens && current) {
      chunks.push(current.trim());
      // Перекрытие: берём последние N слов
      const words = current.split(' ');
      current = words.slice(-options.overlap).join(' ') + ' ' + para;
      currentTokens = estimateTokens(current);
    } else {
      current += (current ? '\n\n' : '') + para;
      currentTokens += paraTokens;
    }
  }

  if (current) chunks.push(current.trim());
  return chunks;
}

async function indexContent(contentItem) {
  const chunks = chunkText(contentItem.body);

  // Batch embeddings (до 2048 входов за раз)
  const batchSize = 100;
  for (let i = 0; i < chunks.length; i += batchSize) {
    const batch = chunks.slice(i, i + batchSize);

    const { data: embeddings } = await openai.embeddings.create({
      model: 'text-embedding-3-small',
      input: batch,
      encoding_format: 'float',
    });

    // Сохраняем в pgvector
    await db.query(`
      INSERT INTO content_chunks (content_id, chunk_text, chunk_index, embedding, metadata)
      SELECT $1, unnest($2::text[]), generate_series(0, $3), unnest($4::vector[]), $5
    `, [
      contentItem.id,
      batch,
      batch.length - 1,
      embeddings.map(e => `[${e.embedding.join(',')}]`),
      JSON.stringify({ title: contentItem.title, url: contentItem.url, type: contentItem.type }),
    ]);
  }
}

Поиск

async function semanticSearch(query, options = {}) {
  const {
    limit = 10,
    threshold = 0.7,
    filter = {},    // { type: 'article', lang: 'ru' }
    hybrid = true,  // Комбинировать с полнотекстовым
  } = options;

  // Векторизуем запрос
  const { data: [{ embedding }] } = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: query,
  });

  let results;

  if (hybrid) {
    // Гибридный поиск: вектор + полнотекст, RRF (Reciprocal Rank Fusion)
    results = await db.query(`
      WITH semantic AS (
        SELECT
          content_id,
          chunk_text,
          1 - (embedding <=> $1::vector) AS score,
          ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) AS rank
        FROM content_chunks
        WHERE metadata->>'type' = ANY($3::text[])
        ORDER BY embedding <=> $1::vector
        LIMIT 20
      ),
      fulltext AS (
        SELECT
          id AS content_id,
          body AS chunk_text,
          ts_rank(to_tsvector('russian', body), plainto_tsquery('russian', $2)) AS score,
          ROW_NUMBER() OVER (ORDER BY ts_rank(to_tsvector('russian', body), plainto_tsquery('russian', $2)) DESC) AS rank
        FROM content
        WHERE to_tsvector('russian', body) @@ plainto_tsquery('russian', $2)
        LIMIT 20
      )
      SELECT
        COALESCE(s.content_id, f.content_id) AS id,
        COALESCE(s.chunk_text, f.chunk_text) AS text,
        (
          COALESCE(1.0 / (60 + s.rank), 0) +
          COALESCE(1.0 / (60 + f.rank), 0)
        ) AS rrf_score
      FROM semantic s
      FULL OUTER JOIN fulltext f ON s.content_id = f.content_id
      ORDER BY rrf_score DESC
      LIMIT $4
    `, [`[${embedding.join(',')}]`, query, Object.values(filter), limit]);
  } else {
    // Чистый семантический поиск
    results = await db.query(`
      SELECT DISTINCT ON (content_id)
        content_id,
        chunk_text,
        1 - (embedding <=> $1::vector) AS score
      FROM content_chunks
      WHERE 1 - (embedding <=> $1::vector) > $2
      ORDER BY content_id, score DESC
      LIMIT $3
    `, [`[${embedding.join(',')}]`, threshold, limit]);
  }

  return results.rows;
}

UI компонент поиска

function SemanticSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  // Debounce запросов
  useEffect(() => {
    if (query.length < 3) { setResults([]); return; }

    const timer = setTimeout(async () => {
      setLoading(true);
      const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
      const data = await res.json();
      setResults(data.results);
      setLoading(false);
    }, 300);

    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div className="search-wrapper">
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Поиск по документации..."
        className="search-input"
      />
      {loading && <Spinner />}
      <ul className="search-results">
        {results.map(r => (
          <li key={r.id}>
            <a href={r.url}>
              <strong>{r.title}</strong>
              <p>{highlightMatch(r.snippet, query)}</p>
              <span className="score">{(r.score * 100).toFixed(0)}% совпадение</span>
            </a>
          </li>
        ))}
      </ul>
    </div>
  );
}

Переранжирование (Reranking)

После векторного поиска точность можно повысить cross-encoder-моделью:

import { CohereClient } from 'cohere-ai';

const cohere = new CohereClient({ token: process.env.COHERE_API_KEY });

async function rerank(query, documents) {
  const response = await cohere.rerank({
    model: 'rerank-multilingual-v3.0',
    query,
    documents: documents.map(d => d.text),
    topN: 5,
  });

  return response.results.map(r => ({
    ...documents[r.index],
    rerankScore: r.relevanceScore,
  }));
}

Поиск по изображениям

Для визуального контента — мультимодальные embeddings (CLIP, OpenAI Vision):

// Индексирование изображения
async function indexImage(imageUrl) {
  const response = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: await generateImageCaption(imageUrl), // Vision API → текст
  });
  // Сохраняем в ту же коллекцию
}

Мониторинг качества

-- Запросы без результатов (расширить базу знаний)
SELECT query, COUNT(*) as count
FROM search_logs
WHERE results_count = 0
GROUP BY query ORDER BY count DESC LIMIT 20;

-- Запросы с низким CTR (результаты нерелевантны)
SELECT query, clicks / impressions AS ctr
FROM search_metrics
WHERE impressions > 100
ORDER BY ctr ASC LIMIT 20;

Сроки

  • Семантический поиск по 10K документов с pgvector — 4–5 дней
  • Гибридный поиск (вектор + полнотекст) — плюс 1–2 дня
  • Переранжирование через Cohere Rerank — плюс 1 день
  • UI с подсветкой результатов, аналитикой — плюс 2–3 дня
  • Инкрементальная переиндексация при обновлении контента — плюс 1–2 дня