Grunderna i Elasticsearch

Elasticsearch är en sökmotor med json rest api, som använder Lucene och är skriven i Java. En beskrivning av alla fördelar med denna motor finns på officiella hemsidaFrån och med nu kommer vi att referera till Elasticsearch som ES.

Sådana sökmotorer används för komplexa sökningar i en dokumentdatabas. Till exempel sökningar som tar hänsyn till språkmorfologi eller sökningar med hjälp av geokoordinater.

I den här artikeln kommer jag att berätta om grunderna i ES med hjälp av exemplet indexering av blogginlägg. Jag kommer att visa dig hur du filtrerar, sorterar och söker i dokument.

För att vara oberoende av operativsystemet kommer jag att göra alla förfrågningar till ES med hjälp av CURL. Det finns också ett plugin för Google Chrome som heter känsla.

Länkar till dokumentation och andra källor finns genomgående i texten. I slutet finns länkar för snabb åtkomst till dokumentationen. Definitioner av okända termer finns i ordlistor.

Installera ES

För detta behöver vi först Java. Utvecklare Rekommendera Installera Java-versioner som är nyare än Java 8 uppdatering 20 eller Java 7 uppdatering 55.

ES-distributionen finns tillgänglig på utvecklarwebbplatsEfter att du har packat upp arkivet måste du köra bin/elasticsearchÄven tillgänglig paket för apt och yum. Det finns officiell bild för docker. Mer om installationen.

Efter installation och start, låt oss kontrollera funktionaliteten:

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

curl -X GET $ES_URL

Vi kommer att få ett svar som ser ut ungefär så här:

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

Låt oss lägga till ett inlägg i 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"
}'

serversvar:

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

ES skapades automatiskt index blogg och тип inlägg. Vi kan dra en villkorlig analogi: indexet är en databas och typen är en tabell i denna databas. Varje typ har sitt eget schema - kartläggning, precis som en relationstabell. Mappning genereras automatiskt när dokumentet indexeras:

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

I serversvaret lade jag till värdena för de indexerade dokumentfälten i kommentarerna:

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

Det är värt att notera att ES inte skiljer mellan ett enskilt värde och en array av värden. Till exempel innehåller titelfältet bara en titel, medan taggfältet innehåller en array av strängar, även om de representeras på samma sätt i mappningen.
Vi kommer att prata mer om kartläggning senare.

förfrågningar

Extrahera ett dokument med hjälp av dess 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"
  }
}

Nya nycklar dök upp i svaret: _version и _sourceGenerellt sett gäller alla nycklar som börjar med _ klassificeras som serviceartiklar.

nyckel _version visar dokumentversionen. Den behövs för att den optimistiska låsmekanismen ska fungera. Till exempel vill vi ändra ett dokument med version 1. Vi skickar det ändrade dokumentet och anger att detta är en redigering av ett dokument med version 1. Om någon annan också redigerade dokumentet med version 1 och skickade ändringarna före oss, kommer ES inte att acceptera våra ändringar, eftersom det lagrar dokumentet med version 2.

nyckel _source innehåller dokumentet som vi indexerade. ES använder inte detta värde för sökåtgärder, eftersom index används för sökning. För att spara utrymme lagrar ES det komprimerade källdokumentet. Om vi bara behöver id:t, och inte hela källdokumentet, kan vi inaktivera lagring av källkoden.

Om vi inte behöver någon ytterligare information kan vi bara hämta innehållet i _source:

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

Du kan också välja endast vissa fält:

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

Låt oss indexera några fler inlägg och köra mer komplexa frågor.

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

Vi har valt det sista inlägget. size begränsar antalet utfärdade dokument. total visar det totala antalet dokument som matchar frågan. sort i utdata innehåller en array av heltal som sorteringen utförs med. Det vill säga, datumet har konverterats till ett heltal. Du kan läsa mer om sortering i dokumentation.

Filter och frågor

ES sedan version 2 skiljer inte mellan filter och frågor, istället begreppet kontexter introduceras.
Frågekontexten skiljer sig från filterkontexten genom att frågan genererar en _score och inte cachas. Jag kommer att visa dig vad _score är senare.

Filtrera efter datum

Vi använder frågan område i samband med filtret:

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

Filtrera efter taggar

Vi använder termfråga för att söka efter dokument-ID som innehåller ett givet ord:

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

Fulltextsökning

Våra tre dokument innehåller följande i innehållsfältet:

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

Vi använder matchningsfråga för att söka efter dokument-ID som innehåller ett givet ord:

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

Men om vi söker efter "berättelser" i innehållsfältet kommer vi inte att hitta något, eftersom indexet bara innehåller originalord, inte deras baser. För att göra en kvalitativ sökning måste du konfigurera analysatorn.

Fält _score visar relevansOm frågan körs i ett filterkontext kommer _score-värdet alltid att vara 1, vilket innebär fullständig överensstämmelse med filtret.

Analysatorer

Analysatorer behövs för att omvandla källtexten till en uppsättning tokens.
Analysatorerna består av en Tokenizer och flera valfria TokenFiltersTokenizer kan föregås av flera TeckenfilterTokenizer delar upp källsträngen i tokens, till exempel med mellanslag och skiljetecken. TokenFilter kan ändra tokens, ta bort eller lägga till nya, till exempel bara lämna ordstammen, ta bort prepositioner, lägga till synonymer. CharFilter - ändrar källsträngen helt, till exempel tar bort html-taggar.

Det finns flera i ES standardanalysatorerTill exempel en analysator ryska.

Låt oss dra fördel api och låt oss se hur standard- och ryska analysatorer omvandlar strängen "Roliga historier om kattungar":

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

Standardanalysatorn delade upp raden med mellanslag och konverterade allt till gemener, den ryska analysatorn tog bort oviktiga ord, konverterade till gemener och lämnade ordstammarna.

Låt oss se vad Tokenizer, TokenFilters, CharFilters den ryska analysatorn använder:

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

Låt oss beskriva vår analysator baserat på ryska, som kommer att ta bort html-taggar. Låt oss kalla den standard, eftersom analysatorn med detta namn kommer att användas som standard.

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

Först tas alla html-taggar bort från den ursprungliga strängen, sedan delas den upp i tokens enligt tokenizer-standarden, de resulterande tokens konverteras till gemener, obetydliga ord tas bort och de återstående tokensen utgör ordstammen.

Skapa ett index

Ovan beskrev vi standardanalysatorn. Den kommer att tillämpas på alla strängfält. Vårt inlägg innehåller en array med taggar, så taggarna kommer också att bearbetas av analysatorn. Eftersom vi letar efter inlägg genom exakt matchning av taggen är det nödvändigt att inaktivera analysen för taggfältet.

Låt oss skapa ett blog2-index med en analysator och mappning, där analysen av taggfältet är inaktiverad:

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

Låt oss lägga till samma 3 inlägg i det här indexet (blogg2). Jag hoppar över den här processen eftersom det liknar att lägga till dokument i bloggindexet.

Fulltextsökning med stöd för uttryck

Låt oss bekanta oss med en annan typ av förfrågningar:

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

Eftersom vi använder en analysator med rysk avstamning kommer den här frågan att returnera alla dokument, även om de bara innehåller ordet "history".

Begäran kan innehålla specialtecken, till exempel:

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

Frågesyntax:

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

referenser

PS

Om du är intresserad av sådana artikellektioner, har idéer till nya artiklar eller har förslag på samarbete, tar jag gärna emot ett meddelande i ett personligt meddelande eller via e-post m.kuzmin+habr@darkleaf.ru.

Källa: will.com