Совсем недавно мне захотелось поискать какую-то информацию в amber-dev мейлинг листе. Оказывается, что никакого встроенного поиска тут нет. Нужно либо пользоваться гуглом и использовать site: оператор, либо использовать поиск почтового клиента. В целом, оба варианта — рабочие, но мне захотелось сделать еще одну опцию, попутно немного поиграв с WebAssembly.
TL;DR Есть демо сайт, на котором можно поиграть с локальной базой мейлинг листов — https://hixon10.github.io/openjdk-mailing-lists-search/. А исходники можно посмотреть тут — https://github.com/Hixon10/openjdk-mailing-lists-search.
Сразу нужно сказать, что идея создания serverless поиска — не нова. За последние несколько лет было несколько статей, как кто-то делал поиск для своего статического сайта, используя одну из баз данных, скомпилированных под wasm. Например, Gunnar Morling писал про это в своем блоге — https://www.morling.dev/blog/how-i-built-a-serverless-search-for-my-blog/.
Плюсы и минусы такого подхода, наверное, и так понятны. Минус — мы должны выкачивать на клиент всю БД, даже если хотим прочитать из базы всего одну запись. Наш пользователь вынужден ждать, а мы будем платить за трафик нашему облачному провайдеру. Однако есть и плюсы — мы можем позволить пользователю выполнять любые, даже самые тяжелые запросы к БД, не боясь положить прод.
На сегодняшний день существует несколько Баз Данных, которые уже кем-то собраны под wasm:
Возможно, уже есть и какие-то другие БД, собранные под wasm. Скорей всего, в следующие год-два многие БД, которые написаны на C/C++/Rust/Go, будут собраны под wasm. С одной стороны, у этого не всегда есть практическая ценность (хотя, очень круто делать playground для вашей БД в виде полностью локального решения, которое работает в браузере клиента). С другой же стороны — сборка под wasm будет всё проще и проще со временем, когда тулчейн станет еще более зрелым.
По сути, мой демо проект состоит из 3 частей — индексера, поискового фронтенда и мониторинга. Наверное, из названий понятно, что каждая часть делает, но давайте проговорим это явно.
Задача индексер периодически проверять OpenJDK мейлинг листы, на которые мы подписаны, и добавлять новые емейлы в индекс (нашу базу данных). Написана эта штука на Java, но тут это совсем не важно. Сначала я хотел собрать индексер в GraalVM Native Image, чтобы сократить время отработки индексатора. Однако потом я увидел, что больше вего времени мы тратим на выкачку старого индекса и сохранение нового индекса, поэтому нейтив имидж тут не поможет.
У нас нет сервера, поэтому нам нужен какой-то способ запускать переодически индексирование вне нашей инфраструктуры. Для этого я воспользовался GitHub Action. Этот экшен может запуститься несколько раз в час, и обновлять индекс. Важно, что гитхаб не гарантирует нам, что экшен запустится мгновенно, по значению из настроек. В нагруженные часы индексер может и не отработать вообще, поэтому эту штуку не стоит использовать для важных задач.
Сам индекс хранится, как часть проекта, в git. Экшен-индексатор клонирует проект, включая индекс, затем — запускает индексатор, который работает с индексом, как с локальной sqlite базой данных. Финальный этап — попытка закоммитить новую версию индекса, если она изменилась.
Я решил взять SQLite в качестве БД. Поэтому я взял проект sql.js. У ребят уже была реализована демка с веб-интерфейсом для доступа к данным, её я и взял в качестве основы фронтенда. Мне пришлось только немного поменять код для загрузки БД из гитхаба.
Самое страшное, что может случится — молчаливое прекращение работы индексатора. С одной стороны, нам будет казаться, что всё работает нормально, и мы даже сможем открыть демо сайт. Однако в индексе мы не сможем найти новые письма, и пока кто-то не пожалуется на это, мы и не узнаем.
Я решил реализовать мониторинг довольно примитивно. Давайте, каждый раз, когда мы добавляем новые емейлы в БД, мы будем обновлять один служебный файл, который содержит текущий размер индекса и текущую дату. Кроме того, создадим Cloudflare Worker, который будет переодически скачивать этот файл, считать, сколько прошло времени после последнего обновления индекса, и, если прошло больше N дней, слать емейл админу. Такое решение позволяет полностью не зависить от работы GitHub Actions, иначе я бы реализовал мониторинг прямо в экшенах.
Гитхаб позволяет, кажется, хранить в репке бинарные файлы до 100MB. Этого, на самом деле, хватит всем, при верном использовании проекта. Но, так как мы хотим хранить индекс в гитхабе, нас этот лимит не устроит. GitHub рекомендует для больших файлов использовать https://git-lfs.github.com/. Это решение хорошо всем, за одним исключением — GitHub Pages не позволяют вставить прямую ссылку на файлы, которые загружены на Git LFS, а нам это необходимо для того чтобы использовать нашу БД локально.
Я нашел два решения этой проблемы. Первое — хостить индекс в каком-то другом месте, вне git. Решение хорошое, но не подходит под наши требования.
Второе решение довольно костыльное. Давайте разобьем наш индекс на части (до 100MB), и будем хранить их. Из-за этого нам придется сделать ряд вещей, но в конечном итоге всё будет работать нормально.
Итак, несколько изменений по коду. Во-первых, мы будем вынуждены выкачавать с клиента не один blob файл, а сразу N частей:
Во-вторых, нам придется разбивать и собирать индекс из частей до/после работы индексатора:
Демо проект сделан, и он как-то работает, но это не важно. Важно, что, wasm базы данных на клиент, скорей всего, будут развиваться и дальше. Возможно, не в таком варианте как у меня в демке. Но, например, для edge computing это решение может быть вполне жизнеспособным, особенно если представить, что у каждого пользователя будет своя мини-БД, с небольшим количеством данных.
TL;DR Есть демо сайт, на котором можно поиграть с локальной базой мейлинг листов — https://hixon10.github.io/openjdk-mailing-lists-search/. А исходники можно посмотреть тут — https://github.com/Hixon10/openjdk-mailing-lists-search.
Про идею проекта
Сразу нужно сказать, что идея создания serverless поиска — не нова. За последние несколько лет было несколько статей, как кто-то делал поиск для своего статического сайта, используя одну из баз данных, скомпилированных под wasm. Например, Gunnar Morling писал про это в своем блоге — https://www.morling.dev/blog/how-i-built-a-serverless-search-for-my-blog/.
Плюсы и минусы такого подхода, наверное, и так понятны. Минус — мы должны выкачивать на клиент всю БД, даже если хотим прочитать из базы всего одну запись. Наш пользователь вынужден ждать, а мы будем платить за трафик нашему облачному провайдеру. Однако есть и плюсы — мы можем позволить пользователю выполнять любые, даже самые тяжелые запросы к БД, не боясь положить прод.
Базы данных и WebAssembly
На сегодняшний день существует несколько Баз Данных, которые уже кем-то собраны под wasm:
- https://github.com/sql-js/sql.js/ — скомпилированная SQLite.
- https://github.com/duckdb/duckdb-wasm — скомпилированный DuckDB.
- https://github.com/snaplet/postgres-wasm — очень хитро собранный PostgreSQL.
Возможно, уже есть и какие-то другие БД, собранные под wasm. Скорей всего, в следующие год-два многие БД, которые написаны на C/C++/Rust/Go, будут собраны под wasm. С одной стороны, у этого не всегда есть практическая ценность (хотя, очень круто делать playground для вашей БД в виде полностью локального решения, которое работает в браузере клиента). С другой же стороны — сборка под wasm будет всё проще и проще со временем, когда тулчейн станет еще более зрелым.
Про требования к моему игрушечному проекту
- Отсутствие сервера. Если мы даже готовы использовать БД из браузера, то все остальные этапы мы должны мочь сделать без сервера и подобно.
- Поиск должен работать хотя бы в последнем Google Chrome. Так как мы не делаем промышленное решение, мы не переживаем про поддержку разных браузеров и версий. Задача со звездочкой — должно работать и в последнем Firefox, которым я сам и пользуюсь.
- К базе данных особо требований нет. Подойдет любая БД, которая уже собрана под wasm.
- Для хранения текущего индекса (БД) мы должны использовать что-то, что не заставит нас покупать аккаунт или сервер.
Детали реализации проекта
По сути, мой демо проект состоит из 3 частей — индексера, поискового фронтенда и мониторинга. Наверное, из названий понятно, что каждая часть делает, но давайте проговорим это явно.
Индексер
Задача индексер периодически проверять OpenJDK мейлинг листы, на которые мы подписаны, и добавлять новые емейлы в индекс (нашу базу данных). Написана эта штука на Java, но тут это совсем не важно. Сначала я хотел собрать индексер в GraalVM Native Image, чтобы сократить время отработки индексатора. Однако потом я увидел, что больше вего времени мы тратим на выкачку старого индекса и сохранение нового индекса, поэтому нейтив имидж тут не поможет.
У нас нет сервера, поэтому нам нужен какой-то способ запускать переодически индексирование вне нашей инфраструктуры. Для этого я воспользовался GitHub Action. Этот экшен может запуститься несколько раз в час, и обновлять индекс. Важно, что гитхаб не гарантирует нам, что экшен запустится мгновенно, по значению из настроек. В нагруженные часы индексер может и не отработать вообще, поэтому эту штуку не стоит использовать для важных задач.
Сам индекс хранится, как часть проекта, в git. Экшен-индексатор клонирует проект, включая индекс, затем — запускает индексатор, который работает с индексом, как с локальной sqlite базой данных. Финальный этап — попытка закоммитить новую версию индекса, если она изменилась.
Поисковый фронтенд
Я решил взять SQLite в качестве БД. Поэтому я взял проект sql.js. У ребят уже была реализована демка с веб-интерфейсом для доступа к данным, её я и взял в качестве основы фронтенда. Мне пришлось только немного поменять код для загрузки БД из гитхаба.
Про мониторинг решения
Самое страшное, что может случится — молчаливое прекращение работы индексатора. С одной стороны, нам будет казаться, что всё работает нормально, и мы даже сможем открыть демо сайт. Однако в индексе мы не сможем найти новые письма, и пока кто-то не пожалуется на это, мы и не узнаем.
Я решил реализовать мониторинг довольно примитивно. Давайте, каждый раз, когда мы добавляем новые емейлы в БД, мы будем обновлять один служебный файл, который содержит текущий размер индекса и текущую дату. Кроме того, создадим Cloudflare Worker, который будет переодически скачивать этот файл, считать, сколько прошло времени после последнего обновления индекса, и, если прошло больше N дней, слать емейл админу. Такое решение позволяет полностью не зависить от работы GitHub Actions, иначе я бы реализовал мониторинг прямо в экшенах.
Лимит в GitHub на размер бинарного файла в репозитории
Гитхаб позволяет, кажется, хранить в репке бинарные файлы до 100MB. Этого, на самом деле, хватит всем, при верном использовании проекта. Но, так как мы хотим хранить индекс в гитхабе, нас этот лимит не устроит. GitHub рекомендует для больших файлов использовать https://git-lfs.github.com/. Это решение хорошо всем, за одним исключением — GitHub Pages не позволяют вставить прямую ссылку на файлы, которые загружены на Git LFS, а нам это необходимо для того чтобы использовать нашу БД локально.
Я нашел два решения этой проблемы. Первое — хостить индекс в каком-то другом месте, вне git. Решение хорошое, но не подходит под наши требования.
Второе решение довольно костыльное. Давайте разобьем наш индекс на части (до 100MB), и будем хранить их. Из-за этого нам придется сделать ряд вещей, но в конечном итоге всё будет работать нормально.
Итак, несколько изменений по коду. Во-первых, мы будем вынуждены выкачавать с клиента не один blob файл, а сразу N частей:
const databaseUrlPrefix = "https://hixon10.github.io/openjdk-mailing-lists-search/";
const databasePartNames = ["db-part-00", "db-part-01", "db-part-02", "db-part-03", "db-part-04", "db-part-05", "db-part-06", "db-part-07", "db-part-08", "db-part-09"];
for (const dbPartName of databasePartNames) {
const currentDbPartPromise = fetch(databaseUrlPrefix+dbPartName, {..})
// ...
}
Во-вторых, нам придется разбивать и собирать индекс из частей до/после работы индексатора:
- name: Build database from parts
run: cat ./docs/db-part* > ./docs/emailindex.db
- name: Build database parts from database
run: cd docs && split --number=l/10 --numeric-suffixes emailindex.db db-part-
Выводы
Демо проект сделан, и он как-то работает, но это не важно. Важно, что, wasm базы данных на клиент, скорей всего, будут развиваться и дальше. Возможно, не в таком варианте как у меня в демке. Но, например, для edge computing это решение может быть вполне жизнеспособным, особенно если представить, что у каждого пользователя будет своя мини-БД, с небольшим количеством данных.