25. Full-text search. Laravel Scout. PGroonga. Postgres extensions. Eloquent filters
Відео версія
(YouTube - для зворотнього зв'язку)
Повнотекстовий пошук — це величезна тема. Повний розбір алгоритмів, готових рішень та їх оптимізації неможливо описати навіть у рамках серії статей.
У цій статті я розглядаю та пояснюю свій вибір для конкретної ситуації. Я маю право помилятися і змінити цей вибір у майбутньому.
TL;DR
- Стандартні не-SQL пошукові двигуни швидкі й ефективні. Але пам'ятайте, що ви втратите SQL-контроль.
- І для повного позитивного досвіду вам доведеться будувати дві архітектури: реляційну й документну, а також думати про їхню синхронізацію.
- Це можливо, але дуже ДОРОГО.
- Мій вибір для повнотекстового пошуку — PGroonga.
- Для продакшену рекомендую використовувати hosted DB сервіси. Але будьте готові до того, що вони можуть не підтримувати необхідні розширення.
- Postgres full-text search гарно працює, але має проблему з установкою словників на hosted сервісах.
- Eloquent filters.
- Якщо хочете підказувати планувальнику Postgres — pg_hint_plan.
- Explain visualizer
PGroonga
Використовуйте або готове рішення — Docker image,
або встановіть власноруч — Інструкція.
Будьте обережні при використанні similar search v2 — у випадку не Index Scan, запити падають з критичною помилкою!
Будь-які розширення треба вмикати вручну.
Migration
<?php
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
DB::unprepared('CREATE EXTENSION IF NOT EXISTS pgroonga');
}
};
Мій приклад індексів:
Migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class() extends Migration {
public function up(): void
{
Schema::create('bands', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->typeColumn('text[]', 'synonyms')->nullable();
$table->jsonb('description')->nullable();
});
DB::unprepared(
"
CREATE INDEX pgroonga_band_name ON bands
USING pgroonga (
name pgroonga_varchar_full_text_search_ops_v2,
synonyms pgroonga_text_array_full_text_search_ops_v2
)
WITH (normalizers='NormalizerNFKC100')"
);
DB::unprepared(
"
CREATE INDEX pgroonga_band_description ON bands
USING pgroonga (
description pgroonga_jsonb_full_text_search_ops_v2
)
WITH (normalizers='NormalizerNFKC100')"
);
}
};
Приклад пошуку
Modules/Band/ModelFilters/BandFilter.php
public function search(string $search): void
{
$this->where(function (Builder $query) use ($search) {
$query
->whereRaw(
"bands.name &@~
(?, ARRAY[3], 'pgroonga_band_name')::pgroonga_full_text_search_condition",
[$search]
)
->orWhereRaw("bands.synonyms &@~
(?, array_fill(2, array[100]), 'pgroonga_band_name')::pgroonga_full_text_search_condition", [$search])
->orWhereRaw('description &@~ ?', [$search]);
});
}