Кастомизация логики поиска

Поиск в библиотеке Slim Select построен вокруг внутреннего механизма фильтрации массива опций. По умолчанию библиотека выполняет простое текстовое сравнение введённой строки с содержимым элементов <option>, однако система допускает глубокую кастомизацию поведения.

Базовая активация поиска:

<select id="users">
  <option value="1">Александр</option>
  <option value="2">Екатерина</option>
  <option value="3">Максим</option>
</select>
new SlimSelect({
  select: '#users',
  settings: {
    search: true
  }
})

При включённом поиске Slim Select:

  • создаёт отдельное поле ввода;
  • отслеживает события input;
  • фильтрует список опций;
  • скрывает несовпадающие элементы;
  • обновляет DOM динамически.

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

Иногда требуется полностью убрать встроенную фильтрацию:

new SlimSelect({
  select: '#users',
  settings: {
    search: false
  }
})

Такой режим полезен:

  • при небольшом количестве элементов;
  • при собственной внешней системе поиска;
  • при использовании AJAX-загрузки;
  • при интеграции с backend-фильтрацией.

Настройка чувствительности к регистру

По умолчанию Slim Select игнорирует регистр символов. Для строгого сравнения используется параметр searchText в пользовательской логике.

Пример кастомного поиска с учётом регистра:

new SlimSelect({
  select: '#users',
  events: {
    search: (search, currentData) => {
      return currentData.filter(option => {
        return option.text.includes(search)
      })
    }
  }
})

Теперь:

  • макс не найдёт Максим;
  • Макс вернёт результат.

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

Главный механизм кастомизации — событие search.

Оно получает:

Аргумент Описание
search Введённая строка
currentData Текущий массив опций

Возвращаемое значение должно содержать массив найденных элементов.

Базовый пример:

new SlimSelect({
  select: '#users',
  events: {
    search: (search, currentData) => {
      return currentData.filter(option => {
        return option.text
          .toLowerCase()
          .includes(search.toLowerCase())
      })
    }
  }
})

Фактически встроенный поиск можно полностью заменить собственной системой фильтрации.


Поиск по нескольким полям

Каждая опция может содержать дополнительные данные.

new SlimSelect({
  select: '#users',
  data: [
    {
      text: 'Александр',
      value: '1',
      data: {
        email: 'alex@example.com',
        city: 'Москва'
      }
    },
    {
      text: 'Мария',
      value: '2',
      data: {
        email: 'maria@example.com',
        city: 'Казань'
      }
    }
  ],

  events: {
    search: (search, currentData) => {
      const query = search.toLowerCase()

      return currentData.filter(option => {
        return (
          option.text.toLowerCase().includes(query) ||
          option.data.email.toLowerCase().includes(query) ||
          option.data.city.toLowerCase().includes(query)
        )
      })
    }
  }
})

Теперь поиск работает одновременно:

  • по имени;
  • email;
  • городу.

Реализация нечёткого поиска

Стандартный includes() ищет только точные подстроки. Для более гибкой логики применяется нечёткое сравнение.

Пример поиска по символам:

function fuzzySearch(text, query) {
  text = text.toLowerCase()
  query = query.toLowerCase()

  let index = 0

  for (let char of text) {
    if (char === query[index]) {
      index++
    }

    if (index === query.length) {
      return true
    }
  }

  return false
}

new SlimSelect({
  select: '#users',

  events: {
    search: (search, currentData) => {
      return currentData.filter(option => {
        return fuzzySearch(option.text, search)
      })
    }
  }
})

Результаты:

Запрос Совпадение
мкс Максим
алс Александр

Подобная схема часто используется:

  • в CRM;
  • административных панелях;
  • системах с большим количеством записей.

Использование регулярных выражений

Поиск можно перевести на RegExp.

new SlimSelect({
  select: '#users',

  events: {
    search: (search, currentData) => {
      const regex = new RegExp(search, 'i')

      return currentData.filter(option => {
        return regex.test(option.text)
      })
    }
  }
})

Поддерживаются сложные шаблоны:

Запрос Результат
Все имена на А
ин$ Окончание “ин”
[0-9] Элементы с цифрами

Защита от ошибок RegExp

Неправильное регулярное выражение вызывает исключение.

Опасный ввод:

(

Без обработки:

new RegExp('(')

возникает ошибка.

Безопасная реализация:

new SlimSelect({
  select: '#users',

  events: {
    search: (search, currentData) => {

      try {
        const regex = new RegExp(search, 'i')

        return currentData.filter(option => {
          return regex.test(option.text)
        })

      } catch {
        return []
      }
    }
  }
})

Нормализация строки поиска

Пользователи часто вводят:

  • лишние пробелы;
  • разные раскладки;
  • символы в верхнем регистре;
  • диакритические знаки.

Перед фильтрацией данные обычно нормализуются.

function normalize(text) {
  return text
    .trim()
    .toLowerCase()
}

new SlimSelect({
  select: '#users',

  events: {
    search: (search, currentData) => {

      const query = normalize(search)

      return currentData.filter(option => {
        return normalize(option.text)
          .includes(query)
      })
    }
  }
})

Игнорирование диакритики

Для международных проектов критически важна поддержка Unicode.

Например:

Ввод Должен найти
resume résumé
cafe café

Решение:

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

new SlimSelect({
  select: '#users',

  events: {
    search: (search, currentData) => {

      const query = removeDiacritics(search.toLowerCase())

      return currentData.filter(option => {

        const normalized = removeDiacritics(
          option.text.toLowerCase()
        )

        return normalized.includes(query)
      })
    }
  }
})

Поиск по началу строки

Иногда необходимо искать только префиксы.

new SlimSelect({
  select: '#users',

  events: {
    search: (search, currentData) => {

      const query = search.toLowerCase()

      return currentData.filter(option => {
        return option.text
          .toLowerCase()
          .startsWith(query)
      })
    }
  }
})

Такой алгоритм особенно полезен:

  • для автодополнения;
  • телефонных справочников;
  • поиска кодов;
  • выбора стран.

Поиск по словам

Частая задача — поиск по отдельным токенам.

new SlimSelect({
  select: '#users',

  events: {
    search: (search, currentData) => {

      const words = search
        .toLowerCase()
        .split(' ')

      return currentData.filter(option => {

        const text = option.text.toLowerCase()

        return words.every(word => {
          return text.includes(word)
        })
      })
    }
  }
})

Запрос:

иван менеджер

найдёт:

Иван Петров — менеджер

Взвешенный поиск

Иногда разные поля должны иметь разный приоритет.

new SlimSelect({
  select: '#users',

  data: [
    {
      text: 'Иван',
      value: '1',
      data: {
        role: 'Менеджер'
      }
    }
  ],

  events: {
    search: (search, currentData) => {

      const query = search.toLowerCase()

      return currentData
        .map(option => {

          let score = 0

          if (option.text.toLowerCase().includes(query)) {
            score += 10
          }

          if (option.data.role.toLowerCase().includes(query)) {
            score += 5
          }

          return {
            option,
            score
          }
        })
        .filter(item => item.score > 0)
        .sort((a, b) => b.score - a.score)
        .map(item => item.option)
    }
  }
})

Преимущества подхода:

  • лучшие совпадения отображаются выше;
  • повышается релевантность;
  • интерфейс ощущается «умнее».

Асинхронный поиск

Slim Select поддерживает возврат Promise.

Это позволяет:

  • отправлять запросы на сервер;
  • подключать API;
  • загружать данные лениво.

Пример:

new SlimSelect({
  select: '#users',

  events: {
    search: async (search) => {

      const response = await fetch(
        `/api/users?q=${encodeURIComponent(search)}`
      )

      const users = await response.json()

      return users.map(user => ({
        text: user.name,
        value: user.id
      }))
    }
  }
})

Debounce при серверном поиске

Без debounce каждый символ вызывает запрос.

Проблемы:

  • перегрузка сервера;
  • лишний трафик;
  • скачущий интерфейс.

Простейший debounce:

function debounce(callback, delay) {

  let timer

  return (...args) => {

    clearTimeout(timer)

    timer = setTimeout(() => {
      callback(...args)
    }, delay)
  }
}

Использование:

const searchUsers = debounce(async (search, resolve) => {

  const response = await fetch(`/api/users?q=${search}`)

  const data = await response.json()

  resolve(data)

}, 300)

new SlimSelect({
  select: '#users',

  events: {
    search: (search) => {

      return new Promise(resolve => {
        searchUsers(search, resolve)
      })
    }
  }
})

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

При повторяющихся запросах полезно сохранять результаты.

const cache = {}

new SlimSelect({
  select: '#users',

  events: {
    search: async (search) => {

      if (cache[search]) {
        return cache[search]
      }

      const response = await fetch(`/api/users?q=${search}`)

      const data = await response.json()

      cache[search] = data

      return data
    }
  }
})

Преимущества:

  • меньше запросов;
  • мгновенный повторный поиск;
  • снижение нагрузки на backend.

Минимальная длина поискового запроса

Поиск с одного символа часто создаёт слишком много результатов.

Ограничение:

new SlimSelect({
  select: '#users',

  events: {
    search: (search, currentData) => {

      if (search.length < 3) {
        return []
      }

      return currentData.filter(option => {
        return option.text
          .toLowerCase()
          .includes(search.toLowerCase())
      })
    }
  }
})

Кастомные сообщения поиска

Настройка текста интерфейса:

new SlimSelect({
  select: '#users',

  settings: {
    searchText: 'Поиск пользователей',
    searchPlaceholder: 'Введите имя',
    searchingText: 'Идёт поиск...',
    searchFocus: true
  }
})

Описание параметров:

Параметр Назначение
searchText Текст поискового блока
searchPlaceholder Placeholder поля
searchingText Сообщение загрузки
searchFocus Автофокус

Полное отключение локальной фильтрации

Иногда поиск должен выполняться только сервером.

new SlimSelect({
  select: '#users',

  events: {
    search: async (search) => {

      const response = await fetch(`/search?q=${search}`)

      const data = await response.json()

      return data
    }
  }
})

В этом режиме:

  • Slim Select не фильтрует локальный массив;
  • управление полностью переходит backend-сервису;
  • можно работать с миллионами записей.

Интеграция Fuse.js

Для профессионального нечёткого поиска часто подключается библиотека Fuse.js.

Установка:

<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.js"></script>

Интеграция:

const data = [
  { text: 'Александр', value: '1' },
  { text: 'Максим', value: '2' },
  { text: 'Екатерина', value: '3' }
]

const fuse = new Fuse(data, {
  keys: ['text'],
  threshold: 0.3
})

new SlimSelect({
  select: '#users',

  events: {
    search: (search) => {

      return fuse.search(search)
        .map(result => result.item)
    }
  }
})

Fuse.js предоставляет:

  • интеллектуальное ранжирование;
  • tolerant search;
  • fuzzy matching;
  • анализ ошибок ввода;
  • поиск с весами.