Поиск в PDF-файлах с помощью MySQL и Laravel
Поисковая функциональность часто оказывается крайне важной в современных веб-приложениях. Если на сайте представлены документы с большим объёмом текста, например, PDF-файлы, то часто возникает необходимость предоставить пользователям возможность перечисления и поиска в содержимом этих документов. Хотя специализированные инструменты, такие как Elasticsearch, MeiliSearch или Typesense, могут показаться привлекательными, важно учитывать их существенное влияние. При включении одного из этих инструментов в свой стек архитектура приложения становится более сложной. Необходимо не только понять, как использовать такой инструмент, но и установить, настроить, поддерживать, обеспечивать безопасность и контролировать его работу.
Однако специализированные поисковые инструменты обладают и неоспоримыми преимуществами. Вот некоторые примеры возможностей, которые может предложить современная поисковая система:
- Допущение опечаток (также известный как нечёткий поиск)
- Частичный поиск слов
- Выделение результатов и предварительный просмотр фрагментов
- Автозаполнение
- Пользовательское ранжирование
- Поисковые синонимы
- Стемминг
- Классификация текстов
- и многое другое.
Принимая решение об использовании поискового инструмента, следует помнить, что этот вид поиска имеет множество тонкостей, присущих только ему, знать как преимущества, так и недостатки использования каждого из них и выбирать с умом.
MySQL может сделать это
Если вас устраивает базовая функция поиска без излишних изысков, то есть интересный более простой подход. Он прост в настройке и обладает привлекательными преимуществами. Давайте разберёмся в этом.
Во-первых, необходимо извлечь текст из PDF-файлов и вставить его в базу данных. Затем MySQL может проиндексировать это содержимое, что позволит осуществлять в нем поиск. Для этого существует пакет PHP, основанный на популярном инструменте pdftotext
, который позволяет легко извлекать текст из PDF-документа.
composer require spatie/pdf-to-text
Он очень прост в использовании. Поскольку эта операция может быть ресурсоёмкой, рекомендуется выполнять её асинхронно в фоновом задании. Обратите внимание, что максимальная длина столбца MySQL типа "text" составляет 65 535 символов (~10000 слов), поэтому содержимое PDF-файла не должно быть слишком большим.
use App\Models\PdfDocument;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Spatie\PdfToText\Pdf;
class StorePdfDocumentAsText implements ShouldQueue
{
public function handle()
{
PdfDocument::create([
'title' => 'My great document about something',
'content' => Str::limit(
Pdf::getText(
Storage::disk('s3')->path('my_file.pdf'),
),
60000,
),
]);
}
}
Хотя использование выражения like
для поиска в содержимом файлов возможно, оно может не подойти по производительности для больших или многочисленных документов. Кроме того, использование выражения like
ограничивает поиск точными совпадениями. Цель данной статьи — продемонстрировать, что в MySQL имеется встроенная функция Полнотекстовый поиск на естественном языке
.
Её можно рассматривать как нечто среднее между выражением like
и специализированным внешним инструментом, например Elasticsearch. MySQL предоставляет такую возможность уже давно, но только с начала 2022 года Laravel позволяет использовать её в Eloquent. Это позволяет осуществлять более сложный поиск с использованием относительно простого синтаксиса. Например:
- Поиск по набору нескольких слов, используя не ключевые слова, а повседневный язык
- Поиск в нескольких различных колонках (например, название документа, имя файла и, конечно, его содержание)
- Упорядочивание результатов по релевантности
- Хорошая производительность
Для его настройки необходимо создать специальный индекс на нужных столбцах с помощью миграции:
class CreateFullTextIndex extends Migration
{
public function up()
{
Schema::table('pdf_documents', function (Blueprint $table) {
$table->fullText(['title', 'content'])->language('english');
});
}
}
Затем настройте его в модели. Поскольку в нем используется синтаксис атрибутов, вам потребуется как минимум PHP 8.
use Laravel\Scout\Searchable;
use Laravel\Scout\Attributes\SearchUsingFullText;
class PdfDocument extends Model
{
use Searchable;
#[SearchUsingFullText(["title", "content"])]
public function toSearchableArray()
{
return [
"title" => $this->title,
"content" => $this->content,
];
}
}
Вот и все! После запуска миграции вы можете сразу воспользоваться функцией поиска в вашей модели, используя традиционный синтаксис Laravel Scout. Синхронизация индекса не требует никаких действий с вашей стороны, MySQL выполняет её автоматически, обеспечивая постоянную актуальность индекса.
Вот пример того, как можно заполнить страницу результатов поиска на основе содержимого PDF-файлов:
$results = PdfDocument::search('my search terms')
->where('user_id', $user->id) //дополнительные условия
->paginate(20);
Запрос будет выглядеть примерно так:
select *
from users
where (match (title, content) against ('my search terms' in natural language mode))
and user_id = 29;
MySQL действительно вычисляет оценку для каждого результата и использует её для упорядочивания. Оценка используется только внутри MySQL, но представлена она таким образом:
+----+-------------------------------------+-----------------+
| id | title | score |
+----+-------------------------------------+-----------------+
| 4 | My first document about flowers | 1.5219271183014 |
| 6 | Another document about flowers | 1.3114095926285 |
+----+-------------------------------------+-----------------+
2 rows in set (0.00 sec)
На сегодня это все. Всем удачного поиска!