Быстрый, легкий и приносит массу удовольствия при сборке!

TL;DR

  • 🦀 Имеется богатый инструментарий для разработки WebAssembly на Rust. Это весело!
  • 🤝 WebAssembly и Next.js довольно хорошо работают вместе, но помните об известных проблемах.
  • 🧑‍🔬 Фильтры Xor — это структуры данных, которые обеспечивают высокую эффективность использования памяти и быстрый поиск существования значения.
  • 🧑‍🍳 Производительность WebAssembly и размер кода не гарантируются. Обязательно измеряйте и оценивайте.

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

Вдохновленный tinysearch: полнотекстовая поисковая система WebAssembly

После некоторых исследований я нашел поисковую систему под названием tinysearch. Это статический поисковик, построенный на Rust и WebAssembly (Wasm). Автор Маттиас Эндлер написал потрясающий пост в блоге о том, как появился tinysearch.

Мне понравилась идея создания минималистичной поисковой системы во время сборки и отправки ее в браузеры в оптимизированном низкоуровневом коде. Поэтому я решил использовать tinysearch в качестве плана и написать свою собственную поисковую систему для интеграции с моим статическим сайтом Next.js.

Я настоятельно рекомендую прочитать кодовую базу tinysearch. Это очень хорошо написано. Реализация моей поисковой системы представляет собой ее упрощенную версию. Основная логика та же.

Как выглядит функция поиска?

Очень просто:

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

Вы можете опробовать функцию поиска на странице Статьи!

Немного статистики

На момент написания этой статьи существуют:

  • 7 статей (больше будет)
  • 13 925 слов
  • 682 КБ файлов данных (генерируются Contentlayer)

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

Как работает функция полнотекстового поиска WebAssembly?

Большинство современных браузеров теперь поддерживают WebAssembly. Они могут запускать собственный код WebAssembly и двоичный код вместе с JavaScript.

Концепция функции поиска проста. Он принимает строку запроса в качестве параметра. В функции мы токенизируем запрос в условия поиска. Затем мы присваиваем рейтинг каждой статье в зависимости от того, сколько поисковых запросов она содержит. Наконец, мы ранжируем статьи по релевантности. Чем выше оценка, тем она более актуальна.

Поток выглядит так:

Оценка статей — это то место, где приходится больше всего вычислений. Наивным подходом было бы преобразование каждой статьи в набор хэшей, содержащий все уникальные слова в статье. Мы можем рассчитать оценку, просто подсчитав количество поисковых запросов в хеш-наборе.

Вы можете себе представить, что это не самый эффективный подход к памяти с набором хэшей. Есть лучшие структуры данных, чтобы заменить его: фильтры xor.

Что такое фильтры Xor?

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

Вместо того, чтобы хранить фактические входные значения, такие как набор хэшей, фильтры xor сохраняют отпечатки пальцев (L-битная хешированная последовательность) входных значений особым образом. При поиске того, существует ли значение в фильтре, он проверяет, присутствует ли отпечаток значения.

Однако фильтры Xor имеют несколько недостатков:

  • Фильтры Xor являются вероятностными, и есть вероятность, что может произойти ложное срабатывание.
  • Фильтры XOR не могут оценить наличие частичных значений. Таким образом, в моем случае полнотекстовый поиск сможет искать только полные слова.

Как я создавал фильтры Xor с помощью Rust?

Поскольку у меня были данные статьи, сгенерированные Contentlayer, я создал фильтры xor, передав им данные до сборки WebAssembly. Затем я сериализовал фильтры xor и сохранил их в файле. Чтобы использовать фильтры в WebAssembly, все, что мне нужно было сделать, это прочитать из файла хранилища и десериализовать фильтры.

Алгоритм создания фильтра выглядит следующим образом:

Крейт xorf — хороший выбор для реализации фильтров xor, потому что он предлагает сериализацию/десериализацию и несколько функций, которые улучшают эффективность использования памяти и частоту ложных срабатываний. Он также предоставляет очень удобную структуру HashProxy для моего варианта использования для создания фильтра xor с фрагментом строк. Написанная на Rust конструкция примерно выглядит так:

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

Собираем все вместе

Интеграция WebAssembly в Next.js

Вот как я интегрировал скрипт генерации фильтра xor и WebAssembly в Next.js.

Структура файла выглядит следующим образом:

my-portfolio
├── next.config.js
├── pages
├── scripts
│   └── fulltext-search
├── components
│   └── Search.tsx
└── wasm
    └── fulltext-search

Для поддержки WebAssembly я обновил конфигурацию Webpack, чтобы загружать модули WebAssembly как асинхронные модули. Чтобы заставить его работать для создания статического сайта, мне понадобился обходной путь для создания модуля WebAssembly в .next/serverdirectory, чтобы статические страницы могли успешно выполнять предварительный рендеринг при запуске скрипта next build.

Код для next.config.js приведен ниже:

webpack: function (config, { isServer }) {
  // it makes a WebAssembly modules async modules
  config.experiments = { asyncWebAssembly: true }
  // generate wasm module in ".next/server" for ssr & ssg
  if (isServer) {
    config.output.webassemblyModuleFilename =
      './../static/wasm/[modulehash].wasm'
  } else {
    config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm'
  }
  return config
},

Вот и все, что нужно для интеграции✨

Использование WebAssembly в компоненте React

Чтобы собрать модуль WebAssembly из кода Rust, я использую wasm-pack.

Сгенерированный файл .wasm и связующий код для JavaScript находятся в wasm/fulltext-search/pkg. Все, что мне нужно было сделать, это использовать next/dynamic для их динамического импорта. Так:

Оптимизация размера кода WebAssembly

Без какой-либо оптимизации исходный размер файла Wasm был 114.56KB. Я использовал Twiggy, чтобы узнать размер кода.

Shallow Bytes  │ Shallow % │ Item
───────────────┼───────────┼─────────────────────
        117314 ┊   100.00% ┊ Σ [1670 Total Rows]

По сравнению с 628KB файлами необработанных данных он оказался намного меньше, чем я ожидал. Я был рад отправить его в производство, но мне было любопытно посмотреть, какой размер кода я могу сократить с помощью Рекомендации по оптимизации Rust And WebAssembly Working Group.

Первый эксперимент заключался в переключении LTO и опробовании разных opt-level. Следующая конфигурация дает наименьший размер кода .wasm:

# Cargo.toml
[profile.release]
+ opt-level = 's'
+ lto = true
Shallow Bytes  │ Shallow % │ Item
───────────────┼───────────┼─────────────────────
        111319 ┊   100.00% ┊ Σ [1604 Total Rows]

Затем я заменил распределитель по умолчанию на wee_alloc.

// wasm/fulltext-search/src/lib.rs
+ #[global_allocator]
+ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
Shallow Bytes  │ Shallow % │ Item
───────────────┼───────────┼─────────────────────
        100483 ┊   100.00% ┊ Σ [1625 Total Rows]

Потом попробовал инструмент wasm-opt в Бинарьене.

wasm-opt -Oz -o wasm/fulltext-search/pkg/fulltext_search_core_bg.wasm wasm/fulltext-search/pkg/fulltext_search_core_bg.wasm
Shallow Bytes  │ Shallow % │ Item
───────────────┼───────────┼─────────────────────
        100390 ┊   100.00% ┊ Σ [1625 Total Rows]

Это на 14.4% меньше исходного размера кода.

В конце концов, я смог запустить полнотекстовый поисковик в:

  • 98,04 КБ в чистом виде
  • 45,92 КБ в сжатом виде

Неплохо.

Это действительно быстро?

Я профилировал производительность с помощью web-sys и собрал некоторые данные:

  • количество поисков: 208
  • мин: 0,046 мс
  • макс: 0,814 мс
  • среднее значение: 0,0994 мс ✨
  • стандартное отклонение: 0,0678

В среднем полнотекстовый поиск занимает менее 0,1 мс.

Это довольно быстро.

Последние мысли

После некоторого эксперимента мне удалось создать быстрый и легкий полнотекстовый поиск с фильтрами WebAssembly, Rust и xor. Он хорошо интегрируется с Next.js и созданием статических сайтов.

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

Поисковые системы SaaS

Статические поисковые системы

Серверные поисковые системы

Поисковики в браузере

Рекомендации

Want to Connect?
This article was originally posted on Daw-Chih’s website.