Сортировка по релевантности

Релевантность в списках автодополнения определяет порядок, в котором пользователю предлагаются варианты. В контексте Awesomplete этот механизм строится вокруг разделения двух этапов обработки: фильтрации и сортировки. Фильтрация отвечает за попадание элемента в набор кандидатов, сортировка — за их упорядочивание внутри этого набора.

Awesomplete использует внутренний массив элементов, каждый из которых проходит через фильтр и затем через функцию сортировки. По умолчанию применяется простая эвристика: элементы, которые лучше совпадают с началом строки ввода и имеют меньшую длину, поднимаются выше в списке.

Логика базовой сортировки строится вокруг сравнения двух значений:

  • позиции совпадения (index of match)
  • длины строки

Чем раньше найдено совпадение и чем короче строка, тем выше элемент в выдаче.

Функция sort в Awesomplete

Ключевой точкой кастомизации является свойство sort, которое можно переопределить. Оно принимает функцию сравнения, аналогичную Array.prototype.sort.

Сигнатура:

awesomplete.sort = function(a, b, input) {
    return 0;
};

Где:

  • a — первый элемент списка
  • b — второй элемент списка
  • input — текущее значение ввода пользователя

Эта функция должна возвращать:

  • отрицательное число, если a релевантнее b
  • положительное число, если b релевантнее a
  • 0, если они равны по приоритету

Простая сортировка по длине

Один из наиболее примитивных способов ранжирования — сортировка по длине строки:

awesomplete.sort = function(a, b) {
    return a.length - b.length;
};

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

Приоритет префиксных совпадений

Более распространённый сценарий — повышение приоритета элементов, начинающихся с введённого текста.

awesomplete.sort = function(a, b, input) {
    let ai = a.toLowerCase().indexOf(input.toLowerCase());
    let bi = b.toLowerCase().indexOf(input.toLowerCase());

    if (ai !== bi) {
        return ai - bi;
    }

    return a.length - b.length;
};

Здесь сначала сравнивается позиция вхождения, затем длина строки как вторичный критерий.

Нормализация регистра и стабильность сравнения

Релевантность сильно зависит от приведения строк к единому виду. Без нормализации регистр может искажать результат сортировки.

const normalize = str => str.toLowerCase();

awesomplete.sort = function(a, b, input) {
    input = normalize(input);

    const aNorm = normalize(a);
    const bNorm = normalize(b);

    const ai = aNorm.indexOf(input);
    const bi = bNorm.indexOf(input);

    if (ai !== bi) {
        return ai - bi;
    }

    return aNorm.length - bNorm.length;
};

Такой подход делает сортировку предсказуемой независимо от регистра входных данных.

Разделение веса признаков релевантности

Более сложные модели используют взвешивание факторов. Например:

  • совпадение в начале строки — высокий приоритет
  • совпадение внутри строки — средний приоритет
  • длина строки — низкий приоритет

Пример реализации через скоринг:

function score(item, input) {
    const value = item.toLowerCase();
    input = input.toLowerCase();

    if (value.startsWith(input)) {
        return 0;
    }

    const index = value.indexOf(input);
    if (index > 0) {
        return 1 + index / 100;
    }

    return 100;
}

awesomplete.sort = function(a, b, input) {
    return score(a, input) - score(b, input);
};

Здесь сортировка превращается в сравнение числового рейтинга релевантности.

Работа с объектами данных (data-value и data-label)

Awesomplete поддерживает использование объектов вместо строк. В этом случае сортировка должна учитывать отображаемое значение.

awesomplete.sort = function(a, b, input) {
    const aValue = a.label || a.value || a;
    const bValue = b.label || b.value || b;

    return aValue.length - bValue.length;
};

При более сложных сценариях можно использовать отдельные поля:

  • label — отображаемый текст
  • value — внутреннее значение

Это позволяет отделять пользовательский интерфейс от логики ранжирования.

Влияние фильтра на релевантность

Фильтр в Awesomplete определяет, попадёт ли элемент в список, но не отвечает за его позицию. Однако комбинация фильтра и сортировки формирует итоговую релевантность.

Типичный фильтр:

awesomplete.filter = function(text, input) {
    return text.toLowerCase().includes(input.toLowerCase());
};

Если фильтр слишком широкий (например, включает все вхождения), сортировка становится основным инструментом формирования качества выдачи.

Позиционно-ориентированная релевантность

Для повышения точности часто используется комбинированный подход:

  • совпадение с началом строки — максимальный приоритет
  • совпадение в первом слове — высокий приоритет
  • совпадение в любом месте — низкий приоритет
awesomplete.sort = function(a, b, input) {
    const norm = s => s.toLowerCase();
    input = norm(input);

    const rank = str => {
        str = norm(str);

        if (str.startsWith(input)) return 0;
        if (str.split(" ")[0].startsWith(input)) return 1;

        const pos = str.indexOf(input);
        return pos >= 0 ? 2 + pos : 1000;
    };

    return rank(a) - rank(b);
};

Такой подход особенно полезен при автодополнении больших справочников.

Устойчивость сортировки и детерминизм

При одинаковых значениях релевантности важно сохранять стабильность порядка. Без этого список может «прыгать» при каждом вводе.

Один из способов стабилизации:

awesomplete.sort = function(a, b, input) {
    const ra = score(a, input);
    const rb = score(b, input);

    if (ra === rb) {
        return a.localeCompare(b);
    }

    return ra - rb;
};

Добавление лексикографического сравнения делает порядок воспроизводимым.

Производительность при сложной сортировке

При больших списках основная нагрузка приходится на повторное вычисление скоринга. Чтобы снизить затраты, используется предрасчёт нормализованных значений или кэширование:

const cache = new Map();

function getNormalized(str) {
    if (!cache.has(str)) {
        cache.set(str, str.toLowerCase());
    }
    return cache.get(str);
}

Это особенно важно при динамическом вводе, где сортировка вызывается на каждом нажатии клавиши.

Интеграция пользовательских весов

В реальных приложениях данные часто имеют дополнительную метаинформацию: популярность, частоту использования, категорию.

awesomplete.sort = function(a, b, input) {
    const getValue = x => x.value || x;

    const popularity = x => x.popularity || 0;

    const score = x => {
        const v = getValue(x).toLowerCase();
        const i = input.toLowerCase();

        const base = v.startsWith(i) ? 0 : v.indexOf(i) >= 0 ? 1 : 100;
        return base - popularity(x) * 0.01;
    };

    return score(a) - score(b);
};

Здесь релевантность становится многокритериальной функцией, где пользовательский опыт учитывается через частоту использования элементов.

Согласование с UX-логикой автодополнения

Сортировка в Awesomplete фактически определяет поведение интерфейса при вводе. Небольшие изменения в формуле ранжирования могут радикально менять восприятие системы:

  • приоритет префиксов усиливает предсказуемость
  • учет длины улучшает читаемость списка
  • учет популярности повышает практическую полезность
  • строгая нормализация снижает шум

Комбинация этих факторов формирует итоговую модель релевантности, которая работает как основа поведения автодополнения в реальных интерфейсах