Индексация содержимого

Индексация в Tom Select строится вокруг подготовки набора данных для быстрого поиска и ранжирования опций. Библиотека не использует полноценный полнотекстовый движок, но реализует собственный индексатор, оптимизированный под интерактивный UI: минимальная задержка, инкрементальные пересчёты, предсказуемое ранжирование.

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


Структура данных, участвующих в индексации

Каждая опция внутри Tom Select проходит этап преобразования в внутренний индексируемый объект:

  • text — основное отображаемое значение
  • value — идентификатор
  • search — набор строк, по которым выполняется поиск
  • score — числовая метрика релевантности
  • tokens — разложенные части строки (при расширенной конфигурации)

Пример базовой структуры:

{
  value: "1",
  text: "San Francisco",
  $order: 1,
  score: 0,
  search: "san francisco"
}

Именно поле search становится ключевым элементом индексации. Оно формируется либо автоматически, либо через render/load/options.


Нормализация входных данных

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

  • приведение к нижнему регистру
  • удаление диакритики (опционально)
  • обрезка лишних пробелов
  • преобразование Unicode-символов

Внутренний процесс можно представить так:

function normalize(str) {
  return str
    .toLowerCase()
    .trim()
    .normalize("NFD")
    .replace(/\p{Diacritic}/gu, "");
}

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


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

Tom Select не строит отдельную структуру вроде inverted index, но формирует вычисляемый индекс на лету.

Алгоритм подготовки:

  1. Берётся список options
  2. Для каждой опции формируется строка search
  3. Создаётся внутренний массив индексируемых элементов
  4. При каждом запросе выполняется фильтрация по этому массиву

Упрощённая модель:

index = options.map(opt => ({
  value: opt.value,
  text: opt.text,
  search: normalize(opt.text + " " + (opt.search || "")),
}));

Механизм сопоставления запроса

При вводе строки пользовательский запрос также нормализуется:

query = normalize(input);

Далее выполняется сравнение с каждой опцией:

  • поиск подстроки
  • проверка начала строки
  • вычисление веса совпадения

Простейшая модель:

function match(option, query) {
  return option.search.indexOf(query) !== -1;
}

Однако в реальной реализации добавляется ранжирование.


Ранжирование результатов

Каждому совпадению присваивается score, который влияет на порядок отображения.

Факторы, влияющие на score:

  • позиция совпадения (чем раньше — тем выше)
  • точное совпадение vs частичное
  • совпадение начала строки
  • длина строки (короче — выше при равенстве условий)

Пример логики:

function scoreOption(option, query) {
  const pos = option.search.indexOf(query);

  if (pos === 0) return 100;
  if (pos > 0) return 50;
  return 0;
}

Далее результаты сортируются:

results.sort((a, b) => b.score - a.score);

Индексация при динамической загрузке данных

При использовании load(query, callback) индекс не строится заранее. Вместо этого:

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

Пример:

new TomSelect("#select", {
  load: function(query, callback) {
    fetch("/search?q=" + encodeURIComponent(query))
      .then(res => res.json())
      .then(data => callback(data));
  }
});

В этом режиме индексация частично переносится на сервер, что уменьшает нагрузку на клиент.


Индексация при ручном добавлении опций

При использовании addOption или addOptions новые элементы сразу включаются в индекс:

select.addOption({
  value: "tokyo",
  text: "Tokyo"
});

После добавления происходит:

  • нормализация текста
  • обновление массива опций
  • пересчёт доступных результатов при следующем вводе

Кастомизация индексируемого содержимого

Поле searchField позволяет управлять тем, какие части объекта участвуют в индексации:

new TomSelect("#select", {
  searchField: ["text", "code", "country"]
});

В этом случае индекс строится не только по text, но и по дополнительным полям:

search = normalize(
  opt.text + " " +
  opt.code + " " +
  opt.country
);

Это расширяет поисковую поверхность и позволяет реализовать поведение, близкое к фасетному поиску.


Управление чувствительностью поиска

Tom Select позволяет контролировать поведение индексации через параметры:

  • ignoreCase — отключение/включение регистрозависимости
  • score — пользовательская функция ранжирования
  • sortField — порядок сортировки результатов

Пример кастомного ранжирования:

new TomSelect("#select", {
  score: function(search) {
    return function(option) {
      if (option.text === search) return 100;
      if (option.text.startsWith(search)) return 75;
      if (option.text.includes(search)) return 25;
      return 0;
    };
  }
});

Здесь индекс становится полностью управляемым.


Составные индексы и сложные структуры

При работе с объектами сложной структуры индексация часто требует явного указания пути:

{
  value: 1,
  name: {
    en: "London",
    ru: "Лондон"
  }
}

Индексация:

searchField: ["name.en", "name.ru"]

Tom Select извлекает значения через обращение к свойствам объекта и объединяет их в общий индекс.


Производительность индексации

Основные факторы, влияющие на скорость:

  • размер массива options
  • количество полей в searchField
  • сложность функции score
  • частота пересборки индекса

Оптимизации:

  • предварительная нормализация данных на этапе загрузки
  • сокращение searchField до минимально необходимых полей
  • кэширование результатов поиска при повторяющихся запросах

Кэширование поисковых результатов

Внутри механизма поиска используется простая форма кэширования:

  • ключ: строка запроса
  • значение: массив результатов

Модель:

cache[query] = results;

При повторном вводе одинакового запроса происходит возврат без пересчёта индекса.


Инкрементальная пересборка индекса

При изменении данных индекс не пересобирается полностью, а обновляется частично:

  • добавление — append
  • удаление — filter
  • обновление — replace по value

Это позволяет сохранять отзывчивость интерфейса даже при большом объёме данных.


Индексация в режиме тегов

В режиме create: true индексация расширяется за счёт пользовательских значений:

new TomSelect("#select", {
  create: true
});

Новые значения автоматически:

  • добавляются в options
  • попадают в индекс
  • становятся доступными для поиска

Это превращает индекс в динамически расширяемую структуру.


Поведение индекса при пустом запросе

При отсутствии поисковой строки:

  • возвращается исходный массив options
  • сортировка определяется sortField
  • индекс фактически не используется

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


Ограничения модели индексации

Подход Tom Select имеет ряд структурных ограничений:

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

Эти ограничения компенсируются простотой архитектуры и предсказуемым поведением в UI-контексте.


Расширение индексации через внешние алгоритмы

При необходимости можно полностью заменить механизм поиска:

new TomSelect("#select", {
  score: function() {
    return function() {
      return 1;
    };
  }
});

Или внедрить внешние библиотеки:

  • Fuse.js
  • MiniSearch
  • Elastic-like клиентские индексы

В этом случае Tom Select остаётся только слоем визуализации, а индексирование переносится наружу.