Basisbeginselen van Elasticsearch

Elasticsearch is een zoekmachine met json rest api, gebruikt Lucene en geschreven in Java. Een beschrijving van alle voordelen van deze motor is beschikbaar op de officiële website. In wat volgt zullen we Elasticsearch noemen als ES.

Soortgelijke zoekmachines worden gebruikt voor complexe zoekopdrachten in een documentendatabase. Zoek bijvoorbeeld rekening houdend met de morfologie van de taal of zoek op geocoördinaten.

In dit artikel zal ik het hebben over de basisprincipes van ES aan de hand van het voorbeeld van het indexeren van blogposts. Ik laat je zien hoe je documenten filtert, sorteert en doorzoekt.

Om niet afhankelijk te zijn van het besturingssysteem, zal ik alle verzoeken aan ES doen met behulp van CURL. Er is ook een plug-in voor Google Chrome genaamd zin.

De tekst bevat links naar documentatie en andere bronnen. Aan het einde vindt u links voor snelle toegang tot de documentatie. Definities van onbekende termen zijn te vinden in woordenlijsten.

ES installeren

Om dit te doen hebben we eerst Java nodig. Ontwikkelaars adviseren installeer Java-versies nieuwer dan Java 8 update 20 of Java 7 update 55.

De ES-distributie is beschikbaar op ontwikkelaarssite. Na het uitpakken van het archief moet u uitvoeren bin/elasticsearch. Ook beschikbaar pakketten voor apt en yum. er bestaat officiële afbeelding voor docker. Meer over installatie.

Laten we na de installatie en lancering de functionaliteit controleren:

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

curl -X GET $ES_URL

Wij krijgen zoiets als 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"
}

Indexering

Laten we een bericht toevoegen aan 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"
}'

serverantwoord:

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

ES automatisch aangemaakt inhoudsopgave bloggen en тип na. We kunnen een voorwaardelijke analogie trekken: een index is een database, en een type is een tabel in deze database. Elk type heeft zijn eigen schema − in kaart brengen, net als een relationele tabel. Toewijzing wordt automatisch gegenereerd wanneer het document wordt geïndexeerd:

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

In het serverantwoord heb ik de waarden van de velden van het geïndexeerde document toegevoegd aan de 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"
          }
        }
      }
    }
  }
}

Het is vermeldenswaard dat ES geen onderscheid maakt tussen een enkele waarde en een reeks waarden. Het titelveld bevat bijvoorbeeld eenvoudigweg een titel, en het tagsveld bevat een reeks tekenreeksen, hoewel deze bij de toewijzing op dezelfde manier worden weergegeven.
We zullen later meer praten over het in kaart brengen.

verzoeken

Een document ophalen op basis van zijn 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"
  }
}

Er verschenen nieuwe sleutels in het antwoord: _version и _source. Over het algemeen beginnen alle toetsen met _ worden als officieel aangemerkt.

sleutel _version toont de documentversie. Het is nodig om het optimistische vergrendelingsmechanisme te laten werken. We willen bijvoorbeeld een document wijzigen dat versie 1 heeft. We dienen het gewijzigde document in en geven aan dat dit een bewerking is van een document met versie 1. Als iemand anders ook een document met versie 1 heeft bewerkt en vóór ons wijzigingen heeft ingediend, dan ES zal onze wijzigingen niet accepteren, omdat het slaat het document op met versie 2.

sleutel _source bevat het document dat we hebben geïndexeerd. ES gebruikt deze waarde niet voor zoekbewerkingen omdat Bij het zoeken worden indexen gebruikt. Om ruimte te besparen slaat ES een gecomprimeerd brondocument op. Als we alleen de id nodig hebben, en niet het hele brondocument, kunnen we de bronopslag uitschakelen.

Als we geen aanvullende informatie nodig hebben, kunnen we alleen de inhoud van _source verkrijgen:

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

U kunt ook alleen bepaalde velden selecteren:

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

Laten we nog een paar berichten indexeren en complexere zoekopdrachten uitvoeren.

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

sorteer-

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

Wij hebben voor het laatste bericht gekozen. size beperkt het aantal af te geven documenten. total toont het totale aantal documenten dat overeenkomt met het verzoek. sort in de uitvoer bevat een array van gehele getallen waarmee wordt gesorteerd. Die. de datum is omgezet naar een geheel getal. Meer informatie over sorteren vindt u in documentatie.

Filters en zoekopdrachten

ES sinds versie 2 maakt in plaats daarvan geen onderscheid tussen filters en query's het concept van contexten wordt geïntroduceerd.
Een querycontext verschilt van een filtercontext doordat de query een _score genereert en niet in de cache wordt opgeslagen. Ik zal je later laten zien wat _score is.

Filter op datum

Wij gebruiken het verzoek reeks in de context van filteren:

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

Filter op tags

We gebruiken termijn vraag om te zoeken naar document-ID's die een bepaald woord bevatten:

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

Zoek volledige text

Drie van onze documenten bevatten het volgende in het inhoudsveld:

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

We gebruiken overeenkomen met de vraag om te zoeken naar document-ID's die een bepaald woord bevatten:

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

Als we echter in het inhoudsveld naar ‘verhalen’ zoeken, zullen we niets vinden, omdat De index bevat alleen de oorspronkelijke woorden, niet hun stammen. Om een ​​zoekopdracht van hoge kwaliteit uit te voeren, moet u de analysator configureren.

Veld _score shows relevantie. Als het verzoek wordt uitgevoerd in een filtercontext, zal de waarde _score altijd gelijk zijn aan 1, wat een volledige match met het filter betekent.

Analysers

Analysers zijn nodig om de brontekst om te zetten in een set tokens.
Analysers bestaan ​​uit één tokenizer en verschillende optioneel TokenFilters. Tokenizer kan worden voorafgegaan door meerdere CharFilters. Tokenizers verdelen de bronreeks in tokens, zoals spaties en leestekens. TokenFilter kan tokens wijzigen, verwijderen of nieuwe toevoegen, bijvoorbeeld alleen de stam van het woord achterlaten, voorzetsels verwijderen en synoniemen toevoegen. CharFilter - wijzigt de volledige bronreeks, verwijdert bijvoorbeeld html-tags.

ES heeft er meerdere standaard analysers. Een analysator bijvoorbeeld Russisch.

Laten we profiteren api en laten we eens kijken hoe de standaard- en Russische analysatoren de string "grappige verhalen over kittens" transformeren:

# используем анализатор 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 standaardanalysator splitste de string op met spaties en converteerde alles naar kleine letters, de Russische analysator verwijderde onbelangrijke woorden, converteerde deze naar kleine letters en liet de stam van de woorden staan.

Laten we eens kijken welke Tokenizer, TokenFilters, CharFilters de Russische analysator gebruikt:

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

Laten we onze analysator beschrijven op basis van het Russisch, waardoor html-tags worden verwijderd. Laten we het standaard noemen, omdat standaard wordt een analysator met deze naam gebruikt.

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

Eerst worden alle HTML-tags uit de bronreeks verwijderd, vervolgens zal de tokenizerstandaard deze in tokens splitsen, de resulterende tokens worden naar kleine letters verplaatst, onbeduidende woorden worden verwijderd en de resterende tokens blijven de stam van het woord.

Een index maken

Hierboven hebben we de standaardanalysator beschreven. Het is van toepassing op alle tekenreeksvelden. Ons bericht bevat een reeks tags, dus de tags worden ook door de analysator verwerkt. Omdat We zoeken naar berichten die exact overeenkomen met een tag. Vervolgens moeten we de analyse voor het tagsveld uitschakelen.

Laten we een indexblog2 maken met een analysator en mapping, waarin de analyse van het tagsveld is uitgeschakeld:

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

Laten we dezelfde 3 berichten toevoegen aan deze index (blog2). Ik zal dit proces achterwege laten omdat... het is vergelijkbaar met het toevoegen van documenten aan de blogindex.

Zoeken in volledige tekst met ondersteuning voor expressies

Laten we eens kijken naar een ander type verzoek:

# найдем документы, в которых встречается слово 'истории'
# 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 We gebruiken een analysator met Russische oorsprong, dan zal dit verzoek alle documenten retourneren, hoewel ze alleen het woord 'geschiedenis' bevatten.

Het verzoek kan speciale tekens bevatten, bijvoorbeeld:

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

Syntaxis van verzoek:

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

referenties

PS

Als u geïnteresseerd bent in soortgelijke artikelen-lessen, ideeën heeft voor nieuwe artikelen of voorstellen heeft voor samenwerking, dan ontvang ik graag een bericht in een persoonlijk bericht of per e-mail [e-mail beveiligd].

Bron: www.habr.com