Погружение в Sphinx. Часть 1

Серия статей о работе с поисковым движке sphinx на примере простого приложения на laravel.

Поиск — важная составляющая любого сайта, которой стоит уделить большое внимание. От работы поиска зависит user experience составляющая вашего приложения, то сможет ли пользователь найти нужную ему информацию или просто уйдет, неудовлетворив свой интерес.

Sphinx (англ. SQL Phrase Index) — система полнотекстового поиска, разработанная Андреем Аксеновым и распространяемая по лицензии GNU GPL. Отличительной особенностью является высокая скорость индексации и поиска, а также интеграция с существующими СУБД (MySQL, PostgreSQL) и API для распространённых языков веб-программирования (официально поддерживаются PHP, Python, Java; существуют реализованные сообществом API для Perl, Ruby,.NET[1] и C++).

Sphinx обладает всем необходимым функционалом для реализации полнотекстового поиска на сайте и имеет ряд преимуществ перед своими конкурентами — высокая скорость поиска и индексации, встроенный механизм индексации, относительная нетребовательность к ресурсам. Если кто-то имел дело к примеру с elasticsearch, тот сразу поймет о чем идет речь. Это и индексация через API, и прожорливость к памяти, и невнятная документация, и еще куча раздражающих моментов. Но все это перекрывается большим количеством полезных фич — различные типы запросов, высокая конфигурируемость, поддержка самописных плагинов и т.п.

В первой части этой серии статей мы подготовим простое приложение на laravel, создадим структуру базы данных и наполним ее фэйковыми записями.

Для начала разберемся с начальными требованиями к системе. Для работы laravel нам понадобится php 5.5.9+ и некоторые расширения, можно почитаться подробней в документации. Также нам понадобится composer. В качестве базы данных мы будем использовать MySQL, она прекрасно совместима со Sphinx.

Создание нового laravel выполняется всего одной командой:

composer create-project --prefer-dist laravel/laravel laravel-sphinx

Теперь нам нужно открыть .env файл и сконфигурировать параметры соединения с БД.

DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

Теперь можно приступать к созданию структуры базы данных. У нас будет 3 таблицы — posts, categories, categories_posts. Сгенерируем файлы миграций:

php artisan make:migration create_posts_table
php artisan make:migration create_categories_table
php artisan make:migration create_categories_posts_pivot_table

И добавим в них немного кода:

<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
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->integer('user_id')->unsigned()->nullable();
            $table->integer('views');
            $table->boolean('published');
            $table->timestamps();
            $table->foreign('user_id')->references('id')->on('users');
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('categories', function(Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('categories');
    }
}
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCategoriesPostsPivotTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('category_post', function(Blueprint $table) {
            $table->integer('category_id')->unsigned();
            $table->integer('post_id')->unsigned();
            $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
            $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('category_post');
    }
}

Файлы миграций готовы, можно запускать:

php artisan migrate
Migration table created successfully.
Migrated: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_100000_create_password_resets_table
Migrated: 2016_03_19_134544_create_posts_table
Migrated: 2016_03_19_134551_create_categories_table
Migrated: 2016_03_19_134602_create_categories_posts_pivot_table

Стоит обратить внимание на сгенерированные фреймворком файлы миграций для таблицы пользователей.

Итак, нам осталось лишь добавить модели для наших сущностей — Post и Category.

php artisan make:model Post
php artisan make:model Category

Добавим связь many-to-many между категориями и постами и заполним массивы $fillable:

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
    protected $table = 'posts';
    protected $fillable = [
        'title',
        'content',
        'user_id',
        'views',
        'published',
    ];
    public function categories()
    {
        return $this->belongsToMany('App\Category');
    }
    public function user()
    {
        return $this->belongsTo('App\User');
    }
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
    protected $table = 'categories';
    protected $fillable = ['name'];
    protected $hidden = [];
    public function posts()
    {
        return $this->belongsToMany('App\Post');
    }
}

Также наши пользователи являются авторами постов, поэтому в модель App\User добавим связь has-many для постов:

<?php
namespace App;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];
    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];
    public function posts()
    {
        return $this->hasMany('App\Post');
    }
}

Наши модели готовы к исполнению своих прямых обязанностей. Осталось лишь наполнить их тестовыми данными. Для этого воспользуемся инструментарием laravel’а под названием model factories. Откроем файл database/factories/ModelFactory.php и добавим фабрики для наших постов и категорий:

$factory->define(App\Category::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->sentence(2),
    ];
});
$factory->define(App\Post::class, function (Faker\Generator $faker) {
    return [
        'title' => $faker->sentence(),
        'content' => $faker->text(300),
        'views' => $faker->numberBetween(0, 1000),
        'published' => $faker->boolean(80),
    ];
});

Теперь создадим seeder наших данных. Код добавим прямо в DatabaseSeeder для упрощения и экономии времени:

<?php
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
use App\User;
use App\Post;
use App\Category;
class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Model::unguard();
        // https://gist.github.com/isimmons/8202227
        DB::statement('SET FOREIGN_KEY_CHECKS=0;');
        User::truncate();
        Category::truncate();
        Post::truncate();
        DB::statement('SET FOREIGN_KEY_CHECKS=1;');
        $categories = factory(App\Category::class, 30)->create();
        factory(App\User::class, 10)->create()->each(function($user) use ($categories) {
            $user->posts()->saveMany(factory(App\Post::class, 50)->create()->each(function($post) use ($categories) {
                $post->categories()->sync($categories->random(3)->pluck('id')->all());
            }));
        });
        Model::reguard();
    }
}

Здесь пройдемся по коду подробней:

  • DB::statement('SET FOREIGN_KEY_CHECKS=0;'); — небольшой фикс сидера, если при выполнении возникает ошибка Syntax error or access violation: 1701 Cannot truncate a table referenced in a foreign key constraint ….
  • ::truncate() очистит существующие данные в таблицах.
  • далее заранее создадем 30 категорий и сохраняем их коллекцию в массив, чтобы позже поназначать их постам.
  • далее генерируем 10 пользователей, для каждого пользователя мы генерируем 50 постов, и каждому посту присваиваем 3 случайные категории из ранее созданного массива.

Собственно и все, цель первой части достигнута. Мы создали скелет приложения на laravel и сгенерировали набор тестовых данных. В следующей части мы установим Sphinx и будем разбираться с его конфигурацией.