使用 Laravel 和 Typesense 构建快速、模糊的网站搜索
现代应用对数据存储能力有很高的要求。过去 10 年里,随着专门构建的数据平台的兴起,围绕数据和分析、交易、相关实体和图形以及搜索和人工智能进行了细分。仅搜索领域就出现了巨大的增长,这要求供应商将他们的平台推向新的和新兴的领域,包括支持向量嵌入。所有这些听起来都很神奇和未来主义,但如果支持人工智能的同一平台也支持传统的搜索呢?那么,支持包括排版错误在内的更人性化的搜索呢?本文将带你一起探索 Typesense,以及它在 PHP 和 Laravel 应用中的 web 健壮性如何体现。
设计详解
在深入代码并了解如何开始使用 Typesense 和 Laravel 之前,我想暂停一下并突出显示我所编写的内容。提前这样做将面向围绕结果的内容。
从这张图中,我将完成以下工作:
- 突出项目大纲
- 创建
Todo
模型,作为数据的基础 - 演示如何为 Typesense 配置 Laravel Scout
- 说明从数据库到 Typesense 的模型同步
- 展示如何构建 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 配对时,同步使使用这个平台变得轻而易举。