Implementasi Elastic Search dan Synonym Filter di Laravel 5.8

Sesuai dengan judul, tulisan kali ini mencoba untuk implementasi elasticsearch di laravel lengkap dengan menambahkan fitur sinonim. Tulisan ini ada 4 bagian, pertama memasang elasticsearch, kedua memasang laravel dan melakukan pencarian sederhana, ketiga implementasi elasticsearch dengan laravel, terakhir menambahkan filter sinonim.

Memasang Elasticsearch

Karena saya tidak mau ribet saya memasang elasticsearch versi docker dengan bantuan docker-compose, di berkas docker-compose.yml saya cukup membuat seperti ini

version: '3.3'
services:
  elasticsearch:
    image: elasticsearch:6.6.0
    ports:
      - '9200:9200'
    volumes:
      - .:/usr/share/elasticsearch/data
    environment:
      ES_JAVA_OPTS: '-Xms256m -Xmx256m'
      network.bind_host: 0.0.0.0
      network.host: 0.0.0.0
      discovery.type: single-node
      cluster.name: my-cluster

Lalu jalankan perintah berikut untuk memasang elasticsearch-nya

docker-compose up --build -d

Untuk mengetesnya kita bisa kunjungi 0.0.0.0:9200 dan akan muncul tampilan seperti berikut.

Memasang laravel

Saya anggap yang membaca di sini sudah paham memasang laravel, maka saya akan berfokus kepada pencarian sederhana saja. Pertama saya akan buat model Product yang memiliki daftar produck tertentu. Mari buat model dan migrationnya.

php artisan make:model Product -m

Isi dari model Product

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $guarded = ['id'];
}

Ubah migration product menjadi

 public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title');
            $table->string('slug');
            $table->text('description');
            $table->timestamps();
        });
    }

Kita butuh contoh data untuk melakukan pencarian, kita buat seed manual bukan dengan factory

  php artisan make:seeder ProductSeeder

Isi dari seeder saya tambahkan seperti berikut

 public function run()
    {
        $data = [
            ['title'=>'Laptop Series T','slug'=>'laptop-series-t','description'=>'Laptop series t'],
            ['title'=>'Laptop Series X','slug'=>'laptop-series-x','description'=>'Laptop series x'],
            ['title'=>'Notebook Series T','slug'=>'notebook-series-t','description'=>'Notebook series t'],
            ['title'=>'Notebook Series x','slug'=>'notebook-series-x','description'=>'Notebook series x'],
            ['title'=>'PC','slug'=>'pc','description'=>'daily pc'],
            ['title'=>'PC Gaming','slug'=>'pc-gaming','description'=>'pc gaming']
        ];
        Product::insert($data);
    }

Setelah siap jalankan perintah berikut

php artisan migrate
php artisan db:seed --class=ProductSeeder

Untuk membuat pencarian sederhana kita buat langsung di route api saja. Buka berkas api.php dan tambahkan route seperti berikut

Route::get('/products', function () {
    return \App\Product::all();
});

Route::get('/products/{param}', function ($param) {
    return \App\Product::where('title','like','%'.$param.'%')->get();
});

Saat akses /products

Saat akses pencarian

Implementasi Elasticsearch

Saya menggunakan paket dari babenkoivan/scout-elasticsearch-driver. Maka dari itu mari pasang terlebih dahulu paket yang dibutuhkan.

composer require babenkoivan/scout-elasticsearch-driver

Jika sudah selesai memasang, kita perlu keluarkan konfigurasinya

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
php artisan vendor:publish --provider="ScoutElastic\ScoutElasticServiceProvider"

Setelah di-publish perbarui berkas .env, tambahkan dua parameter berikut

SCOUT_DRIVER='elastic'
SCOUT_ELASTIC_HOST=0.0.0.0:9200

Seperti biasa perubahan di .env maka jalankan perintah berikut

php artisan config:cache

Selanjutnya kita akan membuat konfigurasi untuk index di elastic-nya, untuk membuat berkas konfigurasinya jalankan perintah

php artisan make:index-configurator ProductIndexConfigurator

Selanjutnya ubah model Product menjadi seperti berikut

<?php

namespace App;

use ScoutElastic\Searchable;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use Searchable;
    protected $guarded = ['id'];
    /**
     * @var string
     */
    protected $indexConfigurator = ProductIndexConfigurator::class;

    /**
     * @var array
     */
    protected $searchRules = [
        //
    ];

    /**
     * @var array
     */
     // mapping ini sesuaikan dengan field di model terkait
    protected $mapping = [
        'properties' => [
            'title' => [
                'type' => 'text',
                'analyzer'=>'standard'
            ],
            'slug' => [
                'type' => 'text',
                'analyzer'=>'standard'
            ],
            'description' => [
                'type' => 'text',
                'analyzer'=>'standard'
            ],
            'created_at' => [
                'type' => 'date',
                'format'=>'yyyy-MM-dd HH:mm:ss'
            ],
        ]
    ];
}

Jika tidak mau ribet, kalian bisa menjalankan perintah berikut agar otomatis terbentuk ( kecuali isi dari mapping ) php artisan make:searchable-model Product –index-configurator=ProductIndexConfigurator

Tapi pastikan model sebelumnya dihapus atau di-rename terlebih dahulu jika sudah ada.

Selanjutnya yang kita butuhkan adalah “mendaftarkan” index nya ke dalam elasticsearch, jalankan perintah berikut untuk membuat index di elastic

php artisan elastic:create-index 'App\ProductIndexConfigurator'

Selanjutnya kita akan melakukan mapping dari model ke index, lakukan perintah berikut:

php artisan elastic:update-mapping 'App\Product'

Selanjutnya kita akan mendaftarkan record dalam database ke elastic menggunakan perintah berikut

php artisan scout:import 'App\Product'

Perintah itu akan menampilkan pesan

Imported [App\Product] models up to ID: 6
All [App\Product] records have been imported.

Untuk mengetes pencarian dengan elastic berhasil atau tidak, kita ubah kode pencariannya menjadi berikut

 return \App\Product::search($param)->get();

Jika berhasil pencarian akan normal seperti tadi

Sinonim

Fitur sinonim ini sesuai namanya mencari data yang merupakan sinonim dari pencariannya, misalkan jika saya mendaftarkan sinonim laptop adalah notebook, maka saat saya mencari dengan kata kunci laptop maka produk notebook pun akan muncul, begitupun sebaliknya

Pertama kita harus membuat filter sinonimnya terlabih dahulu, buka berkas ProductIndexConfigurator, lalu pada bagian variable $settings saya ubah menjadi berikut

protected $settings = [
        'analysis'=>[
            'filter' => [
                'product_synonym_filter' => [
                    'type'=> 'synonym',
                    'synonyms' => [
                        'laptop, notebook'
                    ]
                ]
            ],
            'analyzer' => [
                'product_synonyms' => [
                    'tokenizer' => 'standard',
                    'filter'=> [
                        'lowercase',
                        'product_synonym_filter'
                    ]
                ]
            ]
        ],
        
    ];

Lalu kita ubah mapping dari model juga menjadi

 protected $mapping = [
        'properties' => [
            'title' => [
                'type' => 'text',
                'analyzer'=>'product_synonyms'
            ],
            'slug' => [
                'type' => 'text',
                'analyzer'=>'standard'
            ],
            'description' => [
                'type' => 'text',
                'analyzer'=>'standard'
            ],
            'created_at' => [
                'type' => 'date',
                'format'=>'yyyy-MM-dd HH:mm:ss'
            ],
        ]
    ];

Karena kita melakukan perubahan mapping kita tidak bisa dengan mudah cukup me-update index, karena saat kita mapping ulang kita akan mendapatkan pesan error bentrok dengan mapping yang ada, maka cara paling aman adalah dengan menghapus index dan mengulang dari awal.

 php artisan elastic:drop-index 'App\ProductIndexConfigurator'
 php artisan elastic:create-index 'App\ProductIndexConfigurator'
 php artisan elastic:update-mapping 'App\Product'
 php artisan scout:import 'App\Product'

Jika suatu saat kita hanya butuh mengubah index tanpa merlu mengubah mapping yuang kita perlukan hanyalah perintah update index:

php artisan elastic:update-index ‘App\ProductIndexConfigurator’

Jika sudah lakukan pencarian kembali dengan kata kunci laptop misalnya, seharusnya akan muncul seperti berikut

Source Code

https://gitlab.com/ariesmaulana/laravel-elastic-synonym

Referensi:

https://packagist.org/packages/babenkoivan/scout-elasticsearch-driver

https://www.elastic.co/guide/en/elasticsearch/guide/current/using-synonyms.html