├── .gitignore ├── README.md ├── data └── entities.js ├── package.json └── scripts └── generate_base.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Небольшой курс лекций по Elastic Search 2 | 3 | [План лекций](https://docs.google.com/a/ria.com/document/d/1elQ5KLu-8UyaRsdJfhIN_BuxjQrMHgW5kVVU7xDw4TQ/edit?usp=sharing) 4 | 5 | ## Установка и настройка 6 | 7 | Для ОС Fedora есть свой репозиторий. Для того, чтобы установить себе Elastic Search версии 1.4, необходимо добавить в файл `/etc/yum.repos.d/` следующие настройки: 8 | 9 | ```ini 10 | [elasticsearch-1.4] 11 | name=Elasticsearch repository for 1.4.x packages 12 | baseurl=http://packages.elasticsearch.org/elasticsearch/1.4/centos 13 | gpgcheck=1 14 | gpgkey=http://packages.elasticsearch.org/GPG-KEY-elasticsearch 15 | enabled=1 16 | ``` 17 | 18 | После этого установить его можно командой: 19 | 20 | ```bash 21 | yum install elasticsearch 22 | ``` 23 | 24 | ### С чего начать? 25 | 26 | Elastic Search - база данных с открытым исходным кодом предназначенная для полнотекстового поиска. Она позволяет хранить, анализировать и получать большие объемы данных в режиме реального времени. 27 | 28 | Для анализа и поиска Elastic Search использует библиотеку [Apache Lucene](http://lucene.apache.org/core/). Его можно применять при реализации следующих задач: 29 | 30 | - Полнотекстовый поиск 31 | - Поиск по параметрам 32 | - Агрегация данных для статистики и их последующая визуализация 33 | - Генерация вариантов для автокомплита 34 | 35 | Для работы с данными у Elastic Search есть специальное REST API. 36 | 37 | #### Основные параметры REST API 38 | 39 | Если в запросе передать `?pretty=true`, то JSON в ответе будет отформатирован таким образом, что его можно будет прочитать. 40 | 41 | Добавив параметр `?format=yaml`, ответ получим в yaml. 42 | 43 | #### Терминология 44 | 45 | В Elastic Search используются такие понятия, как индекс (index), тип (type) и документ (document). Если проводить аналогию с MySQL, то это база данных (database), таблица (table) и рядок (row) 46 | 47 | #### Создание нового индекса 48 | 49 | Самый простой вариант создания индекса приведен ниже: 50 | 51 | ```bash 52 | curl -XPUT 'http://localhost:9200/twitter/' 53 | ``` 54 | 55 | Если все прошло нормально, то в ответе мы получим сообщение: 56 | 57 | ```json 58 | {"acknowledged":true} 59 | ``` 60 | 61 | Также при создании индекса можно описать все типы документов, которые будут в нем хранится: 62 | 63 | ```bash 64 | curl -XPOST localhost:9200/test -d '{ 65 | "mappings" : { 66 | "type1" : { 67 | "properties" : { 68 | "field1" : { "type" : "string"} 69 | } 70 | } 71 | } 72 | }' 73 | ``` 74 | 75 | #### Создание нового документа 76 | 77 | ### Инструменты для работы с Elastic Search 78 | 79 | ### Прогнозирование нагрузки 80 | 81 | ## Индексация и поиск 82 | 83 | ### Что важно знать? 84 | 85 | #### Что такое релевантность? 86 | 87 | Как только у нас появился список подходящих документов, необходимо как-то их отсортировать. Не все документы будут содержать все слова (термины) и не все слова одинаково важны. 88 | Вес (значение) слова зависит от [трех факторов](https://www.elastic.co/guide/en/elasticsearch/guide/current/relevance-intro.html): 89 | 90 | 1. Term Frequency (TF) - Как часто встречается слово (термин) в поле документа? Чем чаще - тем больший вес оно (слово) имеет. Поле содержащее пять упоминаний слова (термина) будет более релевантно, чем поле, которое содержит только одно такое слово 91 | 2. Inverse Document Frequency (IDF) - Как часто встречается слово (термин) в индексе (коллекции документов)? Чем чаще, тем меньший вес оно имеет. Слово, которое встречается во многих документах имеет меньший вес, чем то, которое встречается редко 92 | 3. Field-Length Norm - На сколько длинное поле (в документе)? Чем оно длинее, тем менее релевантными будут слова, которые в нем встречаются. Слово, которое встречается в коротком поле `title` несет в себе больший вес, чем то же слово встречающееся в длинном поле `content` 93 | 94 | При помощи этих трех параметров `Elastic Search` вычисляет параметр `score`, по которому и сортирует документы. 95 | 96 | #### Понимание параметра ’score’ 97 | 98 | При отладке сложного запроса порой бывает тяжело понять, как же этот параметр был посчитан. К счастью, у `Elastic Search` есть специальная опция, которая будет добавлять разъяснение к каждому поисковому запросу. Для этого необходимо всего лишь добавить параметр `explain` равный `true`. 99 | 100 | ```bash 101 | GET /_search?explain 102 | { 103 | "query" : { "match" : { "tweet" : "honeymoon" }} 104 | } 105 | ``` 106 | 107 | Сначала в ответе будут идти метаданные, которые возвращаются при обычном поисковом запросе: 108 | 109 | ```json 110 | { 111 | "_index" : "us", 112 | "_type" : "tweet", 113 | "_id" : "12", 114 | "_score" : 0.076713204, 115 | "_source" : { ... trimmed ... }, 116 | ``` 117 | 118 | Также `Elastic Search` добавит информацию о том, с какого ’shard’ и какой `node` пришел документ. 119 | 120 | ```json 121 | "_shard" : 1, 122 | "_node" : "mzIVYCsqSWCG_M_ZffSs9Q", 123 | ``` 124 | 125 | После этого последует объяснение (`_explanation`). Каждый элемент содержит в себе описание (`description`), которое говорит нам о том, какие вычисления были произведены, и их результат (`value`). Поле `details` содержит в себе список любых дополнительных вычислений, которые понадобились. 126 | 127 | ```javascript 128 | "_explanation": { /* 1 */ 129 | "description": "weight(tweet:honeymoon in 0) 130 | [PerFieldSimilarity], result of:", 131 | "value": 0.076713204, 132 | "details": [ 133 | { 134 | "description": "fieldWeight in 0, product of:", 135 | "value": 0.076713204, 136 | "details": [ 137 | { /* 2 */ 138 | "description": "tf(freq=1.0), with freq of:", 139 | "value": 1, 140 | "details": [ 141 | { 142 | "description": "termFreq=1.0", 143 | "value": 1 144 | } 145 | ] 146 | }, 147 | { /* 3 */ 148 | "description": "idf(docFreq=1, maxDocs=1)", 149 | "value": 0.30685282 150 | }, 151 | { /* 4 */ 152 | "description": "fieldNorm(doc=0)", 153 | "value": 0.25, 154 | } 155 | ] 156 | } 157 | ] 158 | } 159 | ``` 160 | 161 | Разъяснение: 162 | 163 | 1. Содержание подсчета `score` для слова `honeymoon` 164 | 2. Term Frequency (TF) 165 | 3. Inverse Document Frequency (IDF) 166 | 4. Field-Length Form 167 | 168 | Первая часть содержания содержит в себе результаты вычислений. Она говорит нам, что был посчитан вес (`weight`) - TF/IDF - слова `honeymoon` в поле `tweet` для документа `0` (Это внутренний идентификатор документа, который используется сугубо в служебных целях - его можно игнорировать). 169 | 170 | Далее следуют детали того, как вес (`weight`) был посчитан: 171 | 172 | * Term Frequency - Сколько раз встретилось слово `honeymoon` в поле ’tweet’ этого документа? 173 | * Inverse Document Frequency - Сколько раз встретилось слово `honeymoon` в поле `tweet` всех документов в коллекции (индексе)? 174 | * Field-Length Form - Какова длина поля `tweet` в этом документе? Чем длиннее поле, тем меньше это число 175 | 176 | #### Почему документ нам подходит? 177 | 178 | Если добавлять параметр `explain` для каждого конкретного результата, можно понять почему документ подходит, и что важнее - почему нет? 179 | 180 | Ссылка выглядит следующим образом `/index/type/id/_explain`: 181 | 182 | ```bash 183 | GET /us/tweet/12/_explain 184 | { 185 | "query" : { 186 | "filtered" : { 187 | "filter" : { "term" : { "user_id" : 2 }}, 188 | "query" : { "match" : { "tweet" : "honeymoon" }} 189 | } 190 | } 191 | } 192 | ``` 193 | 194 | Вместе с полным объяснением, которое мы видели ранее, нам также доступно поле `description`, которое говорит нам, что: 195 | 196 | ```javascript 197 | "failure to match filter: cache(user_id:[2 TO 2])" 198 | ``` 199 | 200 | Другими словами, наш `user_id` в секции фильтра запроса не дает возможности документу подойти под запрос. 201 | 202 | ### Фильтры и токенайзеры 203 | 204 | #### Анализ и Анализаторы 205 | 206 | `Анализ` - это [процесс](https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html), состоящий из следующих этапов: 207 | 208 | 1. Фильтрация символов (`Character filters`) - всегда происходит в первую очередь. Основая задача этого этапа анализа - убрать все ненужные символы, прежде чем строка попадет на обработку токенайзеру 209 | 2. Разбитие на токены (`Tokenizer`) - по специальному символу или набору символов текст разбивается на слова, фразы или наборы символов. 210 | 3. Фильтрация токенов (`Token filters`) - на этом этапе может происходить видоизменение токена (приведение к нижнему регистру, например), удаление (например, список `stopwords`) или добавление нового (синонимы, например `автомобиль`, `машина` и т.п.) 211 | 212 | `Анализатор` - это набор входных фильтров, токенайзеров и выходных фильтров. 213 | 214 | #### Встроенные анализаторы `Elastic Search` 215 | 216 | На [примере](https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html#_built_in_analyzers) строки `Set the shape to semi-transparent by calling set_trans(5)` рассмотрим стандартные анализаторы `Elastic Search`: 217 | 218 | 1. *Стандартный анализатор* (`Standard analyzer`) - он используется в `Elastic Search` по-умолчанию. Среди его основных особенностей: разбивает текст по словам, удаляет большую часть пунктуации и приводит все токены к нижнему регистру. В результате чего он вернет следующий набор токенов: `set, the, shape, to, semi, transparent, by, calling, set_trans, 5` 219 | 2. *Простой анализатор* (`Simple analyzer`) - разбивает текст на слова по любому не буквенному символу и приводит к нижнему регистру токены. Результатом его работы будет: `set, the, shape, to, semi, transparent, by, calling, set, trans` 220 | 3. *Whitespace analyzer* - разбивает текст по пробелу на слова. Он ничего не приводит к нижнему регистру и на выходе мы получим следующий набор токенов: `Set, the, shape, to, semi-transparent, by, calling, set_trans(5)` 221 | 4. *Языковый анализатор* (`Language analyzers`) - разбивает текст на слова, фильтрует "мусор" и обрезает окончания учитывая морфолигию языка. Доступен для [многих языков](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html). Результатами его работы будет следующий набор токенов: `set, shape, semi, transpar, call, set_tran, 5` 222 | 223 | #### Когда необходимо использовать анализаторы? 224 | 225 | Обычно, когда речь заходит о полнотекстовом поиске, причем очень важным является использование одинакового анализатора при индексации документа и при его поиске, чтобы у нас токены формировались по одинаковым правилам. 226 | 227 | #### Тестирование анализаторов 228 | 229 | Все это очень хорошо, но как можно проверить срабатывание анализатора на кокретном тексте? Для того, чтобы нам помочь в этом нелегком деле, в `Elastic Search` есть специальное API: 230 | 231 | ```bash 232 | GET /_analyze?analyzer=standard 233 | Text to analyze 234 | ``` 235 | 236 | или 237 | 238 | ```bash 239 | GET /_analyze?analyzer=standard&text=Text to analyze 240 | ``` 241 | 242 | Результатом выполнения данного запроса будет следующее: 243 | 244 | ```javascript 245 | { 246 | "tokens": [ 247 | { 248 | "token": "text", 249 | "start_offset": 0, 250 | "end_offset": 4, 251 | "type": "", 252 | "position": 1 253 | }, 254 | { 255 | "token": "to", 256 | "start_offset": 5, 257 | "end_offset": 7, 258 | "type": "", 259 | "position": 2 260 | }, 261 | { 262 | "token": "analyze", 263 | "start_offset": 8, 264 | "end_offset": 15, 265 | "type": "", 266 | "position": 3 267 | } 268 | ] 269 | } 270 | ``` 271 | 272 | Где `token` собственно и является тем значением, которое будет проиндексировано. Поле `position` - порядок, в котором токены встречаются в оригинальном тексте. 273 | 274 | #### Создание собственных анализаторов 275 | 276 | Создать их можно при помощи запроса следующего типа: 277 | 278 | ```javascript 279 | PUT /my_index 280 | { 281 | "settings": { 282 | "analysis": { 283 | "char_filter": { ... custom character filters ... }, 284 | "tokenizer": { ... custom tokenizers ... }, 285 | "filter": { ... custom token filters ... }, 286 | "analyzer": { ... custom analyzers ... } 287 | } 288 | } 289 | } 290 | ``` 291 | 292 | Анализаторы можно создавать на основе стандартных фильтров и токенайзеров или описывать свои собственные. Их список можно посмотреть по [ссылке](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis.html). 293 | 294 | Рассмотрим как создать анализатор для марок и моделей автомобилей. Основные требования к нему - учитывать безграмотность пользователя и разные слэнговые варианты названий. 295 | 296 | Для этого нам понадобится [Whitespace Tokenizer](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-whitespace-tokenizer.html), [Phonetic Token Filter](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-phonetic-tokenfilter.html) и [Synonym Token Filter](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-synonym-tokenfilter.html). 297 | 298 | ```javascript 299 | PUT /my_index 300 | { 301 | "settings": { 302 | "analysis": { 303 | "filter": { 304 | "synonym": { 305 | "type": "synonym", 306 | "synonyms": [ 307 | "беха, бумер => bmw", 308 | "mercedes => mercedes-benz" 309 | ] 310 | }, 311 | "metaphone": { 312 | "type": "phonetic", 313 | "encoder": "metaphone", 314 | "replace": false 315 | } 316 | }, 317 | "analyzer": { 318 | "mark_and_model": { 319 | "type": "custom", 320 | "tokenizer": "whitespace", 321 | "filter": [ 322 | "synonym", 323 | "metaphone" 324 | ] 325 | } 326 | } 327 | } 328 | } 329 | } 330 | ``` 331 | 332 | Теперь можно проверить как наш анализатор будет справляться со своими обязанностями. 333 | 334 | Анализ слова `бумер`: 335 | 336 | ```bash 337 | GET /my_index/_analyze?analyzer=mark_and_model&text=%D0%B1%D1%83%D0%BC%D0%B5%D1%80 338 | ``` 339 | 340 | ```javascript 341 | { 342 | "tokens": [ 343 | { 344 | "token": "BM", 345 | "start_offset": 0, 346 | "end_offset": 5, 347 | "type": "SYNONYM", 348 | "position": 1 349 | }, 350 | { 351 | "token": "bmw", 352 | "start_offset": 0, 353 | "end_offset": 5, 354 | "type": "SYNONYM", 355 | "position": 1 356 | } 357 | ] 358 | } 359 | ``` 360 | 361 | Как видим, анализатор сгенерировал два токена - `BM` и `bmw`. 362 | 363 | Анализ слов `e-class e-klasse`: 364 | ```bash 365 | GET /my_index/_analyze?analyzer=mark_and_model&text=e-class e-klasse 366 | ``` 367 | 368 | ```javascript 369 | { 370 | "tokens": [ 371 | { 372 | "token": "EKLS", 373 | "start_offset": 0, 374 | "end_offset": 7, 375 | "type": "word", 376 | "position": 1 377 | }, 378 | { 379 | "token": "e-class", 380 | "start_offset": 0, 381 | "end_offset": 7, 382 | "type": "word", 383 | "position": 1 384 | }, 385 | { 386 | "token": "EKLS", 387 | "start_offset": 8, 388 | "end_offset": 16, 389 | "type": "word", 390 | "position": 2 391 | }, 392 | { 393 | "token": "e-klasse", 394 | "start_offset": 8, 395 | "end_offset": 16, 396 | "type": "word", 397 | "position": 2 398 | } 399 | ] 400 | } 401 | ``` 402 | 403 | Мы получили по два токена на каждое слово. В данном конкретном случае нас интересует то, что не смотря на разность в написании двух слов, токен `EKLS` для них одинаков - это означает, что если мы проиндексируем данное слово используя фонетический анализатор, то мы потом сможем его найти используя различные варианты написания, но которые звучат похоже. 404 | 405 | Как бонус, ко всему этому можно докрутить [Unique Token Filter](https://www.elastic.co/guide/en/elasticsearch/reference/2.0/analysis-unique-tokenfilter.html) и [Shingle Token Filter](https://www.elastic.co/guide/en/elasticsearch/reference/2.0/analysis-shingle-tokenfilter.html) чтобы получить уникальные токены и токены-словосочетания для правильной индексации фраз `бумер x5`, например. 406 | -------------------------------------------------------------------------------- /data/entities.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = [ 4 | { 5 | "title": "Yesterday", 6 | "author": "The Beatles", 7 | "album": "Help!", 8 | "genre": "rock" 9 | }, 10 | { 11 | "title": "Smells Like Teen Spirit", 12 | "author": "Nirvana", 13 | "album": "Nevermind", 14 | "genre": "rock" 15 | }, 16 | { 17 | "title": "We Will Rock You", 18 | "author": "Queen", 19 | "album": "News of the World", 20 | "genre": "rock" 21 | }, 22 | { 23 | "title": "Nothing Else Matters", 24 | "author": "Metallica", 25 | "album": "Metallica", 26 | "genre": "rock" 27 | }, 28 | { 29 | "title": "Money, Money, Money", 30 | "author": "ABBA", 31 | "album": "Arrival", 32 | "genre": "pop" 33 | }, 34 | { 35 | "title": "Eye of the Tiger", 36 | "author": "Survivor", 37 | "album": "Eye of the Tiger", 38 | "genre": "pop" 39 | }, 40 | { 41 | "title": "My Heart Will Go On", 42 | "author": "Celine Dion", 43 | "album": "Let's Talk About Love", 44 | "genre": "pop" 45 | }, 46 | { 47 | "title": "Thriller", 48 | "author": "Michael Jackson", 49 | "album": "Thriller", 50 | "genre": "pop" 51 | } 52 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elastic-search-learning", 3 | "version": "1.0.0", 4 | "description": "[План лекций](https://docs.google.com/a/ria.com/document/d/1elQ5KLu-8UyaRsdJfhIN_BuxjQrMHgW5kVVU7xDw4TQ/edit?usp=sharing)", 5 | "main": "index.js", 6 | "dependencies": { 7 | "request": "^2.65.0" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "generate-base": "node scripts/generate_base.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/ria-com/elastic-search-learning.git" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/ria-com/elastic-search-learning/issues" 22 | }, 23 | "homepage": "https://github.com/ria-com/elastic-search-learning#readme" 24 | } 25 | -------------------------------------------------------------------------------- /scripts/generate_base.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var entities = require("../data/entities"), 4 | request = require("request"), 5 | elasticSearch = { 6 | "host": "localhost", 7 | "port": 9200 8 | }; 9 | 10 | 11 | /* Удаляем предыдущие данные */ 12 | new Promise((resolve, reject) => { 13 | request({ 14 | "url": `http://${elasticSearch.host}:${elasticSearch.port}/songster`, 15 | "method": "DELETE" 16 | }, (err, response, body) => { 17 | console.log("body --> ", body, err); 18 | err ? reject(err) : resolve(body); 19 | }); 20 | }) 21 | .then((body) => { 22 | console.log(23); 23 | return new Promise((resolve, reject) => { 24 | /* Создаем индекс */ 25 | request({ 26 | "url": `http://${elasticSearch.host}:${elasticSearch.port}/songster`, 27 | "method": "POST", 28 | "body": { 29 | "mappings": { 30 | "tracks": { 31 | "properties": { 32 | "title": {"type": "string"}, 33 | "author": {"type": "string"}, 34 | "album": {"type": "string"}, 35 | "genre": {"type": "string"} 36 | } 37 | } 38 | } 39 | }, 40 | "json": true 41 | }, (err, response, body) => { 42 | console.log("body --> ", body); 43 | err ? reject(err) : resolve(body); 44 | }); 45 | }); 46 | }) 47 | .then((body) => { 48 | return Promise.all(entities.map((item) => { 49 | return new Promise((resolve, reject) => { 50 | request({ 51 | "url": `http://${elasticSearch.host}:${elasticSearch.port}/songster/tracks/${item.title}`, 52 | "method": "POST", 53 | "json": true, 54 | "body": item 55 | }, (err, response, body) => { 56 | console.log("body --> ", body); 57 | err ? reject(err) : resolve(body); 58 | }) 59 | }); 60 | })); 61 | }) 62 | .then(() => { 63 | console.log('Экспорт завершен!') 64 | process.exit(0); 65 | }, (err) => { 66 | console.log('Произошла ошибка --> ', err); 67 | process.exit(1); 68 | }); 69 | 70 | 71 | --------------------------------------------------------------------------------