Bazat e Elasticsearch

Elasticsearch — një motor kërkimi me json rest api, që përdor Lucene dhe është shkruar në Java. Përshkrimi i të gjitha avantazheve të këtij motori është në dispozicion në faqen zyrtare. Nga tani e tutje, do ta quajmë Elasticsearch si ES.

Mjetet e tillë përdoren për kërkimin e avancuar në bazën e dokumenteve. Për shembull, kërkimi duke marrë parasysh morfologjinë e gjuhës ose kërkimi sipas koordinatave geo.

Në këtë artikull do të flas për bazat e ES duke përdorur shembuj nga indeksoj postimet e blogut. Do të tregoj si të filtroj, rendit dhe kërkoj dokumente.

Për të mos qenë të varur nga sistemi operativ, të gjitha kërkesat ndaj ES do t'i bëj me ndihmën e CURL. Gjithashtu, ka një plugin për google chrome të quajtur sense.

Në tekst janë vendosur lidhje në dokumentacion dhe burime të tjera. Në fund janë vendosur lidhje për qasje të shpejtë në dokumentacion. Definicionet e terminologjive të panjohura mund të lexohen në glosarin.

Instalimi i ES

Për këtë, së pari do na nevojitet Java. Zhvilluesit rekomandojnë të instaloni versione Java më të reja se Java 8 update 20 ose Java 7 update 55.

Distribucioni i ES është në dispozicion në faqen e zhvilluesit. Pas shpërndarjes së arkivit, duhet të filloni bin/elasticsearch. Gjithashtu janë në dispozicion paketat për apt dhe yum. Ka një imazh zyrtar për docker. Më shumë rreth instalimit.

Pas instalimit dhe fillimit, do të verifikojmë funksionimin:

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

curl -X GET $ES_URL

Do të marrim një përgjigje të tillë:

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

Shtojmë një post në ES:

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

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>Një histori qesharake për kotelet<p>",
  "tags": [
    "kotelet",
    "histori qesharake"
  ],
  "published_at": "2014-09-12T20:44:42+00:00"
}'

përgjigja nga serveri:

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

ES automatikisht krijoi indeksin blog dhe tipin post. Mund të bëjmë një analogji: indeksi është baza e të dhënave, ndërsa tipi është tabela në këtë DB. Çdo tip ka skemën e tij — mapping, ashtu si dhe një tabelë relacionales. Mappingu gjenerohet automatikisht gjatë indeksimit të dokumentit:

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

Në përgjigjen e serverit kam shtuar në komente vlerat e fushave të dokumentit të indeksuar:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Një histori qesharake për kotelet<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ë vlerës së vetme dhe një vargu vlerash. Për shembull, fusha title përmban thjesht titullin, ndërsa fusha tags është një varg strings, megjithëse ato paraqiten në mapping në mënyrë të njëjtë.
Më vonë do të flasim për mapping në një mënyrë më të detajuar.

Kërkesat

Nxjerrja e dokumentit nëpërmjet id-të 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": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
        "content": "<p>Nj\u00eb histori qesharake p\u00ebr kotelet<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"
    }
}

Në përgjigje u shfaqën çelësa të rinj: _version dhe _source. Në përgjithësi, të gjithë çelësat që fillojnë me _ i përkasin shërbimeve.

Çelësi _version tregon versionin e dokumentit. Ai është i nevojshëm për funksionimin e mekanizmit të bllokimeve optimiste. Për shembull, ne duam të ndryshojmë dokumentin që ka versionin 1. Ne dërgojmë dokumentin e ndryshuar dhe tregojmë se kjo është një modifikim i dokumentit me versionin 1. Nëse dikush tjetër gjithashtu e ka redaktuar dokumentin me versionin 1 dhe ka dërguar ndryshimet para nesh, atëherë ES nuk do të pranojë ndryshimet tona, pasi ruan dokumentin me versionin 2.

Çelësi _source përmban dokumentin që ne e kemi indeksuar. ES nuk e përdor këtë vlerë për operacionet e kërkimit, pasi për kërkimet përdoren indekset. Për të kursyer hapësirë, ES ruan dokumentin burimor në formë të kompresuar. Nëse na nevojitet vetëm id, dhe jo e gjithë dokumenti burimor, atëherë mund ta çaktivizojmë ruajtjen e burimit.

Nëse nuk na nevojitet informacion shtesë, mund të marrim vetëm përmbajtjen e _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>Nj\u00eb histori qesharake p\u00ebr kotelet<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"
}

Gjithashtu mund të zgjedhim vetëm disa 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" : "Mace të gëzuara"
  }
}

Le të indeksojmë disa postime të tjerë dhe të bëjmë kërkesa më të komplikuara.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Këlyshët e qeshur",
  "content": "<p>Një histori qesharake për këlyshët<p>",
  "tags": [
    "këlyshë",
    "histori qesharake"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Si e kam bërë macja ime",
  "content": "<p>Një histori emocionuese për një kat të varfër nga rruga<p>",
  "tags": [
    "macja"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Sortimi

# найдем последний пост по дате публикации и извлечем поля 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" : "Mace të gëzuara",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

Ne kemi zgjedhur postimin e fundit. size kufizon numrin e dokumenteve në rezultat. total tregon numrin total të dokumenteve që përputhen me kërkesën. sort në rezultat përmban një varg numrash të plotë, mbi të cilët bëhet renditja. Pra, data është shndërruar në një numër të plotë. Më shumë për renditjen mund të lexoni në dokumentacionin.

Filtrat dhe kërkesat

ES që nga versioni 2 nuk i dallon filtrat nga kërkesat, në vend të kësaj ngjitet koncepti i kontekstet.
Konteksti i kërkesës ndryshon nga konteksti i filtrit në atë se kërkesa gjeneron _score dhe nuk ruhet në cache. Çfarë është _score do ta tregoj më vonë.

Filtrimi sipas dates

Përdorim kërkesën range në kontekstin e filter:

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

Filtrimi sipas etiketave

Përdorni kërkesa term për kërkimin e id të dokumenteve që përmbajnë fjalën e 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" : "Macat e lumtur",
        "tags" : [ "macat", "histori qesharake" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Si u bëra me një macë",
        "tags" : [ "macat" ]
      }
    } ]
  }
}

Kërkimi me tekst të plotë

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

  • <p>Një histori qesharake për kotelet<p>
  • <p>Një histori qesharake për këlyshët<p>
  • <p>Një histori emocionuese për një kat të varfër nga rruga<p>

Përdorni kërkimi me ndeshje për kërkimin e id të dokumenteve që përmbajnë fjalën e 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ë "historitë" në fushën e përmbajtjes, nuk do të gjejmë asgjë, pasi indekset përmbajnë vetëm fjalët origjinale dhe jo format e tyre. Për të kryer një kërkim cilësor, duhet të konfigurojmë analizuesin.

Fusha _score tregon relevanca. Nëse kërkesa ekzekutohet në kontekstin e filtër, atëherë vlera e _score do të jetë gjithmonë 1, që do të thotë përputhje e plotë me filtrin.

Analizuesit

Analizuesit janë të nevojshëm për të transformuar tekstin origjinal në një grup tokenësh.
Analizuesit përbëhen nga një Tokenizer dhe disa TokenFilters. Tokenizer mund të paraprijnë disa CharFilters. Tokenizer ndan stringun origjinal në tokenë, për shembull, sipas hapësirave dhe simboleve të pikësimit. TokenFilter mund të modifikojë tokenët, të fshijë ose të shtojë të rinj, për shembull, duke lënë vetëm bazën e fjalës, duke hequr parafjalët, duke shtuar sinonime. CharFilter ndalon tërësisht stringun origjinal, për shembull, duke prerë etiketat HTML.

Në ES ka disa analizues standardë. Për shembull, analizuesi russian.

Le të përdorim api dhe të shikojmë se si analizuesit standard dhe russian transformojnë stringun "Histori të lumtura për macet":

# используем анализатор 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" : "histori",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "të",
    "start_offset" : 8,
    "end_offset" : 10,
    "type" : "",
    "position" : 1
  }, {
    "token" : "lumtur",
    "start_offset" : 11,
    "end_offset" : 18,
    "type" : "",
    "position" : 2
  }, {
    "token" : "për",
    "start_offset" : 19,
    "end_offset" : 22,
    "type" : "",
    "position" : 3
  }, {
    "token" : "macet",
    "start_offset" : 23,
    "end_offset" : 29,
    "type" : "",
    "position" : 4
  } ]
}
# используем анализатор 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" : "vesel",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "histor",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "kot",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

Analizatori standard e ndarë vargun nëpër hapësira dhe e shndërroi gjithçka në shkronja të vogla; analizatori rus eliminon fjalët e parëndësishme, e shndërron në shkronja të vogla dhe lë rrënjën e fjalëve.

Le të shohim se cilat Tokenizer, TokenFilters, CharFilters përdor analizatori 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 nuk ka */
    }
  }
}

Do të përshkruajmë analizatorin tonë të bazuar në rusisht, i cili do të heqë etiketat html. Do ta quajmë default, pasi analizatori me emrin e tillë do të përdoret si i parazgjedhur.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* shtojmë heqjen e etiketave html */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Fillimisht, të gjitha etiketat html do të hiqen nga vargu origjinal, pastaj do të ndahet në tokene nga tokenizer standard, tokent e marra do të shndërrohen në shkronja të vogla, do të eliminohen fjalët e parëndësishme dhe do të mbetet rrënjën e tokeneve të mbetura.

Krijimi i indeksit.

Më sipër përshkruam analizatorin default. Ai do të aplikohet në të gjitha fushat e vargjeve. Postimi ynë përmban një array etiketash, prandaj, etiketat gjithashtu do të përpunohen nga analizatori. Duke qenë se ne kërkojmë postime sipas përputhjes së saktë me etiketën, është e nevojshme të çaktivizohet analiza për fushën tags.

Do të krijojmë indeksin blog2 me analizatorin dhe mappings, në të cilin është çaktivizuar analiza e fushës 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"
        }
      }
    }
  }
}'

Do të shtojmë të njëjtët 3 postime në këtë indeks (blog2). Do ta lë këtë proces mënjanë, pasi është i njëjtë me shtimin e dokumenteve në indeksin blog.

Kërkimi me tekste të plota me mbështetje për shprehje

Le të njohim një lloj tjetër kërkese:

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

Duke marrë parasysh se po përdorim një analizues me stemming në rusisht, kjo kërkesë do të kthejë të gjitha dokumentet, edhe pse ato përmbajnë vetëm fjalën ‘histori’.

Kërkesa mund të përmbajë simbole speciale, për shembull:

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

Sintaksa e kërkesës:

+ nënkupton operacionin AND
| nënkupton operacionin OR
- mohon një token të vetëm
" rrethon një numër tokenësh për të nënkuptuar një frazë për kërkim
* në fund të një termini nënkupton një kërkesë prefiks
( dhe ) nënkuptojnë prioritet
~N pas një fjale nënkupton distancën e redaktimit (përmirsimin)
~N pas një fraze nënkupton sasinë e slop
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

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

Linket

PS

Nëse jeni të interesuar për të njëjtat artikuj-mësime, keni ide për artikuj të rinj ose keni sugjerime për bashkëpunim, do të isha i lumtur ta dëgjoja në privat ose në email m.kuzmin+habr@darkleaf.ru.

Burimi: habr.com

Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS 🔥 Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS - ProHoster