Elasticsearch pamati

Elasticsearch — meklēšanas dzinējs ar json rest api, kas izmanto Lucene un ir sarakstīts Java valodā. Visu šo dzinēju priekšrocību apraksts ir pieejams oficiālajā mājaslapā. Turpmāk tekstā mēs apzīmēsim Elasticsearch kā ES.

Šādi dzinēji tiek izmantoti, veicot sarežģītu meklēšanu dokumentu bāzē. Piemēram, meklēšana, ņemot vērā valodas morfoloģiju vai meklēšana pēc ģeogrāfiskajām koordinātēm.

Šajā rakstā es pastāstīšu par ES pamatiem, izmantojot bloga ierakstu indeksēšanu. Parādīšu, kā filtrēt, kārtot un meklēt dokumentus.

Lai nebūtu atkarīgs no operētājsistēmas, visus pieprasījumus uz ES es veicu, izmantojot CURL. Ir pieejams arī Google Chrome paplašinājums ar nosaukumu sense.

Tekstā ir norādītas saites uz dokumentāciju un citiem avotiem. Beigās ir novietotas saites ātrai piekļuvei dokumentācijai. Nezināmo terminu definīcijas var izlasīt glosārijā.

ES uzstādīšana

Tam mums vispirms būs nepieciešama Java. Izstrādātāji ieteicams uzstādīt Java versijas, kas ir jaunākas par Java 8 update 20 vai Java 7 update 55.

ES izplatījums ir pieejams izstrādātāja vietnē. Pēc arhīva izpakošanas jāpalaiž bin/elasticsearch. Ir pieejami arī pakeši apt un yum. Ir oficiāls attēls Docker. Sīkāk par uzstādīšanu.

Pēc uzstādīšanas un palaišanas pārbaudīsim darbību:

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

curl -X GET $ES_URL

Mums pienāks aptuveni šāds atbildes teksts:

{
  "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" : "Jūs zināt, meklēšanai"
}

Indeksēšana

Pievienosim ierakstu ES:

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

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>Smieklīga stāsts par kaķēniem<p>",
  "tags": [
    "kaķēni",
    "smieklīga stāsts"
  ],
  "published_at": "2014-09-12T20:44:42+00:00"
}'

servera atbilde:

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

ES automātiski izveidoja indeksu blog un tipu post. Var veikt nosacītu analoģiju: indekss — tas ir datu bāze, bet tips — tabula šajā datu bāzē. Katram tipam ir sava shēma — mapping, tāpat kā attiecīgajai tabulai. Mapping tiek ģenerēts automātiski dokumenta indeksēšanas laikā:

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

Servera atbildē es pievienoju komentāros lauku vērtības par indeksēto dokumentu:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Smieklīga stāsts par kaķēniem<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": ["kaķīši", "jautra stāsts"] */
          "tags" : {
            "type" : "string"
          },
          /*  "title": "Jautrie kaķīši" */
          "title" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

Jāatzīmē, ka ES neveic atšķirības starp vienu vērtību un vērtību masīvu. Piemēram, lauks title satur tikai virsrakstu, savukārt lauks tags — virkni rindu, lai gan tie mapingu attēlo vienādi.
Vēlāk mēs runāsim par mapping detalizētāk.

Pieprasījumi

Dokumenta iegūšana pēc tā 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>Smiekl\u012bga st\u0101sts par ka\u0137\u0113niem<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"
    }
}

Atbildē parādījās jauni atslēgas: _version un _source. Vispār, visi atslēgas, kas sākas ar _ attiecās uz sistēmas parametriem.

Atslēga _version parāda dokumenta versiju. Tas ir nepieciešams optimistiskās bloķēšanas mehānisma darbībai. Piemēram, mēs vēlamies mainīt dokumentu ar versiju 1. Mēs nosūtām modificēto dokumentu un norādām, ka tas ir labots dokuments ar versiju 1. Ja kāds cits arī ir rediģējis dokumentu ar versiju 1 un nosūtījis izmaiņas pirms mums, tad ES nepieņems mūsu izmaiņas, jo tas glabā dokumentu ar versiju 2.

Atslēga _source satur to dokumentu, kuru mēs indeksējām. ES š value nedrīkst izmantot meklēšanas operācijām, jo meklēšanai tiek izmantoti indeksi. Lai ietaupītu vietu, ES glabā saspiestu sākotnējo dokumentu. Ja mums nepieciešams tikai id, nevis viss sākotnējais dokuments, varat izslēgt sākotnējā dokumenta glabāšanu.

Ja mēs nevēlamies papildu informāciju, varam iegūt tikai _source saturu:

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>Smiekl\u012bga st\u0101sts par ka\u0137\u0113niem<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"
}

Arī var izvēlēties tikai noteiktas lauki:

# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Priecīgie kaķēni"
  }
}

Mēs indeksēsim vēl dažus ierakstus un veiksim sarežģītākus pieprasījumus.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Priecīgās kucēni",
  "content": "<p>Smieklīga stāsts par kucēniem<p>",
  "tags": [
    "kucēni",
    "smieklīga stāsts"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Kā man parādījās kaķēns",
  "content": "<p>Sirdi plosoša stāsts par nabaga kaķēnu no ielas<p>"
  "tags": [
    "kaķēni"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Kārtošana

# найдем последний пост по дате публикации и извлечем поля 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" : "Priecīgie kaķēni",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

Mēs izvēlējāmies pēdējo ierakstu. izmērs ierobežo dokumentu skaitu rezultātos. kopā parāda kopējo dokumentu skaitu, kas atbilst pieprasījumam. kārtošana rezultātos satur veselo skaitļu masīvu, pēc kura tiek veikta kārtošana. T.i., datums tika pārvērsts veselos skaitļos. Vairāk par kārtošanu var izlasīt dokumentācijā.

Filtri un pieprasījumi

ES kopš versijas 2 neatšķir filtrus un pieprasījumus, vietā tā ievieš kontekstu jēdzienu.
Pieprasījuma konteksts atšķiras no filtra konteksta ar to, ka pieprasījums ģenerē _score un netiek kešots. Kas ir _score, es parādīšu vēlāk.

Filtrēšana pēc datuma

Izmantosim pieprasījumu diapazons filtra kontekstā:

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

Filtrēšana pēc tagiem

Izmantojam terma pieprasījumu lai meklētu dokumentu id, kas satur noteiktu vārdu:

# найдем все документы, в поле 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" : "Jautrie kaķēni",
        "tags" : [ "kaķēni", "smieklīga stāsts" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Kā man parādījās kaķēns",
        "tags" : [ "kaķēni" ]
      }
    } ]
  }
}

Pilntekstīga meklēšana

Trijos mūsu dokumentos lauka content ir sekojošais:

  • <p>Smieklīga stāsts par kaķēniem<p>
  • <p>Smieklīga stāsts par kucēniem<p>
  • <p>Sirdi plosoša stāsts par nabaga kaķēnu no ielas<p>

Izmantojam saskanības vaicājums lai meklētu dokumentu id, kas satur noteiktu vārdu:

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

Tomēr, ja meklējam „stāsti” lauka content, mēs neko neatradīsim, jo indeksā ir tikai oriģinālās vārdu formas, nevis to pamatformas. Lai veiktu kvalitatīvu meklēšanu, nepieciešams iestatīt analizatoru.

Lauks _score parāda relevancija. Ja pieprasījums tiek izpildīts filter kontekstā, tad vērtība _score vienmēr būs vienāda ar 1, kas nozīmē pilnīgu atbilstību filtram.

Analizatori

Analizatori ir nepieciešami, lai pārvērstu sākotnējo tekstu tokenu kopā.
Analizatori sastāv no viena Tokenizer un vairākiem izvēles TokenFilters. Tokenizer var būt pirms vairākiem CharFilters. Tokenizer sadala sākotnējo virkni tokenos, piemēram, pēc atstarpēm un interpunkcijas simboliem. TokenFilter var mainīt tokenus, izdzēst vai pievienot jaunus, piemēram, atstāt tikai vārda pamatformu, izņemt prievārdus, pievienot sinonīmus. CharFilter — pilnībā maina sākotnējo virkni, piemēram, izgriež html tagus.

ES ir vairāki standarta analizatori. Piemēram, analizators russian.

Izmantojam api un aplūkosim, kā analizatori standard un russian pārvērš virkni „Jautri stāsti par kaķēniem”:

# используем анализатор 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" : "jautri",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "stāsti",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "par",
    "start_offset" : 16,
    "end_offset" : 19,
    "type" : "",
    "position" : 2
  }, {
    "token" : "kaķēniem",
    "start_offset" : 20,
    "end_offset" : 28,
    "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" : "prieka",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "stāsts",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "kaķis",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

Standarta analizators sadalīja virkni pa atstarpēm un pārvērta visu mazos burtiem, analizators russian — izslēdza nenozīmīgus vārdus, pārvērta mazos burtos un atstāja vārdu pamatus.

Apskatīsim, kādi Tokenizer, TokenFilters, CharFilters tiek izmantoti analizatorā 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 nav */
    }
  }
}

Aprakstīsim savu analizatoru, balstoties uz russian, kas izgriezīs HTML tagus. Nosauksim to par default, jo analizators ar šādu nosaukumu tiks izmantots noklusējuma gadījumā.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* pievienojam HTML tagu dzēšanu */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Vispirms no sākotnējās virknes tiks izdzēsti visi HTML tagi, pēc tam to sadalīs uz tokeniem tokenizer standard, iegūtie tokeni tiks pārvērsti mazos burtos, tiks izdzēsti nenozīmīgie vārdi, un no atlikušajiem tokeniem paliks vārdu pamati.

Indeksa izveide

Iepriekš mēs aprakstījām default analizatoru. Tas tiks piemērots visiem virkņu laukiem. Mūsu posts satur tagu masīvu, attiecīgi, tagi arī tiks apstrādāti ar analizatoru. Tā kā mēs meklējam posts pēc precīzas sakritības ar tagu, nepieciešams izslēgt analīzi laukam tags.

Izveidosim indeksu blog2 ar analizatoru un kartējumu, kurā ir izslēgta analīze laukam 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"
        }
      }
    }
  }
}'

Pievienosim tos pašas 3 ziņas šajā indeksā (blog2). Es šo procesu izlaidīšu, jo tas ir līdzīgs dokumentu pievienošanai index blog.

Pilntekstiskā meklēšana ar izteiksmju atbalstu

Iepazīsimies ar vēl vienu pieprasījumu veidu:

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

Tā kā mēs izmantojam analizatoru ar krievu sakņu vārdu analīzi, šis pieprasījums atgriezīs visus dokumentus, lai gan tajos tiek minēts tikai vārds ‘vēsture’.

Pieprasījumā var būt speciāli simboli, piemēram:

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

Pieprasījuma sintakse:

+ nozīmē UN operāciju
| nozīmē OR operāciju
- noraida vienu tokenu
" ietver vairākus tokenus, lai norādītu frāzi meklēšanai
* termiņa beigās nozīmē prefiksa vaicājumu
( un ) nozīmē priekšrocības
~N pēc vārda nozīmē rediģēšanas attālumu (neskaidrību)
~N pēc frāzes nozīmē slopāšanas apjomu
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

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

Saites

PS

Ja interesē līdzīgas raksta mācības, ir idejas jauniem rakstiem vai ir priekšlikumi par sadarbību, būšu priecīgs saņemt ziņojumu privātā ziņojumā vai uz e-pastu m.kuzmin+habr@darkleaf.ru.

Avots: habr.com

Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem 🔥 Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem | ProHoster