搜索引擎:Laravel Scout 和 Meil​​isearch 入门

我们之前都需要在我们的应用程序中添加某种搜索功能,而且很多时候我们倾向于 Algolia 和 Laravel Scout——因为它解决了问题并取得了很好的效果。现在有一个(相对)新的项目,meilisearch。 Meilisearch 在功能方面与 Algolia 非常相似,但它是使用 Rust 编程语言构建的开源项目。因此,你可以免费的在本地使用它,或者在生产环境中使用 Laravel Forge 之类的东西来启动服务器。

本教程将介绍如何开始使用带有 Laravel Scout 的 Meilisearch,这样你就可以看到设置的差异 - 并决定你想走哪条路。与往常一样,我们将从一个全新的 Laravel 应用程序开始 - 我通常使用 Laravel 安装程序,因为我在本地经常使用 Valet,所以本教程一样适用于 Valet 和 Docker。

通过运行以下命令之一为此 demo 创建一个新应用程序:

使用 Laravel 安装程序

laravel new search-demo

使用 Composer 创建项目

composer create-project laravel/laravel search-demo

使用 Laravel 构建

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

无论您选择哪种方式运行上述程序,您都会在一个名为 search-demo 的新目录下获得一个 Laravel 项目,这意味着我们可以开始了。

我们要做的第一件事是通过运行以下 composer 命令来 安装 Laravel Scout

composer require laravel/scout

这会将 Scout 安装到我们的 Laravel 应用程序中,以便我们可以开始与可能想要用于搜索的任何潜在驱动程序进行交互。我们的下一步是通过运行以下 artisan 命令发布 Laravel Scout 的配置:

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

通过运行以下命令之一为此演示创建一个新应用程序:

使用 Laravel 安装程序

laravel new search-demo

使用 Composer 创建项目

composer create-project laravel/laravel search-demo

使用Laravel build和Sail

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

无论您选择哪种方式运行上述程序,您都会在一个名为 search-demo 的新目录下获得一个 Laravel 项目,这意味着我们可以开始了。

我们要做的第一件事是通过运行以下 composer 命令来 install Laravel Scout :

composer require laravel/scout

这会将 Scout 安装到我们的 Laravel 应用程序中,以便我们可以开始与我们可能想要用于搜索的任何潜在驱动程序进行交互。我们的下一步是通过运行以下 artisan 命令发布 Laravel Scout 的配置

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

这将使我们能够任意修改新建的 config/scout.php,但您可能希望保持当前标准。

此时我们有几个选项,驱动程序选项。在本教程中,我们将介绍如何使用 Meilisearch,但您可以使用以下选项作为 Laravel Scout 驱动程序:

  • Algolia:使用 algolia 第三方服务。
  • Meilisearch:使用开源的 meilisearch 服务。
  • Collection:将数据库用作搜索服务 - 仅支持 MySQL 和 PostgreSQL。
  • null:不使用驱动程序 - 通常用于测试。

要开始使用 meilisearch 驱动程序,我们需要安装一个新的包来允许 scout 使用 meilisearch SDK,所以运行以下 composer 命令:

composer require meilisearch/meilisearch-php

所以 Laravel 文档说你还需要安装 http-interop/http-factory-guzzle 但是如果你查看 meilisearch-php 库,就会发现这是该依赖项的一部分。所以我们可以跳过这个,或者如果它让你更舒服就安装它。我们的下一步是在 .env 文件中设置一些 ENV 变量:

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

MEILISEARCH_KEY

我们安装了 Laravel Scout,并安装配置了 meilisearch 客户端。下一步是考虑数据,就像所有好的应用程序一样——它有点需要它。对于这个演示,我们将使用一个相当基本的示例,以便我们可以专注于 meilisearch 和 scout 主题,而不会迷失在演示代码逻辑中。这将是一个简单的博客应用程序,其中我们有博客文章和类别。所以我们可以索引我们需要的一切。

通过运行以下 artisan 命令创建一个名为 Category 的新 Eloquent 模型,注意用于创建迁移和工厂的附加标志,这在此处很重要:

php artisan make:model Category -mf

我们的 Category 将是一个相对轻量级的模型,因此我将在下面向您展示迁移代码,并让您自己处理模型,根据您的个人喜好使用 fillableguarded

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();
    });
}

我们这里有一个名称、slug 和可搜索的布尔标志。这使我们可以将特定类别完全隐藏在我们的搜索中——这有时可能很有用。填写您的 Eloquent 模型,下一步是创建模型工厂:

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,
        ]);
    }
}

我们将 categories 可搜索的默认可能性设置得非常高,但随后我们添加了额外的状态方法,以允许我们出于测试目的控制这种可能性。这为我们提供了测试实现的最佳覆盖率。

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

php artisan make:model Post -mf

再次像以前一样,我将向您展示迁移,并让您处理模型上的可填充或受保护的属性 - 因为这是非常个人的偏好。

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();
    });
}

下一步是为我们的 Post 模型填写我们的 Factory:

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,
        ]);
    }
}

与 Category 模型一样,我们有一个布尔标志 - 但这次是用于模型是否发布 - 以便我们可以拥有一个草稿状态。我们将额外的状态方法添加到我们的工厂中,以便我们在测试环境中更好地控制它。

最后,你可以将关系添加到你的模型中,你的类别应该是 HasMany 帖子,你的帖子应该是 BelongsTo 类别。

现在数据已全部建模并可以使用,我们希望能够填充一些数据。但在我们这样做之前,我们需要安装 meilisearch。如果你使用的是 Laravel Sail,那么当你告诉 Sails 要安装 meilisearch,它会像传递一个选项一样简单——但是使用 Laravel Valet 时它有点不同。 meilisearch 文档 上的安装说明相对容易遵循,任何问题请务必检查在本地运行 meilisearch 的要求。

假设你现在已经启动并运行了 meilisearch,让我们看一些填充数据。我将在我的 Seeder 中添加一个进度条,以便我知道它工作正常,但如果你不想这样做,请随意跳过此步骤:

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();
        });
    }
}

我不希望我的播种有任何副作用,因为我想控制这种行为 - 所以我使用 WithoutModelEvents trait 去阻止它。我们要做的是创建 10 个分类,然后为每个分类创建一个进度条并为该类创建 100 个帖子。这在运行播种机时会提供视觉输出,并确保每个分类都有帖子 - 所以当我们搜索时,我们可以看到我们有什么可用的。

现在我们有了一些数据,我么可以看看让我们的 Post 模型可搜索。为此,我们需要做的是将来自 Laravel Scout 的 Searchable trait 添加到我们的模型中:

class Post extends Model
{
    use Searchable;
    use HasFactory;

    // 模型中其他的东西 ...
}

既然我们的模型是可以搜索的,我们就可以开始向我们的模型添加一些控件,已确定我们希望如何搜索它。几乎 99% 的时间我想使用 Post 模型, 我也想要分类 - 所以我们告诉 Eloquent 模型始终加载分类模型。

class Post extends Model
{
    use Searchable;
    use HasFactory;

    protected $with = [
        'category'
    ];

    // 模型的其他内容...
}

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

class Post extends Model
{
    use Searchable;
    use HasFactory;

    protected $with = [
        'category'
    ];

    public function searchable(): bool
    {
        return $this->published || $this->category->searchable;
    }

    // 模型的其他内容...
}

如果我们的帖子已经被发布了,或者属于可搜索的分类 - 我们希望将其编入索引。这将允许 Scout 在更新时重新评估该模型是否需要被索引。下一步是控制我们希望它如何索引,我们不会担心索引名称 - 因为我通常将此作为小型应用程序的标准 - 但您可以使用 searchableAs 方法和自己设置索引名称。要控制如何将数据添加到 meilisearch,您需要添加 toSearchableArray 方法,该方法允许您定义一个数组来索引数据:

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,
            ]
        ];
    }

    // 模型中其他内容 ...
}

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

最后我们有了可以索引和搜索的内容,所以让我们将所有 Post 记录导入 meilisearch:

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

这应该显示所有被添加到 scout 的 500 条记录块的输出。所以现在我们有一些东西要搜索,需要考虑想要如何搜索。对于 Scout,您可以使用模型上的静态搜索方法进行简单搜索——您向其传递查询并返回水合模型,或者您可以开始查看过滤器等。因此,让我们看一下控制器内部的基本搜索并从那里进行重构。

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: 'categry.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 吗?在推特上告诉我们,让我们知道你是如何找到这篇文章的!

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://laravel-news.com/getting-started...

译文地址:https://learnku.com/laravel/t/68933

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 3
OrangBus

4核8G内存导入5万条数据,卡死了,这个有什么解嘛?

1周前 评论
mengdodo

es, Scout 和 Meil​​isearch 什么区别

1周前 评论
zhaojjiang 1周前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!