среда, 12 декабря 2012 г.

ECL+Quicklisp=костыли


Известный факт: ECL поставляет свой, особенный ASDF. Другой факт: у Quicklisp'а своя версия ASDF, которую он загружает на старте.

Как же быть?

Можно загружать из сети системы с помощью другого лиспа, например SBCL, а в ECL загружать их уже старым добрым asdf:load-system.

Для этого можно воспользоваться простым алиасом.
alias ecl-ql='CL_SOURCE_REGISTRY='\''(:source-registry (:tree (:home "quicklisp/dists/quicklisp/software/")) (:tree (:home "quicklisp/local-projects/")) :inherit-configuration)'\'' ecl -eval "(require :asdf)"'
Теперь можно запускать ecl-ql и компилировать.

воскресенье, 4 ноября 2012 г.

Чистота — залог здоровья

Одна из задач, присутствующих в моём pet-project — очистка фрагментов HTML поступающих из внешних источников (RSS, Atom) от мусора. Сначала я использовал замечательную библиотеку html5lib и всё меня в ней устраивало. HTML очищался, пользователи радовались.

Но однажды процесс оптимизации проекта привёл меня к тому, что очистка HTML стала самой трудоёмкой задачей и потребляющей не менее 50% времени.

Взор мой пал на замечательную библиотеку cl-sanitize. Но, поскольку, мне требовалась возможность использования её из программы на python, я решил написать свою на C.

Таким образом появилась на свет libsanitize. Сначала это был перевод с лиспа на на си один-к-одному. Но потребовалась функциональность, которой оригинальная библиотека не обладала.

Вместо указания допустимого протокола для атрибутов со ссылками (href, src и т. п.) добавил возможность задавать регулярное выражение. Логика перешла из кода в данные. Вот пример, задающий правила для ссылок.

<a href="^(ftp:|http:|https:|mailto:)" href.not="^[^/]+[[:space:]]*:"/>

Ещё одна функция — переименование тегов. Используется для замены всяческих aside, header, nav на старый добрый div. Это позволяет писать меньше CSS.

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

В качестве внешнего формата для хранения настроек очень органично вписался XML. Пример такого файла: relaxed.xml. Выглядит почти как простое перечисление тегов.

Замеры показали, что запросы стали работать на порядок быстрее. Время сократилось с 200 мс до 12-15 мс. Теперь самое слабое звено — база данных.

воскресенье, 21 октября 2012 г.

Бесшовная интеграция PostgreSQL и Sphinx Search

Одной из самых частых задач при разработке сайтов является так называемый полнотекстовый поиск. Популярные РСУБД такие как MySQL и PostgreSQL уже содержат встроенные механизмы. К сожалению, то как они реализованы оставляет желать лучшего. Простая разбивка на слова не подходит к языкам с флексиями, таким как русский.
Тут нам на выручку приходят специализированные инструменты хорошо разбирающиеся в разных языках. Хорошо зарекомендовал себя поисковый сервер Sphinx, поддерживающий большое количество языков. Есть в Sphinx даже такая интересная возможность как фонетический поиск. Это значит, что можно искать такие слова как: «моск», «афтар», «превед» и находить «мозг», «автор», «привет».
К сожалению, за такую замечательную возможность искать (и находить) произвольный текст приходится расплачиваться гибкостью.
Традиционная схема работы с поисковым сервером такая:
  1. программа-индексатор обращается напрямую к базе данных, сканнирует нужные таблицы и строит индексные файлы;
  2. запускается поисковая служба (демон);
  3. приложение обращается к службе с поисковым запросом и получает результат.

Если же данные обновляются (а скорее всего так и есть), то индексы нуждаются в обновлении. И тут схема далека от идеала:
  1. программа-индексатор строит новый индекс в промежуточных файлах;
  2. посылается сигнал поисковой службе, и она переключается на новые индексы;
  3. старые индексы удаляются.
Индексатор нужно запускать самостоятельно (cron) и это приводит к тому, что данные попадают в поисковые индексы не сразу. Во время обновления индекс требует в два раза большего места на диске. Есть возможность делать инкрементальные обновления, но они требуют дополнительной процедуры слияния индексов. Также прийдётся как-то определять какие данные уже проиндексированы, а какие ещё нет. Тут можно придумать много всяких способов: запоминать последний добавленный id, булево поле и т. п. Так или иначе, это потребует написания какого-то кода. Если ещё вспомнить, что данные могут не только добавляться, но и обновляться и даже удаляться, то рисуется безрадостная картина.
К счастью, выход есть! Sphinx предлагает такую замечательную возможность как real-time-индексы. Основное отличие RT-индексов в том, что они не требуют индексатора и позволяют добавлять, обновлять и удалять данные обращаясь непосредственно к поисковой службе.
В традиционной схеме индексатор самостоятельно обращается к базе данных. Здесь же приложение вынуждено обновлять данные. Нельзя ли совместить эти две схемы? Нельзя ли переложить эту работу на базу данных?
Оказывается можно. Современные РСУБД позволяют отслеживать изменения в данных и реагировать на них соответственно (так называемые триггеры). То есть заботу об обновлении поискового индекса можно переложить на триггеры.
К несчастью триггеры не могут устанавливать сетевых соединений и всю чёрную работу прийдётся делать в пользовательских функциях (UDF — user-defined function). В случае PostgreSQL это потребует написания разделяемой библиотеки. Можно было бы написать например на Python, но мы же хотим максимальной производительности!

Перейдём от слов к делу!

Расширение pg-sphinx, написанное для PostgreSQL позволяет обновлять индексы непосредственно из триггеров, хранимых процедур и любых других мест, где возможен вызов функции. Например, нижеприведённый SQL-код обновляет (или вставляет, если её ещё не было) запись №3 в индексе blog_posts.
SELECT sphinx_replace('blog_posts', 3, ARRAY[
  'title', 'Отчёт',
  'content', 'Вот фоточки с последней поездки...'
  ]);
Удаление ещё проще:
SELECT sphinx_delete('blog_posts', 3);
Раз уж у нас есть расширение, то почему бы не пойти дальше? Можно кроме обновления данных также делать и поисковые запросы прямо из базы данных.
SELECT * FROM sphinx_search(
  'blog_posts',      /* индекс */
  'рецепты майонез', /* запрос */
  'author_id = 361', /* дополнительное условие */
  '@relevance DESC', /* порядок сортировки */
  0,                 /* смещение */
  3,                 /* лимит */
  NULL);             /* опции */
Подобный запрос выдаст что-то вроде такого:
idweight
1441661
1351644
1301640
Пока ничего особенного. Такой же результат мы могли получить в приложении обратившись непосредственно к поисковой службе. Однако такая функция в SQL-сервере открывает широкие возможности, ведь её можно использовать и в более сложных запросах. Первое и самое простое, что приходит на ум — использовать её в как источник для INNER JOIN.
SELECT posts.*, ss.weight
FROM posts
INNER JOIN sphinx_search(
    'blog_posts',      /* индекс */
    'рецепты майонез', /* запрос */
    'author_id = 361', /* дополнительное условие */
    '@relevance DESC', /* порядок сортировки */
    0,                 /* смещение */
    3,                 /* лимит */
    NULL)              /* опции */
  AS ss ON ss.id = posts.id;
Такой запрос не просто ищет идентификаторы, но и выбирает сами записи.
idtitlecontentweight
144Бессмысленно и беспощадноДля чего фаршировать котлеты макаронами...1661
135В копилку идейНадеюсь подборочка рецептиков освежит вашу фантазию...1644
130Когда душа требует праздникаХочется карнавала, бразильского...1640
Его уже можно использовать в непосредственно в приложении. Результат такого запроса можно свободно передать в ORM.

Итого

Что получилось?
  1. Избавились от запуска индексатора, нет пиков нагрузки на базу данных.
  2. Данные в поисковом индексе всегда актуальны.
  3. Приложение не заботится об обновлении индексов, этим занимается сама РСУБД.
  4. Поиск выполняется на стороне сервера баз данных и приложению не нужно поддерживать соединение с поисковым сервером.
  5. Поисковые запросы можно произвольным образом смешивать между собой и с запросами к самим данным.

Чего не хватает? Что не реализовано?
  1. Сделано неявное предположение, что все данные хранятся в кодировке UTF-8 и поддержка других кодировок не сделана намеренно.
  2. Подсветка найденых слов не реализована.
  3. Перенастройка подключения к поисковому серверу требует перекомпиляции.
  4. Не реализованы такие функции как транзакции (сейчас AUTOCOMMIT по-умолчанию) и пользовательские функции (нужно разворачивать выражения явно).
  5. ...
Тем не менее, это расширение можно использовать уже сейчас в большинстве простых приложений.

Ссылки

  1. Sphinx
  2. PostgreSQL
  3. pg-sphinx

понедельник, 18 июня 2012 г.

Как компилятор java типы проверял

Жил-был компилятор по имени Javac. Он был хорошо воспитан и отличался изысканным вкусом (строгим статическим). Но однажды попалась ему такая программа:
public class Foo {
    public static int foo() {
        return null;
    }

    public static void main(String[] args) {
        System.out.println(foo());
    }
}
Посмотрел он на неё и резонно заявил:
Foo.java:3: incompatible types
found   :
required: int
        return null;
               ^
1 error
Не угодила она компилятору, оказалась не в его вкусе. Но программа очень хотела понравится компилятору и решила попытать счастья снова:
public class Foo {
    public static int foo() {
        return true ? null : 0;
    }

    public static void main(String[] args) {
        System.out.println(foo());
    }
}
Компилятор ничего не заподозрил и всё получилось. У них появился замечательный малыш Foo.class.

Но счастье их длилось не долго. Коварный NullPointerException прервал их идиллию и выстрелил в колени обоим.
Exception in thread "main" java.lang.NullPointerException
    at Foo.foo(Foo.java:3)
    at Foo.main(Foo.java:7)
По мотивам 2ch.so/pr/