Основы Elasticsearch

Elasticsearch — поисковый движок с json rest api, использующий Lucene и написанный на Java. Описание всех преимуществ этого движка доступно на հունիսի պաշտոնական կայքում. Далее по тексту будем называть Elasticsearch как ES.

Подобные движки используются при сложном поиске по базе документов. Например, поиск с учетом морфологии языка или поиск по geo координатам.

В этой статье я расскажу про основы ES на примере индексации постов блога. Покажу как фильтровать, сортировать и искать документы.

Чтобы не зависеть от операционной системы, все запросы к ES я буду делать с помощью CURL. Также есть плагин для google chrome под названием sense.

По тексту расставлены ссылки на документацию и другие источники. В конце размещены ссылки для быстрого доступа к документации. Определения незнакомых терминов можно прочитать в глоссарии.

Установка ES

Для этого нам сначала потребуется Java. Разработчики рекомендуют установить версии Java новее, чем Java 8 update 20 или Java 7 update 55.

Дистрибутив ES доступен на сайте разработчика. После распаковки архива нужно запустить bin/elasticsearch. Также доступны пакеты для apt и yum. Ապրանքներ официальный image для docker. Подробнее об установке.

После установки и запуска проверим работоспособность:

# для удобства запомним адрес в переменную
#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 автоматически создал индекс blog и тип post. Можно провести условную аналогию: индекс — это база данных, а тип — таблица в этой БД. Каждый тип имеет свою схему — mapping, также как и реляционная таблица. Mapping генерируется автоматически при индексации документа:

# Получим 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 не делает различий между одиночным значением и массивом значений. Например, поле title содержит просто заголовок, а поле tags — массив строк, хотя они представлены в mapping одинаково.
Позднее мы поговорим о маппинге более подобно.

Запросы

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

# извлечем документ с id 1 типа post из индекса blog
curl -XGET "$ES_URL/blog/post/1?pretty"
{
    "_index": "blog",
    "_type": "post",
    "_id": "1",
    "_version": 1,
    "found": true,
    "_source": {
        "title": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
        "content": "<p>\u053f\u0561\u057f\u0561\u056f\u0565\u0580\u0563\u0561\u056f\u0561\u0576 \u057a\u0561\u057f\u0574\u0578\u0582\u0569\u0575\u0578\u0582\u0576 \u056f\u056f\u056b\u056f\u0576\u0565\u0580\u056b \u0574\u0561\u057d\u056b\u0576<p>",
        "tags": [
            "\u043a\u043e\u0442\u044f\u0442\u0430",
            "\u0441\u043c\u0435\u0448\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f"
        ],
        "published_at": "2014-09-12T20:44:42+00:00"
    }
}

В ответе появились новые ключи: _version և _source. Вообще, все ключи, начинающиеся с _ относятся к служебным.

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

Ключ _source նա պարունակում է այն փաստաթուղթը, որը մենք ինդեքսավորված ենք: ES այս արժեքը չի օգտագործում որոնման գործողությունների համար, քանի որ որոնման համար օգտագործվում են ինդեքսներ: Լիարժեքության պահպանման համար ES-ն պահում է սեղմված առաջին փաստաթուղթը: Եթե մեզ միայն id է պետք, այլ ոչ թե ամբողջ սկզբնական փաստաթուղթ, ապա կարող ենք անջատել սկիզբի պահպանումը:

Եթե մեզ ավելորդ տեղեկություն չի պետք, կարող ենք ստանալ միայն _source բովանդակությունը:

curl -XGET "$ES_URL/blog/post/1/_source?pretty"
{
    "title": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
    "content": "<p>\u053f\u0561\u057f\u0561\u056f\u0565\u0580\u0563\u0561\u056f\u0561\u0576 \u057a\u0561\u057f\u0574\u0578\u0582\u0569\u0575\u0578\u0582\u0576 \u056f\u056f\u056b\u056f\u0576\u0565\u0580\u056b \u0574\u0561\u057d\u056b\u0576<p>",
    "tags": [
        "\u043a\u043e\u0442\u044f\u0442\u0430",
        "\u0441\u043c\u0435\u0448\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f"
    ],
    "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 սահմանափակում է փաստաթղթի քանակը արդյունքներում: բարելավված ցուցադրում է ընդհանուր փաստաթղթերի թիվը, որոնք համապատասխանում են որոնմանը: դասավորություն արդյունքում պարունակում է ամբողջ թվերի զանգված, ըստ որի կատարվում է դասավորություն: Այսինքն՝ ամսաթիվը վերածվել է ամբողջ թվի: Դասավորության մասին ավելի մանրամասն կարող եք կարդալ փաստաթղթավորումը.

Ֆիլտրեր և հարցումներ

ES 2-րդ տարբերակից չի տարբերակում ֆիլտրերն ու հարցումները, փոխարենը ճանաչվում է կոնտեքստերի հասկացություն.
Հարցման կոնտեքստը տարբեր է ֆիլտրի կոնտեքստից, քանի որ հարցումը արտադրում է _score և չի պահվում: Ի՞նչ է _score-ն, դա ցույց կտամ ավելի ուշ:

Ամսաթվի միջոցով ֆիլտրում

Օգտագործում ենք հարցում շրջան ֆիլտրի կոնտեքստում։

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

Տեգերի միջոցով ֆիլտրում

Մեր կողմից օգտագործվում է term հարցում փաստաթղթերի id որոնման համար, որոնք պարունակում են նշված բառը:

# найдем все документы, в поле 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" : [ "կատուներ" ]
      }
    } ]
  }
}

Նախագծային որոնում

Մեր երեք փաստաթրեքները պարունակում են content դաշտում հետևյալը:

  • <p>Կատակերգական պատմություն կկիկների մասին<p>
  • <p>Մռայլ պատմություն շան երեխաների մասին<p>
  • <p>Սրտաճեղող պատմություն բախտի կորած կատվիկի մասին, որը փողոցից է<p>

Մեր կողմից օգտագործվում է match հարցում փաստաթղթերի id որոնման համար, որոնք պարունակում են նշված բառը:

# 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
    } ]
  }
}

Այլապես, եթե որոնենք «էլեկտրոնային» content դաշտում, մենք ոչինչ չենք գտնելու, քանի որ ինդեքսում գոյություն ունեն միայն բնագիր բառերը, այլ ոչ թե դրանց հիմքերը: որ ընտրելու համար որակապես որոնում, անհրաժեշտ են կարգավորել վերլուծողը:

Դաշտը _score ցուցադրում է առնչություն. Եթե հարցումը իրականացվում է ֆիլտրի կոնտեքստում, ապա _score արժեքը միշտ կլինի 1, ինչը նշանակում է լիարժեք համապատասխանություն ֆիլտրին:

Վերլուծողներն

Վերլուծողներն պահանջվում են սկզբնական տեքստը տոկենների խմբի վերածելու համար։
Վերլուծիչները բաղկացած են մեկ Tokenizer և մի քանի օպցիոնալ TokenFilters։ Tokenizer-ը կարող է նախորդորդել մի քանի CharFilters։ Tokenizer-ը բաժանում է սկզբնական տողը տոկենների, օրինակ՝ բացատների և նշանների վրա։ TokenFilter-ը կարող է փոխել տոկենները, ջնջել կամ ավելացնել նորերը, օրինակ՝ թողնելով միայն բառի հիմքը, հեռացնելով նախադասությունները, ավելացնելով շատաբանությունները։ CharFilter-ը լիովին փոխում է սկզբնական տողը, օրինակ՝ դուրս է հանում html պիտակները։

ES-ում կան մի քանի ստանդարտ վերլուծիչներ։ Օրինակ, վերլուծիչ russian.

Նվազագույնը կօգտագործենք api և կտաֆիլնենք, թե ինչպես են standard և russian վերլուծիչները զտում «Веселые истории про котят» տողը։

# используем анализатор 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" : "",
    "position" : 0
  }, {
    "token" : "истории",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "про",
    "start_offset" : 16,
    "end_offset" : 19,
    "type" : "",
    "position" : 2
  }, {
    "token" : "котят",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "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" : "",
    "position" : 0
  }, {
    "token" : "истор",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "кот",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

Ստանդարտ վերլուծիչը բաժանում է տողը բացատների վրա և տեղափոխում է ամեն ինչ փոքրատառ, russian վերլուծիչը՝ հեռացնում է աննշան բառերը, է փաստում փոքրատառ և թողնում է բառերի հիմքերը։

Կտեսնենք, թե որ Tokenizer, TokenFilters, CharFilters-ն է օգտագործում russian վերլուծիչը։

{
  "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 բացակայում են */
    }
  }
}

Կզարմանաք մեր վերլուծիչը, որը հիմնված է russian-ում, որը կտա html պիտակները։ Մենք կնվիրենք նրան default, քանի որ վերլուծիչի այս անունով կլինի օգտագործել Ջյայարդաս։

{
  "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 պիտակները, ապա այն կտնտվի token.circular_disable, ստացված տոկենները կվարկեն փոքրալում, զգացվում է ան նշանակալի բառեր և մնացած տոկեններից կմնա բառի հիմքը։

Ինդեքսի ստեղծում

Մեր վերևում նկարագրված է default անալիզատորը: Այն կիրառվելու է բոլոր տողային ոլորտներում: Մեր պոստը պարունակում է թեգերի զանգված, համապատասխանաբար, թեգերը նույնպես կարտահայտվեն անալիզատորով: Քանի որ մենք կատարում ենք պոստերի որոնում ըստ ճշգրիտ համապատասխանության թեգին, պետք է անջատենք անալիզը tags ոլորտի համար:

Ստեղծենք blog2 ինդեքս` անալիզատորով և թերթիկով, որտեղ անջատված է tags ոլորտի անալիզը:

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): Ես կթողնեմ այս գործընթացը, քանի որ այն նման է փաստաթղթերի ավելացմանն blog ինդեքսում:

Լիալուսանկարային որոնում արտահայտությունների աջակցության հետ

Եկեք ծանոթանանք մեկ այլ տիպի запросներին:

# найдем документы, в которых встречается слово 'истории'
# 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"

Запросի սինտաքսը:

+ նշանավորում է AND գործողությունը
| նշանավորում է OR գործողությունը
- հակադարձում է մեկ նշան
" փաթաթում է մի քանի նշան՝ արտահայտություն՝ որոնման համար
* կրում է տերմինի վերջում՝ նախանշանի հարց
( և ) նշում են առաջնահերթությունը
~N բառի հետևում նշում է խմբավորմանը (մեղմություն)
~N արտահայտության հետևում նշում է հայտնի գումարը
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

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

Հղումներ

ՊՍ

Եթե նման հոդված-խոսքեր ձեզ հետաքրքիր են, ունեք նոր հոդվածների գաղափարներ կամ առաջարկություններ համագործակցության մասին, ուրախ կլինեմ հաղորդագրություն ստանալ անձամբ կամ էլ m.kuzmin+habr@darkleaf.ru:

Ընտանիք: habr.com

Գնել հուսալի հյուրընկալում DDoS պաշտպանությամբ, VPS VDS սերվերներով 🔥 Գնել հուսալի հյուրընկալում DDoS պաշտպանությամբ, VPS VDS սերվերներով | ProHoster