воскресенье, 12 октября 2014 г.

Про поиск в одном отдельно взятом проекте

Тема обработки данных в общем и поиска чего-то осмысленного в них, в частности, мне уже очень давно была интересна. С неструктурированными данными сложнее, там всякий ML, так что пока говорим про простой случай - полнотекстовый поиск. На некоторых больших проектах мы лишь обрабатывали и готовили данные, но потом я попал на небольшой проект, где надо было делать полный цикл, от подготовки данных до интерфейса поиска. Там я увидел, что поисковый движок может быть центральным элементом системы и точкой входа для пользователей. За ElasticSearch наблюдаю с тех пор. Мы использовали Solr, но уже тогда ES, ещё не дошедший до 1.0, выглядел гораздо интереснее. Мне интересно делать не абстрактные вещи, а приближенные к реальности, так что давно хотел и вот решил вкрутит его в Targetprocess. Данная статья фактически введение в основные фичи ES. Так что знатоки могут проходить мимо. Во-вторых, они будут рассказываться применительно к полнотекстовому поиску в Targetprocess, поэтому если для кого-то это реклама, писать комментарии об этом не надо, просто не читайте.

Мне кажется поиск в каждом продукте проходит три фазы. Поиск непосредственно по базе sql запросами. Внутреняя реализация с использованием небольшого fulltext движка, типа Lucene. Использования полноценного поискового внешнего сервера как Solr, ES. Некоторые из них могут пропускаться конечно, а так же можно на долго или на совсем остановиться на какой либо из них. Года полтора наверно назад мы в Targetprocess перешли на вторую ступень с использованием совсем небольшой open source библиотеки hOOt. Так как поиск сделан плагином к основному продукту и все плагины мы выкладываем в Open Source, то его можно посмотреть на github Tp.Search. В целом он достаточно успешно справляется со своей задачей, поэтому наврядли в обозримом будущем он будет переписываться, ещё раз. Но это не значит, что его нельзя улучшить. Так как это плагин, то он хорошо отделён от самого продукта и достаточно легко заменить его hOOt движок на ES, например. Что я и пытаюсь сделать. Задачи две, поддержать старую функциональность, заиспользовать крутые дополнительные плюшки от ES. Пока не довел ещё всё до конца и не факт, что мне хватит терпения, поэтому целиком кода не будет, но если всё же получится, обязательно будет вторая статья с более практической информацией и болью.

Работу с поисковым движком можно разделить на две части: формирование/обновление индекса и поиск по нему. Начнем с первого, так как чтобы что-то найти, надо что-то сохранить/проиндексировать.

Targetprocess - это Visual Management Software. Вообщем в нем есть различные таски, баги, стори, релизы и т.п. сущности, их то мы и будем индексировать. Они у нас построены в иерархию, поэтому есть базовый почти для всех тип General. Если вы сходили в исходники поиска, могли найти замечательный интерфейс IEntityIndexer в котором инкапсулирована логика как подготовить разные типы сущностей для индекса. Я сделал ещё одну его реализацию, которая работает с ES. Расмотрим как нам сформировать индекс для General, для остальных типов будет примерно так же.

Основной интерфейс к ES - это REST. Через плагины вроде можно докрутить Thrift и наверно ещё чего. REST у них сделан хорошо, на мой вкус, и им удобно пользоваться, например, из Google Chrome плагина Postman. Но в своём коде не слишком удобно оперировать http запросам и руками разбирать всё, так что радует, что у ES полно клиентских библиотек. Для .net целых две. Я остановился на Elasticsearch.Net, она низкоуровневая, но пока вроде достаточно. NEST выглядит очень интересно, засчёт того что запросы формируются через internal dsl, а значит проверяются в compile time. Подключиться не составляет сложности. При подключении я включил всяко разный трейсинг, просто для удобства пока, чтобы смотреть что посылается.


var node = new Uri("http://localhost:9200");
var config = new ConnectionConfiguration(node)
  .EnableTrace()
  .ExposeRawResponse();
_elasticSearch = new ElasticsearchClient(config);

А теперь самое интересное. Реализуем этот наш интерфейс для индексирования. И сделать это невероятно просто, так как в методы AddGeneralIndex и UpdateGeneralIndex приходит GeneralDTO, который действительно DTO и прекрасно сериализуется. В качестве id для записей в индексе ES для удобства мы будем использовать наши id и посему удалить из индекса сущность для нас не проблема.


public IndexResult AddGeneralIndex(GeneralDTO general, 
            DocumentIndexOptimizeSetup optimizeSetup = null)
{
    var indexResponse = _elasticSearch.Index("test", "general", 
            general.ID.ToString(), general);
    _log.InfoFormat("Add General '{0}', {1}", general.ID, indexResponse);
    return null;
}

public IndexResult UpdateGeneralIndex(GeneralDTO general, 
            ICollection changedFields, 
            DocumentIndexOptimizeSetup optimizeSetup = null)
{
    var indexResponse = _elasticSearch.Index("test", "general", 
            general.ID.ToString(), general);
    _log.InfoFormat("Update General '{0}'", general.ID, indexResponse);
    return null;
}

public IndexResult RemoveGeneralIndex(GeneralDTO general, 
            DocumentIndexOptimizeSetup optimizeSetup = null)
{
    var deleteResponse = _elasticSearch.Delete("test", "general", 
            general.ID.ToString());
    _log.InfoFormat("Remove General '{0}'", general.ID, deleteResponse);   
    return null;
}

Как можно увидеть это буквально одна строчка, причем даже в одном месте скопипащена. Так что это действительно невероятно просто. Так же, использование наших id упрощает и просмотр. По этому id, просто открыв в браузере (или сделав GET) очевидный url (напеример http://localhost:9200/test/general/1) мы можем увидеть нашу проиндексированую сущность. (противный null мы возвращаем, потому что пользователь этого интерфейса ожидает документ в формате hOOt, интерфес не так расширяем сейчас как хотелось бы и надо это поправить, но писали его "правильный пацаны", поэтому null он нормально кушает ака игнорирует)


{
    "_index": "test",
    "_type": "general",
    "_id": "193",
    "_version": 1,
    "found": true,
    "_source": {
        "ID": 193,
        "GeneralID": 193,
        "Name": "Test",
...

Превёл я этот json не просто так. Пытливый читатель незнакомый с ES мог заметить два первых параметра, которые мы передаём в каждый метод и json нам раскрывает тайны. Это индекс и тип. Это такое разграничение ваших коллекций сущностей. В нашем случае выглядит разумным в качестве индекса брать одного кастомера, а для типов ES использовать наши типы, как я это сделал с General. Так же можно обратить внимание, что ES версионирует ваши документы.

Так как ES не MongoDB, а мы не хипстеры, то нам интересно какую такую магию он делает с тем, что мы ему послали. Так же мы знаем, что поисковые движки как-то всё разбивают на слова, строят какие-то деревья, а мы видим тот же json, что послали. К тому же, если есть типы не плохо было бы их определить и помочь ES правильно (как мы этого хотим) обработать наш документ. Этому посвящен огроменный раздел документации Mapping. Переписывать его я не собираюсь, лишь обратим внимание на ключевые моменты. Понятно, что поле может быть всяко разного типа, примерно похоже как в любом другом хранилище. Важнее, что при описании поля документа можно задать ES различные настройки. Можно например вообще его не индексировать, например, если мы хотим просто его в ответе возвращать. Из жизни это могут быть какие-либо ссылки/id на другие сущности и перед тем как пользователю что-то показывать мы можем уже доджойнить в результат из основного хранилища нужную информацию. А можно наоборот - индексировать и не хранить, если поле большое. А так analyzer которым оно будет обрабатываться при индексировании и поиска, об этом чуть попозже. Вообщем настроек много и некоторые специфичны для конкретных типов филдов, как например формат даты. "А что ещё важно" - это то, что как в монге могут быть вложенные объекты. Помню у Solr например с этим были прям серьезные проблемы, вернее не было такой возможности (я его давно не смотрел, может уже запилили). А ещё есть очень крутой тип (как экспериментальный плагин) - attachment - можно закинуть какую-либо pdf и он её распарсит и проиндексирует. Что было бы нам полезно, наш поиск не умеет атачменты, а у людей их много. В целом у ES богатые возможности описать и даже сделать предварительную обработку документа для индексирования.

Как я упомянул выше у поля может быть analyzer. Очень важная штукенция, которая позволяет сказать индексатору как разбить ваш текст на смысловые еденицы. Тех, кто знаком с Lucene этим не удивить, так как именно оттуда ES их и использует. Условно анализ можно разделить на две фазы, для которых есть уже множество подходящих заготовок. Это непосредственно разбитие на лексемы/токены/термы. Тут в 80% отлично подойдет standard tokenizer, он вам разобъет на слова и т.п. Но иногда необходимо другое поведение. Наример, у нас есть стейты, и их разбивать на слова не имеет смысла и уж тем более при поиске по ним пользоваться синонимами, так что это поле можно не разбивать на токены, фактически каждое значение уже и есть токен. Для любителей Perl в наличии pattern tokenizer (правда как не трудно догодаться с джавовыми регэкспами). Когда у нас есть токены, мы можем их пофильтровать, так как возможно среди них есть муссор, по которому искать не нужно. Подходящих фильтров у ES тоже заготовлено. Хотя конечно я соврал, вернее ребята не правильно назвали. Так как это не просто фильтры. Они делают активную работу. Наример lowercase token filter, как не сложно догадаться приводит всё в нижни регистр и будет полезен для всех текстовых полей. А вот если действительно отфильтровать, тобишь исключить из индексирования какие-то токены, нам понадобиться stop token filter. Это стоит делать для всяких артиклей, местоимения и т.п., ведь правда, зачем искать по the. Фильтров готовых дофига, можно весь вечер разбираться.

Если подводить итоги части по индексированию надо отметить, что у ES действительно богатые возможности и следует хорошо проанализировать, что вы храните в ваших документах и каждом конкретном поле, что пользователь будет искать и исходя из этого настроить индекс. Тут даже сложнее чем со схемой базы данных, но оно того стоит. Но тут ещё необходимо эксперементировать. Мы на одном проекте вначале сделали черновой вариант и сразу отдали живым пользователям, чтобы они искали и исходя из того какой им муссор попадался уже оптимизировали.

Теперь приступим к самой интересной части - непосредственно поиск. Я до этого много написал, про то как настроить индексы, но сейчас поработаем с тем что ES сделал по умолчанию и это у него получилось неплохо. Если открыть вот такой url в браузере или для пацанов в консоли curl'ануть http://192.168.1.119:9200/test/general/_search?q=home мы получим результаты поиска по слову home, если точнее по всем полям нашего документа.


"took": 10,
"timed_out": false,
"_shards": {
  "total": 5,
  "successful": 5,
  "failed": 0
},
"hits": {
  "total": 13,
  "max_score": 0.62386453,
  "hits": [
    {
      "_index": "test",
      "_type": "general",
      "_id": "71",
      "_score": 0.62386453,
      "_source": {
        "ID": 71,
        "GeneralID": 71,
        "Name": "Home page",

К сожалению Chrome не умеет форматировать json, так что лично я пользуюсь Postman и для таких вот простых get запросов. Как можно заметить ES возвращает много метаинформации. Это тема отдельной статьи, самое интересное в массиве hits, собственно найденные записи, в _source у нас вся запись как мы это видели, когда запрашивали её по id. Пытливый читатель так же обратит внимание на структуру самого url, весь API как REST так и обертки имеют несколько частей, о которых мы говорили в индексировании, имя индекса и тип. Крутизна ES в том, что можно искать не так конкретно, как я сделал, а например по всем типам или нескольким, а так же по всем индексам. Если мы для каждого клиента выделяем свой индекс, то запросы от него идут только в этот индекс, но мы сами можем поискать и по всем индексам, что удобно для различного анализа.

Так как поиск мы прикручиваем к конкретному проекту, посмотрим как поиск выглядит у нас. В guide отличная статья с подписанным скриншотом Global Search. Есть строка поиска и несколько фильтров. Поисковая строка поддерживает простой синтаксис. Если совсем упростить, то можно просто с клиента и вызывать наш запрос на поиск в ES передавая в 'q' нашу строку поиска. Мы конечно так не будем делать ввиду секурити, пермишинов для пользователей и т.п. вещей. Но все равно, логика такая же. Теперь самый важный вопрос, даже не смотря на то что наш поиск должен поддерживать не сложный синтаксис, насколько такой query в ES ему соответствует. Обратимся уже к гайду для ES - Query string query. Вообщем и целом всё более чем замечательно, мы сможем поддержать все имеющиеся возможности, но и даже больше, как-то булевые операции, указание конкретных полей для поиска, вообщем просто супер. Отдельно хотел обратить внимание, на необычное поле _all, оно включает текст из других полей. Мы в свое время в Solr сами эту функциональность на уровне нашего индексатора делали.

Так же, некоторые из читателей несомненно обратили внимание, что на этой странице запрос пишется в json. Действительно, кроме такой простой формы запросов у ES есть достаточно крутой способ построения запросов на json. Со времён MongoDB я не люблю запросы в json, но у ES два варианта и это отлично. В коде мы будем оперировать json, так как он предоставляет больше возможностей и query string - это просто один из типов. Но для старта нам его хватит. Чтобы поддержать функциональность старого поиска нам достаточно пробросить пришедший запрос от пользователя в ES. Выглядит так, что снова весь наш код уместиться в три строчки. В нашем плагине есть замечательный класс QueryRunner, интерфейса у него нет, но выделить достаточно легко, так как содержит он один метод Run. На вход у нас приходит QueryData где и есть нужный нам Query, а вернуть нам необходимо QueryResult, где самое главное список id найденных сущностей. Вообщем чего это я, вот такой простой код мы получаем и он работает (хотя как можно заметить по количеству пустых массивов я не все реализовал, только для general, но остальное работает ровно так же, просто немного другой url запроса).


var result = client.Search("test", "general",
    new { query = new { query_string = new { query = "home" } } });

    var generalIds = result.Response.hits.hits
        .Select(r => r._source.ID.ToString())
 .ToArray();
return new QueryResult
    {
        GeneralIds = generalIds,
        Total = result.Response.hits.total         
    };

Понятно, это урезанный и совсем не "producation ready" результат, просто то что получили, переслали в ES и транслировали его результат. Фактически такой вот адаптер. Благодаря анонимным типам в сишарпике json можно писать почти как json, разве что new всё портит (или я чего-то не знаю?). Поэтому дальше будем развивать пример добавляя мяса в этот json.

Но начнём с конца. С результатов. Как мы видели в _source нам приходит все наши документы, целиком. А мы из них достаём только id. Как человек работавший некоторое время плохим админом негодую такому нерациональному использованию трафика. На самом деле нам нужен не только id. Если вернуться к тому скриншоту нашего поиска, можно увидеть, что для каждого результата есть проект, статус и дата создания. Так как старый сёрч всего этого не хранит, то UI приходится через наш REST забирать это дополнительным запросом. Что снова не рационально, к тому же ещё один запрос оттягивает момент когда мы можем показать пользователю результат. Поэтому правильно расширить наш QueryResult, чтобы он возвращал не просто id, а и эту необходимую информацию. И теперь мы из _source гораздо больше информации достаём. Но все же хотелось бы ограничить результат и это возможно сделать. Теперь просто в вместе с query нужно ещё одно поле в запросе указать - fields и тогда вместо _source каждый результат будет содержать коллекцию этих полей с их значениями для этого документа. Кстати, если помните, мы при индексации обсуждали возможность указать нужно ли хранить это поле - это было именно для этого. Чтобы размер ES не рос, мы можем указать для хранения только те поля, что собираемся возвращать. ES умеет ещё кое-какие хитрости с полями, но это уже для гуру.


"fields" : ["ID", "ParentProjectID", "CreateDate"],

Как вы могли заметить результаты выдаются на UI по странично. Это уже достаточно давно привычная всюду функциональность, потому что данных может быть очень много (для привлечения внимания надо было наверно по английский это написать - big data). Было бы странно, если бы ES не поддерживал такую возможность и он нас не разочаровывает. В дополнение к двум предыдущим query и fields мы можем указать from и size. Что же, выглядит так, что мы поддержали все возможности старого поиска по выдаче результатов. Даже несколько улучшили, так как сами можем вернуть документ который он отображает, без дополнительного запроса. Хотя погодите, не можем, так как на UI у нас ещё отображаются имя и описание. И не просто так, а в них подсвечиваются заданные в запросе слова и это делается на фронтенде. И делается это не совсем правильно, так как фронтенд не умеет разбирать поисковый запрос, вернее умеет, но делает это по упрощенной схеме, поэтому подсветка не всегда совпадает с результатом. Например запрос "home page", который подразумевает поиск фразы целиком, он подсвечивает отдельные слова. А у ES запросы ещё сложнее бывают и переписывать из query parser на javascript'е у меня нет никакого желания. А у вас? Но разработчики ES позаботились о нас, так как highlighting очевидная фича для поиска. Так что в наш запрос мы добавим ещё одну секцию. Для ES необходимо указать поля, в которых нужно выполнить подсветку. Это из обязательного, ещё можно, например указать в какие теги обрамлять подсветку, так что при правильном css просто выводя значения этих филдов в html мы получим нужный результат.


"highlight" : {
    "pre_tags" : ["", ""],
    "post_tags" : ["", ""],
    "fields" : {
        "Name" : {},
        "Description" : {}
}

Вот теперь мы окончательно избавились от дополнительного запроса, что должно увеличить скорость нашего ответа пользователю. Да и код на фронтенде улучшиться. Как-то легко у нас это получилось, вам так не кажется? Почему бы нам тогда ещё не улучшит нашу выдачу, раз для этого необходимо просто добавить одно поле в json. Как это не странно, но наш поиск не умеет сортировать результаты. Вернее он конечно их сортирует, но сортировка зависит от настроения богов Асгарда, что несколько не удобно, к тому же пользователь никак не может на это повлиять. ES нам снова приходит на помощь, так как он умеет сортировать свои результаты и для этого снова необходимо лишь добавить одно поле в наш запрос с указанием как мы это хотим сделать. Вы конечно можете удивиться и не поверить, но поле это - sort. Сортировать мы можем по разным поля документа, причем по нескольким задавая его приоритет. Такое мы уже видели в любом хранилище. Что интересно именно для поиска - это сортировка по _score, это что-то типа ревалентности, хороший выбор для умолчания. Подробнее как ES его считает можно почитать тут Relevance intro. Кстати вы могли обратить внимание, что он добавляет в результатах поиска к каждому найденному документу этот score.

С результатами вроде все. Мы не только реализовали старые возможности, но и сильно их улучшили. Но мы ещё не поддержали одну возможность нашего поиска - дополнительную фильтрацию. Наряду с запросом пользователь может выбрать, например, тип сущности среди которых он хочет искать. Поэтому в QueryData есть ряд дополнительных полей, например EntityTypeId. Как совсем тривиальный способ в лоб мы перед тем как посылать запрос в ES можем к нему дописать AND EntityTypeID:4, но это просто не красиво. Тем более, что запрос в ES не ограничивается лишь только query string и может иметь сложную вложенную структуру с дополнительными фильтрами и другими типами, чем мы и воспользуемся. Хотя фильтры я тут не уместно употребил, так как у ES есть фильтры, но это несколько другая история и лично я не до конца понимаю их назначение, в доке говорят, что хорошо их использовать для кеширования, ок. А мы будем пользовать query, просто сделаем её чуточку сложнее, как-то так


{
  "query":  {
    "bool" : {
        "must" : [
            {"query_string": { "query": "home" } },
            {"term" : { "EntityTypeID" : 4 } }
        ]
    }
  }
}   

Ладно, не так просто, хотя вполне нормально. К нашей query_string мы добавили term запрос, который проверяет на совпадение значения поля (для остальных полей как-то стейм мы так же просто в массив добавляем). Небольшое усложнение в том, что мы обернули это в bool запрос. Он кстати очень интересный. must - это оба условия, как не трудно догадаться, должны совпадать, но есть более хитрое should и обратное must_not. Теперь всё, все возможности старого поиска мы реализовали. Но я хотел бы, на основании предыдущего опыта немного улучшить часть с фильтрацией через dropdown c типом и т.п. Это не удобно, так как выбрав тип, т.е. уточнив результат ты можешь получишь пустой результат. Хотелось бы о таких штуках узнавать не прокликивая все возможные типы. Такую штуку можно увидеть во многих поисках, когда показывается вместо dropdown список, но к нему ещё добавляются значения сколько из найденных документов этого типа. По правильному, это называется фасетами. Но ребята в ES пошли дальше и объявили их устаревшими и теперь предлагают расширенный механизм агрегации, через который их можно выразить используя тип агрегации - terms. Делается это привычным нам образом - расширив post data нашего запроса ещё одним филдом aggs.


"aggs" : {
    "EntityTypes" : { "terms" : { "field" : "EntityTypeName" } }
}

"aggregations": {
    "EntityTypes": {
        "buckets": [
            {
                "key": "tp.businessobjects.userstory",
                "doc_count": 4
            },
            {
                "key": "tp.businessobjects.bug",
                "doc_count": 2
            }
        ]
    }
}

Выше я сразу привел пример того, что ES нам добавит в результаты. Так что теперь можем дать возможность пользователю не просто дополнительно отфильтровать результаты по типу, но и проинформируем как измениться от этого результаты. Очень удобно.

Теперь минутка рекламы. Три года прошло, так что подписанный кровью NDA уже недействителен и могу упомянуть. Как я тут отмечал у меня есть не плохой опыт с Solr. По многим фичам он как ES, вообщем это два конкурента. Мы его использовали на полную катушку в одном проекте. Это была система для рекрутинга, сливала в Solr кучу данных о разных кандидатах с разных источников и позволяло рекрутерам искать. В чем её фишка. Рекрутеру не нужен однозначный ответ, хотелось бы конечно, чтобы она подбирала идеального кандидата, да ещё чтобы была уверенность, что он согласится, но не в этом мире. Так что она должна была позволять делать анализ и поэтому крутой поиск, с фасетами, сложными запросами был основной фичей и центральной частью. В Targetprocess это не так. Так как в продукте есть отличный DSL, который Андрей за один вечер написал, а так же крутая концепция View, когда результаты вы можете увидеть в разном представлении, вообщем сходите на product page. Так что поиск тут особо и не нужен. По моим наблюдениям, через него ищут какие-то конкретные вещи, например если наткнулся на проблему, ты ищешь баг, чтобы проверить новая она или нет. А анализ и основная работа происходит на View. Так что не смотря на то как крут ES, это всё скорее мое баловство и до продукта не дойдет, просто потому что не надо.

А теперь немного о том где ES нужен и фактически стал стандартом дефакто - централизованные хранилища логов. Когда у вас много машин, заходить на каждую и грепать логи сложно, поэтому нужна центральная точка, где можно поискать по всем логом, а раз надо искать то ES отлично подходит. Но иногда кроме поиска, нужны какие-то метрики и тут у ES тоже есть ответ - Kibana, которая в новой третье ипостаси прекрасна чуть более чем полностью. Она полностью статична, просто кидаете файлики под nginx, а она уже с фронтенда ходит на ES (при этом понятное дело он должен быть вам доступен). Что же она позволяет. Она позволяет строить дашборды. Центральным элементом конечно является строка поиска, но с помощью упомянутой выше функции агрегации в ES на дашборд можно накидать различных графиков, таблиц и т.п результаты которых будут к тому же фильтроваться относительно заданного поиска. Вообщем невероятно крутая штука. Настолько, что ребята из ES решили это обобщить не только для логов, а просто назвать это ELK Stack - search and analytics platform. С помощью L - logstash данные из различных источников загружаются в E - elasticsearch и потом визуализуруются на K - Kibana. В этой статье L не нужен, так как в его роли выступает плагин и данные у нас есть в E, так что подняв Kibana на nginx я для примера визуализировал упомянутый выше facet по типу сущностей, нужна же хотя бы одна картинка этой статье.

На самом деле это лишь маленькая часть ES, я не раскрыл и половину возможностей тех фич, что упомянул, но кроме того он умеет ещё много чего, просто всё в одну статью не уместишь и я попытался сосредоточиться на том, что понадобиться 80% пользователей, просто, чтобы вы прониклись как же крут ES.

Собственно всё это было лишь про одну часть ElasticSearch - Search, несколько слов про Elastic. Это у многих вызывает ассоциацию с Amazon, ведь у них многие продукты с этой приставкой, но Search у них CloudSearch, потому как занято уже ребятами из ES. ES изначально разрабатывался для работы с огромными данными и по задумке авторов отлично скейлится. Но писать об этом на основании знаний почерпнутых из презентаций других людей глупо, поэтому я не буду этого делать. Тем не менее замечу, что он отлично работает уже во многих проектах, из моих любимых это StackOverflow, которым вы наверно каждый день пользуетесь. Есть обзорная статья про их архитектуру там можно посмотреть на цифры относительно ES. Более интересны две статьи от ещё одних классных ребята Github, которые перешли на ES. Есть радостная статья какой у них теперь классный поиск благодаря ES и поучительна о том как он у них сразу после запуска и упал. И напоследок есть замечательный цикл статей про распределенные системы - Call me maybe и ES там недавно рассматривался. Не все хорошо в датском королевстве. Тем не менее ES не должен быть основным хранилищем и чуть что всё можно переиндексировать, хотя конечно это будет боль.

Комментариев нет:

Отправить комментарий