Pull to refresh

Как найти любовь или приключения с помощью 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»
Only registered users can participate in poll. Log in, please.
На какую тему мне написать следующую статью?
59.26% Написание простейшего поискового робота, извлечение информации из веб страниц 144
37.45% Распределенный поисковый робот и управления его заданиями в кластере 91
31.69% Более подробный рассказ про elasticsearch/crate.io, распределенная система, разработка плагинов для elasticsearch 77
26.75% В топку сайты знакомств, даешь статьи про энтерпрайз и java!!! 65
Nobody voted yet. Nobody abstained.
Tags:elasticsearchkibanacrate.ioпредставление технологиисайт знакомств
Hubs: Search engines Open source SQL NoSQL
Total votes 21: ↑19 and ↓2+17
Views33K

Popular right now

Top of the last 24 hours