Basisprincipes van Elasticsearch

Elasticsearch — een zoekmachine met een JSON REST API, die Lucene gebruikt en in Java is geschreven. Een beschrijving van alle voordelen van deze motor is beschikbaar op officiële website. Voortaan zullen we Elasticsearch als ES aanduiden.

Dergelijke motoren worden gebruikt voor geavanceerd zoeken in documentendatabases. Bijvoorbeeld zoeken met inachtneming van de morfologie van de taal of zoeken op geo-coördinaten.

In dit artikel zal ik de basisprincipes van ES uitleggen aan de hand van het indexeren van blogposts. Ik zal laten zien hoe je documenten kunt filteren, sorteren en zoeken.

Om niet afhankelijk te zijn van het besturingssysteem, zal ik alle verzoeken naar ES doen met behulp van CURL. Daarnaast is er een plugin voor Google Chrome genaamd sense.

In de tekst zijn links naar documentatie en andere bronnen opgenomen. Aan het einde zijn links geplaatst voor snelle toegang tot de documentatie. Definities van onbekende termen zijn te lezen in de woordenlijst.

Installatie van ES

Hiervoor hebben we eerst Java nodig. De ontwikkelaars Je kunt je ook abonneren op moeten Java-versies installeren die nieuwer zijn dan Java 8 update 20 of Java 7 update 55.

De distributie van ES is beschikbaar op de ontwikkelaarswebsite. Na het uitpakken van het archief moet je bin/elasticsearchstarten. Ook zijn er pakketten voor apt en yum. Er is officiële Docker-image. Meer over de installatie.

Na de installatie en opstarten controleren we de werking:

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

curl -X GET $ES_URL

We krijgen ongeveer deze reactie terug:

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

Indexatie

Laten we een post aan ES toevoegen:

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

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

serverresponse:

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

ES heeft automatisch gecreëerd index blog en type post. Je kunt een voorzichtige analogie trekken: een index is een database, terwijl een type een tabel in deze database is. Elke type heeft zijn eigen schema — mapping, net als een relationele tabel. Mapping wordt automatisch gegenereerd tijdens de indexatie van een document:

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

In de serverresponse heb ik in de opmerkingen de waarden van de velden van het geïndexeerde document toegevoegd:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Grappig verhaal over kittens<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 belangrijk op te merken dat ES geen onderscheid maakt tussen een enkele waarde en een array van waarden. Bijvoorbeeld, het veld title bevat gewoon een titel, terwijl het veld tags een array van strings bevat, hoewel ze in de mapping identiek worden gepresenteerd.
Later zullen we dieper ingaan op de mapping.

Queries

Het ophalen van een document 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": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
        "content": "<p>Grappig verhaal over kittens<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"
    }
}

In de respons zijn nieuwe sleutels verschenen: _version en _source. Over het algemeen zijn alle sleutels die beginnen met _ zijn systeemgerelateerd.

Sleutel _version toont de versie van het document. Dit is nodig voor de werking van het mechanisme voor optimistische sloten. Bijvoorbeeld, we willen een document wijzigen dat versie 1 heeft. We sturen het gewijzigde document en geven aan dat dit een bewerking is van het document met versie 1. Als iemand anders ook het document met versie 1 heeft bewerkt en eerder wijzigingen heeft ingediend, zal ES onze wijzigingen niet accepteren, omdat het document met versie 2 wordt opgeslagen.

Sleutel _source bevat het document dat we hebben geïndexeerd. ES gebruikt deze waarde niet voor zoekopdrachten, omdat zoekopdrachten gebruikmaken van indices. Om opslagruimte te besparen, slaat ES het gecomprimeerde oorspronkelijke document op. Als we alleen de id nodig hebben en niet het volledige oorspronkelijke document, kan het opslaan van de bron worden uitgeschakeld.

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

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>Grappig verhaal over kittens<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"
}

We kunnen 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" : "Vrolijke kittens"
  }
}

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

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Vrolijke puppy's",
  "content": "<p>Een grappig verhaal over puppy's<p>",
  "tags": [
    "puppy's",
    "grappig verhaal"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Hoe ik een kitten kreeg",
  "content": "<p>Ontroerend verhaal over een arme straatkitten<p>",
  "tags": [
    "kittens"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Sorteren

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

We hebben de laatste post geselecteerd. size beperkt het aantal documenten in de uitgave. totaal geeft het totale aantal documenten weer dat aan de zoekopdracht voldoet. sort de uitgave bevat een array van gehele getallen waarop de sortering plaatsvindt. Dat wil zeggen, de datum is omgezet in een geheel getal. Meer informatie over sorteren vind je in de documentatie.

Filters en verzoeken

ES maakt sinds versie 2 geen onderscheid meer tussen filters en verzoeken, in plaats daarvan wordt het begrip contexten geïntroduceerd.
De context van de aanvraag verschilt van de context van het filter doordat de aanvraag een _score genereert en niet gecached wordt. Wat _score is, zal ik later laten zien.

Filtering op datum

We gebruiken de aanvraag range in de context van filter:

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

Labelfiltering

We gebruiken termquery om id's van documenten te zoeken die het opgegeven 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" : "Vrolijke kittens",
        "tags" : [ "kittens", "grappig verhaal" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Hoe ik een kitten kreeg",
        "tags" : [ "kittens" ]
      }
    } ]
  }
}

Volledige tekstzoektocht

Onze drie documenten bevatten het volgende in het veld content:

  • <p>Grappig verhaal over kittens<p>
  • <p>Een grappig verhaal over puppy's<p>
  • <p>Ontroerend verhaal over een arme straatkitten<p>

We gebruiken match query om id's van documenten te zoeken die het opgegeven 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
    } ]
  }
}

Echter, als we naar 'verhalen' in het contentveld zoeken, zullen we niets vinden, omdat de index alleen de originele woorden bevat, en niet hun basisvormen. Om een kwalitatieve zoekopdracht te maken, moet de analyzer worden ingesteld.

Veld _score toont relevantie. Als de vraag wordt uitgevoerd in de filtercontext, zal de waarde van _score altijd gelijk zijn aan 1, wat volledige overeenstemming met het filter betekent.

Analyzers

Analyzers zijn nodig om de oorspronkelijke tekst om te zetten naar een set tokens.
Analyzers bestaan uit één Tokenizer en verschillende optionele TokenFilters. Tokenizer kan voorafgaan aan verschillende CharFilters. Tokenizer splitst de oorspronkelijke string op in tokens, bijvoorbeeld door spaties en leestekens. TokenFilter kan tokens wijzigen, verwijderen of nieuwe toevoegen, bijvoorbeeld alleen de stam van een woord behouden, voorzetsels verwijderen, synoniemen toevoegen. CharFilter - verandert de oorspronkelijke string helemaal, bijvoorbeeld door HTML-tags te verwijderen.

In ES zijn er verschillende standaard analyzers. Bijvoorbeeld de analyzer russian.

Laten we het gebruiken api en laten we zien hoe de analyzers standard en russian de string 'Vrolijke verhalen over kittens' omzetten:

# используем анализатор 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" : "vrolijke",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "verhalen",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "over",
    "start_offset" : 16,
    "end_offset" : 20,
    "type" : "",
    "position" : 2
  }, {
    "token" : "kittens",
    "start_offset" : 21,
    "end_offset" : 27,
    "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" : "vrolijk",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "geschiedenis",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "kat",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

De standaardanalyzer heeft de string op spaties gebroken en alles naar kleine letters omgezet, de russian-analyzer heeft niet-significante woorden verwijderd, naar kleine letters omgezet en de woordstam behouden.

Laten we kijken welke Tokenizers, TokenFilters en CharFilters de russian-analyzer gebruikt:

{
  "filter": {
    "russian_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "russian_keywords": {
      "type":       "keyword_marker",
      "keywords":   []
    },
    "russian_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "russian": {
      "tokenizer":  "standard",
      
      "filter": [
        "lowercase",
        "russian_stop",
        "russian_keywords",
        "russian_stemmer"
      ]
      
    }
  }
}

Laten we onze analyzer op basis van russian beschrijven, die HTML-tags zal verwijderen. We noemen deze default, omdat een analyzer met deze naam als standaard zal worden gebruikt.

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

Eerst worden alle HTML-tags uit de oorspronkelijke string verwijderd, vervolgens wordt deze door de tokenizer standard in tokens verdeeld, de verkregen tokens worden naar kleine letters omgezet, niet-significante woorden worden verwijderd en van de overgebleven tokens blijft de wortel van het woord over.

Index maken

Boven hebben we de default-analyzer beschreven. Deze zal worden toegepast op alle stringvelden. Onze post bevat een array van tags, dus de tags zullen ook door de analyzer worden verwerkt. Aangezien we posts zoeken op exacte overeenkomst met de tag, moet de analyse voor het veld tags worden uitgeschakeld.

Laten we de index blog2 maken met een analyzer en mapping, waarin de analyse van het veld tags 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 aan deze index (blog2) toevoegen. Ik zal dit proces overslaan, omdat het vergelijkbaar is met het toevoegen van documenten aan de blogindex.

Volledige tekstzoektocht met ondersteuning voor expressies

Laten we kennismaken met 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"
      ]
    }
  }
}'

Aangezien we een analyzer met Russische stemming gebruiken, zal dit verzoek alle documenten retourneren, hoewel alleen het woord 'geschiedenis' erin voorkomt.

Een verzoek kan speciale symbolen bevatten, bijvoorbeeld:

""gebakken eieren" +(aubergine | aardappel) -frittata"

De syntaxis van het verzoek:

+ betekent de AND-operatie
| betekent de OR-operatie
- negating een enkel token
" om een aantal tokens te omvatten om een frase te zoeken
* aan het einde van een term betekent een prefix query
( en ) betekenen precedentie
~N na een woord betekent bewerkafstand (fuzziness)
~N na een frase betekent de hoeveelheid slop
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

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

Links

PS

Als je geïnteresseerd bent in soortgelijke artikelen, ideeën voor nieuwe artikelen hebt of voorstellen voor samenwerking, dan hoor ik graag van je via privébericht of per e-mail op m.kuzmin+habr@darkleaf.ru.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster