Bazat e Elasticsearch

Elasticsearch është një motor kërkimi me një JSON REST API, që përdor Lucene dhe është shkruar në Java. Një përshkrim i të gjitha përfitimeve të këtij motori është i disponueshëm në faqen zyrtare të internetitQë tani e tutje, ne do t'i referohemi Elasticsearch si ES.

Motorë të tillë përdoren për kërkime komplekse në bazën e të dhënave të dokumenteve, siç është kërkimi bazuar në morfologjinë gjuhësore ose kërkimi sipas koordinatave gjeografike.

Në këtë artikull, do të trajtoj bazat e ES duke përdorur shembullin e indeksimit të postimeve në blog. Do t'ju tregoj se si të filtroni, renditni dhe kërkoni dokumente.

Për të qenë i pavarur nga sistemi operativ, do t'i bëj të gjitha kërkesat e mia ES duke përdorur CURL. Ekziston gjithashtu një plugin për Google Chrome i quajtur kuptim.

Lidhjet për dokumentacionin dhe burime të tjera ofrohen në të gjithë tekstin. Lidhjet për qasje të shpejtë në dokumentacion ofrohen në fund. Përkufizimet e termave të panjohura mund të gjenden në fjalorë.

Instalimi i ES

Për këtë na duhet së pari Java. Zhvilluesit rekomandoj Instaloni versione Java më të reja se Java 8 përditësimi 20 ose Java 7 përditësimi 55.

Shpërndarja ES është e disponueshme në uebsajti i zhvilluesitPasi të keni shpaketuar arkivin, duhet të ekzekutoni bin/elasticsearchGjithashtu në dispozicion paketa për apt dhe yum... ka imazh zyrtar për Docker. Më shumë rreth instalimit.

Pas instalimit dhe nisjes, le të kontrollojmë funksionalitetin:

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

curl -X GET $ES_URL

Do të marrim një përgjigje që duket diçka si kjo:

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

Indeksimi

Le të shtojmë një postim në 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"
}'

përgjigja e serverit:

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

ES u krijua automatikisht indeks blog dhe një tip post. Mund të nxirret një analogji e përafërt: një indeks është një bazë të dhënash dhe një lloj është një tabelë brenda asaj baze të dhënash. Çdo lloj ka skemën e vet— planifikim, njësoj si një tabelë relacionale. Hartimi gjenerohet automatikisht kur një dokument indeksohet:

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

Në përgjigjen e serverit, shtova vlerat e fushës së dokumentit të indeksuar në komente:

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

Vlen të përmendet se ES nuk bën dallim midis një vlere të vetme dhe një vargu vlerash. Për shembull, fusha e titullit përmban vetëm një titull, ndërsa fusha e etiketave përmban një varg vargjesh, edhe pse ato përfaqësohen në mënyrë identike në hartëzim.
Do të flasim për hartografinë në më shumë detaje më vonë.

Kërkesat

Nxjerrja e një dokumenti sipas ID-së së tij:

# извлечем документ с 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"
  }
}

Çelësa të rinj u shfaqën në përgjigje: _version и _sourceNë përgjithësi, të gjitha çelësat që fillojnë me _ klasifikohen si artikuj shërbimi.

Ключ _version Tregon versionin e dokumentit. Kjo është e nevojshme që mekanizmi i bllokimit optimist të funksionojë. Për shembull, duam të ndryshojmë një dokument me versionin 1. Ne e paraqesim dokumentin e modifikuar dhe tregojmë se ky është një rishikim i dokumentit me versionin 1. Nëse dikush tjetër e ka modifikuar gjithashtu dokumentin me versionin 1 dhe i ka paraqitur ndryshimet para nesh, ES nuk do t'i pranojë ndryshimet tona, pasi e ruan dokumentin me versionin 2.

Ключ _source Përmban dokumentin që indeksuam. ES nuk e përdor këtë vlerë për operacionet e kërkimit, pasi indekset përdoren për kërkim. Për të kursyer hapësirë, ES ruan një version të kompresuar të dokumentit origjinal. Nëse na duhet vetëm ID-ja dhe jo i gjithë dokumenti origjinal, mund ta çaktivizojmë ruajtjen e origjinalit.

Nëse nuk na duhen informacione shtesë, mund të marrim vetëm përmbajtjen e _source:

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

Gjithashtu mund të zgjidhni vetëm fusha të caktuara:

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

Le të indeksojmë disa postime të tjera dhe të ekzekutojmë pyetje më komplekse.

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

klasifikim

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

Ne kemi zgjedhur postimin e fundit. size kufizon numrin e dokumenteve të lëshuara. total tregon numrin total të dokumenteve që përputhen me kërkesën. sort Rezultati përmban një varg numrash të plotë me anë të të cilëve kryhet renditja. Kjo do të thotë, data është konvertuar në një numër të plotë. Mund të lexoni më shumë rreth renditjes në dokumentacionin.

Filtra dhe pyetje

ES meqenëse versioni 2 nuk bën dallim midis filtrave dhe pyetjeve, përkundrazi prezantohet koncepti i konteksteve.
Konteksti i pyetjes ndryshon nga konteksti i filtrit në atë që pyetja gjeneron një _score dhe nuk ruhet në memorien e përkohshme. Do ta shpjegoj se çfarë është _score më vonë.

Filtro sipas datës

Duke përdorur pyetjen varg në kontekstin e filtrit:

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

Filtro sipas etiketave

Ne përdorim pyetje termi Për të kërkuar ID-të e dokumenteve që përmbajnë një fjalë të caktuar:

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

Kërkimi i tekstit të plotë

Tre dokumentet tona përmbajnë sa vijon në fushën e përmbajtjes:

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

Ne përdorim pyetje për përputhjen Për të kërkuar ID-të e dokumenteve që përmbajnë një fjalë të caktuar:

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

Megjithatë, nëse kërkojmë për "histori" në fushën e përmbajtjes, nuk do të gjejmë asgjë, pasi indeksi përmban vetëm fjalët origjinale, jo rrënjët e tyre. Për të kryer një kërkim cilësor, duhet të konfigurojmë analizuesin.

Fushë _score tregon rëndësiNëse pyetja ekzekutohet në një kontekst filtri, vlera _score do të jetë gjithmonë 1, që do të thotë se filtri përputhet plotësisht.

Analizuesit

Analizuesit janë të nevojshme për të transformuar tekstin burimor në një grup tokenësh.
Analizuesit përbëhen nga një Tokenizer dhe disa opsionale Filtrat e TokenëveTokenizeri mund të paraprijë disa CharFiltersTokenizuesit e ndajnë një varg burimor në tokena, për shembull, me anë të hapësirave dhe shenjave të pikësimit. TokenFilters mund të modifikojnë tokenat, t'i heqin ato ose të shtojnë të reja, për shembull, duke lënë vetëm rrënjën e fjalës, duke hequr parafjalët ose duke shtuar sinonime. CharFilters e modifikojnë tërësisht vargun burimor, për shembull, duke hequr etiketat HTML.

Ka disa në ES analizues standardëPër shembull, një analizues rusisht.

Le të përfitojmë api dhe le të shohim se si analizuesit standardë dhe rusë e transformojnë vargun "Histori qesharake për kotele":

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

Analizuesi standard e ndau vargun në hapësira dhe e shndërroi gjithçka në shkronja të vogla, analizuesi rus hoqi fjalët e parëndësishme, i shndërroi ato në shkronja të vogla dhe la rrënjët e fjalëve.

Le të shohim se cilët Tokenizer, TokenFilters dhe CharFilters përdor analizuesi rus:

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

Le të përshkruajmë analizuesin tonë me bazë në Rusi që do të heqë etiketat HTML. Do ta quajmë atë parazgjedhje, pasi ky do të jetë analizuesi parazgjedhur.

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

Së pari, të gjitha etiketat HTML do të hiqen nga vargu burimor, pastaj ai do të ndahet në tokena nga standardi i tokenizuesit, tokenat që rezultojnë do të konvertohen në shkronja të vogla, fjalët e parëndësishme do të hiqen dhe tokenat e mbetura do të jenë rrënja e fjalës.

Krijimi i një indeksi

Më sipër, përshkruam analizuesin e parazgjedhur. Ai do të aplikohet në të gjitha fushat e vargjeve. Postimi ynë përmban një sërë etiketash, kështu që etiketat do të përpunohen gjithashtu nga analizuesi. Meqenëse po kërkojmë postime që përputhen me etiketën e saktë, duhet të çaktivizojmë analizën për fushën "etiketa".

Le të krijojmë një indeks blog2 me një analizues dhe hartëzim, në të cilin analiza e fushës së etiketave është e çaktivizuar:

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

Le t'i shtojmë të tre postimet e njëjta në këtë indeks (blog2). Do ta anashkaloj këtë proces, pasi është i ngjashëm me shtimin e dokumenteve në indeksin e blogut.

Kërkim me tekst të plotë me mbështetje për shprehje

Le të njihemi me një lloj tjetër pyetjeje:

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

Meqenëse po përdorim një analizues me origjinë ruse, kjo pyetje do të kthejë të gjitha dokumentet, edhe nëse ato përmbajnë vetëm fjalën 'histori'.

Pyetja mund të përmbajë karaktere të veçanta, për shembull:

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

Sintaksa e pyetjes:

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

Referencat

PS

Nëse jeni të interesuar për artikuj të ngjashëm mësimorë, keni ide për artikuj të rinj ose keni ndonjë sugjerim bashkëpunimi, do të jem i lumtur të dëgjoj nga ju me anë të një mesazhi privat ose email-i në m.kuzmin+habr@darkleaf.ru.

Burimi: www.habr.com