Алгоритм поиска

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

После инициализации компонента библиотека:

  1. Считывает все доступные элементы <select>
  2. Преобразует их во внутреннюю структуру данных
  3. Индексирует текстовые значения
  4. Выполняет последовательное сравнение введённой строки с содержимым элементов
  5. Отображает только совпадающие результаты

Поиск активируется автоматически при включённом параметре showSearch.

<select id="users">
  <option>Александр</option>
  <option>Андрей</option>
  <option>Мария</option>
  <option>Екатерина</option>
</select>
new SlimSelect({
  select: '#users',
  settings: {
    showSearch: true
  }
})

После ввода текста библиотека скрывает элементы, не удовлетворяющие условию поиска.


Базовый алгоритм фильтрации

По умолчанию Slim Select использует простой алгоритм поиска по подстроке. Проверка выполняется методом, аналогичным includes().

Условно внутреннюю логику можно представить следующим образом:

function search(query, options) {
  return options.filter(option => {
    return option.text
      .toLowerCase()
      .includes(query.toLowerCase())
  })
}

Основные особенности алгоритма:

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

Пример:

Поисковый запрос Результат
ан Андрей, Александр
ма Мария
ка Екатерина

Обработка регистра символов

Для корректного сравнения Slim Select приводит обе строки к нижнему регистру.

option.text.toLowerCase()
query.toLowerCase()

Это позволяет находить совпадения независимо от написания.

<select id="cities">
  <option>Москва</option>
  <option>Алматы</option>
  <option>Berlin</option>
</select>

Поиск:

мос
МОС
Мос

во всех случаях вернёт:

Москва

Поиск по частичному совпадению

Алгоритм не требует полного совпадения строки.

Если имеется список:

<select id="frameworks">
  <option>React</option>
  <option>Angular</option>
  <option>Vue</option>
  <option>Svelte</option>
</select>

Запрос:

ang

вернёт:

Angular

А запрос:

act

найдёт:

React

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


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

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

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

new SlimSelect({
  select: '#frameworks',
  events: {
    search: (search, currentData) => {
      return currentData.filter(item => {
        return item.text
          .toLowerCase()
          .startsWith(search.toLowerCase())
      })
    }
  }
})

Теперь:

Запрос Результат
re React
ac Нет результатов
ang Angular

Пользовательский алгоритм поиска

Slim Select предоставляет событие search, позволяющее полностью заменить встроенный механизм.

Структура обработчика:

search: (searchValue, currentData) => {
  return filteredData
}

Параметры:

Параметр Описание
searchValue введённая строка
currentData массив всех доступных элементов

Каждый элемент массива содержит объект:

{
  text: 'React',
  value: 'react'
}

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

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

Пример структуры:

[
  {
    text: 'Иван',
    value: '1',
    email: 'ivan@test.com'
  },
  {
    text: 'Мария',
    value: '2',
    email: 'maria@test.com'
  }
]

Кастомный поиск:

new SlimSelect({
  select: '#users',
  events: {
    search: (search, data) => {
      return data.filter(item => {
        const query = search.toLowerCase()

        return (
          item.text.toLowerCase().includes(query) ||
          item.email.toLowerCase().includes(query)
        )
      })
    }
  }
})

Теперь поиск будет учитывать:

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

Алгоритм поиска без учёта пробелов

При работе с телефонными номерами, артикулами или кодами часто необходимо игнорировать пробелы.

Пример:

new SlimSelect({
  select: '#phones',
  events: {
    search: (search, data) => {
      const normalizedSearch =
        search.replace(/\s/g, '')

      return data.filter(item => {
        const text = item.text
          .replace(/\s/g, '')
          .toLowerCase()

        return text.includes(
          normalizedSearch.toLowerCase()
        )
      })
    }
  }
})

Поиск:

777123

сможет найти:

777 123
777-123
777 123 456

Нормализация символов

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

Пример:

José
François
Müller

По умолчанию поиск может работать некорректно для:

jose
francois
muller

Решение через нормализацию Unicode:

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

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

new SlimSelect({
  select: '#people',
  events: {
    search: (search, data) => {
      return data.filter(item => {
        return normalize(item.text)
          .includes(normalize(search))
      })
    }
  }
})

Поиск по ключевым словам

Для сложной фильтрации можно хранить скрытые ключевые слова.

[
  {
    text: 'JavaScript',
    value: 'js',
    keywords: ['frontend', 'browser', 'web']
  }
]

Алгоритм:

new SlimSelect({
  select: '#skills',
  events: {
    search: (search, data) => {
      const query = search.toLowerCase()

      return data.filter(item => {
        const inText =
          item.text.toLowerCase().includes(query)

        const inKeywords =
          item.keywords.some(keyword => {
            return keyword
              .toLowerCase()
              .includes(query)
          })

        return inText || inKeywords
      })
    }
  }
})

Поиск:

browser

вернёт:

JavaScript

Алгоритм ранжирования результатов

Стандартная фильтрация не сортирует результаты по релевантности. Все совпадения выводятся в исходном порядке.

Для улучшения поиска можно реализовать собственную систему приоритетов.

Пример:

new SlimSelect({
  select: '#languages',
  events: {
    search: (search, data) => {
      const query = search.toLowerCase()

      return data
        .map(item => {
          let score = 0

          const text = item.text.toLowerCase()

          if (text === query) {
            score += 100
          }

          if (text.startsWith(query)) {
            score += 50
          }

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

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

При запросе:

rea

результаты будут отсортированы так:

  1. React
  2. Reanimated
  3. Create React App

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

Slim Select поддерживает асинхронную загрузку результатов.

Это особенно важно при:

  • работе с API;
  • больших объёмах данных;
  • серверной фильтрации;
  • поиске по базе данных.

Пример:

new SlimSelect({
  select: '#users',
  events: {
    search: async (search) => {

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

      const users = await response.json()

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

В таком режиме:

  1. пользователь вводит запрос;
  2. выполняется HTTP-запрос;
  3. сервер возвращает подходящие записи;
  4. Slim Select отображает новые результаты.

Debounce в алгоритме поиска

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

Для оптимизации применяется debounce.

Пример:

function debounce(fn, delay) {
  let timeout

  return function (...args) {
    clearTimeout(timeout)

    timeout = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

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

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

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

  return await response.json()

}, 300)

Debounce:

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

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

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

Пример:

search: async (search) => {

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

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

  return await response.json()
}

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

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

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

При повторяющихся запросах можно использовать локальный кэш.

Пример:

const cache = {}

new SlimSelect({
  select: '#products',
  events: {
    search: async (search) => {

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

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

      const data = await response.json()

      cache[search] = data

      return data
    }
  }
})

Кэширование:

  • ускоряет интерфейс;
  • снижает нагрузку на API;
  • уменьшает задержки;
  • предотвращает повторные запросы.

Алгоритм поиска в больших списках

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

Основные причины:

  • постоянная фильтрация массива;
  • частые перерисовки DOM;
  • большое количество сравнений строк.

Типичная оптимизация:

const normalizedData = data.map(item => ({
  ...item,
  normalized: item.text.toLowerCase()
}))

Далее поиск выполняется уже по подготовленным строкам:

normalizedData.filter(item => {
  return item.normalized.includes(query)
})

Такой подход:

  • уменьшает количество преобразований;
  • ускоряет поиск;
  • снижает нагрузку на процессор.

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

Для сложных сценариев можно применять RegExp.

Пример:

new SlimSelect({
  select: '#files',
  events: {
    search: (search, data) => {

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

      return data.filter(item => {
        return regex.test(item.text)
      })
    }
  }
})

Примеры запросов:

Запрос Совпадение
^test строки, начинающиеся с test
\.js$ JavaScript-файлы
[0-9] строки с цифрами

Нечёткий поиск

Для улучшенного UX иногда используется fuzzy search.

Пример простого алгоритма:

function fuzzyMatch(text, search) {
  let index = 0

  for (const char of text.toLowerCase()) {
    if (char === search[index]) {
      index++
    }
  }

  return index === search.length
}

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

new SlimSelect({
  select: '#frameworks',
  events: {
    search: (search, data) => {
      return data.filter(item => {
        return fuzzyMatch(item.text, search)
      })
    }
  }
})

Запрос:

rct

сможет найти:

React

Обработка пустого результата поиска

Если совпадений нет, Slim Select отображает специальное сообщение.

Настройка:

new SlimSelect({
  select: '#countries',
  settings: {
    searchText: 'Ничего не найдено'
  }
})

При отсутствии совпадений список будет содержать:

Ничего не найдено

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

Алгоритм поиска можно полностью отключить.

new SlimSelect({
  select: '#simple',
  settings: {
    showSearch: false
  }
})

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

  • поле поиска скрывается;
  • фильтрация не выполняется;
  • компонент работает как обычный select.

Архитектура поисковой системы Slim Select

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

Получение ввода

keypress → input

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

toLowerCase()
trim()
normalize()

Фильтрация данных

filter()
includes()
startsWith()
RegExp

Сортировка

sort()
score
ranking

Отрисовка результатов

DOM update
re-render

Каждый из этапов может быть переопределён через пользовательский алгоритм поиска.