Laravel Scout 和 Meilisearch 入门

发布时间 作者

Getting started with Laravel Scout and Meilisearch image

我们之前都遇到过需要在应用中添加搜索功能的情况,大多数时候我们会选择 Algolia 和 Laravel Scout - 因为它们开箱即用且能够取得很好的效果。但现在出现了一个(相对)新的竞争者,meilisearch。Meilisearch 在功能上与 Algolia 很相似,但它是一个使用 Rust 编程语言构建的开源项目。因此,你可以免费在本地运行它,或者在生产环境中使用 Laravel Forge 等工具启动一个服务器。

本教程将指导你如何使用 Meilisearch 和 Laravel Scout 入门,以便你了解它们之间的设置差异 - 然后做出选择。

我们从一个新的 Laravel 应用开始,我通常使用 Laravel 安装程序,因为我在本地大量使用 Valet - 但本教程应该适用于 Valet 和 Docker。

运行以下命令之一,为本演示创建一个新应用

laravel new search-demo

使用 Laravel 安装程序

composer create-project laravel/laravel search-demo

使用 Composer create-project

curl -s "https://laravel.build/search-demo" | bash

使用 Laravel build 和 Sail

无论你选择以上哪种方式,你都会在名为 search-demo 的新目录下获得一个 Laravel 项目,这意味着我们已经准备好了。

composer require laravel/scout

第一步,我们要 安装 Laravel Scout,运行以下 Composer 命令

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

这会将 Scout 安装到我们的 Laravel 应用中,这样我们就可以开始与可能用到的任何搜索驱动程序进行交互。下一步是发布 Laravel Scout 的配置,运行以下 Artisan 命令

这样我们就完成了配置,可以根据需要修改新创建的 config/scout.php 文件,但你可能希望保持其标准设置。

  • 此时,我们有一些驱动程序选项。本教程将介绍如何使用 Meilisearch,但 Laravel Scout 还提供了以下驱动程序选项:
  • Algolia:使用第三方 Algolia 服务。
  • Meilisearch:使用开源 Meilisearch 服务。
  • Collection:使用数据库作为搜索服务 - 仅支持 MySQL 和 PostgreSQL。

null:不使用驱动程序 - 通常用于测试。

composer require meilisearch/meilisearch-php

要开始使用 Meilisearch 驱动程序,我们需要安装一个新包,它将允许 Scout 使用 Meilisearch SDK,因此运行以下 Composer 命令

SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=masterKey

Laravel 文档中指出你还需要安装 http-interop/http-factory-guzzle,但是如果你查看 Meilisearch-PHP 库,它现在已包含在该依赖项中。因此,我们可以跳过这一步,或者如果你觉得更方便,也可以安装它。下一步是在 .env 文件中设置一些环境变量

MEILISEARCH_KEY 环境变量很有意思,如果你在本地安装 Meilisearch,则可以在启动服务时传递一个可选标志来设置它。在生产环境中,你需要确保将其设置为安全措施,但在本地,如果你愿意,可以将其保留为空。我个人建议设置它,因为这是一个好习惯,它会提醒我需要设置它。

我们已经安装了 Laravel Scout,并且已经安装并配置了 Meilisearch 客户端。下一步是考虑数据,就像所有好的应用一样 - 它需要数据。在本演示中,我们将使用一个相当基本的示例,这样我们就可以专注于 Meilisearch 和 Scout 主题,而不会在演示代码逻辑中迷失方向。这将是一个简单的博客应用,其中包含博客文章和类别,这样我们就可以索引所需的所有内容。

php artisan make:model Category -mf

运行以下 Artisan 命令,创建一个名为 Category 的新 Eloquent 模型,请注意额外的标志,它们将用于创建迁移和工厂,这里很重要

public function up()
{
Schema::create('categories', static function (Blueprint $table): void {
$table->id();
 
$table->string('name');
$table->string('slug')->unique();
 
$table->boolean('searchable')->default(true);
 
$table->timestamps();
});
}

我们的 Category 将是一个相对较轻的模型,因此我将向你展示迁移代码,并让你自己处理模型,使用 fillableguarded 属性,具体取决于你的个人偏好。

class CategoryFactory extends Factory
{
protected $model = Category::class;
 
public function definition(): array
{
$name = $this->faker->unique()->word();
 
return [
'name' => $name,
'slug' => Str::slug(
title: $name,
),
'searchable' => $this->faker->boolean(
chanceOfGettingTrue: 85,
),
];
}
 
public function searchable(): static
{
return $this->state(fn (array $attributes): array => [
'searchable' => true,
]);
}
 
public function nonsearchable(): static
{
return $this->state(fn (array $attributes): array => [
'searchable' => false,
]);
}
}

这里我们有一个名称、一个 slug 和一个可搜索的布尔标志。这让我们可以完全隐藏某些类别,使其不参与搜索 - 有时这可能会有用。根据你的习惯填写 Eloquent 模型,下一步是创建模型工厂

我们让类别可搜索的默认可能性很高,但我们还添加了一些额外的状态方法,以便在测试时控制这种可能性。这为我们提供了最全面的测试覆盖范围。

php artisan make:model Post -mf

接下来,我们需要另一个名为 Post 的模型,它将是我们搜索的主要入口点,因此运行以下 Artisan 命令

public function up(): void
{
Schema::create('posts', static function (Blueprint $table): void {
$table->id();
 
$table->string('title');
$table->string('slug')->unique();
$table->mediumText('content');
 
$table->boolean('published')->default(true);
 
$table
->foreignId('category_id')
->index()->constrained()->cascadeOnDelete();
 
$table->timestamps();
});
}

同样,我将向你展示迁移,并让你自己处理模型的 fillableguarded 属性 - 因为这完全取决于个人偏好。

class PostFactory extends Factory
{
protected $model = Post::class;
 
public function definition(): array
{
$title = $this->faker->unique()->sentence();
 
return [
'title' => $title,
'slug' => Str::slug(
title: $title,
),
'content' => $this->faker->paragraph(),
'published' => $this->faker->boolean(
chanceOfGettingTrue: 85,
),
'category_id' => Category::factory(),
];
}
 
public function published(): static
{
return $this->state(fn (array $attributes): array => [
'published' => true,
]);
}
 
public function draft(): static
{
return $this->state(fn (array $attributes): array => [
'published' => false,
]);
}
}

下一步是填写 Post 模型的工厂

与 Category 模型一样,我们也有一个布尔标志 - 但这一次它是用于指示模型是否已发布 - 这样我们就可以拥有草稿状态。我们在工厂中添加了额外的状态方法,以便在测试环境中对其进行良好的控制。

最后,你可以在模型中添加关系,Category 模型应该 HasMany Posts,而 Post 模型应该 BelongsTo Category。

现在我们的数据已经全部建模并准备好了,我们需要播种一些数据。但在播种之前,我们需要安装 Meilisearch。如果你使用的是 Laravel Sail,这很简单,你只需在告诉 Sail 安装时传递一个选项即可 - 然而,在 Laravel Valet 中情况有所不同。安装说明可以在 Meilisearch 文档 中找到,操作起来相对容易,如果遇到任何问题,请确保检查本地运行 Meilisearch 的要求。

class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
 
public function run(): void
{
$categories = Category::factory(10)->create();
 
$categories->each(function (Category $category) {
$this->command->getOutput()->info(
message: "Creating posts for category: [$category->name]",
);
 
$bar = $this->command->getOutput()->createProgressBar(100);
 
for ($i = 0; $i < 100; $i++) {
$bar->advance();
Post::factory()->create();
}
 
$bar->finish();
});
}
}

假设你已经启动并运行了 Meilisearch,让我们看看如何播种数据。我将在播种程序中添加一个进度条,这样我就能知道它是否正常工作,但如果你不想添加,可以跳过这一步

现在我不想让播种有任何副作用,因为我希望控制这种行为 - 所以,我使用了 WithoutModelEvents 特性来阻止这些行为。我们在这里创建了 10 个 Category,然后为每个 Category 创建一个进度条,并为其创建 100 个 Post。这在运行播种程序时提供了可视化输出,并确保每个 Category 都有 Post - 这样,当我们搜索时,就可以看到可用的内容。

class Post extends Model
{
use Searchable;
use HasFactory;
 
// Other model stuff here ...
}

现在我们已经有了一些数据,我们可以开始让 Post 模型可搜索了。要做到这一点,我们只需将 Laravel Scout 中的 Searchable 特性添加到模型中即可

class Post extends Model
{
use Searchable;
use HasFactory;
 
protected $with = [
'category'
];
 
// Other model stuff here ...
}

现在我们的模型可搜索了,我们可以开始在模型中添加一些控制,以控制我们希望它如何被搜索。在几乎 99% 的情况下,我需要使用 Post 模型,也希望使用 Category - 所以,我将告诉 Eloquent 模型始终加载 Category 模型。

class Post extends Model
{
use Searchable;
use HasFactory;
 
protected $with = [
'category'
];
 
public function searchable(): bool
{
return $this->published || $this->category->searchable;
}
 
// Other model stuff here ...
}

现在,我们可以添加一个新方法,允许 Laravel Scout 检查模型是否可以被搜索或添加到索引中

class Post extends Model
{
use Searchable;
use HasFactory;
 
protected $with = [
'category'
];
 
public function searchable(): bool
{
return $this->published || $this->category->searchable;
}
 
public function toSearchableArray(): array
{
return [
'title' => $this->title,
'slug' => $this->slug,
'content' => $this->content,
'category' => [
'name' => $this->category->name,
'slug' => $this->category->slug,
]
];
}
 
// Other model stuff here ...
}

我们希望在每个帖子中添加类别信息,以便我们可以在 UI 上正确地显示帖子本身的信息,例如 "帖子标题(类别名称)" 之类的东西。

最后,我们得到了一些可以索引和搜索的东西,所以让我们将所有 Post 记录导入 meilisearch。

php artisan scout:import "App\Models\Post"

这将显示将 500 条记录的块添加到 scout 的输出。所以现在我们有了可以搜索的东西,我们需要考虑如何搜索。在使用 Scout 时,您可以使用模型上的静态 search 方法进行简单搜索——您将查询传递给它,它将返回已填充的模型,或者您可以开始查看过滤器等等。所以让我们看看控制器中的基本搜索,并从那里进行重构。

class SearchController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
return new JsonResponse(
data: Post::search(
query: trim($request->get('search')) ?? '',
)->get(),
status: Response::HTTP_OK,
);
}
}

现在,让我们在我们的 api 路由下注册这条路由,以便我们可以在不创建 UI 的情况下查看结果。

Route::get(
'search',
App\Http\Controllers\SearchController::class
)->name('search');

现在,我们可以查看基于搜索查询参数的搜索的 JSON 输出,看看并测试它如何响应。尝试针对完整词和部分词进行搜索。这是 Laravel Scout 和 Meilisearch 的基础知识,我们现在可以索引模型并针对它们进行搜索——所以从这个角度来看,我们很好。下一步是考虑如何获得更多内容。

过滤器是很棒的东西,它允许我们通过简单地请求它们,在搜索中获得更有针对性的结果。因此,我们将在 Post 模型中添加一些过滤器,以便我们可以轻松地过滤查询。这是我的方法,但它不必是你的方法,所以请带着一点盐度来看待我要做的事情,并根据自己的需要进行调整。

class Post extends Model
{
use Searchable;
use HasFactory;
 
protected $with = [
'category'
];
 
public function searchable(): bool
{
return $this->published || $this->category->searchable;
}
 
public function toSearchableArray(): array
{
return [
'title' => $this->title,
'slug' => $this->slug,
'content' => $this->content,
'category' => [
'name' => $this->category->name,
'slug' => $this->category->slug,
]
];
}
 
public static function getSearchFilterAttributes(): array
{
return [
'category.name',
'category.slug',
];
}
 
// Other model stuff here ...
}

我在模型中添加了一个静态函数来定义搜索过滤器属性,正如你所看到的,我希望能够按类别名称或 slug 进行过滤。下一步是创建一个命令来将这些可过滤属性注册到 meilisearch。我通常创建一个控制台命令来执行此操作,因为 scout 默认情况下没有方法来执行此操作。

php artisan make:command Search/SetupSearchFilters

然后添加以下代码片段。

class SetupSearchFilters extends Command
{
protected $signature = 'scout:filters
{index : The index you want to work with.}
';
 
protected $description = 'Register filters against a search index.';
 
public function handle(Client $client): int
{
$index = $this->argument(
key: 'index',
);
 
$model = match($index) {
'posts' => Post::class,
};
 
try {
$this->info(
string: "Updating filterable attributes for [$model] on index [$index]",
);
 
$client->index(
uid: $index,
)->updateFilterableAttributes(
filterableAttributes: $model::getSearchFilterAttributes(),
);
} catch (ApiException $exception) {
$this->warn(
string: $exception->getMessage(),
);
 
return self::FAILURE;
}
 
return 0;
}
}

我们在这里做的是传递一个索引,以防我们将来扩展要索引的内容,然后使用 match/switch 语句将其与模型匹配。然后,由于控制台命令的工作方式,我们可以在 handle 方法中解析 meilisearch 客户端——并使用它来更新索引,同时获取搜索过滤器属性。如果失败,我们将显示异常并返回失败。

现在,我们可以使用以下命令运行它。

php artisan scout:filters 'posts'

如果一切按计划进行,meilisearch 现在将了解索引上可用的过滤器。所以让我们看看是否可以做到?我们将重构 SearchController 以接受过滤器进行搜索。

class SearchController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
return new JsonResponse(
data: Post::search(
query: $request->get('search'),
callback: function (Indexes $meilisearch, string $query, array $options) use ($request) {
if ($request->has(key: 'category.slug')) {
$options['filter'] = "category.slug = {$request->get(key: 'category.slug')}";
}
 
return $meilisearch->search(
query: $query,
options: $options,
);
},
)->get(),
status: Response::HTTP_OK,
);
}
}

现在,如果你在搜索中添加另一个查询参数 category.slug={something},那么你应该会获得你正在执行的搜索的过滤结果,我的当前结果看起来像:/api/search?search=rem&category.slug=voluptatibus,它很好地过滤了结果。你可以根据需要尽可能多地扩展这些,包括类别名称的过滤器,甚至更多,具体取决于你选择如何建模数据。你甚至可以创建过滤器来根据时间进行过滤,如果你需要的话。

这只是将 Laravel Scout 集成到你的应用程序中并使用过滤器进行微调的一种方式,如果你需要的话。Laravel Scout 可以使用许多驱动程序,并且创建自己的驱动程序并非不可能——事实上,如果它们适合你的用例,你已经可以使用几个开源驱动程序了!

你是如何处理应用程序的搜索的?你尝试过 meilisearch 吗?在推特上告诉我们,并让我们知道你对这篇文章的看法!

Steve McDougall photo

Laravel News 的技术作家,Treblle 的开发者倡导者。API 专家,资深 PHP/Laravel 工程师。 YouTube 直播主

Cube

Laravel 新闻

加入 40k+ 其他开发者,永远不会错过新的技巧、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理服务器,并在几秒钟内部署 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和本地和生产环境的即时反馈进行微调。

Tinkerwell
No Compromises logo

无妥协

Joel 和 Aaron,来自 No Compromises 播客的两名经验丰富的开发者,现在可以为你的 Laravel 项目提供服务。 ⬧ 每月固定价格 7500 美元。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

无妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保你的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?立即自动执行 Laravel 升级和代码现代化,使你的应用程序保持最新。

Shift
Bacancy logo

Bacancy

使用经验丰富的 Laravel 开发人员为你的项目赋能,他们拥有 4-6 年的经验,每月仅需 2500 美元。获得 160 小时的专业知识和 15 天的无风险试用期。立即预约电话!

Bacancy
Lucky Media logo

Lucky Media

现在就变得幸运——Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar:Laravel 电子商务

Laravel 的电子商务。一个开源软件包,将现代无头电子商务功能的强大功能带到 Laravel。

Lunar:Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 职位板

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit:Laravel SaaS 启动工具包

SaaSykit 是一个 Laravel SaaS 启动工具包,它具有运行现代 SaaS 所需的所有功能。支付、漂亮的结账、管理面板、用户仪表板、身份验证、现成的组件、统计、博客、文档等等。

SaaSykit:Laravel SaaS 启动工具包
Rector logo

Rector

你的无缝 Laravel 升级合作伙伴,降低成本,为成功公司加速创新

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成增强你的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解 MongoDB 的灵活、现代数据库如何改变你的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到你的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的准确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel Prompts 构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包将评论添加到你的 Laravel 应用程序

阅读文章