Перейти к содержанию

Как измерить качество поиска

К этому моменту поиск имеет индекс, веса полей, варианты запроса и удобный интерфейс. Но число реализованных возможностей не является оценкой полезности. Чтобы понять, стало ли лучше, нужно сравнить результаты с задачами читателя на одном и том же наборе данных.

Вернёмся к queries.json первого урока. Для каждого запроса уже указаны подходящие ID. В этом уроке подготовим небольшую оценку верхних результатов и отдельно учтём запросы, для которых подходящих материалов нет.

Оценка начинается с вопроса

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

Нельзя назначить документ релевантным только потому, что он содержит слова запроса. Наш пример с XML в материале о Fetch показывает границу: упоминание формата ответа не всегда решает задачу обработки XML через LINQ.

Оценка принадлежит редактору или человеку, понимающему задачу. Если возможны несколько хороших ответов, сохраняйте несколько ID. Не заставляйте поиск выбирать один материал лишь ради удобства подсчёта.

С другой стороны, список ожидаемых документов не должен бесконечно расширяться после каждого неудобного результата. Иначе алгоритм будет всегда выглядеть правильным. При изменении редакционной оценки запишите причину отдельно от изменения поиска.

Верхние результаты

Используем две простые величины. Первая — доля запросов, у которых подходящий документ попал в первые три результата. Вторая — среднее обратное место первого подходящего документа.

Если нужный материал находится первым, обратное место равно 1. Если вторым — 1/2, третьим — 1/3. Если не найден, значение 0. Такое представление сильнее ценит раннее появление полезного результата.

Для запроса с пустым relevant эти величины не подходят. Там ожидается отсутствие подходящих материалов, поэтому отдельно отмечаем, вернул ли поиск какие-либо карточки. Это не абсолютное доказательство ошибки: редакторский список тоже требует проверки, но сигнал помогает разбирать лишние совпадения.

Не будем объединять качество и миллисекунды в одно число. Сначала сравниваем полезность результата, затем время и объём ресурсов. Один показатель, скрывающий все различия, затрудняет объяснение решения.

Функция оценки

Создайте evaluation.js. Ниже полная функция, которая принимает контрольные случаи и поисковый адаптер. Адаптер может обращаться к Worker или серверу, но должен возвращать объект с hits.

export async function evaluate(cases, search) {
  const rows = [];
  for (const item of cases) {
    const result = await search(item.query);
    const ids = result.hits.map(doc => doc.id);
    const relevant = new Set(item.relevant);
    const first = ids.findIndex(id => relevant.has(id));
    rows.push({
      id: item.id,
      query: item.query,
      ids,
      positive: relevant.size > 0,
      hitAt3: first >= 0 && first < 3,
      reciprocalRank: first < 0 ? 0 : 1 / (first + 1),
      unexpectedResult: relevant.size === 0 && ids.length > 0
    });
  }
  const positive = rows.filter(row => row.positive);
  const negative = rows.filter(row => !row.positive);
  return {
    rows,
    hitAt3: positive.length
      ? positive.filter(row => row.hitAt3).length / positive.length : null,
    mrr: positive.length
      ? positive.reduce((sum, row) => sum + row.reciprocalRank, 0) / positive.length : null,
    negativeWithResults: negative.filter(row => row.unexpectedResult).length,
    negativeCount: negative.length
  };
}

Здесь проверяется только возвращённая часть выдачи. Если адаптер отдаёт десять карточек, первый подходящий документ за её пределами считается ненайденным для этого наблюдения. Поэтому размер получаемой части должен быть одинаков у сравниваемых версий.

Функция сохраняет ID каждого результата. Среднее полезно для обзора, но разбор причины происходит по конкретной строке. Два алгоритма с одинаковым средним могут ошибаться на разных запросах.

null означает, что положительных случаев вообще не было. Ноль означал бы измеренное отсутствие успехов, а это другое утверждение. Такое различие полезно и для технических журналов, и для редакционных выводов.

Подключение браузерного поиска

Для Worker можно передать такой адаптер. Переменная client создаётся по предыдущему уроку; cases — прочитанный массив из queries.json.

const report = await evaluate(cases, async query => {
  const response = await client.request("search", {
    query, options: { page: 1, pageSize: 10 }
  });
  return response.data;
});
console.table(report.rows);

Это пример будущего выполнения оценки, а не готовые результаты ProfessorWeb. Показатели должны появиться из реальной работы конкретной версии. Не подставляйте красивую долю успешных запросов из ожидаемого поведения.

Для нашей библиотеки точные названия и некоторые варианты уже должны находиться. Свободный вопрос «получить данные с сервера» может оставаться трудным из-за строгого пересечения слов. Такая строка объясняет ограничение, которое позже будет рассматриваться в семантическом блоке.

Разбор ухудшения

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

Для каждого изменившегося случая сравните нормализованные группы, найденные ID и оценки. Возможно, синоним слишком широк, опечатка дала неверную альтернативу или вес заголовка оказался чрезмерным. Исправление должно относиться к установленной причине.

Полезно сохранять версию корпуса и версию алгоритма вместе с отчётом. Иначе нельзя отличить эффект нового текста от эффекта новой формулы. Даже один исправленный заголовок способен изменить выдачу нескольких запросов.

Не используйте синтетически размноженные документы для оценки релевантности. Повторение одной статьи создаёт много похожих карточек и меняет состав верхних результатов. Такой набор подходит для нагрузки, но не для редакционной оценки качества.

Контрольный и настроечный набор

Если всё время изменять правила под одни семь запросов, можно подогнать алгоритм под них. Поэтому постепенно разделите запросы на набор для настройки и набор для последующей сверки.

Необязательно сразу создавать большую коллекцию. Добавьте несколько новых формулировок, которые не использовались при выборе весов и синонимов. Их результаты помогут увидеть, переносится ли улучшение на соседние задачи.

Сохраняйте также сложные случаи, где ожидаемый ответ пока не согласован. Их лучше обозначить отдельно, чем насильно присвоить единственный ID. Неопределённость редакционной оценки не должна маскироваться точным числом.

Поведение реальных посетителей даёт ещё один источник наблюдений, но само по себе не заменяет оценку. Клик может означать интерес, ошибку выбора или попытку разобраться. Для использования запросов в аналитике потребуется отдельная политика приватности.

При пересмотре ожидаемых документов сохраняйте причину изменения. Например, в библиотеке появился отдельный урок о нужной операции, и прежний обзор теперь является лишь дополнительным ответом. Это изменение редакционной разметки корпуса. Его не следует смешивать с улучшением алгоритма: сначала оцените обе реализации на одном обновлённом наборе ожиданий. Иначе рост показателя может объясняться новой трактовкой правильного ответа, а не тем, что программа стала лучше выбирать документы.

Теперь у нас есть способ сравнивать реализации по понятным задачам и конкретным карточкам. Следующий урок согласует обновление документов с версией индекса, чтобы новая статья действительно попадала в ту библиотеку, которую оценивает поиск.