Фильтрация по вхождению подстроки

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

Ключевой момент заключается в том, что библиотека не ограничивает разработчика жёсткой логикой поиска. Вместо этого используется заменяемая функция, позволяющая реализовать как строгий поиск по началу строки, так и более гибкий поиск по вхождению подстроки.


Базовая модель фильтрации Awesomplete

Каждый элемент списка проходит проверку через фильтрующую функцию:

  • входной текст пользователя (input)
  • элемент списка (text)

Результатом является булево значение:

  • true — элемент отображается
  • false — элемент исключается

Упрощённо механизм можно представить так:

function filter(text, input) {
  return match(text, input);
}

Где match — логика сравнения, которую можно полностью переопределить.


Фильтрация по началу строки и её ограничения

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

Пример поведения:

["Apple", "Apricot", "Banana", "Grape"]

При вводе:

ap

Результат:

  • Apple
  • Apricot

Но при вводе:

pp

Результат пустой, хотя логически пользователь мог искать часть слова.

Ограничение подхода заключается в том, что он плохо работает для поиска внутри строки, особенно при работе с составными словами, именами и описаниями.


Фильтрация по вхождению подстроки

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

Классический вариант реализации:

new Awesomplete(input, {
  list: ["Apple", "Pineapple", "Grape", "Apricot"],
  filter: function(text, input) {
    return text.toLowerCase().includes(input.trim().toLowerCase());
  }
});

Логика работы

  1. Оба значения приводятся к одному регистру
  2. Ввод пользователя очищается от пробелов по краям
  3. Проверяется наличие подстроки внутри элемента списка

Такой подход делает поиск более «интуитивным» для пользователя.


Использование встроенных фильтров Awesomplete

Библиотека предоставляет готовые стратегии фильтрации, которые можно использовать без написания собственной логики.

Типовые варианты:

  • фильтр по началу строки
  • фильтр по вхождению подстроки

Пример переключения стратегии:

new Awesomplete(input, {
  list: ["Apple", "Pineapple", "Banana"],
  filter: Awesomplete.FILTER_CONTAINS
});

или:

new Awesomplete(input, {
  list: ["Apple", "Pineapple", "Banana"],
  filter: Awesomplete.FILTER_STARTSWITH
});

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


Кастомная фильтрация как основной инструмент гибкости

Фильтр в Awesomplete можно полностью заменить. Это открывает возможность реализовать:

  • многоуровневый поиск
  • приоритеты совпадений
  • игнорирование символов
  • нормализацию текста
  • поддержку разных языков

Пример расширенной логики

function normalize(str) {
  return str
    .toLowerCase()
    .replace(/\s+/g, " ")
    .trim();
}

new Awesomplete(input, {
  list: ["New York", "Newark", "Yorkshire", "Little York"],
  filter: function(text, input) {
    const t = normalize(text);
    const i = normalize(input);
    return t.includes(i);
  }
});

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


Работа с регистрами и нечёткими совпадениями

Регистронезависимый поиск является базовым требованием для большинства интерфейсов автодополнения. Однако простого toLowerCase() часто недостаточно.

Усиленная нормализация

function normalize(text) {
  return text
    .toLowerCase()
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "");
}

Такая обработка позволяет:

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

Пример:

  • “café” → “cafe”
  • “naïve” → “naive”

Приоритет совпадений при подстрочном поиске

При использовании includes возникает проблема: все совпадения равнозначны. Однако пользовательский интерфейс часто требует сортировки по релевантности.

Можно расширить поведение фильтрации:

new Awesomplete(input, {
  list: ["JavaScript", "TypeScript", "CoffeeScript", "Java"],
  filter: function(text, input) {
    return text.toLowerCase().includes(input.toLowerCase());
  },
  sort: function(a, b, input) {
    const i = input.toLowerCase();
    const aIndex = a.toLowerCase().indexOf(i);
    const bIndex = b.toLowerCase().indexOf(i);

    return aIndex - bIndex;
  }
});

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


Частичные совпадения и разбиение строки

Подстрочный поиск можно усложнить до анализа отдельных токенов. Это полезно для длинных описаний и тегов.

function tokenMatch(text, input) {
  const tokens = text.toLowerCase().split(" ");
  const query = input.toLowerCase();

  return tokens.some(t => t.includes(query));
}

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


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

В реальных интерфейсах часто используется гибридный подход:

  • сначала проверка точного совпадения
  • затем совпадение по началу строки
  • затем поиск по подстроке

Пример:

function smartFilter(text, input) {
  const t = text.toLowerCase();
  const i = input.toLowerCase();

  if (t === i) return true;
  if (t.startsWith(i)) return true;
  return t.includes(i);
}

Такая логика обеспечивает баланс между точностью и гибкостью.


Поведение фильтрации при пустом вводе

Особое значение имеет обработка пустой строки. В зависимости от реализации можно:

  • показывать весь список
  • скрывать все элементы
  • отображать только популярные варианты

Пример:

filter: function(text, input) {
  if (!input) return true;
  return text.toLowerCase().includes(input.toLowerCase());
}

Такой вариант позволяет пользователю видеть подсказки до начала ввода.


Производительность при подстрочном поиске

При больших списках (тысячи элементов) фильтрация по includes может стать узким местом.

Оптимизационные подходы:

  • предварительная нормализация списка
  • кэширование lowercase-версий
  • ограничение числа отображаемых результатов
  • дебаунс ввода

Пример кэширования:

const prepared = list.map(item => ({
  original: item,
  normalized: item.toLowerCase()
}));

filter: function(text, input) {
  const i = input.toLowerCase();
  return prepared.filter(x => x.normalized.includes(i));
}

Поведение в реальных интерфейсах

Подстрочная фильтрация особенно эффективна в случаях:

  • поиск городов и стран
  • базы продуктов
  • списки пользователей
  • теги и категории
  • справочники с длинными названиями

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


Ограничения подстрочного поиска

Несмотря на гибкость, у подхода есть слабые стороны:

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

Эти факторы компенсируются комбинированной фильтрацией и сортировкой, но требуют внимания при проектировании интерфейса.