26 November 2014

Как найти любовь или приключения с помощью crate.io и kibana

Search enginesOpen sourceSQLNoSQL
Про результативность, качество и КПД сайтов знакомств можно спорить, можно искать 101 повод чем лучше в клубе/баре/_дополнить_варианты_/парке искать знакомства. То что еще лет десять-пятнадцать назад вызывало смех — теперь мейнстрим. Так не проще ли попытаться использовать еще одну возможность для поиска и общения в интернет с переходом к знакомству в жизни…



Гиковский вариант технологии поиска, скринкаст приложения под катом. В конце статьи ссылка на архив с работающим приложением под Apache License v2.0 и небольшим набором данных для примера.


Звучит приободряюще, не правда ли!? Реальность несколько сложнее: армии ботов и фейк аккаунтов, работниц древнейшей профессии, попытки сервисами знакомств выжать максимум денег с минимумом результата и даже воры в поисках добычи. Еще интереснее? Не все так грустно и при правильном подходе игра стоит свеч!

Обещаный скринкаст приложения:


Рассмотрим програмную часть для поиска. Делим задачу на две части, как с рисованием совы:

  • Первая часть — рисуем овал. Для нас это найти, собрать и структурировать данные для дальнейшего поиска. Любой язык программирования с библиотекой html клиента, с регулярными выражениями или работой с DOM/xPath. Для меня эта часть не была проблемой, как разработчика с солидным опытом в интеграции ИТ систем и разработчика распределенного поискового робота для поискового стартапа Visuvi. Если вы считаете, что эта тема интересна, выскажитесь в голосовании за новую тему статьи.
  • Вторая часть — дорисовываем оставшуюся часть совы. Это как сохранить данные в хранилищее информации, проиндексировать их и написать фронтэнд для поиска и просмотра данных.


На помощь нам спешит crate.io — это набор плагинов для хранения двоичных данных в файловой системе и выполнения распределенных SQL запросов с помощью возможностей, которые уже есть в поисковом сервере elasticsearch. В двух словах это NoSQL shared nothing база в основе и facebook Presto SQL парсер и планировщик надстройкой над ней. Распределенное решение из мира big data, которое мы будем использовать пока в виде одного процесса на одном компьютере.

Почему crate.io? Нам нужно где-то хранить фото и при этом нужен Elasticsearch, да и SQL может пригодиться для статистики и отчетов в будущем. Успокою вас и в этот раз обойдемся без энтерпрайза, hibernate и JPA). Как увидете, работать crate не сложнее, чем с реляционной базой.

Kibana — HTML5 приложение, позволяющее визуализировать данные из elasticsearch, работать с временными рядами, фильтровать данные, сохранять параметры поиска в виде дашбордов.

Как это может помочь в поисках!? Минимум программирования и максимум результата.
Работать с crate.io можно из Python, Ruby, PHP, Java — jdbc type 4 драйвера. Но мне удобнее было включить REST API elasticsearch, который зачем-то скрывают в crate и буду работать через него.

В файле config/crate.yml добавляем параметры
es.api.enabled: true
udc.enabled: false

Второй параметр отключает отчеты об использовании crate.io, отправляемые по UDP на сервер проекта и я сразу же удалил двоичные файлы из библиотеки мониторинга sigar, чтобы не смущать ваш антивирус.

В таком виде «ящик» становится дружелюбным для работы через elasticsearch REST и с помощью spring data elasticsearch.

Для запуска сервера обязательно нужна java jre версии 7 или старше.
Запускаю проект bin/crate ( в случае с windows нужен файл bin\crate.bat)

С помощью утилиты коммандной строки crash или веб консоли
http://localhost:4200/_plugin/crate-admin/#/console

создаю хранилище для фотографий с названием images.

bin/crash -c "create blob table images clustered into 7 shards 
with (number_of_replicas=0)"
+-----------------------+-----------+---------+-----------+---------+
| server_url            | node_name | version | connected | message |
+-----------------------+-----------+---------+-----------+---------+
| http://127.0.0.1:4200 | Brigade   | 0.45.3  | TRUE      | OK      |
+-----------------------+-----------+---------+-----------+---------+
CONNECT OK
CREATE OK (1.104 sec)


Elasticsearch не требует чтобы мы определяли формат данных. В таком решении дьявол кроется в деталях, это скорее тема для обсуждения в комментариях к статье. Я все же укажу типы данных явно с помощью Mapping API, чтобы не было проблем с поиском и отображением в kibana.

Типы данных
{
  "info": {
    "mappings": {
      "default": {
        "properties": {
          "accommodation": {
            "type": "string",
            "index": "not_analyzed"
          },
          "age": {
            "type": "long"
          },
          "build": {
            "type": "string",
            "index": "not_analyzed"
          },
          "drinkingHabits": {
            "type": "string",
            "index": "not_analyzed"
          },
          "education": {
            "type": "string",
            "index": "not_analyzed"
          },
          "ethnicity": {
            "type": "string",
            "index": "not_analyzed"
          },
          "first": {
            "type": "date",
            "format": "basic_date_time"
          },
          "height": {
            "type": "long"
          },
          "images": {
            "type": "string"
          },
          "info": {
            "properties": {
              "": {
                "type": "string"
              },
              "Вес": {
                "type": "string"
              },
              "Внешность": {
                "type": "string"
              },
              "Дети": {
                "type": "string"
              },
              "Знание языков": {
                "type": "string"
              },
              "Кого я хочу найти": {
                "type": "string"
              },
              "Материальное положение": {
                "type": "string"
              },
              "Образование": {
                "type": "string"
              },
              "Ориентация": {
                "type": "string"
              },
              "Отношение к алкоголю": {
                "type": "string"
              },
              "Отношение к курению": {
                "type": "string"
              },
              "Отношения": {
                "type": "string"
              },
              "Познакомлюсь": {
                "type": "string"
              },
              "Проживание": {
                "type": "string"
              },
              "Рост": {
                "type": "string"
              },
              "Телосложение": {
                "type": "string"
              }
            }
          },
          "kids": {
            "type": "string",
            "index": "not_analyzed"
          },
          "last": {
            "type": "date",
            "format": "basic_date_time"
          },
          "login": {
            "type": "string"
          },
          "mainImage": {
            "type": "string",
            "index": "not_analyzed"
          },
          "message": {
            "type": "string"
          },
          "readableLogin": {
            "type": "boolean"
          },
          "realName": {
            "type": "string"
          },
          "relationship": {
            "type": "string",
            "index": "not_analyzed"
          },
          "replyRate": {
            "type": "long"
          },
          "searchingFor": {
            "type": "string"
          },
          "self": {
            "properties": {
              "В друзьях я больше всего ценю": {
                "type": "string"
              },
              "В женщинах я особенно ценю": {
                "type": "string"
              },
              "В жизни я ставлю перед собой цель": {
                "type": "string"
              },
              "В мужчинах я особенно ценю": {
                "type": "string"
              },
              "Есть ли у меня домашние животные": {
                "type": "string"
              },
              "Из всех известных людей я хотела бы быть": {
                "type": "string"
              },
              "Как долго я смогу прожить без общения": {
                "type": "string"
              },
              "Место, где я бы хотела жить": {
                "type": "string"
              },
              "Мое любимое блюдо": {
                "type": "string"
              },
              "Мое образование": {
                "type": "string"
              },
              "Мое свободное время я хотела бы провести так": {
                "type": "string"
              },
              "Мои любимые литературные герои": {
                "type": "string"
              },
              "Мои любимые музыкальные исполнители": {
                "type": "string"
              },
              "Мои любимые писатели": {
                "type": "string"
              },
              "Мои любимые фильмы": {
                "type": "string"
              },
              "Мои любимые художники": {
                "type": "string"
              },
              "Мой девиз": {
                "type": "string"
              },
              "Мой любимый город": {
                "type": "string"
              },
              "Наивысшее счастье для меня": {
                "type": "string"
              },
              "Самое поразительное открытие для меня": {
                "type": "string"
              },
              "Самой привлекательной чертой своего характера я считаю": {
                "type": "string"
              },
              "Самый ценный совет, который я получила в жизни": {
                "type": "string"
              },
              "Хотела бы я иметь детей": {
                "type": "string"
              },
              "Я больше всего горжусь этим достижением": {
                "type": "string"
              },
              "Я мечтаю о работе": {
                "type": "string"
              }
            }
          },
          "smoker": {
            "type": "string",
            "index": "not_analyzed"
          },
          "updated": {
            "type": "date",
            "format": "basic_date_time"
          },
          "viewed": {
            "type": "long"
          },
          "weight": {
            "type": "long"
          }
        }
      }
    }
  }
}



Запускаем скрипт, который выкачивает html страницы с сайтов, парсит html и извлекает нужные нам данные и сохраняет с помощью REST API/ elasticsearch java client.
Обязательно загружаю json с index type = «default», чтобы можно было выполнять SQL запросы.



Пример одного из json документов.



cr> select count(*) from info;
+----------+
| count(*) |
+----------+
|      291 |
+----------+
SELECT 1 row in set (0.030 sec)


Какой средний возраст в данных из примера?

cr> select avg(age) from info;
+---------------+
|      avg(age) |
+---------------+
| 24.7275862069 |
+---------------+
SELECT 1 row in set (0.038 sec)


Этот же скрипт скачивает изображения, считает sha1 дайджест и делает http PUT для каждой фотографии в crate.io:
"http://127.0.0.1:4200/_blobs/images/"+fileDigest


Можем проверить, что появились записи в blob.images:

cr> select count(*) from blob.images;
+----------+
| count(*) |
+----------+
|     2813 |
+----------+
SELECT 1 row in set (0.029 sec)


Отлично, данные в базе!

Скачиваю архив с kibana и распаковываю в директорию plugins/kibana/_site. При перезапуске сервер найдет фронтэнд как плагин site.

В plugins/kibana/_site/config.js указываем адрес к REST API Elasticserch

<b>elasticsearch: "http://"+window.location.host,</b>


Все изменения в kibana — незначительные, скорее хаки. По правильному надо было бы сделать свой компонент с возможностью конфигурирования.

Этот фрагмент angularJS шаблона выводит селектор оценки для поля _id в основоной таблице и фотографию, при видимом поле mainImage.

plugins/kibana/_site/app/panels/table/module.html

Код отображение фото в таблице, голосование за оценку
                    <tr ng-click="toggle_details(event)" class="pointer">
                        <td ng-if="panel.fields.length<1"
                            bo-text="event._source|stringify|tableTruncate:panel.trimFactor:1"></td>
                        <td ng-show="panel.fields.length>0" ng-repeat="field in panel.fields"><span
                                ng-if="(!panel.localTime || panel.timeField != field) && field!='mainImage' && field!='_id'"
                                bo-html="(event.kibana.highlight[field]||event.kibana._source[field]) |tableHighlight | tableTruncate:panel.trimFactor:panel.fields.length"
                                class="table-field-value"></span>
                        <span ng-if="field=='_id' ">
                            <span ng-repeat="t in [0,2,3,4,5]">
                                <input type="radio" name="item_{{event.kibana._source[field]}}" value="{{t}}" onclick="postESUpdate('{{event.kibana._source["_index"]}}','{{event.kibana._source["_type"]}}','{{event.kibana._source[field]}}',{{t}})" ng-if="event.kibana._source["rate"]!=t">
                                <input type="radio" name="item_{{event.kibana._source[field]}}" value="{{t}}" onclick="postESUpdate('{{event.kibana._source["_index"]}}','{{event.kibana._source["_type"]}}','{{event.kibana._source[field]}}',{{t}})" ng-if="event.kibana._source["rate"]==t" checked>{{t}}
                            </span>
                        </span>
                        <span ng-if="field=='mainImage' "><img src="/_blobs/images/{{event.kibana._source[field]}}"/></span>


                            <span
                                ng-if="panel.localTime && panel.timeField == field && field!='mainImage'"
                                bo-html="event.sort[1]|tableLocalTime:event" class="table-field-value"></span>

                        </td>
                    </tr>



Чтобы отобразить несколько изображений для одной записи при просмотре записи:

Код отображения всех фотографий
                                <tr ng-repeat="(key,value) in event.kibana._source track by $index"
                                    ng-class-odd="'odd'">
                                    <td style="word-wrap:break-word" bo-text="key"></td>
                                    <td style="white-space:nowrap"><i class="icon-search pointer"
                                                                      ng-click="build_search(key,value)"
                                                                      bs-tooltip="'Add filter to match this value'"></i>
                                        <i class="icon-ban-circle pointer" ng-click="build_search(key,value,true)"
                                           bs-tooltip="'Add filter to NOT match this value'"></i> <i
                                                class="pointer icon-th" ng-click="toggle_field(key)"
                                                bs-tooltip="'Toggle table column'"></i></td>
                                    <td style="white-space:pre-wrap;word-wrap:break-word">
                                        <span ng-if=" key != 'images' " bo-html="value|noXml|urlLink|stringify"></span>
                                    <span ng-if=" key == 'images' "><div ng-repeat="img in value"><img src="/_blobs/images/{{img}}"/></div></span></td>
                                </tr>



Для скрипта голосования, воспользуемся jquery, который уже есть в kibana

plugins/kibana/_site/index.html

Обновление оценки в json документе, запрос на сервер
        function postESUpdate(index, type, id, rate){
            $.ajax({
                type: "POST",
                url: "http://"+window.location.host+"/"+index+"/"+type+"/"+id+"/_update",
                data: '{"doc":{"rate":'+rate+'}}'
            }).done(function(){//alert("success"
            }).fail(function(){alert("error")});
        }


Это вызов elasticsearch Update API для обновления поля документа rate.

На этом программирование заканчивается. Дальше только веб интерфейс!



Кратко про создание фильтров вы уже посмотрели в скринкасте в начале статьи.
Там же показано как выбрать поддиапазон времени на гистограмме или с помощью timepicker. Все ваши фильтры и настройки можно сохранить в виде дашборда в kibana и загрузить когда нужно по имени.

За рамками этой статьи остались поиск по регулярным выражениям, безопасность сервиса, мониторинг и администрирования crate.io, SQL запросы через jdbc или клиентов для вашего языка программирования.

Повторюсь, что для запуска проекта необходима jvm 7 или старше.

Приложение, с данными для примера, вы можете скачать c дропбокса (234MB tar.gz), распаковать и запустить в *nix командой:
bin/crate
или windows:
bin\crate.bat

Откройте готовый дашборд в браузере:
http://localhost:4200
/_plugin/kibana/#/dashboard/elasticsearch/When%20first%20photo%20was%20uploaded


Желаю удачи с crate.io/kibana и в реальных знакомствах!!!

P.S. Dropboxs решил не выдавать сегодня(27.11.2014) архив. Подскажите пожалуйста в комментариях какой общедоступный хостинг файлов позволит выложить 234Мб файл без ограничений на количество скачиваний.


По результатам вашего голосования написал статью «Что нам стоит сайт распарсить. Основы webdriver API»
Tags:elasticsearchkibanacrate.ioпредставление технологиисайт знакомств
Hubs: Search engines Open source SQL NoSQL
+17
33k 191
Comments 25