ساخت یک جستجوگر ساده با لاراول و ElasticSearch

اخیرا برای یکی از پروژه هام مشتری از من خواست تا یک موتور جستجو برای وبسایتش بسازم که دقیق عمل کنه .  من به طور معمول از جستجوی متن کامل MySQL استفاده میکنم و با استفاده از کوئری های لاراول اینکارو انجام میدم اما این بار ترجیح دادم کمی ماجراجویی کنم . این آموزش نتیجه ی آموخته های من در حین یادگیری ElasticSearch است. هدف من نشان دادن نحوه ی راه‌اندازی پکیج لاراول Elasticquent و روش های پایه برای تنظیم خوب موتور جستجویتان است.

نکته: این آموزش برای کسانی نوشته شده که با لاراول آشنا هستند اما تا به حال با ElasticSearch کار نکرده‌اند و نیاز به یک راهنما برای راه‌اندازی این دو با هم دارند.

 

نصب ElasticSearch

اگر تا به حال ElasticSearch را نصب نکرد‌ه‌اید برای درک اصول کار ElasticSearch ادامه ی این آموزش را بخوانید. همچنین اگر از Chrome استفاده میکنید، من پلاگین Postman را به شما پیشنهاد میکنم. این پلاگین  امکان اجرای دستورات REST را در یک محیط گرافیکی به جای ترمینال فراهم میکند . بعد از نصب پلاگین روی chorme با استفاده از دستور زیر مطمئن شوید ElasticSearch در حال اجرا است.

curl -XPOST 'http://localhost:9200/?pretty'

اگر همه چیز درست کار کند باید یک پاسخ json به شکل زیر نمایش داده شود.

{
    "status": 200,
    "name": "Mystique",
    "cluster_name": "elasticsearch_mitch",
    "version": {
        "..."
    },
    "tagline": "You Know, for Search"
}

 

نصب Laravel

خب در این مرحله نیاز هست که یک لاراول جدید روی سیستم نصب کنید و من از قبل نصب کردم و آماده دارم .

پکیج ها

در این آموزش از ۲ پکیج استفاده خواهیم کرد، Elasticquent و Faker. اگر میخواهید از داده های خودتان استفاده کنید میتوانید از Faker استفاده نکنید. من برای آموزش از Faker اسفتاده میکنم.

فایل composer.json را باز کرده و و این مقادیر را به شیء(Object) Require اضافه کنید.

"fairholm/elasticquent": "1.0.*",
"fzaninotto/Faker": "1.4.*"

دستور composer update –prefer-distرا اجرا کنید تا بعد جداول پایگاه داده را ایجاد کنیم.

تولید جدول‌های پایگاه داده

میخواهیم یک جدول “posts” ایجاد کنیم که ۳ ستون “title”، “content”و “tags” دارد.

php artisan migrate:make create_posts_table

در فایل مایگریشن(که در آدرسapp/db/migrations/<DATETIME>_create_posts_table.php قرار دارد) از کد زیر استفاده کنید:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CreatePostsTable extends Migration {

        /**
         * Run the migrations.
         *
         * @return void
         */
        public function up()
        {
                Schema::create('posts', function(Blueprint $table)
                {
                        $table->increments('id');
                        $table->string('title');
                        $table->text('content');
                        $table->string('tags');
                        $table->timestamps();
                });
        }


        /**
         * Reverse the migrations.
         *
         * @return void
         */
        public function down()
        {
                Schema::drop('posts');
        }

}

مایگریشن را انجام دهید و بعد مقداری داده ی ساختگی تولید میکنیم.

داده‌ی ساختگی

ابتدا مقداری داده‌ی آزمایشی را در یک فایل Seeder میریزیم. این کار به ما کمک میکند که بدون دردسر وارد کردن دستی اطلاعات سیستم جستوجو را تست کنیم.

مدل Post اولیه‌‌ ما باید چیزی شبیه به این باشد:

<?php

class Post extends Eloquent {
  public $fillable = ['title', 'content', 'tags'];
}

فایل app/database/seeds/PostsTableSeeder.php را بسازید و کد زیر را در آن بنویسید:

<?php 

class PostsTableSeeder extends Seeder {


    public function run()
    {
        // Remove any existing data
        DB::table('pages')->truncate();

        $faker = Faker\Factory::create();
        
        // Generate some dummy data
        for($i=0; $i<30; $i++) {
          Post::create([
            'title' => $faker->sentence(3),
            'content' => $faker->paragraph(5),
            'tags' => join(',', $faker->words(5))
          ]);
        }
    }

}

بعد از اجرای دستور php artisan db:seed –class="PostsTableSeederباید مقدار زیادی داده‌ی آزمایشی برای کارمان داشته باشیم!

 

نصب Elasticquent و استفاده در model

بیایید با وارد کردن کد زیر به (app/models/Post.php) model خودمان را تغییر دهیم.

<?php
use Elasticquent\ElasticquentTrait;

class Post extends \Eloquent {
  use ElasticquentTrait;

  public $fillable = ['title', 'content', 'tags'];

  protected $mappingProperties = array(
    'title' => [
      'type' => 'string',
      "analyzer" => "standard",
    ],
    'content' => [
      'type' => 'string',
      "analyzer" => "standard",
    ],
    'tags' => [
      'type' => 'string',
      "analyzer" => "stop",
      "stopwords" => [","]
    ],
  );
}

در خط دوم میانبر Elasticquent Trait را میسازیم و در خط پنجم آن را در کلاسمان include مکنیم. در خط ۹ تنطیمات mapping برای ElasticSearch را اضافه میکنیم.میتوانید با مراجعه به این لینک در مورد mapping بخوانید.

هر mapping یک نوع و یک آنالیزور دارد. نوع آن میتوانید مختلف باشد: رشته، عدد و یا تاریخ. فعلا فقط از رشته ها استفاده میکنیم، اما حواستان باشد که انواع داده‌ی مختلف امکان استفاده از چیز های متفاوتی را در اختیارتان میگذارد.در اینجا میتوانید در مورد نوع ها و ElasticSearch بیشتر بخوانید. آنالیزور تعیین میکند که ElasticSearch چطور اطلاعات شما را برای جستجو ذخیره کند. من برای “title” و “content” از standard و برای “tags” از stop استفاده کردم. آنالیزور standard گرامر، HTML را حذف میکند و هر کلمه را جدا نگهداری میکند. آنالیزور stop میتواند تنظیم شود که چه کاراکتر هایی کلمه ها را جدا کنند.

به عنوان مثال جمله ی زیر را در نظر بگیرید.

“من لاراول را دوست دارم، ElasticSearch و لاراول با هم عالی کار میکنند”

ElasticSearch با آنالیز استاندارد جمله یک لیست مانند این لیست زیر تولید میکند:

  • من
  • لاراول
  • را
  • دوست
  • دارم،
  • ElasticSearch
  • و
  • لاراول
  • با
  • هم
  • عالی
  • کار
  • میکنند
با تنظیماتی که بالاتر برای آنالیزور stop در نظر گرفتیم، این آنالیزور متن را به این صورت دسته بندی میکند :
  • من لاراول را دوست دارم
  • ElasticSearch و لاراول با هم عالی کار میکنند

این قابلیت زمانی که بخواید اولویت خاصی به عبارت های مشخصی بدهید میتواند مفید باشد.حالا که طرز کار جستجوگرمان را تنظیم کردیم زمان فهرست بندی پایگاه داده است. بیایید از REPL لاراول برای ساخت داده ی ElasticSearch استفاده کنیم. دستور زیر را در ترمینال وارد کنید:

php artisan tinker

دستورات زیر را بنویسید:

Post::createIndex($shards = null, $replicas = null);

Post::putMapping($ignoreConflicts = true);

Post::addAllToIndex();
  • دستور اول فهرستمان را را‌ه‌اندازی میکند. یک فهرست تا حدودی شبیه یک جدول پایگاه داده در دنیای ElasticSearch است.
  • دستور putMapping() مشخصات mapping که در model تنظیم کردیم را میگیرد تا ElasticSearch بداند چطور داده‌های ما را فهرست بندی کند.
  • دستور addAllToIndex() همه‌ی داده‌ها را از پایگاه داده گرفته و وارد ElasticSearch میکند.

 

دستورات کاربردی ElasticSearch API

Elasticquent فهرست ما را به صورت پیشفرض به عنوان “default” تنظیم میکند. میتوانیم mapping هایمان را با استفاده از curl request زیر ببینیم:
curl localhost:9200/default/_mapping?pretty
{
  "default" : {
    "mappings" : {
      "posts" : {
        "properties" : {
          "content" : {
            "type" : "string",
            "analyzer" : "standard"
          },
          "created_at" : {
            "type" : "string"
          },
          "id" : {
            "type" : "long"
          },
          "tags" : {
            "type" : "string",
            "analyzer" : "stop"
          },
          "title" : {
            "type" : "string",
            "analyzer" : "standard"
          },
          "updated_at" : {
            "type" : "string"
          }
        }
      }
    }
  }
}
در ElasticSearch به جدول،‌ type گفته میشود. با این query متیوانیم همه ی پرونده های از یک نوع خاص را ببینیم:
curl 'localhost:9200/default/posts/_search?pretty'
با دستور زیر میتوانیم یک جستجوی ساده را انجام دهیم:
curl 'localhost:9200/default/posts/_search?q=title:searchterm&pretty'
و میتوانیم یک پرونده‌ی مشخص را با این دستور ببینیم:
curl 'localhost:9200/default/posts/1?pretty'

برای اطلاعات بیشتر در مورد ElasticSearch API این لینک را ببینید.

 

ساخت فرانت اند (front end)

یک rout به app/routes.php اضافه کنید.

<?php
Route::get('/', ['as' => 'search', 'uses' => function() {

  // Check if user has sent a search query
  if($query = Input::get('query', false)) {
    // Use the Elasticquent search method to search ElasticSearch
    $posts = Post::search($query);
  } else {
    // Show all posts if no query is set
    $posts = Post::all(); 
  }

  return View::make('home', compact('posts'));
  
}]);

یک قالب در app/views/home.blade.php بسازید:

<html>
<body>
{{ Form::open(['method' => 'get', 'route' => 'search']) }}

  {{ Form::input('search', 'query', Input::get('query', ''))}}
  {{ Form::submit('Filter results') }}

{{ Form:: close() }}

@foreach($posts as $post)
 <div>
  <h2>{{{ $post->title }}}</h2>
  <div>{{{ $post->content }}}</div>
  <div><small>{{{ $post->tags }}}</small></div>
 </div>
@endforeach
</body>
</html>

در قطعه کد بالا میتوانیم یک فرم بسازیم که امکان وارد کردن عبارتی برای جستجو را فراهم کند. زیر فرم میتوانیم تمام پست ها یا نتیجه ی جستجو را در صورتی که کاربر عبارتی را جستجو کرده باشد یکی یکی نمایش دهیم. میتوانیم در اینجا متوقف شویم و جستجوگر به خوبی کار خواهد کرد اما زیبایی کار در کجاست؟ بیایید ببینیم چطور میشود نتایج جستجو را بهبود داد.

تنظیم جستجو

Elasticquent یک متد دیگر به نام searchByQuery() دارد که امکان اضافه کردن جزئیات بیشتر به این که چطور میخواهیم ElasticSearch داده‌هایمان را پیدا کند را فراهم میکند. یک مثال:

<?php
$posts = Post::searchByQuery(['match' => ['title' => Input::get('query', '')]]);

در مثال بالا فقط در “title” جستجو انجام میشود. اما تفاوت عملکرد آن با متد search() در کجاست؟ درخواست search() عبارت جستجو شده را با همه ی پارامتر ها شامل “content” و “tags” تطبیق میدهد. حال اگر در داده‌هایمان فقط در ستون “content” بگردیم نتایج بسیار متفاوتی را مشاهده میکنیم.
حتی ممکن است اگر در بخش “title” جستجو کنیم هم نتایج متفاوتی بدست بیاوریم.دلیل این اتفاق این است که ElasticSearch یک امتیاز از داده در جستجو ها تولید میکند.هر متن مرتبطی در بخش هایی که در آن ها جستجو میکنیم امتیاز را افزایش میدهد.

بیایید به “title” اولویت بدهیم. بنابراین نتایجی که با “title” ها مطابقت دارند بالاتر از آن‌هایی که تنها در “content” هستند نمایش داده میشوند.

<?php
$posts = Post::searchByQuery([
  'multi_match' => [
    'query' => Input::get('query', ''),
    'fields' => [ "title^5", "content"]
  ],
]);

نماد ‘^’ به ElasticSearch میگوید که به مقدار عددی که بعد از آن می‌آید میخواهیم به آن وزن دهیم.
تا اینجا همه چیز خوب است، اما حالا میخواهیم در “tags” جستجو کنیم، چون کلمات کلیدی و عبارات خاصی در آن است که میخواهیم با نتایج جستوجو منطبق باشد.

<?php
$posts = Post::searchByQuery([
  'match_phrase' => [
    'tags' => Input::get('query', '')
  ]
]);

برای استفاده از هر دو جستوجو باید یک query ترکیبی بسازیم. Query های ترکیبی زیادی وجود دارند که ما میخواهیم از bool query استفاده کنیم.

<?php
$posts = Post::searchByQuery([
  "bool" => [
    'must' => [
      'multi_match' => [
        'query' => Input::get('query', ''),
        'fields' => [ "title^2", "content"]
      ],
    ],
    "should" => [
      'match' => [
        'tags' => [
          "query" => Input::get('query', ''),
          "type" => "phrase"
        ]
      ]
    ]
  ]
]);

در یک bool query میتوانیم ۳ پارامتر تعریف کنیم: must, should و must_not. در مثال ما تعیین کردیم که باید با “title” و “content” تطبیق داشته باشیم و همنیطور میتوان با “tags” ها تطبیق داشت. همینطور میتوانیم مواردی که بی‌ربط به فیلترهایمان هستند را از فیلتر خارج کنیم. در اینجا ما از not_filter استفاده کردیم که با مراجعه به این لینک میتوانید بیشتر در مورد فیلترها بخوانید.

<?php
$posts = Post::searchByQuery([
  'filtered' => [
    'filter' => [
      'not' => [
        'terms' => ['title' => ['impedit', 'voluptatem']]
      ]
    ],
    'query' => [
      "bool" => [
        'must' => [
          'multi_match' => [
            'query' => Input::get('query', ''),
            'fields' => [ "title^2", "content"]
          ],
        ],
        "should" => [
          'match' => [
            'tags' => [
              "query" => Input::get('query', ''),
              "type" => "phrase"
            ]
          ]
        ]
      ]
    ],
  ],
]);

بین خط های ۲-۷ زمانی را مشخص کرده ایم که “impedit” یا “voluptatem” در “title” نیستند.اگر یک ستون “published” نیز داشتیم، یک فیلتر خوب میتوانست جستجو کردن تنها در میان پست های منتشر شده باشد.

<?php
$pages = $this->page->searchByQuery([
  'filtered' => [
    'filter' => [
      'term' => ['published' => '1']
    ],
    'query' => [
      'multi_match' => [
        'query' => Input::get('query', ''),
        'fields' => [ "title^2", "content"]
      ],
    ],
  ],
]);

 

خلاصه

تمامی موارد بالا برای جستجو کردن بود.ما نگاهی به راه‌اندازی Elasticquent با مدل خودمان انداختیم، همچنین راه هایی برای شخصی سازی نتایج جستجو را بررسی کردیم. میتوانیم از query ها برای ترتیب دادن به نتایج جستجویمان با استفاده از وزن ها استفاده کنیم. همینطور میتوانیم query های ترکیبی برای جستجوهای پیچیده تر و فیلتر ها را برای query های boolean بسازیم. هرچند Elasticquent برای یک موتور جستجوی ابتدایی عالی است اما ElasticSearch client for PHP نیز به طور رسمی برای زمانی که نیاز به موارد پیشرفته تر مثل اشکال یابی متن یا حدس ادامه ی متن دارید معرفی شده است.

به شخصه واقعا از چیزهایی که تا الان در مورد ElasticSearch یادگرفتم لذت میبرم و بسیار افتخار میکنم که آن را انتخاب کردم . همنیطور ElasticSearch Server – Second Editionرا پیشنهاد میکنم. امید وارم این آموزش مورد استفادتان قرار بگیرد .
لطفا نظرات ارزشمند خود را با ما به اشتراک بگذارید


مدیر کل

لیسانس حسابداری هستم ولی به دلیل علاقه ام به برنامه نویسی چندین ساله تو این زمینه فعالیت میکنم .


در شبکه های اجتماعی
نظرات کاربران

پاسخی بگذارید

شما میتوانید برای وارد کردن لینک و کدهایHTML از تگ های زیر استفاده کنید : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>