pdphilip/elasticlens

使用Eloquent的便利性和Elasticsearch的强大功能来搜索您的Laravel模型

v1.2.2 2024-09-19 22:13 UTC

This package is auto-updated.

Last update: 2024-09-19 22:18:49 UTC


README

ElasticLens

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status

使用Eloquent的便利性和Elasticsearch的强大功能来搜索您的 Laravel模型

ElasticLens for Laravel使用Elasticsearch为您创建并同步Eloquent模型的可搜索索引。

ElasticLens Migrate
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的所有功能,以搜索您的基础模型

功能

需求

  • 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索引中实现更复杂的数据结构。

示例

  1. 如果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');
            });
        });
    }
  1. 如果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');
                });
            });
        });
    }
  1. 如果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');
            });
        });
    }
  1. 如果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
        });
    }
  1. 如果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,请按照以下步骤操作

  1. HasWatcher特质添加到ProfileStatus
use PDPhilip\ElasticLens\HasWatcher;

class ProfileStatus extends Eloquent
{
    use HasWatcher;
  1. 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 ModelmigrationMap()

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

注意

php artisan lens:migrate User

此命令将删除现有索引,运行迁移,并重建所有记录。

第7步:使用Artisan命令监控和管理所有索引

使用以下Artisan命令来管理和监控您的Elasticsearch索引

  1. 检查总体状态
php artisan lens:status 

显示所有索引和ElasticLens配置的总体状态。

ElasticLens Build
  1. 检查索引健康
php artisan lens:health User
ElasticLens Build 提供了特定索引的全面状态,在本例中是对 `User` 模型的状态。
  1. 迁移和构建/重建索引
php artisan lens:migrate User

删除现有的用户索引,运行迁移,并重建所有记录。

ElasticLens Migrate
  1. Base Model 创建新的 Index Model
php artisan lens:make Profile

Profile 模型生成新的索引。

ElasticLens Build
  1. Base Model 批量(重新)构建索引
php artisan lens:build Profile

重建 Profile 模型的所有 IndexedProfile 记录。

ElasticLens Build

步骤 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)。请参阅 许可证文件 了解更多信息。