Про результативность, качество и КПД сайтов знакомств можно спорить, можно искать 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 нужен файл bincrate.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 запросы.
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
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:
bincrate.bat
Откройте готовый дашборд в браузере:
http://localhost:4200
/_plugin/kibana/#/dashboard/elasticsearch/When%20first%20photo%20was%20uploaded
Желаю удачи с crate.io/kibana и в реальных знакомствах!!!
Автор: igor_suhorukov