eelcol/laravel-meilisearch

Meilisearch 的简单 Laravel 封装

2.3.5 2024-06-29 14:04 UTC

README

当你想在 Laravel 应用中使用 Meilisearch 时,可以使用 Laravel Scout。这是一种简单的方式,将你的模型同步到 Meilisearch 并快速搜索模型。然而,有时 Laravel Scout 并不足以满足需求。例如,如果你想要

  • 更多控制你的 Meilisearch 数据库:例如不仅保存模型。
  • 设置可搜索、可筛选或可排序的属性。
  • 向 Meilisearch 执行更复杂的查询,例如使用多个筛选器。
  • 使用查询构建器从 Meilisearch 数据库中检索数据。
  • 使用 Meilisearch 不提供的一些功能。例如,以随机顺序显示文档或在筛选文档中显示不可用的分面。

此包处理这类情况。你决定向 Meilisearch 发送哪些信息,以及你想得到哪些信息。专为 Meilisearch 定制的查询构建器有助于构建更复杂的查询。

使用此包时,你应该自己决定何时以及发送哪些数据到 Meilisearch。所以如果你想在模型保存或创建后自动将模型发送到 Meilisearch,Laravel Scout 可能是更好的解决方案。

与 Meilisearch 的兼容性

目前,此包支持 Meilisearch 0.27 版本。Meilisearch 0.28 版本引入了一些重大更改。兼容 0.28 版本的新版本将很快发布。

安装

根据 Meilisearch 版本确定需要哪个版本

例如,当使用 Meilisearch 1.0.2 时,使用以下命令

composer require eelcol/laravel-meilisearch:~2.0.0

设置 .env

修改 .env 以包括以下变量

MEILISEARCH_HOST=...
MEILISEARCH_KEY=...

如果不使用 Meilisearch 密钥,.env 变量 MEILISEARCH_KEY 可以是任何值。

发布资源

php artisan vendor:publish --tag=laravel-meilisearch

入门

创建索引

首先你需要创建一个索引来保存文档。例如,你需要一个索引来保存我们的产品目录。所以可以使用以下命令

php artisan meilisearch:create-index products

此命令将创建一个文件 database/meilisearch/products.php。在这个文件中,你可以调整此索引的设置。这不是必须的,但它非常推荐。如果你保留标准设置,Meilisearch 将使用你的数据的所有列进行搜索。为了实现这一点,Meilisearch 必须索引你的数据的所有列。这将花费更多时间,并使用更多服务器资源。这就是为什么推荐指定哪些列应该是可搜索的、可筛选的和可排序的。

每次你想更改设置时,只需更改此文件。更改后,运行以下命令。

将索引迁移到 Meilisearch 数据库

现在必须实际创建索引。为此,运行以下命令

php artisan meilisearch:set-index-settings

这与 Laravel 的数据库迁移类似。首先你必须创建一个数据库迁移,然后运行迁移以实际创建表或进行调整。

每次你更改 database/meilisearch/products.php 文件时,都要运行此命令。还要在每次部署时运行此命令,以确保生产中有最新的 Meilisearch 实例。

如果你想在不同的 Meilisearch 安装上设置索引设置,可以使用 --mshost--mskey 选项

php artisan meilisearch:set-index-settings --mshost=http://another-meilisearch-installation:7700 --mskey=secret-key

Meilisearch 中的主数据

Meilisearch 文档中提到的所有功能都包含在此包中。最重要的功能列在下述

插入数据

要在索引products中插入文档,你可以执行以下操作之一:

Meilisearch::addDocument('products', [
    'id' => 1,
    'title' => 'iPhone SE'
]);
Meilisearch::addDocuments('products', [
    [
        'id' => 1,
        'title' => 'iPhone SE'
    ],
    [
        'id' => 2,
        'title' => 'Samsung Galaxy'
    ]
]);

你也可以直接插入一个模型或集合。一个模型会被转换成数组。为了做到这一点,该包会检查对象上是否存在以下方法,顺序如下:

- toMeilisearch()
- toSearchableArray()
- toArray()

例如,一个Product模型可以看起来像这样:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

class Product extends Model
{
    use HasFactory;

    protected $guarded = ['id'];

    public function toMeilisearch(): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => Str::slug($this->title),
        ];
    }
}

模型可以按如下方式插入:

$product = App\Models\Product::find(1);
Meilisearch::addDocument('products', $product);

// the product will be inserted like:
// [
//      'id' => 1,
//      'title' => 'iPhone SE',
//      'slug' => 'iphone-se'
// ]

集合也可以直接插入

$products = App\Models\Product::all();
Meilisearch::addDocuments('products', $products);

检索数据

可以使用getDocuments方法检索索引中的文档。当你想应用过滤器时,建议使用查询构建器。数据将自动分页。

$documents = Meilisearch::getDocuments('products');

删除文档

可以使用deleteDocumentdeleteDocuments方法通过ID删除文档。这两个方法都返回一个MeilisearchTask对象。

$task = Meilisearch::deleteDocument(index: 'products', id: 1);
$task = Meilisearch::deleteDocuments(index: 'products', ids: [1,2,3]);

也可以使用查询构建器来删除文档,请参阅下面的内容。

检索分面值

你可以检索给定分面所有可用的值。例如,下面的代码将搜索所有包含字母'a'的商标。

$collection = Meilisearch::searchFacetValues(index: 'products', facetName: 'brand', facetQuery: 'a');

使用查询构建器

如果你想应用过滤或排序,我建议使用查询构建器。你可以在测试文件夹中查看一些示例。以下是一些简单的示例。

基于属性过滤

可以使用where方法进行简单的过滤

$documents = MeilisearchQuery::index('products')
    ->where('title', '=', 'iPhone SE')
    ->get();

$documents = MeilisearchQuery::index('products')
    ->where('price', '<', 100)
    ->get();

多个wheres也可以组合使用

$documents = MeilisearchQuery::index('products')
    ->where('title', '=', 'iPhone SE')
    ->where('price', '<', 100)
    ->get();

使用'or'进行过滤

目前,在顶层使用'OR'进行过滤是不可行的。如果你想使用'OR'进行过滤,你必须首先创建一个'where-group'。以下调用将引发错误

$documents = MeilisearchQuery::index('products')
    ->where('title', '=', 'iPhone SE')
    ->orWhere('title', '=', 'Samsung Galaxy')
    ->get();

但是以下代码将有效

$documents = MeilisearchQuery::index('products')
    ->where(function ($q) {
        $q->where('title', '=', 'iPhone SE');
        $q->orWhere('title', '=', 'Samsung Galaxy');
    })
    ->get();

这是因为Meilisearch过滤的工作方式以及此包如何呈现过滤器的结果。它还防止了在组合'AND'和'OR'语句时可能出现的潜在问题。例如,以下查询可能会返回意外的结果

$documents = MeilisearchQuery::index('products')
    ->where('title', '=', 'iPhone SE')
    ->orWhere('title', '=', 'Samsung Galaxy')
    ->where('price', '<', 100)
    ->get();

这个查询应该是这样的

- (title = 'iPhone SE' OR title = 'Samsung Galaxy') AND price < 100
- title = 'iPhone SE' OR (title = 'Samsung Galaxy' AND price < 100)
- etc...

所以现在,在使用'OR'语句时,你应该首先开始一个where-group。

Where in

这最适合与数组一起使用。例如,你有一个包含多个类别的产品

[
    'id' => 1,
    'title' => 'iPhone SE',
    'categories' => [
        'phones',
        'smartphones',
        'iphones'
    ],
    'id' => 2,
    'title' => 'Samsung Galaxy',
    'categories' => [
        'phones',
        'smartphones',
        'samsung'
    ],
]

这些数据可以查询

MeilisearchQuery::index('products')
    ->whereIn('categories', ['phones', 'iphones'])
    ->get();

whereIn方法将检查至少有一个值在模型上存在。所以上面的查询将返回所有文档。

Where matches

whereIn方法将检查至少有一个值存在于模型上。而whereMatches方法将检查所有值都存在于模型上

// this query will return both iPhone SE and Samsung Galaxy
MeilisearchQuery::index('products')
    ->whereMatches('categories', ['phones', 'smartphones'])
    ->get();

// this query will return ONLY the iPhone SE
MeilisearchQuery::index('products')
    ->whereMatches('categories', ['phones', 'iphone'])
    ->get();

// this query will return ONLY the Samsung Galaxy
MeilisearchQuery::index('products')
    ->whereMatches('categories', ['phones', 'samsung'])
    ->get();

空数据

可以使用whereIsEmptywhereNotEmpty来选择具有给定属性空值的文档。这匹配以下JSON值:"", [], {}

可以使用whereNullwhereNotNull来选择具有给定属性NULL值的文档。

MeilisearchQuery::index('products')
    ->whereEmpty('brand')
    ->get();

MeilisearchQuery::index('products')
    ->whereNull('brand')
    ->get();

使用分面

被标记为filterable的列可以用作分面。查询构建器将返回这些分面以及附加的产品计数。可以通过使用setFacetsaddFacet方法来定义分面。

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->setFacets([
        'color',
        'brand'
    ])
    ->get();

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->addFacet('color')
    ->addFacet('brand')
    ->get();

使用查询构建器删除文档

你可以在查询上使用过滤器来删除文档。查询上的其他元素,如分页或排序,将不会应用。

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->delete();

上面的查询将删除所有具有类别phones的产品。

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->orderBy('title')
    ->limit(20)
    ->delete();

上面的查询与另一个查询完全相同!记住:当你使用查询构建器删除文档时,只有过滤器会被应用。限制、排序和其他操作将不会应用。

指定要搜索的属性

你可以指定每个查询要搜索的属性。默认情况下,查询会搜索在设置文件中指定的所有属性。但如果你想搜索另一组属性,你可以使用searchOnAttributes方法。这些属性必须是默认搜索属性的一个子集。

例如,当您默认设置了一个搜索标题、描述和品牌的索引时,您可以执行一个仅搜索标题的查询。

MeilisearchQuery::index('products')
    ->search("Nike")
    ->searchOnAttributes(['title'])
    ->get();

这个查询只找到标题中包含Nike的产品。如果一个产品标题中没有Nike,但描述中有,上述查询将不会返回此产品。

析取分面分布

在Meilisearch当前版本中,当您正在对该特定属性进行过滤时,不会返回该属性的分面。请参阅以下讨论:meilisearch/product#187

例如,当您运行上述查询时,返回的颜色有灰色银色金色黄色。接下来,您只想显示具有黄色颜色的产品。因此,您应用一个过滤器

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->where('color', '=', 'yellow')
    ->setFacets([
        'color',
        'brand',
        'size',
    ])
    ->get();

但是,当您这样做时,分面颜色现在将仅返回黄色。这使得向最终用户显示所有可能的颜色变得更加困难。这就是为什么这个包有一个keepFacetsInMetadata方法。您可以在该方法内部应用过滤器,这些过滤器在获取元数据时将不会被应用。

从Meilisearch版本1.1开始,可以使用多搜索端点解决这个问题。这就是我在这个包中解决这个问题的方式。包将为查询中使用的每个过滤器执行一个额外的查询。但是,所有查询都在单个请求中组合,以减少所需的资源数量。请看以下示例

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->keepFacetsInMetadata(function ($q) {
        $q->where('color', '=', 'yellow');
        $q->where('size', '=', 'XL');
    })
    ->setFacets([
        'color',
        'brand',
        'size',
    ])
    ->get();

此查询将执行以下请求

  • 获取所有品牌,其中产品属于手机类别且匹配颜色=黄色和尺寸=XL
  • 获取所有颜色,其中产品属于手机类别且匹配尺寸=XL
  • 获取所有尺寸,其中产品属于手机类别且匹配颜色=黄色

从Meilisearch版本1.1开始,这是推荐使用多选择分面的方式。

限制和偏移量

限制和偏移量可以轻松地添加到查询中。以下查询将返回10个结果,从第20个结果开始

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->limit(10)
    ->offset(20)
    ->get();

分页结果

就像使用Laravel查询构建器对数据库进行查询一样,您可以使用Meilisearch查询分页结果。只需使用paginate方法。当使用此方法时,较早对limitoffset的调用将被忽略。

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->paginate(10);

可选地提供要用于获取当前页的查询参数名称。默认使用'page'。

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->paginate(10, 'pageNumber');

排序结果

随机顺序

默认情况下,Meilisearch不提供随机排序文档的选项。然而,有时您想显示一些随机产品。为了使这一点成为可能,此包添加了此功能。请注意,包将为每个随机元素向您的Meilisearch数据库执行查询,额外加1次。因此,如果您想以随机顺序获取100个文档,将执行101次查询。Meilisearch查询非常快,但是当您执行此类数量的查询时,它仍然可能会变慢。因此,我建议只在使用少量文档(少于10个)或例如缓存结果时使用此方法。

MeilisearchQuery::index('products')
    ->where('categories', '=', 'phones')
    ->inRandomOrder()
    ->limit(10)
    ->get();