Elasticsearchin perusteet

Elasticsearch on hakukone, jossa on JSON REST API, Lucene ja joka on kirjoitettu Javalla. Kuvaus tämän hakukoneen kaikista eduista on saatavilla osoitteessa virallisilla verkkosivuillaTästä eteenpäin kutsumme Elasticsearchiä nimellä ES.

Tällaisia ​​hakukoneita käytetään monimutkaisiin asiakirjatietokantojen hakuihin, kuten kielen morfologiaan tai maantieteellisten koordinaattien perusteella tehtävään hakuun.

Tässä artikkelissa käsittelen ES:n perusteita blogikirjoitusten indeksoinnin esimerkin avulla. Näytän, miten dokumentteja suodatetaan, lajitellaan ja haetaan.

Ollakseni käyttöjärjestelmästä riippumaton, teen kaikki ES-pyyntöni CURL:n avulla. Google Chromelle on myös olemassa laajennus nimeltä tunne.

Linkkejä dokumentaatioon ja muihin lähteisiin on tarjolla koko tekstissä. Nopeat linkit dokumentaatioon ovat lopussa. Tuntemattomien termien määritelmät löytyvät kohdasta sanastot.

ES:n asentaminen

Tätä varten tarvitsemme ensin Javan. Kehittäjät suositella Asenna Java-versiot, jotka ovat uudempia kuin Java 8 update 20 tai Java 7 update 55.

ES-jakelu on saatavilla osoitteessa kehittäjäsivustoArkiston purkamisen jälkeen sinun on suoritettava bin/elasticsearchMyös saatavilla apt- ja yum-paketit. On olemassa Dockerin virallinen kuva. Lisätietoja asennuksesta.

Asennuksen ja käynnistyksen jälkeen tarkistamme toiminnallisuuden:

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

curl -X GET $ES_URL

Saamme vastauksen, joka näyttää suunnilleen tältä:

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

indeksointi

Lisätäänpä ES:ään viesti:

# Добавим документ 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"
}'

palvelimen vastaus:

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

ES luotiin automaattisesti indeksi blogi ja tyyppi viesti. Voidaan vetää karkea analogia: indeksi on tietokanta ja tyyppi on taulukko kyseisen tietokannan sisällä. Jokaisella tyypillä on oma skeemansa— kartoitus, aivan kuten relaatiotaulukko. Yhdistämismääritykset luodaan automaattisesti, kun asiakirja indeksoidaan:

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

Palvelimen vastauksessa lisäsin indeksoidun dokumentin kenttäarvot kommentteihin:

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

On syytä huomata, että ES ei erottele yksittäistä arvoa ja arvotaulukkoa. Esimerkiksi otsikkokenttä sisältää vain otsikon, kun taas tags-kenttä sisältää merkkijonotaulukon, vaikka ne on esitetty identtisesti kartoituksessa.
Kerromme kartoituksesta tarkemmin myöhemmin.

pyynnöt

Dokumentin poimiminen sen id:n perusteella:

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

Vastauksessa näkyi uusia avaimia: _version и _sourceYleisesti ottaen kaikki avaimet, jotka alkavat _ luokitellaan palvelutuotteiksi.

avain _version Näyttää dokumentin version. Tämä on välttämätöntä, jotta optimistinen lukitusmekanismi toimisi. Esimerkiksi haluamme muuttaa dokumenttia, jonka versio on 1. Lähetämme muokatun dokumentin ja ilmoitamme, että tämä on version 1 dokumentin revisio. Jos joku muu on myös muokannut dokumenttia, jonka versio on 1, ja lähettänyt muutokset ennen meitä, ES ei hyväksy muutoksiamme, koska se tallentaa dokumentin, jonka versio on 2.

avain _source Sisältää indeksoimamme dokumentin. ES ei käytä tätä arvoa hakutoimintoihin, koska indeksejä käytetään hakuun. Tilan säästämiseksi ES tallentaa pakatun version alkuperäisestä dokumentista. Jos tarvitsemme vain tunnuksen emmekä koko alkuperäistä dokumenttia, voimme poistaa alkuperäisen tallennuksen käytöstä.

Jos emme tarvitse lisätietoja, voimme saada vain _source-muuttujasta saatavan sisällön:

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

Voit myös valita vain tietyt kentät:

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

Indeksoidaan muutama viesti lisää ja suoritetaan monimutkaisempia kyselyitä.

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

Olemme valinneet viimeisen postauksen. size rajoittaa myönnettyjen asiakirjojen määrää. total näyttää kyselyä vastaavien dokumenttien kokonaismäärän. sort Tuloste sisältää kokonaislukutaulukon, jonka mukaan lajittelu suoritetaan. Toisin sanoen päivämäärä on muunnettu kokonaisluvuksi. Lisätietoja lajittelusta löytyy kohdasta dokumentointi.

Suodattimet ja kyselyt

ES versiosta 2 lähtien ei erottele suodattimia ja kyselyitä, vaan kontekstien käsite esitellään.
Kyselykonteksti eroaa suodatinkontekstista siten, että kysely luo _score-arvon eikä sitä tallenneta välimuistiin. Selitän myöhemmin, mitä _score on.

Suodata päivämäärän mukaan

Kyselyn käyttäminen alue suodattimen yhteydessä:

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

Suodata tunnisteiden mukaan

käyttö termikysely Voit etsiä tietyn sanan sisältäviä dokumenttitunnuksia seuraavasti:

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

Koko tekstihaku

Kolmen dokumenttimme sisältökentässä on seuraavat tiedot:

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

käyttö osumakysely Voit etsiä tietyn sanan sisältäviä dokumenttitunnuksia seuraavasti:

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

Jos kuitenkin haemme sisältökentästä "tarinoita", emme löydä mitään, koska hakemisto sisältää vain alkuperäisiä sanoja, ei niiden vartaloita. Laadukkaan haun suorittamiseksi meidän on konfiguroitava analysaattori.

Kenttä _score osoittaa relevanssiJos kysely suoritetaan suodatinkontekstissa, _score-arvo on aina 1, mikä tarkoittaa, että suodatin on täysin osunut.

Analysaattorit

Analysaattorit tarvitaan lähdetekstin muuntamiseksi joukoksi tokeneja.
Analysaattorit koostuvat yhdestä Tokenizer ja useita valinnaisia TokenFiltersTokenisaattori voi edeltää useita CharFiltersTokenisoijat jakavat lähdemerkkijonon tokeneiksi esimerkiksi välilyönneillä ja välimerkeillä. TokenFilterit voivat muokata tokeneita, poistaa niitä tai lisätä uusia esimerkiksi jättämällä vain sanarungon, poistamalla prepositioita tai lisäämällä synonyymejä. CharFilterit muokkaavat lähdemerkkijonon kokonaan esimerkiksi poistamalla HTML-tagit.

ES:ssä on useita standardianalysaattoritEsimerkiksi analysaattori Venäjän kieli.

Hyödynnetään api ja katsotaanpa, miten standardi ja venäläinen jäsennin muuntavat merkkijonon "Hauskoja tarinoita kissanpennuista":

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

Vakioanalysaattori jakoi merkkijonon välilyöntien kohdalta ja muunsi kaiken pieniksi kirjaimiksi, venäläinen analysaattori poisti merkityksettömät sanat, muunsi ne pieniksi kirjaimiksi ja jätti sanarunkonsa.

Katsotaanpa, mitä Tokenizer-, TokenFilter- ja CharFilter-suodattimia venäläinen analysaattori käyttää:

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

Kuvaillaan omaa venäläistä analysaattoriamme, joka poistaa HTML-tagit. Kutsumme sitä oletusarvoiseksi, koska tämä on oletusarvoinen analysaattori.

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

Ensin kaikki HTML-tagit poistetaan lähdemerkkijonosta, sitten tokenizer-standardi jakaa sen tokeneihin, tuloksena olevat tokenit muunnetaan pieniksi kirjaimiksi, merkityksettömät sanat poistetaan ja loput tokenit ovat sanarunko.

Indeksin luominen

Yllä kuvailimme oletusanalysaattoria. Sitä käytetään kaikkiin merkkijonokenttiin. Viestimme sisältää taulukon tageja, joten analysaattori käsittelee myös tagit. Koska etsimme viestejä, jotka vastaavat täsmälleen samaa tagia, meidän on poistettava käytöstä "tagit"-kentän analyysi.

Luodaan blog2-indeksi analysaattorilla ja kartoituksella, jossa tags-kentän analysointi on poistettu käytöstä:

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

Lisätään samat kolme julkaisua tähän hakemistoon (blog2). Ohitan tämän prosessin, koska se on samanlainen kuin dokumenttien lisääminen blogihakemistoon.

Kokotekstihaku lausekkeilla

Tutustutaanpa toisenlaiseen kyselytyyppiin:

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

Koska käytämme analysaattoria, jossa on venäjänkieliset sanat, tämä kysely palauttaa kaikki dokumentit, vaikka ne sisältäisivät vain sanan 'historia'.

Kysely voi sisältää erikoismerkkejä, esimerkiksi:

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

Kyselyn syntaksi:

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

viittaukset

PS

Jos olet kiinnostunut vastaavanlaisista opetusohjelmista, sinulla on ideoita uusiin artikkeleihin tai yhteistyöehdotuksia, kuulen mielelläni sinusta yksityisviestillä tai sähköpostitse osoitteeseen m.kuzmin+habr@darkleaf.ru.

Lähde: will.com