Основи на Elasticsearch

Elasticsearch е търсачка с json rest api, използваща Lucene и написана на Java. Описание на всички предимства на този двигател можете да намерите на официален сайт. По-нататък ще наричаме Elasticsearch ES.

Подобни машини се използват за сложни търсения в база данни с документи. Например търсене, като се вземе предвид морфологията на езика или търсене по географски координати.

В тази статия ще говоря за основите на ES, като използвам примера за индексиране на публикации в блогове. Ще ви покажа как да филтрирате, сортирате и търсите документи.

За да не завися от операционната система, ще правя всички заявки към ES с помощта на CURL. Има и плъгин за google chrome, наречен смисъл.

Текстът съдържа връзки към документация и други източници. В края има връзки за бърз достъп до документацията. Дефиниции на непознати термини могат да бъдат намерени в речници.

Инсталация

За да направим това, първо се нуждаем от Java. Разработчици препоръчвам инсталирайте версии на Java, по-нови от Java 8 актуализация 20 или Java 7 актуализация 55.

Разпределението на ES е достъпно на сайт за разработчици. След като разопаковате архива, трябва да стартирате bin/elasticsearch. Също достъпно пакети за apt и yum, Има официално изображение за докер. Повече за монтажа.

След инсталиране и стартиране, нека проверим функционалността:

# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200

curl -X GET $ES_URL

Ще получим нещо подобно:

{
  "name" : "Heimdall",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "2.2.1",
    "build_hash" : "d045fc29d1932bce18b2e65ab8b297fbf6cd41a1",
    "build_timestamp" : "2016-03-09T09:38:54Z",
    "build_snapshot" : false,
    "lucene_version" : "5.4.1"
  },
  "tagline" : "You Know, for Search"
}

Индексирането

Нека добавим публикация към ES:

# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>Смешная история про котят<p>",
  "tags": [
    "котята",
    "смешная история"
  ],
  "published_at": "2014-09-12T20:44:42+00:00"
}'

отговор на сървъра:

{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : false
}

ES се създава автоматично индекс блог и тип пост. Можем да направим условна аналогия: индексът е база данни, а типът е таблица в тази база данни. Всеки тип има своя собствена схема − картография, точно като релационна таблица. Картографирането се генерира автоматично, когато документът е индексиран:

# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"

В отговора на сървъра добавих стойностите на полетата на индексирания документ в коментарите:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Смешная история про котят<p>", */ 
          "content" : {
            "type" : "string"
          },
          /* "published_at": "2014-09-12T20:44:42+00:00" */
          "published_at" : {
            "type" : "date",
            "format" : "strict_date_optional_time||epoch_millis"
          },
          /* "tags": ["котята", "смешная история"] */
          "tags" : {
            "type" : "string"
          },
          /*  "title": "Веселые котята" */
          "title" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

Струва си да се отбележи, че ES не прави разлика между една стойност и масив от стойности. Например, полето за заглавие просто съдържа заглавие, а полето за етикети съдържа масив от низове, въпреки че те са представени по същия начин в картографирането.
Ще говорим повече за картографирането по-късно.

искания

Извличане на документ по неговия идентификатор:

# извлечем документ с id 1 типа post из индекса blog
curl -XGET "$ES_URL/blog/post/1?pretty"
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Веселые котята",
    "content" : "<p>Смешная история про котят<p>",
    "tags" : [ "котята", "смешная история" ],
    "published_at" : "2014-09-12T20:44:42+00:00"
  }
}

В отговора се появиха нови ключове: _version и _source. Като цяло, всички ключове, започващи с _ са класифицирани като официални.

ключ _version показва версията на документа. Той е необходим, за да работи оптимистичният заключващ механизъм. Например искаме да променим документ, който има версия 1. Изпращаме променения документ и посочваме, че това е редакция на документ с версия 1. Ако някой друг също редактира документ с версия 1 и изпрати промени преди нас, тогава ES няма да приеме нашите промени, т.к той съхранява документа с версия 2.

ключ _source съдържа документа, който индексирахме. ES не използва тази стойност за операции за търсене, защото За търсене се използват индекси. За да спести място, ES съхранява компресиран изходен документ. Ако се нуждаем само от идентификатора, а не от целия изходен документ, тогава можем да деактивираме съхранението на източника.

Ако нямаме нужда от допълнителна информация, можем да получим само съдържанието на _source:

curl -XGET "$ES_URL/blog/post/1/_source?pretty"
{
  "title" : "Веселые котята",
  "content" : "<p>Смешная история про котят<p>",
  "tags" : [ "котята", "смешная история" ],
  "published_at" : "2014-09-12T20:44:42+00:00"
}

Можете също да изберете само определени полета:

# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Веселые котята"
  }
}

Нека индексираме още няколко публикации и да изпълним по-сложни заявки.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Веселые щенки",
  "content": "<p>Смешная история про щенков<p>",
  "tags": [
    "щенки",
    "смешная история"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Как у меня появился котенок",
  "content": "<p>Душераздирающая история про бедного котенка с улицы<p>",
  "tags": [
    "котята"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

сортиране

# найдем последний пост по дате публикации и извлечем поля title и published_at
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "size": 1,
  "_source": ["title", "published_at"],
  "sort": [{"published_at": "desc"}]
}'
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : null,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : null,
      "_source" : {
        "title" : "Веселые котята",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

Избрахме последния пост. size ограничава броя на издаваните документи. total показва общия брой документи, отговарящи на заявката. sort в изхода съдържа масив от цели числа, по които се извършва сортирането. Тези. датата беше преобразувана в цяло число. Повече информация за сортирането можете да намерите в документация.

Филтри и заявки

Вместо това ES от версия 2 не прави разлика между филтри и заявки въвежда се понятието контексти.
Контекстът на заявка се различава от контекста на филтър по това, че заявката генерира _score и не се кешира. Ще ви покажа какво е _score по-късно.

Филтриране по дата

Използваме заявката обхват в контекста на филтъра:

# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "filter": {
    "range": {
      "published_at": { "gte": "2014-09-01" }
    }
  }
}'

Филтриране по тагове

употреба термин заявка за търсене на идентификатори на документи, съдържащи дадена дума:

# найдем все документы, в поле tags которых есть элемент 'котята'
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "_source": [
    "title",
    "tags"
  ],
  "filter": {
    "term": {
      "tags": "котята"
    }
  }
}'
{
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 1.0,
      "_source" : {
        "title" : "Веселые котята",
        "tags" : [ "котята", "смешная история" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Как у меня появился котенок",
        "tags" : [ "котята" ]
      }
    } ]
  }
}

Пълнотекстово търсене

Три от нашите документи съдържат следното в полето за съдържание:

  • <p>Смешная история про котят<p>
  • <p>Смешная история про щенков<p>
  • <p>Душераздирающая история про бедного котенка с улицы<p>

употреба заявка за съвпадение за търсене на идентификатори на документи, съдържащи дадена дума:

# source: false означает, что не нужно извлекать _source найденных документов
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "_source": false,
  "query": {
    "match": {
      "content": "история"
    }
  }
}'
{
  "took" : 13,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.11506981,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "2",
      "_score" : 0.11506981
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 0.11506981
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 0.095891505
    } ]
  }
}

Ако обаче търсим „истории“ в полето за съдържание, няма да намерим нищо, т.к Индексът съдържа само оригиналните думи, а не техните корени. За да направите висококачествено търсене, трябва да конфигурирате анализатора.

Област _score предавания уместност. Ако заявката се изпълнява в контекст на филтър, тогава стойността _score винаги ще бъде равна на 1, което означава пълно съответствие с филтъра.

Анализатори

Анализатори са необходими за преобразуване на изходния текст в набор от токени.
Анализаторите се състоят от един Токенизатор и няколко по желание TokenFilters. Tokenizer може да бъде предшестван от няколко CharFilters. Токенизаторите разделят изходния низ на токени, като интервали и препинателни знаци. TokenFilter може да променя жетони, да изтрива или добавя нови, например да оставя само основата на думата, да премахва предлози, да добавя синоними. CharFilter - променя целия изходен низ, например изрязва html тагове.

ES има няколко стандартни анализатори. Например анализатор Руски.

Да се ​​възползваме API и нека да видим как стандартните и руски анализатори трансформират низа „Забавни истории за котенца“:

# используем анализатор standard       
# обязательно нужно перекодировать не ASCII символы
curl -XGET "$ES_URL/_analyze?pretty&analyzer=standard&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"
{
  "tokens" : [ {
    "token" : "веселые",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истории",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "про",
    "start_offset" : 16,
    "end_offset" : 19,
    "type" : "<ALPHANUM>",
    "position" : 2
  }, {
    "token" : "котят",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "position" : 3
  } ]
}
# используем анализатор russian
curl -XGET "$ES_URL/_analyze?pretty&analyzer=russian&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"
{
  "tokens" : [ {
    "token" : "весел",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истор",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "кот",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "position" : 3
  } ]
}

Стандартният анализатор раздели низа на интервали и преобразува всичко в малки букви, руският анализатор премахна маловажните думи, преобразува ги в малки букви и остави основата на думите.

Нека видим кой Tokenizer, TokenFilters, CharFilters използва руският анализатор:

{
  "filter": {
    "russian_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "russian_keywords": {
      "type":       "keyword_marker",
      "keywords":   []
    },
    "russian_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "russian": {
      "tokenizer":  "standard",
      /* TokenFilters */
      "filter": [
        "lowercase",
        "russian_stop",
        "russian_keywords",
        "russian_stemmer"
      ]
      /* CharFilters отсутствуют */
    }
  }
}

Нека опишем нашия анализатор, базиран на руски, който ще изреже html тагове. Нека го наречем по подразбиране, защото анализатор с това име ще се използва по подразбиране.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* добавляем удаление html тегов */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Първо, всички HTML тагове ще бъдат премахнати от изходния низ, след това стандартът на токенизатора ще го раздели на токени, получените токени ще се преместят в малки букви, незначителните думи ще бъдат премахнати, а останалите токени ще останат основата на думата.

Създаване на индекс

По-горе описахме анализатора по подразбиране. Ще се прилага за всички низови полета. Нашата публикация съдържа масив от тагове, така че таговете също ще бъдат обработени от анализатора. защото Търсим публикации по точно съвпадение с етикет, след което трябва да деактивираме анализа за полето за етикети.

Нека създадем индексен блог2 с анализатор и картографиране, в който анализът на полето за етикети е деактивиран:

curl -XPOST "$ES_URL/blog2" -d'
{
  "settings": {
    "analysis": {
      "filter": {
        "ru_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        },
        "ru_stemmer": {
          "type": "stemmer",
          "language": "russian"
        }
      },
      "analyzer": {
        "default": {
          "char_filter": [
            "html_strip"
          ],
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "ru_stop",
            "ru_stemmer"
          ]
        }
      }
    }
  },
  "mappings": {
    "post": {
      "properties": {
        "content": {
          "type": "string"
        },
        "published_at": {
          "type": "date"
        },
        "tags": {
          "type": "string",
          "index": "not_analyzed"
        },
        "title": {
          "type": "string"
        }
      }
    }
  }
}'

Нека добавим същите 3 публикации към този индекс (blog2). Ще пропусна този процес, защото... това е подобно на добавянето на документи към индекса на блога.

Пълнотекстово търсене с поддръжка на изрази

Нека да разгледаме друг тип заявка:

# найдем документы, в которых встречается слово 'истории'
# query -> simple_query_string -> query содержит поисковый запрос
# поле title имеет приоритет 3
# поле tags имеет приоритет 2
# поле content имеет приоритет 1
# приоритет используется при ранжировании результатов
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "истории",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

защото Ние използваме анализатор с руски произход, тогава тази заявка ще върне всички документи, въпреки че те съдържат само думата "история".

Заявката може да съдържа специални знаци, например:

""fried eggs" +(eggplant | potato) -frittata"

Синтаксис на заявката:

+ signifies AND operation
| signifies OR operation
- negates a single token
" wraps a number of tokens to signify a phrase for searching
* at the end of a term signifies a prefix query
( and ) signify precedence
~N after a word signifies edit distance (fuzziness)
~N after a phrase signifies slop amount
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

# получим 2 поста про котиков

Позоваването

PS

Ако се интересувате от подобни статии-уроци, имате идеи за нови статии или имате предложения за сътрудничество, тогава ще се радвам да получа съобщение в лично съобщение или по имейл [имейл защитен].

Източник: www.habr.com