تمرین عملی استفاده از Elasticsearch Query DSL در Laravel

تمرین عملی استفاده از Elasticsearch Query DSL در Laravel

امیدوارم که تا این بخش از دوره تونسته باشم محتوای مفیدی رو برای شما آماده کرده باشم. در آخرین قسمت از این فصل قصد دارم تا یک مثال عملی جستجو با سرویس Elasticsearch رو در فریمورک laravel باهم پیاده سازی کنیم.

فایل‌های پروژه رو از قبل آماده کردم و می‌تونید از اینجا پروژه‌ی لاراولی رو دانلود کنید. برای اجرای پروژه علاوه بر اینکه باید سرویس elasticsearch روی سیستم‌تون نصب باشه، از نصب بودن PHP و Composer هم مطمئن باشید (برای نصب PHP در ویندوز می‌تونید از XAMMP هم استفاده کنید!). پیشنهاد بنده اینه که kibana رو هم نصب کنید تا راحت‌تر بتونید API های سرویس elasticsearch رو اجرا کنید!

اگر PHP روی سیستم نصب باشه با دستور زیر می‌تونید نسخه‌ی اون رو مشاهده کنید:

php -v

و پاسخی مشابه زیر دریافت کنید:

PHP 7.4.13 (cli) (built: Nov 24 2020 12:43:32) ( ZTS Visual C++ 2017 x64 )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies

و در خصوص composer از دستور زیر استفاده کنید:

composer -v

و پاسخ زیر را دریافت کنید:

Composer version 2.0.8 2020-12-03 17:20:38

در مورد elasticsearch که هم که خودتون استادین! 😊

بعد از مراحل بالا پوشه‌ی پروژه‌ رو با IDE دلخواه باز کنید و از طریق terminal در مسیر اصلی پروژه، دستور زیر رو برای نصب پکیج‌های مورد نیاز وارد کنید:

composer install

صبر کنید تا composer تمامی پکیج‌ها رو دانلود (نیاز به اتصال اینترنت) و نصب کنه! خوب اگر به لاگی که دستور composer توی ترمینال میندازه دقت کنید می‌بینید در کنار پکیج‌هایی مثل پکیج اصلی laravel، از پکیجی به نام elasticsearch/elasticsearch نیز در لیست دانلود‌ها نام برده شده است.

 این پکیج یک کلاینت تحت زبان php برای ارتباط با سرویس elasticsearch در اختیار ما میذاره و به صورت رسمی توسط تیم Elastic توسعه داده شده است. برای سایر زبان‌های برنامه‌نویسی هم پکیج‌هایی ارائه شده که می‌تونید از اینجا به مستندات‌شون دسترسی داشته باشید.

نکته: در فایل composer.json پروژه‌ای که دانلود کردین، مشخص شده است که این پروژه به پکیج کلاینت elasticsearch نیاز دارد و به همین دلیل با دستور composer install این پکیج به صورت خودکار دانلود و نصب شد. اگر در پروژه‌ی لاراولی جدیدی بخواین که این پکیج رو استفاده کنید اول باید به لیست dependency های پروژه‌تون اضافه کنید. برای اینکار از دستور زیر استفاده کرده:

composer require elasticsearch/elasticsearch

سپس دستور composer install را مانند توضیحات بالا استفاده کنید.

بسیار خوب تا اینجای کار پروژه‌ی خودتون رو آماده به کار کردین. اما هنوز داده‌ای نداریم که بخوایم روی اون‌ها سرچ کنیم. نگران نباشید چون از اینجا می‌تونید فایل‌های مربوط به API های ایجاد ایندکس و ثبت مجموعه‌ای از داده‌های نمونه رو دانلود کنید.

بعد از دانلود فایل‌های بالا، ابتدا متن فایل create_index.txt رو در محیط dev tools کیبانا (اگر کیبانا رو نصب کردین!) یا در نرم‌افزاری مثل postman کپی و اجرا کنید. بعد از اون متن فایل bulk_index_data.txt رو اجرا کنید تا داده‌ها در ایندکس "sokanacademy_sample_data_2" ثبت شوند. 

خوب تبریک میگم در حال حاضر داده‌ی نمونه برای تست پروژه‌ هم آماده‌اس.

حالا وقت اون رسیده بریم ساختار پروژه رو بررسی کنیم و کانفیگ لازم برای اتصال به elasticsearch رو توی اون تنظیم کنیم.

اگر قبلا با لاراول کار کرده باشین توضیحات پیش‌رو برای شما آشنا خواهد بود و در غیر این‌صورت هم به سرعت با فایل‌های پروژه آشنا میشین. 

اولین قدم رو با ایجاد یک controller برای api مربوط به جستجو شروع می‌کنیم. در مسیر app/Http/Controllers یک فایل به نام SearchController وجود دارد:

<?php

namespace App\Http\Controllers;

use App\Adapters\Contracts\SearchEngineContract;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class SearchController
{
    /**
     * searches the value in search engine
     *
     * @param SearchEngineContract $searchEngineContract
     * @param Request $request
     * @return JsonResponse
     */
    public function __invoke(SearchEngineContract $searchEngineContract, Request $request)
    {
        $searchHits = $searchEngineContract->search(
            $request->query('q'),
            $request->query('size')
        );

        return response()->json($searchHits);
    }
}

در این فایل یک action به نام invoke__ تعریف شده است که در آن متد search از اینترفیس SearchEngineContract را صدا می‌زنیم. ورودی‌های این تابع مقادیر q و size هستند که به ترتیب بیانگر متن مورد جستجو و تعداد نتایج درخواستی می‌باشند. 

برای بررسی SearchEngineContract وارد مسیر app/Adapters میشیم و در این مسیر یک پوشه به نام Contracts و یک فایل به نام ElasticsearchContract می‌بینیم. در پوشه‌ی Contracts اینترفیس SearchEngineContract وجود داشته و پیاده سازی آن در کلاس ElasticsearchContract انجام شده است. پس وارد کلاس ElasticsearchContract می‌شویم:

<?php

namespace App\Adapters;

use App\Adapters\Contracts\SearchEngineContract;
use Elasticsearch\Client;
use Elasticsearch\ClientBuilder;

class ElasticsearchContract implements SearchEngineContract
{
    /**
     * @var Client
     */
    private Client $elasticsearchClient;

    /**
     * ElasticsearchContract constructor.
     */
    public function __construct()
    {
        $this->elasticsearchClient = ClientBuilder::create()->setHosts(
            config('elasticsearch.hosts')
        )->build();
    }

    /**
     * searches the given value in elasticsearch indices
     *
     * @param string $value
     * @param int $size
     * @return array
     */
    public function search(string $value, int $size): array
    {
        $hits = $this->elasticsearchClient->search([
            'index' => 'sokanacademy_sample_data_2',
            'size' => $size,
            '_source' => ['title', 'description'],
            'body' => [
                'query' => [
                    'dis_max' => [
                        'tie_breaker' => 0,
                        'queries' => [
                              …                                  
                        ]
                    ]
                ]
            ]
        ])['hits']['hits'];

        return collect($hits)->transform(function ($hit) {
            return array_merge(
                $hit['_source'],
                [
                    '_score' => $hit['_score']
                ]
            );
        })->toArray();
    }
}

در تابع construct__ این کلاس یک کلاینت جدید از elasticsearch ساخته شده و در پراپرتی elasticsearchClient این کلاس مقداردهی می‌شود:

/**
 * ElasticsearchContract constructor.
 */
public function __construct()
{
    $this->elasticsearchClient = ClientBuilder::create()->setHosts(
        config('elasticsearch.hosts')
    )->build();
}

در کدهای بالا آدرس سرور elasticsearch از فایل config خونده شده:

config(elasticsearch.hosts)

فایل کانفیگ مربوطه در مسیر config/elasticsearch.php قرار دارد و محتوای آن به صورت زیر است:

<?php

return [
    'hosts' => explode(',', env('ELASTICSEARCH_HOSTS')),
];

همونطور که می‌بینید یک کلید به نام hosts در این فایل تعریف شده و آدرس سرور elasticsearch رو از کلیدی به نام 'ELASTICSEARCH_HOSTS' در فایل env پروژه برمیداره. شما بعد از آماده سازی پروژه می‌تونید فایل env. در مسیر اصلی پروژه رو باز کرده و آدرس مدنظر خودتون که از طریق اون میشه به elasticsearch دسترسی داشت رو در آخرین خط این فایل وارد کنید:

ELASTICSEARCH_HOSTS=127.0.0.1:9200

بعد از تغییر فایل env. نیاز هست تا دستور زیر رو در ترمینال اجرا کنید تا تغییرات env بازخوانی شود:

php artisan optimize

نکته مهم: در نسخه‌ی Elasticsearch 8 تنظیمات basic security و پروتکل SSL به صورت خودکار فعال هست (توضیحات بیشتر در این قسمت قرار داده شده). بنابراین زمانی که بخوایم کلاینت Elasticsearch رو به سرور متصل کنیم، علاوه بر آدرس node ها باید نام کاربری و رمز عبور و همچنین آدرس فایل گواهینامه SSL رو هم به کلاینت بدیم تا کانکشن با موفقیت برقرار بشه.

 قطعه کدی که برای ساخت یک نمونه از کلاینت Elasticsearch  دیدیم، تنها آدرس node ها رو تعیین کرده بود (متود ()setHosts). برای تعیین نام کاربری و رمز عبور و همچنین آدرس دادن فایل گواهی SSL باید به روش کد زیر نمونه کلاینت رو ایجاد کنیم:

 $this->elasticsearchClient = ClientBuilder::create()
 	->setHosts(config('elasticsearch.hosts'))
 	->setBasicAuthentication('elastic', 'password for elastic user')
 	->setCABundle('path/to/http_ca.crt')->build();

همونطور که مشخصه باید توابع setBasicAuthentication و setCABundle نیز به ترتیب برای احراز هویت و معرفی گواهی SSL در زمان ایجاد نمونه‌ی کلاینت صدا زده بشن.

دقت کنید ورودی متود setCABundle آدرس فایل http_ca.crt هست که در نسخه‌ی 8 به صورت خودکار ساخته میشه و در مسیر /etc/elasticsearch/certs برای توزیع Debian قرار می‌گیره. می‌تونید این فایل رو کپی کرده و در مسیر پروژه تون قرار بدین تا آدرس دهی فایل راحت‌تر باشه.

بسیار خوب برگردیم به کلاس ElasticsearchContract ...

در متد search این کلاس کد‌های مربوط به کار با کلاینت elasticsearch و اجرای یک search api پیاده‌سازی شده است (در مثال بالا کدهای مربوط به بخش کوئری‌ها با سه نقطه جایگزین شده است تا از طولانی شدن متن کد در اینجا جلوگیری بشه! می‌تونید اصل کد‌ها رو توی پروژه مشاهده کنید.)

برای اینکه بتونید نحوه‌ی ایجاد کوئری در کلاینت زبان php رو با نوشتن کوئری در API های elasticsearch مقایسه کنید، در کنار فایل‌های مربوط به ایجاد داده‌های نمونه که دانلود کردین، یک فایل به نام search_query.txt وجود دارد که متن کوئری مورد نظر در قالب api رو می‌تونید اونجا ببینید و با کد پیاده سازی شده مقایسه کنید:

GET sokanacademy_sample_data_2/_search
{
  "query": {
    "dis_max": {
     "tie_breaker": 0,
      "queries": [
        {
         "bool": {
           "boost": 1.5,
           "should": [
              {
               "match_phrase": {
                 "title.standard": {
                   "query": ""
                  }
                }
              },
              {
               "multi_match": {
                 "query": "",
                 "fields": [
                   "description",
                   "body"
                  ],
                 "minimum_should_match": "-30%"
                }
              }
            ]
          }
        },
        {
         "bool": {
           "should": [
              {
               "match": {
                 "title": {
                   "query": "",
                   "boost": 3,
                   "fuzziness": "AUTO:4,7"
                  }
                }
              },
              {
                "match": {
                 "description": {
                   "query": "",
                   "minimum_should_match": "30%",
                   "boost": 2
                  }
                }
              },
              {
               "match": {
                 "body": {
                   "query": "",
                   "minimum_should_match": "-30%"
                  }
                }
              }
            ]
          }
        }
      ]
    }
  }
}

در متد Search پس از فراخوانی search api از طریق کلاینت، نتایج خروجی از elasticsearch رو که در مسیر [hits][hits] قرار دارن، در متغیری به نام hits$ قرار داده و نهایتا با استفاده از متد transform، مقدار داخل کلید source_ هریک از نتایج را به همراه کلید score_ در خروجی برمی‌گردانیم. خروجی این متد آرایه‌ای شبیه ساختار زیر است:

[
       {
              "title": "…",
              "description": "…",
              "_score": 23.7987
       },
       {
              "title": "…",
              "description": "…",
              "_score": 21.2321
       },
       …
]

سپس یک فایل به نام search.blade.php در مسیر resources/views ایجاد شده و مقداری کد CSS ، HTML و Javascript در آن به کار رفته تا ضمن فراخوانی api موردنظر بتونیم نتایج خروجی جستجو رو در لایه‌ی UI نمایش دهیم.

در نهایت هم نیاز به تعریف یک api برای جستجو داریم که اینکار رو در فایل api.php در مسیر routes انجام دادم:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/


Route::get('/search', [\App\Http\Controllers\SearchController::class, '__invoke']);

خوب وقت اجرای پروژه‌ی و دیدن نتیجه هست. در ترمینال دستور زیر رو اجرا کنید تا پروژه در آدرس http://127.0.0.1:8000/ قابل فراخوانی بشه:

Php artisan serve

مروگر خودتون رو باز کنید و آدرس رو در اون وارد کنید. اگر پروژه به درستی تنظیم شده باشه و مشکلی نداشته باشه باید بتونید صفحه‌ی زیر رو مشاهده کنید:

تبریک ... 

شما در حال حاضر در حال مشاهده‌ی صفحه‌ی search پروژه هستین 😊

حالا وقت اون رسیده که به جمله‌ی "Let’s play with Elasticsearch" عمل کنید و با تایپ در input قرار داده شده در صفحه ببینید elasticsearch چه نتایجی رو در اختیارتون میذاره!

سعی کنید کوئری جستجو به کار رفته رو کاملا بررسی کنید و قابلیت‌هایی که در اون استفاده شده رو مورد آزمایش قرار بدین (مثلا استفاده از fuzziness) تا بتونید هرچه بهتر با کارکرد کوئری‌های جستجوی elasticsearch بهتر آشنا بشین. 

هر سوالی یا ایرادی در خصوص هر قسمت از این پروژه داشتین با من درمیون بذارین و سعی می‌کنم حتما به اون‌ها پاسخ بدم.

 

موفق باشین ...