Статический полнотекстовый поиск в Next.js с фильтрами WebAssembly, Rust и Xor

Статический полнотекстовый поиск в Next.js с фильтрами WebAssembly, Rust и Xor

9 апреля 2022 г.

TL;DR


  • 🦀 Для разработки WebAssembly на Rust имеется богатый набор инструментов. Это весело!

  • 🤝 WebAssembly и Next.js довольно хорошо работают вместе, но помните об известных проблемах.

  • 🧑‍🔬 Фильтры Xor — это структуры данных, которые обеспечивают высокую эффективность использования памяти и быстрый поиск существования значения.

  • 🧑‍🍳 Производительность WebAssembly и размер кода не гарантируются. Обязательно измеряйте и оценивайте.

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


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


После некоторых исследований я нашел поисковую систему под названием «tinysearch». Это статическая поисковая система, созданная с помощью [Rust] (https://www.rust-lang.org/) и [WebAssembly] (https://webassembly.org/) (Wasm). Автор Матиас Эндлер написал потрясающий пост в блоге о том, как появился tinysearch.


Мне понравилась идея создания минималистичной поисковой системы во время сборки и отправки ее в браузеры в оптимизированном низкоуровневом коде. Поэтому я решил использовать tinysearch в качестве схемы и написать свою собственную поисковую систему для интеграции с моим [статическим сайтом Next.js] (https://nextjs.org/).


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


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



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


  • Пользователи вводят что-либо в поле поиска.

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

  • Пользовательский интерфейс отображает ранжированный список результатов поиска.

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


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


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


  • 7 статей (еще больше🚀)

  • 13 925 слов

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

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


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


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


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


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



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


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


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


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


Вместо того, чтобы хранить фактические входные значения, такие как набор хэшей, фильтры xor сохраняют отпечатки пальцев (L-битная хэш-последовательность) входных значений особым образом. /cs166.1216/lectures/13/Slides13.pdf#page=49). При поиске того, существует ли значение в фильтре, он проверяет, присутствует ли отпечаток значения.


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


  • Фильтры Xor являются вероятностными, и есть шанс, что может произойти ложное срабатывание.

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

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


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


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



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


ржавчина


используйте std::collections::hash_map::DefaultHasher;


используйте xorf::{Filter, HashProxy, Xor8};


мод утилиты;


fn build_filter(title: String, body: String) -> HashProxy {


let title_tokens: HashSet = utils::tokenize(&title);


let body_tokens: HashSet = utils::tokenize(&body);


пусть токены: Vec = body_tokens.union(&title_tokens).cloned().collect();


HashProxy::from(&токены)


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


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



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


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


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


мое портфолио


├── next.config.js


├── страницы


├── скрипты


│ └── полнотекстовый поиск


├── компоненты


│ └── Search.tsx


└── васм


└── полнотекстовый поиск


Для поддержки WebAssembly я обновил конфигурацию Webpack, чтобы загружать модули WebAssembly как асинхронные модули. Чтобы заставить его работать для создания статического сайта, мне понадобился [обходной путь] (https://github.com/vercel/next.js/issues/25852) для создания модуля WebAssembly в каталоге .next/server, чтобы статические страницы могут успешно выполнять предварительный рендеринг при запуске скрипта next build.


следующий.config.js


```javascript


веб-пакет: функция (конфигурация, { isServer }) {


// это делает модули WebAssembly асинхронными модулями


config.experiments = { asyncWebAssembly: true }


// сгенерировать модуль wasm в ".next/server" для ssr и ssg


если (сервер) {


config.output.webassemblyModuleFilename =


'./../static/wasm/[modulehash].wasm'


} еще {


config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm'


вернуть конфигурацию


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


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


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


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


компоненты/Search.tsx


```машинопись


импортировать React, {useState, useCallback, ChangeEvent, useEffect} из 'реагировать'


импортировать динамический из «следующего/динамического»


введите Заголовок = строка;


введите URL = строка;


введите SearchResult = [Title, Url][];


константный поиск = динамический ({


загрузчик: асинхронный () => {


const wasm = await import('../../wasm/fulltext-search/pkg')


возврат () => {


const [термин, setTerm] = useState('')


const [результаты, setResults] = useState([])


const onChange = useCallback((e: ChangeEvent) => {


setTerm (e.target.value)


использоватьЭффект(() => {


const pending = wasm.search(термин, 5)


setResults (в ожидании)


}, [срок])


возврат (


<дел>


<ввод


значение = {термин}


при изменении={при изменении}


placeholder="🔭 поиск..."


{results.map(([название, URL]) => (


{title}



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


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


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


```javascript


Неглубокие байты │ Неглубокие % │ Элемент


───────────────┼───────────┼───────────────────


117314 ┊ 100,00% ┊ Σ [всего 1670 строк]


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


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


Груз.томл


``ямл


[профиль.релиз]


  • уровень выбора = 's'

  • лто = правда

``` ударить


Неглубокие байты │ Неглубокие % │ Элемент


───────────────┼───────────┼───────────────────


111319 ┊ 100,00% ┊ Σ [всего 1604 строки]


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


wasm/полнотекстовый поиск/src/lib.rs


ржавчина


  • [глобальный_аллокатор]


  • статический ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

``` ударить


Неглубокие байты │ Неглубокие % │ Элемент


───────────────┼───────────┼───────────────────


100483 ┊ 100,00% ┊ Σ [всего 1625 строк]


Затем я попробовал инструмент wasm-opt в Binaryen.


```javascript


wasm-opt -Oz -o wasm/полнотекстовый-поиск/pkg/полнотекстовый_поиск_core_bg.wasm wasm/полнотекстовый-поиск/pkg/полнотекстовый_поиск_core_bg.wasm


``` Баш


Неглубокие байты │ Неглубокие % │ Элемент


───────────────┼───────────┼───────────────────


100390 ┊ 100,00% ┊ Σ [всего 1625 строк]


Это на 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


  • [Алголия] (https://www.algolia.com/)

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


  • [маленький поиск] (https://github.com/tinysearch/tinysearch)

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


  • [Meilisearch] (https://www.meilisearch.com/)

  • [Typesense] (https://typesense.org/)

Встроенные в браузер поисковые системы


  • [FlexSearch] (https://github.com/nextapps-de/flexsearch)

  • [Мини-поиск] (https://github.com/lucaong/minisearch)

  • [Elasticlunr.js] (http://elasticlunr.com/)

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


  • Статья: [Крошечная, статическая, полнотекстовая поисковая система с использованием Rust и WebAssembly — Матиас Эндлер] (https://endler.dev/2019/tinysearch)







  • Веб-сайт: [Портфолио До-Чи Лиу] (https://dawchihliou.github.io/)



  • Веб-сайт: [Алголия] (https://www.algolia.com/)


  • Веб-сайт: Rust





  • Лекция: [Приблизительные запросы на членство — Стэнфорд] (https://web.stanford.edu/class/archive/cs/cs166/cs166.1216/lectures/13/Slides13.pdf#page=49)


  • GitHub: [пример Next.js и WebAssembly] (https://github.com/vercel/next.js/tree/canary/examples/with-webassembly)



  • GitHub: [Webpack 5 прерывает динамический импорт wasm для SSR] (https://github.com/vercel/next.js/issues/25852)










  • GitHub: [Bincode] (https://github.com/bincode-org/bincode)



  • GitHub: [В любом случае] (https://github.com/dtolnay/anyhow)




Эта статья также была размещена на веб-сайте Доу-Чи.



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE