Základy elastického vyhledávání

Elasticsearch je vyhledávač s json rest api, který používá Lucene a je napsaný v Javě. Popis všech výhod tohoto motoru je k dispozici na oficiální stránky. V následujícím budeme označovat Elasticsearch jako ES.

Podobné nástroje se používají pro komplexní vyhledávání v databázi dokumentů. Například vyhledávání s přihlédnutím k morfologii jazyka nebo vyhledávání podle geo souřadnic.

V tomto článku budu hovořit o základech ES na příkladu indexování blogových příspěvků. Ukážu vám, jak dokumenty filtrovat, třídit a vyhledávat.

Abych nebyl závislý na operačním systému, budu všechny požadavky na ES provádět pomocí CURL. Existuje také plugin pro google chrome tzv význam.

Text obsahuje odkazy na dokumentaci a další zdroje. Na konci jsou odkazy pro rychlý přístup k dokumentaci. Definice neznámých pojmů naleznete v glosáře.

Instalace

K tomu potřebujeme nejprve Javu. Vývojáři Doporučit nainstalujte verze Java novější než Java 8 update 20 nebo Java 7 update 55.

Distribuce ES je k dispozici na vývojářský web. Po rozbalení archivu musíte spustit bin/elasticsearch. Také dostupný balíčky pro apt a yum. Tam je oficiální obrázek pro docker. Více o instalaci.

Po instalaci a spuštění zkontrolujeme funkčnost:

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

curl -X GET $ES_URL

Obdržíme něco takového:

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

Indexování

Přidáme příspěvek do 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"
}'

odpověď serveru:

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

ES automaticky vytvořeno index blog a Typ pošta. Můžeme nakreslit podmíněnou analogii: index je databáze a typ je tabulka v této databázi. Každý typ má své vlastní schéma − mapování, stejně jako relační tabulka. Mapování se generuje automaticky, když je dokument indexován:

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

V odpovědi serveru jsem do komentářů přidal hodnoty polí indexovaného dokumentu:

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

Stojí za zmínku, že ES nerozlišuje mezi jednou hodnotou a polem hodnot. Například pole title jednoduše obsahuje titulek a pole tagů obsahuje pole řetězců, i když jsou v mapování reprezentovány stejným způsobem.
Více o mapování si povíme později.

žádosti

Načtení dokumentu podle jeho 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"
  }
}

V odpovědi se objevily nové klíče: _version и _source. Obecně platí, že všechny klíče začínající na _ jsou klasifikovány jako oficiální.

Klíč _version zobrazuje verzi dokumentu. Je potřeba, aby optimistický zamykací mechanismus fungoval. Například chceme změnit dokument, který má verzi 1. Odešleme změněný dokument a uvedeme, že se jedná o úpravu dokumentu s verzí 1. Pokud někdo jiný také upravoval dokument s verzí 1 a odeslal změny před námi, pak ES nepřijme naše změny, protože ukládá dokument s verzí 2.

Klíč _source obsahuje dokument, který jsme indexovali. ES nepoužívá tuto hodnotu pro vyhledávací operace, protože K vyhledávání se používají indexy. Pro úsporu místa ES ukládá komprimovaný zdrojový dokument. Pokud potřebujeme pouze id a ne celý zdrojový dokument, můžeme zakázat zdrojové úložiště.

Pokud nepotřebujeme další informace, můžeme získat pouze obsah _source:

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

Můžete také vybrat pouze určitá pole:

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

Pojďme indexovat několik dalších příspěvků a spustit složitější dotazy.

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

Třídit

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

Vybrali jsme poslední příspěvek. size omezuje počet vydávaných dokladů. total zobrazuje celkový počet dokumentů odpovídajících požadavku. sort ve výstupu obsahuje pole celých čísel, podle kterých se provádí řazení. Tito. datum bylo převedeno na celé číslo. Více informací o třídění naleznete v dokumentace.

Filtry a dotazy

ES od verze 2 nerozlišuje mezi filtry a dotazy je zaveden pojem kontextů.
Kontext dotazu se liší od kontextu filtru v tom, že dotaz generuje _score a neukládá se do mezipaměti. Později vám ukážu, co je _skóre.

Filtrovat podle data

Používáme žádost rozsah v kontextu filtru:

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

Filtrování podle značek

Používáme termínový dotaz vyhledání ID dokumentů obsahujících dané slovo:

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

Fulltextové vyhledávání

Tři z našich dokumentů obsahují v poli obsahu následující:

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

Používáme vyhledávací dotaz vyhledání ID dokumentů obsahujících dané slovo:

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

Pokud však hledáme „příběhy“ v poli obsahu, nic nenajdeme, protože Rejstřík obsahuje pouze původní slova, nikoli jejich kmeny. Abyste mohli provádět vysoce kvalitní vyhledávání, musíte nakonfigurovat analyzátor.

Pole _score ukazuje relevantnost. Pokud je požadavek proveden v kontextu filtru, bude hodnota _score vždy rovna 1, což znamená úplnou shodu s filtrem.

Analyzátory

Analyzátory jsou potřebné k převodu zdrojového textu na sadu tokenů.
Analyzátory se skládají z jednoho Tokenizer a několik volitelných TokenFilters. Tokenizeru může předcházet několik CharFilters. Tokenizéry rozdělují zdrojový řetězec na tokeny, jako jsou mezery a interpunkční znaménka. TokenFilter umí měnit tokeny, mazat nebo přidávat nové, například ponechat pouze kmen slova, odstraňovat předložky, přidávat synonyma. CharFilter - změní celý zdrojový řetězec, například vyřízne html tagy.

ES jich má několik standardní analyzátory. Například analyzátor ruština.

Pojďme použít api a podívejme se, jak standardní a ruské analyzátory transformují řetězec „Funny stories about koťata“:

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

Standardní analyzátor rozdělil řetězec mezerami a vše převedl na malá písmena, ruský analyzátor odstranil nedůležitá slova, převedl je na malá písmena a ponechal kmen slov.

Podívejme se, který Tokenizer, TokenFilters, CharFilters používá ruský analyzátor:

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

Pojďme si popsat náš analyzátor založený na ruštině, který vystřihne html tagy. Říkejme tomu výchozí, protože ve výchozím nastavení bude použit analyzátor s tímto názvem.

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

Nejprve budou ze zdrojového řetězce odstraněny všechny HTML tagy, poté jej standard tokenizer rozdělí na tokeny, výsledné tokeny se přesunou na malá písmena, nepodstatná slova budou odstraněna a zbývající tokeny zůstanou kmenem slova.

Vytvoření indexu

Výše jsme popsali výchozí analyzátor. Bude platit pro všechna pole řetězce. Náš příspěvek obsahuje pole tagů, takže tagy budou také zpracovány analyzátorem. Protože Hledáme příspěvky podle přesné shody se značkou, pak musíme zakázat analýzu pro pole značek.

Vytvořme index blog2 s analyzátorem a mapováním, ve kterém je analýza pole značek zakázána:

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

Pojďme přidat stejné 3 příspěvky do tohoto indexu (blog2). Tento proces vynechám, protože... je to podobné jako přidávání dokumentů do indexu blogu.

Fulltextové vyhledávání s podporou výrazů

Podívejme se na další typ žádosti:

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

Protože Používáme analyzátor s ruským původem, pak tento požadavek vrátí všechny dokumenty, i když obsahují pouze slovo 'historie'.

Žádost může obsahovat speciální znaky, např.

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

Syntaxe požadavku:

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

reference

PS

Pokud máte zájem o podobné články-lekce, nápady na nové články nebo návrhy na spolupráci, pak budu rád, když mi pošlete zprávu do osobní zprávy nebo emailu [chráněno e-mailem].

Zdroj: www.habr.com