编程

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

997 2024-08-08 19:08:00

现代应用对数据存储能力有很高的要求。过去 10 年里,随着专门构建的数据平台的兴起,围绕数据和分析、交易、相关实体和图形以及搜索和人工智能进行了细分。仅搜索领域就出现了巨大的增长,这要求供应商将他们的平台推向新的和新兴的领域,包括支持向量嵌入。所有这些听起来都很神奇和未来主义,但如果支持人工智能的同一平台也支持传统的搜索呢?那么,支持包括排版错误在内的更人性化的搜索呢?本文将带你一起探索 Typesense,以及它在 PHP 和 Laravel 应用中的 web 健壮性如何体现。

设计详解

在深入代码并了解如何开始使用 Typesense 和 Laravel 之前,我想暂停一下并突出显示我所编写的内容。提前这样做将面向围绕结果的内容。

从这张图中,我将完成以下工作:

  1. 突出项目大纲
  2. 创建 Todo 模型,作为数据的基础
  3. 演示如何为 Typesense 配置 Laravel Scout
  4. 说明从数据库到 Typesense 的模型同步
  5. 展示如何构建 Laravel 控制器和视图以支持该模型

项目创建

以下段落的基础是这个 GitHub 存储库。其中包含的代码需要一些更新才能用于“生产”,但真正感兴趣的是 Typesense 组件以及 Laravel 如何简单地保持数据库和 Typesense 存储同步。

开始

克隆项目时,你会发现它是一个基本的 Laravel 模板设置,是通过运行 composer create-project laravel/laravel typesense-app创建的。当我打开 app/Models/Todo.php 中的文件时,从 Typesense 的角度来看,有趣的部分开始出现

Todo 模型

我的 Todo 模型是一个典型的用例,需要存储一些有意义但不太具体的东西,以至于很难联系或理解。值得指出的是,它特别涉及 Typesense,其中包含了 use Searchable ,这是一个模型 trait。通过引入此 trait,将注册一个模型观察器,自动使模型与数据库和 Typesense 保持同步。只需一行代码,我就能获得所有这些功能。

Todo 模型中与 Typesense 相关的第二个部分是 toSearchableArray() 函数。默认情况下,Typesense 将使用 string 类型的 id 字段作为本文中引用的文档的键。除了将 id 转换为字符串外,建议将时间戳存储为 Unix 纪元,使其成为整数。

<?php
namespace App\Models;
 
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
 
class Todo extends Model
{
    use HasFactory;
    use Searchable;
 
    protected $fillable = [
        'name',
        'description'
    ];
 
    public function toSearchableArray()
    {
        return array_merge($this->toArray(),[
            'id' => (string) $this->id,
            'created_at' => $this->created_at->timestamp,
        ]);
    }
}

配置 Scout

定义完模型并准备号存储,需要配置 Scout,这样观察者才知道要将数据存储在何处。scout.php 文件在项目的 config 目录中,如下图。

Scout 配置文件本质上是一个巨大的数组,其中包含用于不同存储平台和驱动的对象。这使得添加 Typesense 配置变得易于维护。Typesense 配置有两个部分需要注意。

第一部分重点介绍客户端配置。在本节中,我将设置 API key、主机、端口和路径等内容。把它当作设置的驱动部分。从这个角度来看,它感觉就像一个正常的数据库配置。

'client-settings' => [
    'api_key' => env('TYPESENSE_API_KEY', '<your-api-key>'),
    'nodes' => [
        [
            'host' => env('TYPESENSE_HOST', 'localhost'),
            'port' => env('TYPESENSE_PORT', '8108'),
            'path' => env('TYPESENSE_PATH', ''),
            'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
        ],
    ],
    'nearest_node' => [
        'host' => env('TYPESENSE_HOST', 'localhost'),
        'port' => env('TYPESENSE_PORT', '8108'),
        'path' => env('TYPESENSE_PATH', ''),
        'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
    ],
    'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
    'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
    'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
],
    'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),

需要注意的第二部分更多的是实体的集合。对于每个要在 Typesense 中存储和同步的类,我需要配置其映射。对于 Todo 模型,这些映射包括我希望在文档中出现的每个字段。这包括字段名和该字段的数据类型。此外,我还可以配置 default_sorting,并为我想通过搜索参数(search-parameters)进行搜索的特定字段设置索引。

'model-settings' => [
    Todo::class => [
        'collection-schema' => [
            'fields' => [
                [
                    'name' => 'id',
                    'type' => 'string',
                ],
                [
                    'name' => 'name',
                    'type' => 'string',
                ],
                [
                    'name' => 'description',
                    'type' => 'string',
                ],
                [
                    'name' => 'created_at',
                    'type' => 'int64',
                ],
            ],
            'default_sorting_field' => 'created_at',
        ],
        'search-parameters' => [
            'query_by' => 'name'
        ],
    ],
],

视图和控制器

既然已经为观察者配置了 Todo 模型、Scout 也被调整来匹配文档了,那么是时候将它们与 Laravel 视图和控制器结合起来了。

在我的 web.php 路由定义中,我建立了如下端点。

  • /todos 从 Typesense 返回 Todos 列表
  • /todos/new 返回带有新曾 Todo 的表单视图
  • /todos/save 将新的 Todo 从表单写入到数据库
  • /todos/search 对 Typesense 执行查询
Route::get("/todos", [TodoController::class, 'index']);
Route::get('/todos/new', [TodoController::class, 'newTodo']);
Route::post('/todos/save', [TodoController::class, 'store']);
Route::post('/todos/search', [TodoController::class, 'search']);

Todos 列表

在我的 Todo 列表中,我可以从 SQL 数据库中获取数据,但我选择从 Typesense 中获取。我更愿意从搜索数据库返回数据,这样我在使用网格时就可以获得一致的数据和行为。控制器上 index handler 内的代码执行查询,然后将形成数据给 Todo 模型。

$array = Todo::search('')->get()->toArray();
$todos = [];
 
foreach ($array as $todo) {
    $t = new Todo;
    $t->id = $todo['id'];
    $t->name = $todo['name'];
    $t->description = $todo['description'];
    $t->created_at = $todo['created_at'];
    $t->updated_at = $todo['updated_at'];
    array_push($todos, $t);
}
 
return view('todo')->with( ['todos' => $todos] );

可以看见,它在我的网格中渲染出来了,该字段与 Todo 上的字段相匹配。

创建新的 Todo

我网格顶部的链接将带我进入新的 Todo 表单。正如我的模型所示,我有一个名字和描述,就表格中的要求而言非常简单。使用 Laravel 非常好和干净,我的模型包括用于处理数据库的 save() 方法。

$todo = new Todo;
$todo->name = $request->name;
$todo->description = $request->description;
$todo->save();
 
return redirect('/todos')->with('status', 'Todo Data Has Been inserted');

搜索引擎 Todo

使用我新创建的 Todo,我可以通过网格顶部的输入框字段运行搜索。就像 index 中一样,我将执行搜索,但这次我将使用表单输入中的值。

我真正欣赏使用 Scout 和 Typesense 的是,作为一名开发人员,它大多是抽象的,与我无关。我可以专注于用户体验,而不必担心一些低级细节。

public function search(Request $request): View
{
    $search = '';
    if ($request->search) {
        $search = $request->search;
    }
 
    $array = Todo::search($search)->get()->toArray();
    $searched = [];
 
    foreach ($array as $todo) {
        $t = new Todo;
        $t->id = $todo['id'];
        $t->name = $todo['name'];
        $t->description = $todo['description'];
        $t->created_at = $todo['created_at'];
        $t->updated_at = $todo['updated_at'];
        array_push($searched, $t);
    }
 
    return view('todo')->with( ['todos' => $searched ]);
}

总结

在构建了视图和控制器之后,我现在有了一个执行以下功能的工作解决方案。

  • 在 Typesense 中列出 Todo
  • 允许通过表单字段的查询搜索 Typesense
  • 一个 HTML 表单创建新的 Todo 项目
  • 一个 Laravel 控制器在 SQLite 中持久化数据
  • Laravel 会自动将我新保存的数据与 Typesense 同步

通过使用 Laravel 框架与 Typesense 集成,我获得了比实际编写的代码更多的配置的大功能。随着我在项目中添加更多模型,我有了可重复和可扩展的模式,可以根据需要将这些模型包含在我的搜索中。

最后

搜索是所有用户在某种程度上都会要求的,但它通常是以一系列类似于 %<string>% 类型的语句对数据库中的字段进行查询实现的。这在某些情况下可能有用,但会给开发人员带来沉重的负担,使其无法覆盖所有场景,也会给数据库带来很大的负担。它还迫使开发人员与数据库管理员联系,设置全文索引等内容,而对这些内容的支持并不普遍。

这就是 Typesense 和 Laravel 大放异彩的地方。Typesense 是一个专门构建的搜索数据库,可以很好地处理一些默认情况。像拼写错误这样的事情都要考虑在内。文档构建和索引是通过简单的配置进行管理的。然后,当与 Laravel 和 Scout 配对时,同步使使用这个平台变得轻而易举。