Nette a Elasticsearch

Elasticsearch je škálovatelné úložiště umožňující nejen fultextové vyhledávání v reálném čase. Využít se dá několika způsoby, primárně byl ale vytvořen právě pro vyhledávání. Pokud máte data v MySQL nebo jiné relační databázi a zjišťujete, že vyhledávání pomocí LIKE už vám nestačí, nasazení Elasticsearch může být další logický krok.

Trocha motivace úvodem

Co vám tedy Elasticsearch nabízí a na co jej můžete použít? Pokusím se to shrnout ve třech bodech:

1. Elasticsearch jako fulltextové vyhledávání

S pomocí volně dostupných slovníků můžete například vyhledávat klíčové slovo nehledě na skloňování, nebo naimplementovat našeptávač, "měli jste na mysli" a další pokročilé nástroje. Elasticsearch je postaven nad Apache Lucene a plně jej využívá.

2. Elasticsearch jako rychlé distribuované úložiště

Do Elasticsearch můžete uložit jakýkoli dokument ve formátu JSON. Pokud vám pak běží na více strojích, jsou vytvářeny kopie dokumentů a o data tak v případě výpadku nebo poruchy některého z serverů nepříjdete. Navíc je pro většinu výsledků používána cache, takže jsou odpovědi opravdu rychlé (za cenu pomalejšího ukládání).

3. Elasticsearch jako analytický nástroj

V kombinaci s nástrojem Kibana je možné Elasticsearch využít jako pokročilý analytický nástroj poskytující reporty v reálném čase. Můžete tak do Elasticsearch ukládat například access log webového serveru a v Kibaně pak vytvořit grafy s počty přístupů, mapy a další statistiky.

Instalace Elasticsearch

1. Stažení

Pro použití v Nette projektu je spuštění otázkou několika minut. Nejprve navštivte stránky společnosti Elastic a v sekci downloads stáhněte aktuální verzi v zip archivu:

Stažení Elasticsearch

Pokud budete chtít rozběhnout Elasticsearch na serveru s OS Linux, doporučuji využít repozitářů - dojde také k instalaci init skriptů. ZIP archiv rozbalte do vaší oblíbené složky (v mém případě Downloads) a vznikne adresářová struktura podobná této:

Stažený a rozbalený Elasticsearch

2. Konfigurace

Před samotným spuštěním doporučuji upravit konfiguraci v souboru elasticsearch.yml (druhá šipka). Otevřete jej v libovolném editoru - jde o prostý textový soubor. Vzorová konfigurace pro lokální vývoj by mohla vypadat takto:

cluster.name: mylocalcluster  
node.name: "My Node"  
node.local: true  
index.number_of_shards: 1  
index.number_of_replicas: 0  

Pokusím se vysvětlit jednotlivé řádky konfigurace.

cluster.name je název clusteru, ve kterém se daný uzel (node) nachází. Elasticsearch sám prozkoumává síť a snaží se připojit ostatní uzly se stejným jménem cluster.name do jednoho clusteru. Změnou tak předejdete tomu, že se vám Elasticsearch chová podivně, pokud jej používá více vývojářů na lokální síti.

node.name je název uzlu který spouštíte, slouží pro jeho jednoznačnou identifikaci například v pluginu kopf. Pokud necháte parametr prázdný, vygeneruje se náhodné jméno.

node.local umožní pouze lokální přístup k Elasticsearch. Pozor na bezpečnost, pokud necháte na produkci zvenčí dostupný port 9200 - i tam se může hodit "lokální" přístup k nodu.

index.number_of_shards a index.number_of_replicas nemá smysl pro lokální vývoj nastavovat jinak. Pokud vám vypadne jediný počítač, na kterém máte data, je jedno v kolika kopiích byla uložena :). Při nasazení na produkci doporučuji rozmyslet nastavení index.number_of_shards, protože po vytvoření indexu jej již nezměníte.

3. Spuštění

Před spuštěním se ujistěte, že máte nainstalovanou javu, pokud ne, bude třeba ji stáhnout. Nyní spusťte soubor elasticsearch ve složce bin (první šipka v obrázku s rozbaleným archivem). Můžete tak provést z konzole:

$ pwd
/Users/ludekvesely/Downloads/elasticsearch-1.7.1
$ ./bin/elasticsearch

To že se povedlo Elasticsearch spustit ověříte na adrese http://localhost:9200. Nové verze vznikají velmi rychle, nelekejte se tedy mé starší verze 1.5.2. ;-)

Spuštěný Elasticsearch

Vytvoření Nette projektu

Předpokládám že s Nette již máte zkušenosti a pravděpodobně jen chcete využít možností Elasticsearch v existujícím projektu. Pokud přesto nemáte žádný projekt vytvořený, nainstalujte si composer a následně pomocí příkazu

composer create-project nette/web-project  
cd web-project  
chmod -R a+rw temp log  

Vytvořte prázdný projekt. Tato varianta je od verze Nette 2.3 preferovaná před nette/sandbox, více na fóru Nette. Projekt přesuňte do veřejně přístupného adresáře vašeho webového serveru a po přístupu na soubor index.php ve složce web-project/www byste měli vidět úvodní obrazovku:

Nette Web Project

Instalace Kdyby/ElasticSearch

Otevřte terminál ve složce s projektem a zadejte příkaz

composer require kdyby/elastic-search  

Nyní se nainstalovaly knihovny ruflin/elastica (samotná knihovna pracující s Elasticsearch) a kdyby/elastic-search (napojení na Nette). Zaktualizovaly se také soubory composer.json a composer.lock.

Dále je třeba rozšíření zaregistrovat v Nette a nakonfigurovat. To je maximálně jednoduché - do souboru app/config/config.neon přidejte následující řádky:

extensions:  
    search: Kdyby\ElasticSearch\DI\SearchExtension

search:  
    host: 127.0.0.1
    port: 9200

No a to je vlastně všechno! Nyní si stačí kdekoli vyžádat Kdyby\ElasticSearch\Client, která dědí od Elastica\Client a s připojením dále pracovat podle dokumentace knihovny Elastica.

Použití knihovny Elastica v Nette aplikaci

Ve vzorové aplikaci pro zachování jednoduchosti ukáži použití v již vytvořeném presenteru HomepagePresenter.php, ve vaší aplikaci ale tuto logiku doporučuji přenést do modelu. Otevřte tedy zmíněný soubor HomepagePresenter.php a do třídy HomepagePresenter přidejte:

private $client;

public function __construct(\Kdyby\ElasticSearch\Client $client)  
{
    $this->client = $client;
}

Nyní můžete využívat připojení k Elasticsearch pomocí proměnné $client. Například můžu nejprve vytvořit index s názvem my-index, nastavím mu počet shardů roven jedné a budu pracovat s type nazvaným data.

$index = $this->client->getIndex('my-index');
$index->create(['number_of_shards' => 1]);
$type = $index->getType('data');

Následně mohu definovat mapování. Například chci, aby se pro koaždé pole nazvané @timestamp použil datový typ datum:

$mapping = new \Elastica\Type\Mapping();
$mapping->setType($type);
$mapping->setParam('dynamic_templates', [
    ['template_timestamp' => [
        'match' => '@timestamp',
        'mapping' => [
            'type' => 'date', 
            'format' => 'date_time_no_millis'
        ]
    ]]
]);
$mapping->send();
$type->setMapping($mapping);

Nyní lze zaindexovat dokument, v tomto příkladu pole $raw:

$raw = [
    'name' => 'Nette',
    'year' => 2000
];
$document = new \Elastica\Document(1, $raw);
$type->addDocument($document);
$index->refresh();

Tím jsme dokument zaindexovali do Elasticsearch a měl by být vyhledatelný. Ověřit to můžeme jednoduchým vyhledáním všech dokumentů:

foreach ($type->search([])->getResults() as $result) {  
    dump($result->getSource());
}

Výstupem (http://localhost/web-project/www/) by pak měl být námi zaindexovaný dokument:

array (2)  
   name => "Nette" (5)
   year => 2000

Výsledný soubor HomepagePresenter.php by pak měl vypadat takto:

<?php

namespace App\Presenters;

use Nette;


class HomepagePresenter extends Nette\Application\UI\Presenter  
{
    /**
     * @var \Kdyby\ElasticSearch\Client
     */
    private $client;

    public function __construct(\Kdyby\ElasticSearch\Client $client)
    {
        $this->client = $client;
    }

    public function actionDefault()
    {
        // name of index is 'my-index'
        $index = $this->client->getIndex('my-index');

        // create index with single shard
        $index->delete();
        $index->create(['number_of_shards' => 1]);

        // name of type is 'data'
        $type = $index->getType('data');

        // put mappings (every field called @timestamp will be stored as a date)
        $mapping = new \Elastica\Type\Mapping();
        $mapping->setType($type);
        $mapping->setParam('dynamic_templates', [
            ['template_timestamp' => [
                'match' => '@timestamp',
                'mapping' => ['type' => 'date', 'format' => 'date_time_no_millis']
            ]]
        ]);
        $mapping->send();
        $type->setMapping($mapping);

        // index an array into es
        $raw = [
            'name' => 'Nette',
            'year' => 2000
        ];
        $document = new \Elastica\Document(1, $raw);
        $type->addDocument($document);

        // refresh es index
        $index->refresh();

        // now we can search indexed document
        foreach ($type->search([])->getResults() as $result) {
            dump($result->getSource());
        }

        $this->terminate();
    }
}

Kam dál?

Jsme u konce návodu jak rozběhnout knihovnu Kdyby/Elasticsearch v Nette projektu. Pro další informace o Elasticsearch doporučuji přímo web společnosti Elastic a jejich oficiální dokumentaci, pokud máte radši tištěnou literaturu, tak by měla být dobrá kniha The Definitive Guide. Pro práci s knihovnou Elastica je veškeré podrobné info na jejich webu elastica.io. Zdrojové kódy knihovny Kdyby/ElasticSearch pak naleznete na githubu.

Luděk Veselý

PHP Developer