Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Стратегии поиска
retrieval_strategies

Стратегии поиска

Retrieval: семантический поиск, гибридный поиск, reranking и оптимизация выдачи

Стратегии поиска (Retrieval Strategies)

Хороший поиск — это не только векторы. Это комбинация подходов.

#Семантический поиск: основа RAG

Семантический поиск — это то, что мы уже освоили: embedding вопроса, cosine similarity, top-k результатов. Он хорош для понимания смысла, но у него есть слепые зоны.

from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity import numpy as np model = SentenceTransformer("intfloat/multilingual-e5-small") documents = [ "Политика компании по охране труда", "Инструкция по пожарной безопасности", "Правила внутреннего трудового распорядка" ] doc_embeddings = model.encode(documents) query = "Охрана труда" query_embedding = model.encode([query]) similarities = cosine_similarity(query_embedding, doc_embeddings)[0] for doc, sim in zip(documents, similarities): print(f"[{sim:.4f}] {doc}")

Семантический поиск отлично находит «Политика компании по охране труда» для запроса «Охрана труда». Но что если запрос содержит точный термин, которого нет в документах?

#Keyword-поиск (BM25)

BM25 — классический алгоритм полнотекстового поиска. В отличие от семантического, он ищет по точному совпадению слов с учётом частотности.

from rank_bm25 import BM25Okapi import re def tokenize(text): """Простая токенизация по словам.""" return re.findall(r'\w+', text.lower()) documents = [ "Политика компании по охране труда обновлена в 2026 году", "Инструкция по пожарной безопасности действует для всех отделов", "Правила внутреннего трудового распорядка утверждены приказом №42" ] # Токенизируем документы tokenized_docs = [tokenize(doc) for doc in documents] # Создаём BM25 индекс bm25 = BM25Okapi(tokenized_docs) # Поиск query = "приказ 42" tokenized_query = tokenize(query) scores = bm25.get_scores(tokenized_query) for doc, score in zip(documents, scores): print(f"[{score:.4f}] {doc}")

BM25 найдёт документ с «приказом №42», даже если семантически запрос «приказ 42» не очень близок к другим документам. Keyword-поиск силён в:

  • Точных совпадениях терминов, номеров, артикулов
  • Поиске по редким словам (уникальные имена, коды)
  • Ситуациях, когда семантическая модель не обучена на специфичной предметной области

#Гибридный поиск

Гибридный поиск объединяет семантический и keyword-подход. Каждый документ получает два скорa — от BM25 и от cosine similarity — которые объединяются.

def hybrid_search(query, documents, model, bm25, alpha=0.7, top_k=2): """ Гибридный поиск: комбинация семантического и BM25. alpha: вес семантического поиска (0.7 = 70% семантика, 30% BM25) """ # Семантический поиск doc_embeddings = model.encode(documents) query_embedding = model.encode([query]) semantic_scores = cosine_similarity(query_embedding, doc_embeddings)[0] # Нормализуем семантические скоры в диапазон [0, 1] min_s, max_s = semantic_scores.min(), semantic_scores.max() if max_s > min_s: semantic_norm = [(s - min_s) / (max_s - min_s) for s in semantic_scores] else: semantic_norm = [0.5] * len(semantic_scores) # BM25 поиск tokenized_query = tokenize(query) bm25_scores = bm25.get_scores(tokenized_query) # Нормализуем BM25 скоры min_b, max_b = bm25_scores.min(), bm25_scores.max() if max_b > min_b: bm25_norm = [(s - min_b) / (max_b - min_b) for s in bm25_scores] else: bm25_norm = [0.5] * len(bm25_scores) # Комбинируем combined = [alpha * s + (1 - alpha) * b for s, b in zip(semantic_norm, bm25_norm)] # Возвращаем top-k top_indices = np.argsort(combined)[-top_k:][::-1] return [(documents[i], combined[i]) for i in top_indices] results = hybrid_search("приказ 42", documents, model, bm25) for doc, score in results: print(f"[{score:.4f}] {doc}")

alpha контролирует баланс: alpha=0.7 означает 70% семантики и 30% keyword. Для общих задач 0.5–0.7 — хороший выбор. Если в вашей предметной области много точных терминов (артикулы, коды, номера документов) — снижайте alpha.

#Reranking: переранжирование результатов

Reranking — это второй этап поиска. Сначала мы находим кандидатов быстрым методом (семантический + BM25), а затем более точной моделью переранжируем.

#Зачем нужен reranking

Embedding-модель создаёт один вектор на весь чанк и сравнивает с вектором запроса. Это быстро, но теряет детализацию. Cross-Encoder (reranker) обрабатывает пару «запрос-документ» целиком и выдаёт точную оценку релевантности.

from sentence_transformers import CrossEncoder # Загружаем reranker-модель reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") # Кандидаты с первого этапа query = "Как оформить удалённую работу?" candidates = [ "Политика компании по удалённой работе обновлена в январе 2026", "Процедура оформления отпуска: заявление за 2 недели через HR-портал", "Удалённая работа разрешена до 3 дней в неделю по согласованию с руководителем" ] # Создаём пары запрос-документ pairs = [(query, doc) for doc in candidates] # Reranking scores = reranker.predict(pairs) for doc, score in zip(candidates, scores): print(f"[{score:.4f}] {doc}")

Reranker точнее embeddings, потому что видит полную картину взаимодействия запроса и документа. Но он медленный — поэтому применяется только к топ-10 или топ-20 кандидатам, а не ко всей базе.

#Двухэтапный пайплайн поиска

def two_stage_search(query, all_documents, model, top_k_initial=20, top_k_final=5): """ Этап 1: быстрый семантический поиск → top_k_initial кандидатов Этап 2: reranking кандидатов → top_k_final результатов """ # Этап 1: embedding поиск doc_embeddings = model.encode(all_documents) query_embedding = model.encode([query]) similarities = cosine_similarity(query_embedding, doc_embeddings)[0] # Берём топ-N кандидатов top_indices = np.argsort(similarities)[-top_k_initial:][::-1] candidates = [all_documents[i] for i in top_indices] # Этап 2: reranking reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") pairs = [(query, doc) for doc in candidates] rerank_scores = reranker.predict(pairs) # Финальный top-k final_indices = np.argsort(rerank_scores)[-top_k_final:][::-1] return [(candidates[i], rerank_scores[i]) for i in final_indices] results = two_stage_search("удалённая работа", documents, model) for doc, score in results: print(f"[{score:.4f}] {doc}")

#Query Expansion: расширение запроса

Иногда пользователь формулирует вопрос слишком узко. Query Expansion генерирует дополнительные варианты запроса для улучшения поиска.

def expand_query(query): """Генерирует варианты запроса для расширения поиска.""" # Простой подход: синонимы и перефразирование expansions = [ query, query.replace("как", "каким образом"), query.replace("что", "какие документы"), f"инструкция: {query}", f"правила: {query}" ] return list(set(expansions)) # Убираем дубликаты query = "как оформить отпуск" expanded = expand_query(query) print(f"Оригинал: {query}") print(f"Расширенные: {expanded}")

Для каждого варианта запроса мы ищем документы и объединяем результаты. Это помогает, когда пользователь не знает точной терминологии.

Более продвинутый подход — использовать LLM для перефразирования:

def llm_expand_query(query): """Используем LLM для генерации вариантов запроса.""" prompt = f"""Сгенерируй 3 варианта вопроса на русском языке, которые означают то же самое: "{query}" Ответь списком, каждый вариант с новой строки.""" # Здесь был бы вызов LLM # Для примера — статические варианты return [ "каким образом подать заявление на отпуск", "процедура получения отпускных дней", "что нужно сделать для оформления отпуска" ]

#Выбор top-k и пороговые значения

#Как выбрать k

k — количество документов, возвращаемых поиском. Слишком маленькое k — пропустим релевантные документы. Слишком большое k — передадим LLM лишний шум.

СценарийРекомендуемый k
Короткие чанки (100–200 слов)5–10
Средние чанки (300–500 слов)3–5
Длинные чанки (500+ слов)2–3

#Пороговое отсечение

Вместо фиксированного k можно использовать порог по схожести:

def threshold_search(query, documents, model, threshold=0.6): """Возвращает все документы со схожестью выше порога.""" doc_embeddings = model.encode(documents) query_embedding = model.encode([query]) similarities = cosine_similarity(query_embedding, doc_embeddings)[0] results = [ (doc, float(sim)) for doc, sim in zip(documents, similarities) if sim >= threshold ] return sorted(results, key=lambda x: x[1], reverse=True) results = threshold_search("отпуск", documents, model, threshold=0.65) print(f"Найдено {len(results)} документов выше порога") for doc, score in results: print(f"[{score:.4f}] {doc}")

Порог 0.6–0.7 для косинусного сходства — хорошая отправная точка. Если ни один документ не превышает порог — RAG-система может ответить «информация не найдена» вместо того чтобы генерировать ответ из нерелевантных чанков.

Далее: Интеграция с LLM