pdphilip / elasticlens
使用Eloquent的便利性和Elasticsearch的强大功能来搜索您的Laravel模型
Requires
- php: ^8.2
- illuminate/contracts: ^10.0||^11.0
- pdphilip/elasticsearch: ^4.2
- pdphilip/omniterm: ^1.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^2.9
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^9.0.0||^8.22.0
- pestphp/pest: ^2.34
- pestphp/pest-plugin-arch: ^2.7
- pestphp/pest-plugin-laravel: ^2.3
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1
- phpstan/phpstan-phpunit: ^1.3
README
使用Eloquent的便利性和Elasticsearch的强大功能来搜索您的 Laravel模型
ElasticLens for Laravel使用Elasticsearch为您创建并同步Eloquent模型的可搜索索引。
User::viaIndex()->searchPhrase('loves dogs')->where('status','active')->get();
等等,这不是Laravel Scout所做的吗?
是的,但又不完全是。
ElasticLens从头开始围绕Elasticsearch构建.
它直接与Laravel-Elasticsearch包(使用Eloquent的Elasticsearch)集成,创建一个专用的索引模型
,该模型完全可访问,并自动与您的基础模型
同步。
如何做到这一点?
索引模型
充当由ElasticLens管理的独立Elasticsearch模型,同时您仍可以完全控制它,就像其他任何Laravel模型一样。除了直接与索引模型
交互外,ElasticLens还提供了在构建过程中映射字段(包括将模型关系映射为嵌入字段)的工具,以及管理索引迁移。
例如,一个基本的
User
模型将与一个Elasticsearch的IndexedUser
模型同步,该模型提供了Laravel-Elasticsearch的所有功能,以搜索您的基础模型
。
功能
- 零配置设置:以最小配置开始索引。
- Eloquent-like查询:使用Eloquent的方式搜索您的模型,同时拥有Elasticsearch的全部功能。
- 自定义字段映射:控制索引的构建方式,包括将模型关系映射为嵌入字段。
- 控制被观察的模型:定制哪些模型被观察以进行更改。
- 管理Elasticsearch迁移:定义索引迁移所需的蓝图。
- 全面的CLI工具:使用Artisan命令管理索引健康、迁移/重建索引等。
- 内置的IndexableBuildState模型:跟踪索引的构建状态。
- 内置的迁移日志:跟踪索引的构建状态。
需求
- Laravel 10.x & 11.x
- Elasticsearch 8.x
安装
您可以通过composer安装此包
composer require pdphilip/elasticlens
发布配置文件并运行迁移
php artisan lens:install
用法
以下教程将通过示例演示所有功能。
在此示例中,我们将索引一个User
模型。
步骤1:零配置设置
1. 将Indexable
特质添加到您的基模型
use PDPhilip\ElasticLens\Indexable; class User extends Eloquent implements Authenticatable, CanResetPassword { use Indexable;
2. 为您的基模型创建一个索引模型
- ElasticLens期望
索引模型
命名为Indexed
+BaseModelName
,并位于App\Models\Indexes
目录中。
/** * Create: App\Models\Indexes\IndexedUser.php */ namespace App\Models\Indexes; use PDPhilip\ElasticLens\IndexModel; class IndexedUser extends IndexModel{}
- 这就完成了!您的用户模型现在会在更改发生时自动与索引用户模型同步。您可以轻松搜索用户模型,例如
User::viaIndex()->searchTerm('running')->orSearchTerm('swimming')->get();
步骤 2:搜索您的模型
执行快速简便的全文搜索
User::search('loves espressos');
搜索短语
loves espressos
涵盖所有字段,并返回基本User
模型
很可爱。但这并不是我们在这里的原因...
要真正发挥 Laravel-Elasticsearch 的强大功能进行优雅的查询,您可以使用更高级的查询
BaseModel::viaIndex()->{build your ES Eloquent query}->first(); BaseModel::viaIndex()->{build your ES Eloquent query}->get(); BaseModel::viaIndex()->{build your ES Eloquent query}->paginate(); BaseModel::viaIndex()->{build your ES Eloquent query}->avg('orders'); BaseModel::viaIndex()->{build your ES Eloquent query}->distinct(); BaseModel::viaIndex()->{build your ES Eloquent query}->{etc}
示例
1. 基本术语搜索
User::viaIndex()->searchTerm('nara') ->where('state','active') ->limit(3)->get();
这搜索所有活动用户,在所有字段中搜索术语 'nara' 并返回前 3 个结果。
2. 短语搜索
User::viaIndex()->searchPhrase('Ice bathing') ->orderByDesc('created_at') ->limit(5)->get();
搜索所有字段中的短语 'Ice bathing' 并返回 3 个最新结果。短语匹配按顺序排列的精确单词。
3. 增强术语字段
User::viaIndex()->searchTerm('David',['first_name^3', 'last_name^2', 'bio'])->get();
搜索术语 'David',将第一个名字字段增强 3 倍,姓增强 2 倍,并检查个人资料字段。结果按得分排序。
4. 地理位置过滤
User::viaIndex()->where('status', 'active') ->filterGeoPoint('home.location', '5km', [0, 0]) ->orderByGeo('home.location',[0, 0]) ->get();
找到所有在坐标 [0, 0] 5 公里半径内的活动用户,按从近到远的顺序排列。不是开玩笑的。
5. 正则表达式搜索
User::viaIndex()->whereRegex('favourite_color', 'bl(ue)?(ack)?')->get();
找到所有最喜欢的颜色是蓝色或黑色的用户。
6. 分页
User::viaIndex()->whereRegex('favorite_color', 'bl(ue)?(ack)?')->paginate(10);
分页搜索结果。
7. 嵌套对象搜索
User::viaIndex()->whereNestedObject('user_logs', function (Builder $query) { $query->where('user_logs.country', 'Norway') ->where('user_logs.created_at', '>=',Carbon::now()->modify('-1 week')); })->get();
搜索在过去一周内从挪威登录的用户,搜索嵌套的用户日志。
8. 模糊搜索
User::viaIndex()->searchTerm('quikc')->asFuzzy() ->orSearchTerm('brwn')->asFuzzy() ->orSearchTerm('foks')->asFuzzy() ->get();
没有拼写,没问题。进行模糊搜索。
9. 高亮搜索结果
User::viaIndex()->searchTerm('espresso')->withHighlights()->get();
在所有字段中搜索 'espresso' 并突出显示其位置。
10. 短语前缀搜索
User::viaIndex()->searchPhrasePrefix('loves espr')->withHighlights()->get();
在所有字段中搜索短语前缀 'loves espr' 并突出显示其位置。
索引模型
与 基础模型
结果的说明
- 由于
viaIndex()
使用IndexModel
,返回的结果将是IndexedUser
的实例,而不是基本User
模型。 - 这可以用于显示目的,例如突出显示嵌入式字段。
- 然而,在大多数情况下,您需要返回并处理
基础模型
要搜索和返回 基础模型
的结果,请使用 asModel()
- 只需在查询末尾链式添加
->asModel()
User::viaIndex()->searchTerm('david')->orderByDesc('created_at')->limit(3)->get()->asModel(); User::viaIndex()->whereRegex('favorite_color', 'bl(ue)?(ack)?')->get()->asModel(); User::viaIndex()->whereRegex('favorite_color', 'bl(ue)?(ack)?')->first()->asModel();
要搜索和分页 基础模型
的结果,请使用: paginateModels()
- 将查询字符串补充完整,并添加
->paginateModels()
// Returns a pagination instance of Users ✔️: User::viaIndex()->whereRegex('favorite_color', 'bl(ue)?(ack)?')->paginateModels(10); // Returns a pagination instance of IndexedUsers: User::viaIndex()->whereRegex('favorite_color', 'bl(ue)?(ack)?')->paginate(10); // Will not paginate ❌ (but will at least return a collection of 10 Users): User::viaIndex()->whereRegex('favorite_color', 'bl(ue)?(ack)?')->paginate(10)->asModel();
步骤 3:创建字段映射
您可以在 索引模型
中定义 fieldMap()
方法来控制索引在同步期间如何构建。
use PDPhilip\ElasticLens\Builder\IndexBuilder; use PDPhilip\ElasticLens\Builder\IndexField; class IndexedUser extends IndexModel { protected $baseModel = User::class; public function fieldMap(): IndexBuilder { return IndexBuilder::map(User::class, function (IndexField $field) { $field->text('first_name'); $field->text('last_name'); $field->text('email'); $field->bool('is_active'); $field->type('state', UserState::class); //Maps enum $field->text('created_at'); $field->text('updated_at'); }); }
注意
IndexedUser
记录将只包含在fieldMap()
中定义的字段。$user->id
的值将与$indexedUser->_id
对应。- 字段也可以从
基础模型
的属性中派生。例如,$field->bool('is_active')
可以从基础模型
中的自定义属性中派生。public function getIsActiveAttribute(): bool { return $this->updated_at >= Carbon::now()->modify('-30 days'); }
- 在映射枚举时,请确保您也在
索引模型
中将它们进行类型转换。 - 如果在构建过程中找不到值,它将被存储为
null
。
第4步:更新fieldMap()
以包含作为嵌入式字段的关联
您可以通过在索引模型中将关联嵌入为嵌套对象来进一步自定义索引过程。构建器允许您定义字段和嵌入关联,从而在Elasticsearch索引中实现更复杂的数据结构。
示例
- 如果
User
有多个Profiles
use PDPhilip\ElasticLens\Builder\IndexBuilder; use PDPhilip\ElasticLens\Builder\IndexField; class IndexedUser extends IndexModel { protected $baseModel = User::class; public function fieldMap(): IndexBuilder { return IndexBuilder::map(User::class, function (IndexField $field) { $field->text('first_name'); $field->text('last_name'); $field->text('email'); $field->bool('is_active'); $field->type('type', UserType::class); $field->type('state', UserState::class); $field->text('created_at'); $field->text('updated_at'); $field->embedsMany('profiles', Profile::class)->embedMap(function (IndexField $field) { $field->text('profile_name'); $field->text('about'); $field->array('profile_tags'); }); }); }
- 如果
Profile
有一个ProfileStatus
use PDPhilip\ElasticLens\Builder\IndexBuilder; use PDPhilip\ElasticLens\Builder\IndexField; class IndexedUser extends IndexModel { protected $baseModel = User::class; public function fieldMap(): IndexBuilder { return IndexBuilder::map(User::class, function (IndexField $field) { $field->text('first_name'); $field->text('last_name'); $field->text('email'); $field->bool('is_active'); $field->type('type', UserType::class); $field->type('state', UserState::class); $field->text('created_at'); $field->text('updated_at'); $field->embedsMany('profiles', Profile::class)->embedMap(function (IndexField $field) { $field->text('profile_name'); $field->text('about'); $field->array('profile_tags'); $field->embedsOne('status', ProfileStatus::class)->embedMap(function (IndexField $field) { $field->text('id'); $field->text('status'); }); }); }); }
- 如果
User
属于一个Account
use PDPhilip\ElasticLens\Builder\IndexBuilder; use PDPhilip\ElasticLens\Builder\IndexField; class IndexedUser extends IndexModel { protected $baseModel = User::class; public function fieldMap(): IndexBuilder { return IndexBuilder::map(User::class, function (IndexField $field) { $field->text('first_name'); $field->text('last_name'); $field->text('email'); $field->bool('is_active'); $field->type('type', UserType::class); $field->type('state', UserState::class); $field->text('created_at'); $field->text('updated_at'); $field->embedsMany('profiles', Profile::class)->embedMap(function (IndexField $field) { $field->text('profile_name'); $field->text('about'); $field->array('profile_tags'); $field->embedsOne('status', ProfileStatus::class)->embedMap(function (IndexField $field) { $field->text('id'); $field->text('status'); }); }); $field->embedsBelongTo('account', Account::class)->embedMap(function (IndexField $field) { $field->text('name'); $field->text('url'); }); }); }
- 如果
User
属于一个Country
并且您不需要观察Country
模型
use PDPhilip\ElasticLens\Builder\IndexBuilder; use PDPhilip\ElasticLens\Builder\IndexField; class IndexedUser extends IndexModel { protected $baseModel = User::class; public function fieldMap(): IndexBuilder { return IndexBuilder::map(User::class, function (IndexField $field) { $field->text('first_name'); $field->text('last_name'); $field->text('email'); $field->bool('is_active'); $field->type('type', UserType::class); $field->type('state', UserState::class); $field->text('created_at'); $field->text('updated_at'); $field->embedsMany('profiles', Profile::class)->embedMap(function (IndexField $field) { $field->text('profile_name'); $field->text('about'); $field->array('profile_tags'); $field->embedsOne('status', ProfileStatus::class)->embedMap(function (IndexField $field) { $field->text('id'); $field->text('status'); }); }); $field->embedsBelongTo('account', Account::class)->embedMap(function (IndexField $field) { $field->text('name'); $field->text('url'); }); $field->embedsBelongTo('country', Country::class)->embedMap(function (IndexField $field) { $field->text('country_code'); $field->text('name'); $field->text('currency'); })->dontObserve(); // Don't observe changes in the country model }); }
- 如果
User
有多个UserLogs
并且您只想嵌入最后10个
use PDPhilip\ElasticLens\Builder\IndexBuilder; use PDPhilip\ElasticLens\Builder\IndexField; class IndexedUser extends IndexModel { protected $baseModel = User::class; public function fieldMap(): IndexBuilder { return IndexBuilder::map(User::class, function (IndexField $field) { $field->text('first_name'); $field->text('last_name'); $field->text('email'); $field->bool('is_active'); $field->type('type', UserType::class); $field->type('state', UserState::class); $field->text('created_at'); $field->text('updated_at'); $field->embedsMany('profiles', Profile::class)->embedMap(function (IndexField $field) { $field->text('profile_name'); $field->text('about'); $field->array('profile_tags'); $field->embedsOne('status', ProfileStatus::class)->embedMap(function (IndexField $field) { $field->text('id'); $field->text('status'); }); }); $field->embedsBelongTo('account', Account::class)->embedMap(function (IndexField $field) { $field->text('name'); $field->text('url'); }); $field->embedsBelongTo('country', Country::class)->embedMap(function (IndexField $field) { $field->text('country_code'); $field->text('name'); $field->text('currency'); })->dontObserve(); // Don't observe changes in the country model $field->embedsMany('logs', UserLog::class, null, null, function ($query) { $query->orderBy('created_at', 'desc')->limit(10); // Limit the logs to the 10 most recent })->embedMap(function (IndexField $field) { $field->text('title'); $field->text('ip'); $field->array('log_data'); }); }); }
IndexField $field
方法
text($field)
integer($field)
array($field)
bool($field)
type($field, $type)
- 设置自己的类型(如枚举)embedsMany($field, $relatedModelClass, $whereRelatedField, $equalsLocalField, $query)
embedsBelongTo($field, $relatedModelClass, $whereRelatedField, $equalsLocalField, $query)
embedsOne($field, $relatedModelClass, $whereRelatedField, $equalsLocalField, $query)
注意:对于embeds,$whereRelatedField
、$equalsLocalField
和$query
参数是可选的。
$whereRelatedField
是外键
,而$equalsLocalField
是本地键
,如果没有提供,它们将根据关系自动推断。$query
是一个闭包,允许您自定义相关模型的查询。
嵌入式关系构建器方法
embedMap(function (IndexField $field) {})
- 定义嵌入式关系的映射dontObserve()
- 不观察$relatedModelClass
的变化
第5步:微调观察者
默认情况下,基模型将观察更改(保存)和删除。当Base Model
被删除时,相应的Index Model
也将被删除,即使在软删除的情况下。
处理嵌入式模型
当您定义包含嵌入式字段的fieldMap()
时,也会观察相关模型。例如
- 对
ProfileStatus
的保存或删除操作将触发连锁反应,检索相关的Profile
然后是User
,这反过来又启动了该用户记录索引的重建。
但是,为了确保这些观察者被加载,您需要明确引用用户模型
//This alone will not trigger a rebuild $profileStatus->status = 'Unavailable'; $profileStatus->save(); //This will new User::class $profileStatus->status = 'Unavailable'; $profileStatus->save();
自定义观察者
如果您希望ElasticLens观察ProfileStatus
而不需要引用User
,请按照以下步骤操作
- 将
HasWatcher
特质添加到ProfileStatus
use PDPhilip\ElasticLens\HasWatcher; class ProfileStatus extends Eloquent { use HasWatcher;
- 在
elasticlens.php
配置文件中定义观察者
'watchers' => [ \App\Models\ProfileStatus::class => [ \App\Models\Indexes\IndexedUser::class, ], ],
禁用基模型观察
如果您想禁用Base Model
的自动观察,请将以下内容包含在您的Index Model
中
class IndexedUser extends IndexModel { protected $baseModel = User::class; protected $observeBase = false;
第6步:定义您的Index Model
的migrationMap()
Elasticsearch会自动索引遇到的新字段,但它可能不会始终以您需要的方式索引它们。为了确保索引结构正确,您可以在索引模型中定义一个migrationMap()
。
由于Index Model
使用了Laravel-Elasticsearch包,因此您可以使用IndexBlueprint
来自定义您的migrationMap()
。
use PDPhilip\Elasticsearch\Schema\IndexBlueprint; class IndexedUser extends IndexModel { //...... public function migrationMap(): callable { return function (IndexBlueprint $index) { $index->text('name'); $index->keyword('first_name'); $index->text('first_name'); $index->keyword('last_name'); $index->text('last_name'); $index->keyword('email'); $index->text('email'); $index->text('avatar')->index(false); $index->keyword('type'); $index->text('type'); $index->keyword('state'); $index->text('state'); //...etc }; }
注意
- 文档:有关迁移的更多详细信息,请参阅:https://elasticsearch.pdphilip.com/migrations
- 运行迁移:要执行迁移并重建所有索引,请使用以下命令
php artisan lens:migrate User
此命令将删除现有索引,运行迁移,并重建所有记录。
第7步:使用Artisan命令监控和管理所有索引
使用以下Artisan命令来管理和监控您的Elasticsearch索引
- 检查总体状态
php artisan lens:status
显示所有索引和ElasticLens配置的总体状态。
- 检查索引健康
php artisan lens:health User
- 迁移和构建/重建索引
php artisan lens:migrate User
删除现有的用户索引,运行迁移,并重建所有记录。
- 为
Base Model
创建新的Index Model
php artisan lens:make Profile
为 Profile
模型生成新的索引。
- 为
Base Model
批量(重新)构建索引
php artisan lens:build Profile
重建 Profile
模型的所有 IndexedProfile
记录。
步骤 8:可选地访问内置的 IndexableBuildState
模型以跟踪索引构建状态
ElasticLens 包含一个内置的 IndexableBuildState
模型,允许您监视和跟踪索引构建的状态。该模型记录每个索引构建的状态,为您提供对索引过程的深入了解。
字段
模型字段
- string
$model
:正在索引的基础模型。 - string
$model_id
:基础模型的 ID。 - string
$index_model
:相应的索引模型。 - string
$last_source
:构建状态的最后来源。 - IndexableStateType
$state
:索引构建的当前状态。 - array
$state_data
:与构建状态相关的附加数据。 - array
$logs
:索引过程的日志。 - Carbon
$created_at
:构建状态创建的时间戳。 - Carbon
$updated_at
:构建状态最后更新的时间戳。
属性
- @property-read string
$state_name
:当前状态名称。 - @property-read string
$state_color
:与当前状态关联的颜色。
内置方法包括
IndexableBuildState::returnState($model, $modelId, $indexModel); IndexableBuildState::countModelErrors($indexModel); IndexableBuildState::countModelRecords($indexModel);
注意:虽然您可以直接查询 IndexableBuildState
模型,但请避免手动在其中写入或删除记录,因为这可能会干扰健康检查和索引过程的整体完整性。该模型应仅用于读取目的,以确保准确的监控和报告。
步骤 9:可选地访问内置的 IndexableMigrationLog
模型以获取索引迁移状态
ElasticLens 包含一个内置的 IndexableMigrationLog
模型,用于监控和跟踪索引迁移的状态。该模型记录与 Index Model
相关的每个迁移。
字段
- string
$index_model
:迁移的索引模型。 - IndexableMigrationLogState
$state
:迁移的状态 - array
$map
:传递给 Elasticsearch 的迁移映射。 - int
$version_major
:索引过程的较大版本。 - int
$version_minor
:索引过程的较小版本。 - Carbon
$created_at
:迁移创建的时间戳。
属性
- @property-read string
$version
:解析的版本 ex v2.03 - @property-read string
$state_name
:当前状态名称。 - @property-read string
$state_color
:表示当前状态的颜色。
内置方法包括
IndexableMigrationLog::getLatestVersion($indexModel); IndexableMigrationLog::getLatestMigration($indexModel);
注意:虽然您可以直接查询 IndexableMigrationLog
模型,但请避免手动在其中写入或删除记录,因为这可能会干扰迁移的版本控制。该模型应仅用于读取目的,以确保准确性。
致谢
许可证
MIT 许可证(MIT)。请参阅 许可证文件 了解更多信息。