Elasticsearch Basics

Elasticsearch is in sykmasine mei json rest api, mei Lucene en skreaun yn Java. In beskriuwing fan alle foardielen fan dizze motor is beskikber op offisjele webside. Yn wat folget sille wy ferwize nei Elasticsearch as ES.

Fergelykbere motors wurde brûkt foar komplekse sykopdrachten yn in dokumintdatabase. Sykje bygelyks mei de morfology fan 'e taal of sykje op geokoordinaten.

Yn dit artikel sil ik prate oer de basis fan ES mei it foarbyld fan yndeksearjen fan blogposten. Ik sil jo sjen litte hoe't jo dokuminten filterje, sortearje en sykje.

Om net ôfhinklik te wêzen fan it bestjoeringssysteem, sil ik alle oanfragen oan ES meitsje mei CURL. D'r is ek in plugin foar google chrome neamd sin.

De tekst befettet keppelings nei dokumintaasje en oare boarnen. Oan 'e ein binne d'r keppelings foar rappe tagong ta de dokumintaasje. Definysjes fan ûnbekende termen kinne fûn wurde yn wurdlisten.

Ynstallaasje fan ES

Om dit te dwaan, moatte wy earst Java hawwe. Untwikkelders oanbefelje ynstallearje Java ferzjes nijer dan Java 8 update 20 of Java 7 update 55.

De ES distribúsje is beskikber by developer site. Nei it útpakke fan it argyf moatte jo rinne bin/elasticsearch. Ek beskikber pakketten foar apt en yum... dêr is offisjele ôfbylding foar docker. Mear oer ynstallaasje.

Nei ynstallaasje en lansearring, litte wy de funksjonaliteit kontrolearje:

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

curl -X GET $ES_URL

Wy krije sa'n ding as dit:

{
  "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"
}

Yndeksearje

Litte wy in post tafoegje oan 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"
}'

tsjinner antwurd:

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

ES automatysk oanmakke yndeks blog en Typ peal. Wy kinne in betingste analogy tekenje: in yndeks is in databank, en in type is in tabel yn dizze databank. Elk type hat syn eigen skema - mappen, krekt as in relasjonele tabel. Mapping wurdt automatysk oanmakke as it dokumint wurdt yndeksearre:

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

Yn 'e serverantwurd haw ik de wearden tafoege fan' e fjilden fan it yndeksearre dokumint yn 'e opmerkingen:

{
  "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"
          }
        }
      }
    }
  }
}

It is de muoite wurdich op te merken dat ES gjin ûnderskied makket tusken in inkele wearde en in array fan wearden. Bygelyks, it titelfjild befettet gewoan in titel, en it tagsfjild befettet in array fan snaren, hoewol se op deselde wize fertsjintwurdige binne yn mapping.
Wy sille letter mear oer mapping prate.

Fersiken

In dokumint ophelje troch syn 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" : "Веселые котята",
    "content" : "<p>Смешная история про котят<p>",
    "tags" : [ "котята", "смешная история" ],
    "published_at" : "2014-09-12T20:44:42+00:00"
  }
}

Nije toetsen ferskynden yn it antwurd: _version и _source. Yn it algemien, alle kaaien begjinnend mei _ wurde klassifisearre as offisjele.

Key _version toant de dokumintferzje. It is nedich foar it optimistyske slotmeganisme om te wurkjen. Wy wolle bygelyks in dokumint feroarje dat ferzje 1 hat. Wy stjoere it feroare dokumint yn en jouwe oan dat dit in bewurking is fan in dokumint mei ferzje 1. As in oar ek in dokumint mei ferzje 1 bewurke hat en feroarings foar ús yntsjinne, dan ES sil net akseptearje ús feroarings, omdat it bewarret it dokumint mei ferzje 2.

Key _source befettet it dokumint dat wy yndeksearre. ES brûkt dizze wearde net foar sykaksjes omdat Yndeksen wurde brûkt foar sykjen. Om romte te besparjen, bewarret ES in komprimearre boarnedokumint. As wy allinich de id nedich binne, en net it heule boarnedokumint, dan kinne wy ​​boarne opslach útskeakelje.

As wy gjin ekstra ynformaasje nedich hawwe, kinne wy ​​allinich de ynhâld krije fan _source:

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

Jo kinne ek allinich bepaalde fjilden selektearje:

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

Litte wy in pear mear berjochten yndeksearje en kompleksere fragen útfiere.

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"
}'

Sortearje

# найдем последний пост по дате публикации и извлечем поля 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 ]
    } ]
  }
}

Wy hawwe de lêste post keazen. size beheint it oantal út te jaan dokuminten. total toant it totale oantal dokuminten dat oerienkomt mei it fersyk. sort yn de útfier befettet in array fan hiele getallen wêrmei sortearring wurdt útfierd. Dy. de datum waard omboud ta in hiel getal. Mear ynformaasje oer sortearjen is te finen yn dokumintaasje.

Filters en fragen

ES sûnt ferzje 2 gjin ûnderskied tusken filters en queries, ynstee it begryp kontekst wurdt yntrodusearre.
In querykontekst ferskilt fan in filterkontekst yn dat de query in _score genereart en net yn it cache stiet. Ik sil jo letter sjen litte wat _score is.

Filter op datum

Wy brûke it fersyk berik yn it kontekst fan filter:

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

Filterje op tags

Wy brûke term query om te sykjen nei dokumint-id's mei in opjûn wurd:

# найдем все документы, в поле 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" : [ "котята" ]
      }
    } ]
  }
}

Folsleine tekst sykjen

Trije fan ús dokuminten befetsje it folgjende yn it ynhâldfjild:

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

Wy brûke oerienkomst query om te sykjen nei dokumint-id's mei in opjûn wurd:

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

As wy lykwols sykje nei "ferhalen" yn it ynhâldfjild, sille wy neat fine, om't De yndeks befettet allinnich de oarspronklike wurden, net harren stammen. Om in sykopdracht fan hege kwaliteit te meitsjen, moatte jo de analysator konfigurearje.

fjild _score shows relevânsje. As it fersyk wurdt útfierd yn in filterkontekst, dan sil de _score-wearde altyd gelyk wêze oan 1, dat betsjut in folsleine oerienkomst mei it filter.

Analyzers

Analyzers binne nedich om de boarnetekst te konvertearjen yn in set tokens.
Analyzers besteane út ien Tokenizer en ferskate opsjoneel TokenFilters. Tokenizer kin wurde foarôfgien troch ferskate CharFilters. Tokenizers brekke de boarnestring yn tokens, lykas spaasjes en ynterpunksjetekens. TokenFilter kin tokens feroarje, wiskje of nije tafoegje, bygelyks allinich de stam fan it wurd litte, ferhâldingswurden fuortsmite, synonimen tafoegje. CharFilter - feroaret de hiele boarne string, bygelyks, snijt út html tags.

ES hat ferskate standert analyzers. Bygelyks, in analyzer Russysk.

Lit ús profitearje api en lit ús sjen hoe't de standert en Russyske analysatoren de snaar "grappige ferhalen oer kittens" transformearje:

# используем анализатор 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
  } ]
}

De standert analysator splitte de snaar troch spaasjes en konvertearre alles nei lytse letters, de Russyske analysator ferwidere ûnbelangrike wurden, konvertearre it nei lytse letters en liet de stam fan 'e wurden.

Litte wy sjen hokker Tokenizer, TokenFilters, CharFilters de Russyske analysator brûkt:

{
  "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 отсутствуют */
    }
  }
}

Litte wy ús analysator beskriuwe op basis fan Russysk, dy't html-tags sil snije. Litte wy it standert neame, om't in analyzer mei dizze namme wurdt standert brûkt.

{
  "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"
      ]
    }
  }
}

Earst wurde alle HTML-tags fuortsmiten fan 'e boarne-string, dan sil de tokenizer-standert it yn tokens splitse, de resultearjende tokens sille ferpleatse nei lytse letters, ûnbelangrike wurden wurde fuortsmiten, en de oerbleaune tokens sille de stam fan it wurd bliuwe.

It meitsjen fan in yndeks

Hjirboppe beskreaun wy de standert analysator. It sil jilde foar alle string fjilden. Us post befettet in array fan tags, sadat de tags ek wurde ferwurke troch de analysator. Omdat Wy sykje nei berjochten troch krekte oerienkomst mei in tag, dan moatte wy analyse foar it tagsfjild útskeakelje.

Litte wy in yndeksblog2 meitsje mei in analysator en mapping, wêryn de analyze fan it tagsfjild is útskeakele:

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"
        }
      }
    }
  }
}'

Litte wy deselde 3 berjochten tafoegje oan dizze yndeks (blog2). Ik sil dit proses weglitte om't ... it is gelyk oan it tafoegjen fan dokuminten oan 'e blogyndeks.

Folsleine tekstsykjen mei útdrukkingstipe

Litte wy nei in oar type oanfraach sjen:

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

Omdat Wy brûke in analysator mei Russyske stemming, dan sil dit fersyk alle dokuminten weromjaan, hoewol se allinich it wurd 'histoarje' befetsje.

It fersyk kin spesjale tekens befetsje, bygelyks:

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

Syntaksis oanfreegje:

+ 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 поста про котиков

referinsjes

PS

As jo ​​​​ynteressearre binne yn ferlykbere artikels-lessen, ideeën hawwe foar nije artikels, of foarstellen hawwe foar gearwurking, dan krij ik graach in berjocht yn in persoanlik berjocht of fia e-post [e-post beskerme].

Boarne: www.habr.com