Grunnleggende om Elasticsearch

Elasticsearch er en søkemotor med json rest api, som bruker Lucene og er skrevet i Java. Beskrivelse av alle fordelene ved denne motoren er tilgjengelig på den offisielle nettsiden. I teksten vil vi referere til Elasticsearch som ES.

Slike motorer brukes for avansert søk i dokumentdatabaser. For eksempel søk med hensyn til språkets morfologi eller søk basert på geo-koordinater.

I denne artikkelen skal jeg fortelle om grunnleggende om ES med eksempler fra indeksering av blogginnlegg. Jeg vil vise hvordan man filtrerer, sorterer og søker etter dokumenter.

For ikke å være avhengig av operativsystemet, vil jeg utføre alle forespørslene til ES ved hjelp av CURL. Det finnes også et plugin for Google Chrome kalt sense.

Teksten inneholder lenker til dokumentasjonen og andre kilder. På slutten er det lenker for rask tilgang til dokumentasjonen. Definisjoner av ukjente termer kan leses i glossaret.

Installasjon av ES

For dette trenger vi først Java. Utviklerne å lese bøker " må installere versjoner av Java nyere enn Java 8 update 20 eller Java 7 update 55.

ES-distribusjonen er tilgjengelig på utviklerens nettsted. Etter å ha pakket ut arkivet, må vi kjøre bin/elasticsearch. Det er også tilgjengelige pakker for apt og yum. Det finnes offisiell image for docker. Mer om installasjonen.

Etter installasjonen og oppstarten, la oss sjekke funksjonaliteten:

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

curl -X GET $ES_URL

Vi vil motta omtrent et slikt svar:

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

Indeksering

La oss legge til et innlegg i ES:

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

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

serverens svar:

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

ES har automatisk opprettet indeks blog og type post. Man kan gjøre en omtrentlig analogi: en indeks er en database, mens en type er en tabell i denne databasen. Hver type har sitt eget skjema — mapping, akkurat som en relasjonell tabell. Mapping genereres automatisk ved indeksering av dokumentet:

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

I serverens svar har jeg lagt til verdiene for feltene i det indekserte dokumentet i kommentarene:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          "content" : {
            "type" : "string"
          },
          "published_at" : {
            "type" : "date",
            "format" : "strict_date_optional_time||epoch_millis"
          },
          "tags" : {
            "type" : "string"
          },
          "title" : {
            "type" : "string"
          }
        }
      }
    }
  }
}<p>Morsom historie om kattunger<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": ["katter", "morsom historie"] *\/ 
          "tags" : {
            "type" : "string"
          },
          \/*  "title": "Morsomme katter" *\/ 
          "title" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

Det er verdt å merke seg at ES ikke skiller mellom enkeltverdier og arrays av verdier. For eksempel inneholder feltet title bare en overskrift, mens feltet tags er en array av strenger, selv om de er presentert i mapping på samme måte.
Senere skal vi snakke om mapping mer detaljert.

Forespørsel

Henting av dokumentet ved hjelp av 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>Morsom historie om kattunger<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"
    }
}

Det dukket opp nye nøkler i svaret: _version og _source. Generelt sett, alle nøkler som begynner med _ tilhører systemet.

Nøkkel _version viser versjonen av dokumentet. Den er nødvendig for å arbeide med mekanismen for optimistiske låsing. For eksempel, hvis vi ønsker å endre et dokument som har versjon 1. Vi sender det endrede dokumentet og angir at dette er en redigering av dokumentet med versjon 1. Hvis noen andre også har redigert dokumentet med versjon 1 og sendt endringer før oss, vil ES ikke akseptere våre endringer, siden den lagrer dokumentet med versjon 2.

Nøkkel _source inneholder det dokumentet som vi indekserte. ES bruker ikke denne verdien for søkeoperasjoner, siden indekser brukes for søk. For å spare plass lagrer ES den komprimerte originalen av dokumentet. Hvis vi bare trenger id-en og ikke hele originaldokumentet, kan vi deaktivere lagringen av originalen.

Hvis vi ikke trenger tilleggsinformasjon, kan vi bare få innholdet til _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>Morsom historie om kattunger<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"
}

Vi kan også velge kun bestemte felter:

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

La oss indeksere noen flere innlegg og utføre mer komplekse forespørsler.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Morsomme valper",
  "content": "<p>En morsom historie om valper<p>"
  "tags": [
    "valper",
    "morsom historie"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Hvordan jeg fikk kattunge",
  "content": "<p>En hjerteskjærende historie om den stakkars kattungen fra gata<p>",
  "tags": [
    "kattunger"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Sortering

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

Vi valgte det siste innlegget. size begrenser antall dokumenter i resultatene. total viser det totale antallet dokumenter som passer til forespørselen. sort i resultatet inneholder et array av heltall, som brukes til sortering. Dvs. datoen er konvertert til et heltall. Mer om sortering kan leses i dokumentasjonen.

Filtre og forespørsel

ES siden versjon 2 skiller ikke mellom filtre og forespørslere, i stedet innføres begrepet kontekster.
Konseptet av forespørselen skiller seg fra konteksten av filteret ved at forespørselen genererer _score og ikke caches. Hva _score er, vil jeg vise senere.

Filtrering etter dato

Vi bruker forespørsel range i konteksten filter:

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

Filtrering etter tagger

Bruker term query for å søke etter id-er av dokumenter som inneholder det angitte ordet:

# найдем все документы, в поле 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" : "Gøye kattunger",
        "tags" : [ "kattunger", "morsom historie" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Hvordan jeg fikk en kattunge",
        "tags" : [ "kattunger" ]
      }
    } ]
  }
}

Fulltekst søk

Tre av dokumentene våre inneholder følgende i feltet content:

  • <p>Morsom historie om kattunger<p>
  • <p>En morsom historie om valper<p>
  • <p>En hjerteskjærende historie om den stakkars kattungen fra gata<p>

Bruker match query for å søke etter id-er av dokumenter som inneholder det angitte ordet:

# 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 hvis vi søker etter «historier» i content-feltet, finner vi ingenting, fordi indeksen bare inneholder originale ord, ikke deres stammer. For å utføre et kvalitets søk, må analysatoren konfigureres.

Felt _score viser relevans. Hvis forespørselen utføres i filter-konteksten, vil verdien av _score alltid være 1, noe som betyr at det er full overensstemmelse med filteret.

Analysatorer

Analysatorer er nødvendige for å konvertere den opprinnelige teksten til et sett med token.
Analysatorer består av én Tokenizer og flere valgfrie TokenFilters. Tokenizer kan gå foran flere CharFilters. Tokenizer deler den opprinnelige strengen opp i tokens, for eksempel etter mellomrom og tegnsetting. TokenFilter kan endre tokens, fjerne eller legge til nye, for eksempel ved å beholde bare stammen av ordet, fjerne preposisjoner eller legge til synonymer. CharFilter endrer den opprinnelige strengen helt, for eksempel ved å kutte ut html-tagger.

I ES finnes det flere standardanalysatorer. For eksempel analysatoren russian.

La oss bruke api og se hvordan analysatorene standard og russian konverterer strengen «Gøye historier om kattunger»:

# используем анализатор 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" : "gøye",
    "start_offset" : 0,
    "end_offset" : 5,
    "type" : "",
    "position" : 0
  }, {
    "token" : "historier",
    "start_offset" : 6,
    "end_offset" : 14,
    "type" : "",
    "position" : 1
  }, {
    "token" : "om",
    "start_offset" : 15,
    "end_offset" : 17,
    "type" : "",
    "position" : 2
  }, {
    "token" : "kattunger",
    "start_offset" : 18,
    "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" : "morsom",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "historie",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "katt",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

Standardanalysatoren delte opp strengen etter mellomrom og konverterte alt til små bokstaver, den russiske analysatoren fjernet uvesentlige ord, konverterte til små bokstaver og beholdt ordstammen.

La oss se hvilke Tokenizer, TokenFilters, CharFilters som den russiske analysatoren bruker:

{
  "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 finnes ikke */
    }
  }
}

La oss beskrive vår analysator basert på russisk, som vil kutte ut HTML-tagger. Vi kaller den default, da analysatoren med dette navnet skal brukes som standard.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* legg til fjerning av HTML-tagger */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Først vil alle HTML-tagger bli fjernet fra den opprinnelige strengen, så vil den deles opp i tokens av standard tokenizer, de resulterende tokenene blir konvertert til små bokstaver, uvesentlige ord vil bli fjernet, og fra de gjenværende tokenene vil bare ordstammen være igjen.

Opprettelse av indeks

Over har vi beskrevet default analysator. Den vil bli brukt på alle strengfelt. Innlegget vårt inneholder et array med tags, så tags vil også bli behandlet av analysatoren. Siden vi søker innlegg etter nøyaktig samsvar med taggen, er det nødvendig å deaktivere analysen for feltet tags.

La oss opprette indeksen blog2 med analysator og mapping, der analysen av feltet tags er deaktivert:

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

Vi legger til de samme 3 innleggene i denne indeksen (blog2). Jeg vil utelate denne prosessen, ettersom den er lik tillegget av dokumenter til indeksen blog.

Fulltekstsøk med støtte for uttrykk

La oss bli kjent med en annen type forespørsel:

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

Siden vi bruker en analyser med russisk stemming, vil denne forespørselen returnere alle dokumenter, selv om de bare inneholder ordet ‘historie’.

Forespørselen kan inneholde spesielle symboler, for eksempel:

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

Syntaks for forespørselen:

+ betyr AND-operasjon
| betyr OR-operasjon
- negaterer et enkelt token
" omslutter et antall tokens for å indikere en frase for søk
* på slutten av et begrep indikerer en prefiksforespørsel
( og ) indikerer prioritet
~N etter et ord indikerer redigeringsavstand (uskarphet)
~N etter en frase indikererslop-mengde
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

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

Lenker

PS

Hvis du er interessert i lignende artikler-kurs, har ideer til nye artikler eller har forslag til samarbeid, vil jeg gjerne høre fra deg privat eller på e-post m.kuzmin+habr@darkleaf.ru.

Kilde: habr.com

Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere 🔥 Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster