[Из песочницы] Riak и Riak Search. Первое знакомство

459f23cff564449389a03f1f2093226e.png

В статье в ознакомительных целях рассматривается процесс создания простого хранилища простых текстовых документов на базе Riak версии 2.1.1 и организация поиска по ним с помощью Riak Search (Yokozuna). В качестве клиентской библиотеки используется официальный клиент для Erlang.

Для начала представим, что у нас есть огромное количество таких документов:

  • title — заголовок;
  • body — содержимое;
  • tags — тэги;
  • created_at — время создания;
  • smiles — количество смайликов (плюсиков, лайков, как хотите)


и огромное количество пользователей, желающих их изменять. Кому интересно, начнём.
Я подразумеваю, что читатели уже имеют некоторое представление о Riak. Если нет, то лучше сначала почитать здесь и тут, или, конечно, официальную документацию.

Установка и начальная настройка


Для запуска Riak Search в системе должна быть установлена Java. Установить сам Riak в OSX можно через Homebrew, а Erlang, если потребуется, будет установлен автоматически:

brew install riak


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

## To enable Search set this 'on'.
##
## Default: off
##
## Acceptable values:
##   - on or off
search = on


Если Riak установлен с помощью Homebrew, то конфиг лежит здесь — /usr/local/Cellar/riak/2.1.1/libexec/etc/riak.conf

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

echo kern.maxfiles=65536 >> /etc/sysctl.conf
echo kern.maxfilesperproc=65536 >> /etc/sysctl.conf
sudo sysctl -w kern.maxfiles=65536
sudo sysctl -w kern.maxfilesperproc=65536
echo ulimit -n 65536 65536 >> ~/.bash_profile
ulimit -n 65536 65536


Теперь можно запустить Riak:

riak console


И можно протестировать из другой shell-сессии:

riak-admin test


Ответ должен быть примерно таким: Successfully completed 1 read/write cycle to 'riak@127.0.0.1'

Подробнее об установке и настройках:


Riak Data Types


Eventually Consistent хранилища, к которым относится Riak, допускают возникновение т.н. Data Incostistent ситуаций, когда содержимое одного и того же ключа на разных репликах отличается. В зависимости от настроек, Riak может попытаться решить конфликты сам с помощью vector clocks или timestamps, или же переложить обязанность определить правильную версию значения на приложение, предоставив ему все имеющиеся версии (siblings). В реальной ситуации, если ваши документы будут редактироваться в многопользовательском режиме и реплицироваться на несколько узлов, мердж конфликтных данных может стать весьма непростой задачей. В этом случае, возможно, лучшим решением будет использование Riak Data Types (также известных как CRDT). Эта технология позволяет описать данные с помощью специальных типов, которые возьмут на себя решение задачи конвергентности данных кластера и освободят приложение от обязанностей по решению конфликтов.

Riak Data Types, на текущий момент, реализуют следующие пять типов CRDT:

  • flag — Битовый флаг. Доступные операции — снять, установить. Может использоваться только внутри map;
  • counter — Счётчик. Доступные операции — увеличить, уменьшить. Тоже может использоваться только внутри map;
  • register — Какое-либо значение (хранится как строка);
  • set — Множество значений. Доступные операции — добавить элемент, удалить элемент;
  • map — Контейнер для других типов. Позволяет хранить внутри себя флаги, счётчики, регистры, множества и вложенные map’ы. Доступные операции — добавить поле, удалить поле, а также операции для внутренних полей, соответствующие их типам


В соответствие с этими типами, будем представлять наши документы как map со следующей структурой:

  • title — register;
  • tags — set;
  • body — register;
  • created_at — register;
  • smiles — counter


Создадим и активируем bucket-type под названием documents-type для хранения наших документов:

riak-admin bucket-type create documents-type '{"props":{"datatype":"map"}}'
riak-admin bucket-type activate documents-type


Более подробная информация по Eventually Consistent, Riak Data Types и CRDT:


Riak Search


Мы хотим реализовать поиск наших документов по тэгам, по заголовку, по дате создания и по содержимому. Для этого мы будем использовать технологию Riak Search под кодовым названием Yokozuna, которая по своей сути является посредником между хранилищем Riak и поисковиком Apache Solr. Yokuzuna сама запускает и мониторит на каждом узле кластера отдельный JVM-процесс с Solr, передаёт ему поисковые запросы и изменения в данных.

Для того, чтобы Solr знал как индексировать наши документы, нам необходимо создать поисковую схему. Вообще Riak Search имеет и дефолтную схему на все случаи жизни — _yz_default, которую удобно использовать во время разработки, но для рабочего окружения лучше создать свою.

Так как структура данных у нас уже определена, мы создадим схему сразу. В схеме нужно перечислить поля документа, для каждого из них указать тип, необходимо ли строить по нему индекс и хранить его значения, чтобы потом вернуть их в поисковой выдаче. Также в схему нужно обязательно включить служебные поля Riak Search. Следует заметить, что, при использовании Riak Data Types, к названиям полей добавляется суффикс, соответствующий их типу. Таким образом, у нас получится следующее описание:

    
    
    
    


Под спойлером полное содержимое файла со схемой:

docs_seacrh_schema.xml


    
        
        
        
        

        
        

        
        
        
        
        
        
        
        
        
        
    

    _yz_id

    
        
        

        
            
               
               
               
               
               
            
        

        

        
        
    




Подробности по Riak Search:


Доступ к Riak из Erlang


Пришло время наконец подключиться к нашей ноде с помощью клиента для Erlang:

# скачаем и скомпилируем клиент:
git clone https://github.com/basho/riak-erlang-client.git
cd riak-erlang-client/
make
cd ..
# запустим REPL:
erl -pa riak-erlang-client/ebin riak-erlang-client/deps/*/ebin


Подключаемся

{ok, RiakPid} = riakc_pb_socket:start_link("127.0.0.1", 8087).
% Проверим соединение
pong = riakc_pb_socket:ping(RiakPid).


Создаём поисковую схему и индекс

{ok, Schema} = file:read_file("docs_search_schema.xml").
ok = riakc_pb_socket:create_search_schema(RiakPid, <<"documents-schema">>, Schema).
ok = riakc_pb_socket:create_search_index(RiakPid, <<"documents-index">>, <<"documents-schema">>, []).


Создаём bucket и назначаем поисковый индекс

ok = riakc_pb_socket:set_search_index(RiakPid, {<<"documents-type">>, <<"documents-bucket">>}, <<"documents-index">>).


Создаём новый документ

Map = riakc_map:new().

% Заголовок документа - регистр (register)
Map1 = riakc_map:update({<<"title">>, register},
                        fun(Reg) ->
                            riakc_register:set(<<"DocumentTitle">>, Reg)
                        end,
                        Map).

% Тело документа - тоже регистр (register)
Map2 = riakc_map:update({<<"body">>, register},
                        fun(Reg) ->
                            riakc_register:set(<<"Some Document Body">>, Reg)
                        end,
                        Map1).

% Тэги - множество (set)
Map3 = riakc_map:update({<<"tags">>, set},
                        fun(Set) ->
                            Set1 = riakc_set:add_element(<<"Tag One">>, Set),
                            Set2 = riakc_set:add_element(<<"Tag Two">>, Set1),
                            Set2
                        end,
                        Map2).

% Дата создания - регистр (register)
Map4 = riakc_map:update({<<"created_at">>, register},
                        fun(Reg) ->
                            % Cледует заметить что Solr понимает только даты в формате ISO8601.
                            % https://cwiki.apache.org/confluence/display/solr/Working+with+Dates.
                            ISODateFmtStr = "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ",
                            {{Year, Month, Day}, {Hour, Min, Sec}} = calendar:universal_time(),
                            ISODate = list_to_binary(io_lib:format(ISODateFmtStr, [Year, Month, Day, Hour, Min, Sec])),

                            riakc_register:set(ISODate, Reg)
                        end,
                        Map3).

% Выполним операции по созданию нашего документа в хранилище
MapOperations = riakc_map:to_op(Map4).
ok = riakc_pb_socket:update_type(RiakPid, {<<"documents-type">>, <<"documents-bucket">>}, <<"DocumentKey">>, MapOperations).


Находим и получаем документы

% Загрузим определения записей Riak Client.
rr("riak-erlang-client/include/riakc.hrl").

% Пример запроса по заголовку тэгу, содержимому и дате создания
{ok, Results} = riakc_pb_socket:search(RiakPid, <<"documents-index">>, <<"title_register:DocumentTitle AND tags_set:\"Tag One\" AND body_register:Some AND created_at_register:[1972-05-20T17:33:18Z TO NOW]">>).

% Получение документов
Docs = Results#search_results.docs.
lists:foldr(fun({_Index, Doc}, Acc) ->
                {_, DocumentId} = lists:keyfind(<<"_yz_rk">>, 1, Doc),
                {ok, {map, Image, _, _, _}} = riakc_pb_socket:fetch_type(RiakPid, {<<"documents-type">>, <<"documents-bucket">>}, DocumentId),
                Image
            end,
            [],
            Docs).


Меняем документ

riakc_pb_socket:modify_type(RiakPid, fun(Map) ->
                                         % Обновим содержимое
                                         UpdatedMap1 = riakc_map:update({<<"body">>, register},
                                                                 fun(Register) ->
                                                                     riakc_register:set(<<"Новое содержимое">>, Register)
                                                                 end, Map),

                                         % Удалим тэг
                                         UpdatedMap2 = riakc_map:update({<<"tags">>, set},
                                                                 fun(Set) ->
                                                                     riakc_set:del_element(<<"Tag One">>, Set)
                                                                 end, UpdatedMap1),

                                         % Добавим 10 смайликов
                                         UpdatedMap3 = riakc_map:update({<<"smiles">>, counter},
                                                                 fun(Counter) ->
                                                                     riakc_counter:increment(10, Counter)
                                                                 end, UpdatedMap2),
                                         UpdatedMap3
                                     end,
                            {<<"documents-type">>, <<"documents-bucket">>}, <<"DocumentKey">>, []).

% Проверим
riakc_pb_socket:search(RiakPid, <<"documents-index">>, <<"body_register:\"Содержимое\"">>).


Удаляем документ

riakc_pb_socket:delete(RiakPid, {<<"documents-type">>, <<"documents-bucket">>}, <<"DocumentKey">>).

% Проверим
riakc_pb_socket:search(RiakPid, <<"documents-index">>, <<"body_register:\"Содержимое\"">>).


Больше информации по работе с клиентской библиотекой:


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

© Habrahabr.ru