среда, 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/

воскресенье, 9 октября 2011 г.

Впечатления от Clojure

Пришлось мне столкнуться по-службе с Clojure. Мои впечатления от знакомства:
  1. Первое, что бросается в глаза — это дурацкие [] и {} вместо расово верных (). Не скажу, что они упрощают чтение кода, ИМХО разница невелика. А вот написание усложняют (по крайней мере мне). Набирать закрывающие сущее мучение; вылазит вот такое ))}))]) безобразие и сиди считай скобки. Так недалеко и до JavaScript с его вечными });});});
  2. Зато, эти же самые [] и {} позволяют сделать конфиг с симпатичным синтаксисом почти-JSON.
    {
      :listen {
        :host "localhost"
        :port 8080
      }
      :db {
        :host "localhost"
        :port 5432
        :database "database"
        :user "root"
        :password "secret"
      }
    }
  3. Жутко неудобно писать императивный код. Мутабельность здесь не любят. Код, конечно, от этого становится чище, но времени на написание уходит чуть больше.
  4. Нет многострочных комментариев. Пришлось написать reader macro, добавляющий комментарии в стиле Common Lisp.
    (defn dispatch-reader-macro [ch fun]
      (let [dm (.get (doto
                       (.getDeclaredField clojure.lang.LispReader
                                          "dispatchMacros")
                       (.setAccessible true))
                     nil)]
        (aset dm (int ch) fun)))

    (defn read-comment [rdr pipe]
      (loop [s nil]
        (let [c (.read rdr)]
          (if-not (and s (= c (int \#)))
                  (recur (= c pipe))))))

    (dispatch-reader-macro \| read-comment)
    Единственное неудобство: нужно делать require во всех файлах, где эти комментарии используются.
  5. Очень понравился Leiningen. Собрать с его помощью jar-файл и запустить его на сервере оказалось очень просто. Сделано "для людей".

пятница, 1 апреля 2011 г.

GtkFileChooser и Win32

Вчера всплыла интересная проблема. File-chooser в CL-GTK2 не правильно работает с не-ASCII именами файлов.

Оказалось, что win32-версия GTK+ определяет два набора функций:
  • первый содержит те-же функции, что и unix-версия, но работающие с локальной кодировкой (а не utf8);
  • второй набор работает с utf8, но все функции имеют суффикс _utf8.
Самое интересное то, что файл gtkfilechooser.h с помощью препроцессора переименовывает функции из второго набора и скрывает из первого. DLL же экспортирует оба набора и CL-GTK2 как ни в чём ни бывало использует первый, неправильный набор.

Оказалось, что не все функции легко исправить потому, что они генерировались отдельным скриптом. Трудность заключалась в том, что нужно было генерировать reader macros. Символы #+win32 упорно не желали печататься как следует. Получалась никуда не годная ерунда вроде такой: |#+WIN32|

После долгих мучений родился такой workaround:

(defclass print-readtime-condition ()
  ((condition :initarg :condition)))

(defmethod print-object ((o print-readtime-condition) stream)
  (format stream "#~A" (slot-value o 'condition)))

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

(make-instance 'print-readtime-condition :condition :+win32)


Результаты ночных бдений: https://github.com/andy128k/cl-gtk2


вторник, 2 марта 2010 г.

Ещё один велосипед

В программе, которую я пишу, понадобилась компрессия/декомпрессия из ZLib. Казалось бы, такая простая и распространённая штука должна быть уже реализована и отлажена. Но оказалось, что не всё так просто, есть несколько библиотек, но ни одна из них не покрывает всех моих скромных нужд.

CL-ZIP: привязка к zlib. Всё бы ничего, но сделана на UFFI;
Salza/Salza2: умеет только компрессировать;
zlib: умеет всё, реализована на CL, но, как оказалось, работает через раз.

В итоге я написал ещё одну :) cl-z. Это простой биндинг к ZLib через CFFI.