Elasticsearch alapok

Elasticsearch — egy JSON REST API-val rendelkező keresőmotor, amely a Lucene-re épül és Java nyelven íródott. Ennek a motorának minden előnyét a hivatalos oldalonszövegben Elasticsearch-ot ES-nek fogjuk nevezni.

Hasonló motorokat használnak összetett keresések végrehajtására dokumentum adatbázisokban. Például nyelvi morfológia figyelembevételével vagy geo koordináták alapján történő kereséshez.

Ebben a cikkben az ES alapjairól fogok beszélni egy blogbejegyzések indexelésének példáján keresztül. Megmutatom, hogyan lehet szűrni, rendezni és keresni a dokumentumokat.

Annak érdekében, hogy ne függjek az operációs rendszertől, minden kérést az ES-hez CURL segítségével fogok végrehajtani. Emellett elérhető egy Google Chrome bővítmény, amelynek neve sense.

. A szövegben találhatók hivatkozások a dokumentációra és más forrásokra. A cikk végén gyorslinkek találhatók a dokumentáció eléréséhez. Az ismeretlen kifejezések definícióit a glosszáriumban.

Az ES telepítése

Ehhez először is szükségünk lesz Java-ra. A fejlesztők recommend ajánlják, hogy telepített Java verziója újabb legyen, mint a Java 8 update 20 vagy a Java 7 update 55.

Az ES disztribúciója elérhető a fejlesztői webhelyen. A csomag kicsomagolása után indítsuk el a bin/elasticsearchfájlt. Emellett elérhetők apt és yum csomagok. Létezik hivatalos Docker image. További információ a telepítésről.

A telepítés és indítás után ellenőrizzük a működést:

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

curl -X GET $ES_URL

Hasonló válasz érkezik majd:

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

Indexelés

Adjunk hozzá egy bejegyzést az ES-hez:

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

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>Vicces történet a kiscicákról<p>",
  "tags": [
    "kiscicák",
    "vicces történet"
  ],
  "published_at": "2014-09-12T20:44:42+00:00"
}'

a szerver válasza:

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

Az ES automatikusan létrehozta a indexre blog-ot és típus post-ot. Ezeket a fogalmakat egy hasonlat alapján is érthetjük: az index egy adatbázis, míg a típus egy tábla ebben az adatbázisban. Minden típusnak saját sémája van — mapping, akárcsak egy relációs táblának. A mapping automatikusan generálódik a dokumentum indexelésekor:

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

A szerver válaszában megjegyzésben hozzáadtam a megindexelt dokumentum mezőinek értékeit:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Vicces történet a kiscicákról<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": ["kiscicák", "vicces történet"] */
          "tags" : {
            "type" : "string"
          },
          /*  "title": "Vidám kiscicák" */
          "title" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

Érdemes megjegyezni, hogy az ES nem különbözteti meg az egyedi értéket és az értékek tömbjét. Például a title mező egyszerűen a címet tartalmazza, míg a tags mező sztringek tömbje, bár a mappingben ugyanolyan módon van bemutatva.
Később a mappingről részletesebben fogunk beszélni.

Kérések

Dokumentum lekérése az id-ja alapján:

# извлечем документ с 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>Vicces t\u00f6rt\u00e9net a kiscic\u00e1kr\u00f3l<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"
    }
}

A válaszban új kulcsok jelentek meg: _version és _source. Általánosságban elmondható, hogy minden kulcs, amely a _ elemmel kezdődik, rendszerkulcsokhoz tartozik.

Kulcs _version megmutatja a dokumentum verzióját. Ez szükséges az optimista zárak mechanizmusának működéséhez. Például, ha módosítani szeretnénk egy dokumentumot, amelynek verziója 1, akkor elküldjük a módosított dokumentumot, és jelezzük, hogy ez a dokumentum 1-es verziójának módosítása. Ha valaki más is szerkesztette az 1-es verziójú dokumentumot, és korábban küldte el a változtatásokat, akkor az ES nem fogadja el a mi módosításainkat, mivel ő már a 2-es verziójú dokumentumot tárolja.

Kulcs _source tartalmazza azt a dokumentumot, amelyet indexeltünk. Az ES nem használja ezt az értéket keresési műveletekhez, mivel a kereséshez indexek alkalmazandók. A helytakarékosság érdekében az ES a kiindulási dokumentumot tömörítve tárolja. Ha csak az azonosítóra van szükség, nem pedig az egész kiindulási dokumentumra, akkor le lehet tiltani a kiindulási dokumentum tárolását.

Ha nincs szükségünk kiegészítő információkra, akkor csak a _source tartalmát kaphatjuk meg:

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>Vicces t\u00f6rt\u00e9net a kiscic\u00e1kr\u00f3l<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"
}

Továbbá, csak bizonyos mezők is választhatók:

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

Indexáljunk be még néhány bejegyzést, és végezzünk el összetettebb lekérdezéseket.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Vidám kiskutyák",
  "content": "<p>Vicces történet a kiskutyákról<p>"
  "tags": [
    "kiskutyák",
    "vicces történet"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Hogyan lett egy kiscicám",
  "content": "<p>Szívszorító történet egy szegény utcai kiscicáról<p>",
  "tags": [
    "kiscicák"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

Rendezés

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

A legutolsó bejegyzést választottuk ki. méret korlátozza a dokumentumok számát a kimenetben. összesen megmutatja az összes olyan dokumentum számát, amely megfelel a lekérdezésnek. sort a kimenet tartalmaz egy egész számokból álló tömböt, amely alapján a rendezés történik. Azaz, a dátum egy egész számmá alakult. További információk a rendezésről olvashatók itt: dokumentáció.

Szűrők és lekérdezések

Az ES 2-es verziójától kezdve nem különíti el a szűrőket és a lekérdezéseket, ehelyett bevezetésre kerül a kontextusok fogalma.
A lekérdezések kontextusa eltér a szűrők kontextusától, mivel a lekérdezés _score-t generál, és nem kerül tárolásra. A _score-ról később fogok beszélni.

Dáta szerinti szűrés

Használjuk a lekérdezést range a filter kontextusában:

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

Címkékre történő szűrés

Használjuk term query azoknak a dokumentumoknak az azonosítójának keresésére, amelyek tartalmazzák a megadott szót:

# найдем все документы, в поле 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" : "Vidám kiscicák",
        "tags" : [ "kiscicák", "vicces történet" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Hogyan lett kiscicám",
        "tags" : [ "kiscicák" ]
      }
    } ]
  }
}

Teljes szöveg keresés

Három dokumentumunk tartalmazza a következőket a content mezőben:

  • <p>Vicces történet a kiscicákról<p>
  • <p>Vicces történet a kiskutyákról<p>
  • <p>Szívszorító történet egy szegény utcai kiscicáról<p>

Használjuk match query azoknak a dokumentumoknak az azonosítójának keresésére, amelyek tartalmazzák a megadott szót:

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

Ellenben, ha a 'történetek' kifejezésre keresünk a content mezőben, akkor semmit sem találunk, mivel az indexben csak az eredeti szavak találhatóak, nem pedig azok alapformái. A minőségi keresés érdekében be kell állítani az elemzőt.

Mező _score megmutatja relevancia. Ha a lekérdezés filter context-ben valósul meg, akkor a _score értéke mindig 1 lesz, ami a szűrő teljes megfelelődését jelenti.

Elemzők

Elemzők szükségesek az eredeti szöveg tokenekre bontásához.
Az elemzők egy Tokenizálóból és néhány opcionális TokenFilterből állnak.. A Tokenizáló előzheti meg a több CharFiltert.A Tokenizáló a bemeneti szöveget tokenekre bontja, például szóközök és írásjelek szerint. A TokenFilter módosíthatja a tokeneket, eltávolíthat vagy újakat adhat hozzá, például megtartva a szó alapját, eltávolítva a névelőt, szinonimákat hozzáadva. A CharFilter teljes egészében módosítja a bemeneti szöveget, például eltávolítja a html tageket.

Az ES-ben több szabványos elemző található.Például az russian elemzőt.

Használjuk ki api és nézzük meg, hogy a standard és a russian elemző hogyan alakítja a 'Vidám történetek a kiscicákról' szöveget:

# используем анализатор 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" : "vidám",
    "start_offset" : 0,
    "end_offset" : 5,
    "type" : "",
    "position" : 0
  }, {
    "token" : "történetek",
    "start_offset" : 6,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "a",
    "start_offset" : 16,
    "end_offset" : 17,
    "type" : "",
    "position" : 2
  }, {
    "token" : "kiscicákról",
    "start_offset" : 18,
    "end_offset" : 30,
    "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" : "vidám",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "történet",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "macska",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

A standard elemző szóközök mentén bontotta fel a szöveget, és az összes karaktert kisbetűsre alakította. Az orosz elemző eltávolította a jelentéktelen szavakat, átkonvertálta az összeset kisbetűsre, és megőrizte a szavak gyökerét.

Nézzük meg, hogy az orosz elemző milyen Tokenizer, TokenFilters, CharFilters elemeket használ:

{
  "filter": {
    "orosz_stop": {
      "type":       "stop",
      "stopwords":  "_orosz_"
    },
    "orosz_keywords": {
      "type":       "keyword_marker",
      "keywords":   []
    },
    "orosz_stemmer": {
      "type":       "stemmer",
      "language":   "orosz"
    }
  },
  "analyzer": {
    "orosz": {
      "tokenizer":  "standard",
      /* TokenFilters */
      "filter": [
        "lowercase",
        "orosz_stop",
        "orosz_keywords",
        "orosz_stemmer"
      ]
      /* CharFilters hiányoznak */
    }
  }
}

Írjuk le az orosz alapján felépített elemzőt, amely eltávolítja a HTML tageket. Nevezzük el defaultnak, mivel az így elnevezett elemző lesz az alapértelmezett.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_orosz_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "orosz"
    }
  },
  "analyzer": {
    "default": {
      /* HTML tagek eltávolítása */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Először az eredeti szövegből eltávolítják az összes HTML taget, majd az a standard tokenizáló segítségével tokenekre bontódik, az így kapott tokenek kisbetűsre alakulnak, eltávolítják a jelentéktelen szavakat, és végül a megmaradt tokenekből a szavak gyökerét kapjuk.

Index létrehozása

Fent leírtuk a default elemzőt. Ez alkalmazásra kerül minden szöveges mezőnél. A bejegyzésünk egy címke tömböt tartalmaz, így a címkék is feldolgozásra kerülnek az elemző által. Mivel pontos egyezés alapján keresünk a címkék között, szükséges letiltani az elemzést a tags mezőnél.

Hozzuk létre a blog2 indexet az elemzővel és a mappinggel, amelyben le van tiltva a tags mező elemzése:

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

Hozzáadjuk ugyanazokat a 3 bejegyzést ehhez az indexhez (blog2). Ezt a folyamatot kihagyom, mivel hasonló a dokumentumok indexbe való hozzáadásához.

Teljes szöveges keresés kifejezés támogatással

Ismerkedjünk meg egy másik lekérdezéstípussal:

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

Mivel orosz stemming analizátort használunk, ez a lekérdezés minden dokumentumot visszaad, bár csak az 'történet' szó szerepel bennük.

A lekérdezés speciális karaktereket tartalmazhat, például:

""sült tojás" +(padlizsán | burgonya) -frittata"

A lekérdezés szintaxisa:

+ jelzi az ÉS műveletet
| jelzi a VAGY műveletet
- egyetlen tokent tagad
" körülvesz egy csoportnyi tokent, hogy kifejezést keresni tudjunk
* a kifejezés végén prefix lekérdezést jelez
( és ) a precedenciát jelzik
~N egy szó után a szerkesztési távolságot (fuzziness) jelenti
~N egy kifejezés után a slop mennyiségét jelzi
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

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

Hivatkozások

PS

Ha érdeklik az ilyen cikkek-oktatóanyagok, van ötlete új cikkekre, vagy javaslatai vannak együttműködésre, örömmel várom az üzenetét személyesen vagy e-mailben a m.kuzmin+habr@darkleaf.ru címen.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster