datashaman/elasticsearch-model

Laravel/Eloquent 与 Elasticsearch 集成

2.1.0 2023-08-08 11:06 UTC

README

Laravel 方向的 elasticsearch-model 实现。

支持 Laravel 5.4 及以上版本,基于 PHP 7.0/7.1。一旦 Travis 支持它,PHP 7.2 将得到支持和测试。

注意:至少需要 PHP 7.1 才能支持 Laravel 5.6+。

Build Status StyleCI

注意:此版本软件目前为 BETA 质量。请在生产环境中自行承担风险,并注意 API 在下一个版本或两个版本中可能进一步简化。

目的是在 Laravel 中保持对 Ruby on Rails 实现的较高忠实度。

安装

使用 composer 安装包

composer require datashaman/elasticsearch-model

config/app.php 中配置服务提供者

...
Datashaman\Elasticsearch\Model\ServiceProvider::class,
...

config/app.php 中配置别名

...
'Elasticsearch' => Datashaman\Elasticsearch\Model\ElasticsearchFacade::class,
...

将基本配置复制到您的应用中

php artisan vendor:publish --tag=config --provider='Datashaman\Elasticsearch\Model\ServiceProvider'

根据需要编辑 config/elasticsearch.php,在 .env 中设置 ELASTICSEARCH_HOSTS(以逗号分隔的主机:端口号定义)应该覆盖大多数使用情况。

使用方法

假设您有一个 Article 模型

Schema::create('articles', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
});

class Article extends Eloquent
{
}

Article::create([ 'title' => 'Quick brown fox' ]);
Article::create([ 'title' => 'Fast black dogs' ]);
Article::create([ 'title' => 'Swift green frogs' ]);

设置

要为该模型添加 Elasticsearch 集成,请在您的类中使用 Datashaman\Elasticsearch\Model\ElasticsearchModel 特性。您还必须添加一个受保护的静态属性 $elasticsearch 用于存储

use Datashaman\Elasticsearch\Model\ElasticsearchModel;

class Article extends Eloquent
{
    use ElasticsearchModel;
    protected static $elasticsearch;
}

这将扩展模型,使其具有与 Elasticsearch 相关的功能。

代理

该包包含大量的类和实例方法,以提供所有这些功能。

为了防止污染您的模型命名空间,几乎所有功能都是通过静态方法 Article::elasticsearch() 访问的。

Elasticsearch 客户端

模块将设置一个默认连接到 localhost:9200客户端。您可以像使用任何其他 Elasticsearch::Client 一样访问和使用它

Article::elasticsearch()->client()->cluster()->health();
=> [ "cluster_name" => "elasticsearch", "status" => "yellow", ... ]

要使用具有不同配置的客户端,请使用 Elasticsearch\ClientBuilder 为模型设置客户端

Article::elasticsearch()->client(ClientBuilder::fromConfig([ 'hosts' => [ 'api.server.org' ] ]));

导入数据

您首先想要做的是将数据导入索引

Article::elasticsearch()->import([ 'force' => true ]);

您可以导入特定作用域或查询的记录,使用转换和预处理选项转换批处理,或者通过使用强制选项删除并创建带有正确映射的新索引来重新创建索引--请查看方法文档中的示例。

导入过程中没有报告错误,所以...让我们搜索索引!

搜索

首先,我们可以尝试 简单 类型的搜索

$response = Article::search('fox dogs');

$response->took();
=> 3

$response->total();
=> 2

$response[0]->_score;
=> 0.02250402

$response[0]->title;
=> "Fast black dogs"

搜索结果

返回的 response 对象是对 Elasticsearch 返回的 JSON 的丰富包装,提供了对响应元数据和实际结果(hits)的访问。

response 对象委托给一个内部的 LengthAwarePaginator。您可以通过委托 getCollection 方法获取一个 Collection,尽管分页器也委托了方法到其 Collection 上,所以这两种方法都适用

$response->results()
    ->map(function ($r) { return $r->title; })
    ->all();
=> ["Fast black dogs", "Quick brown fox"]

$response->getCollection()
    ->map(function ($r) { return $r->title; })
    ->all();
=> ["Fast black dogs", "Quick brown fox"]

$response->filter(function ($r) { return preg_match('/^Q/', $r->title); })
    ->map(function ($r) { return $r->title; })
    ->all();
=> ["Quick brown fox"]

如上面的示例所示,使用 Collection::all() 方法获取一个常规数组。

每个 Elasticsearch hit 都被包装在 Result 类中。

Result 有一个动态获取器

  • indextypeidscoresource 都从 hit 的顶层获取。例如,indexhit[_index]typehit[_type]
  • 如果不是上述之一,它会在顶层的 hit 中查找现有项目。例如,_versionhit[_version](如果已定义)
  • 如果不是上述之一,则会在hit[_source](文档)中查找现有项目。例如,titlehit[_source][title](如果已定义)。
  • 如果上述都没有解决问题,则触发通知并返回null。

它还有一个toArray方法,可以返回作为数组的命中结果。

搜索结果作为数据库记录

与从Elasticsearch返回文档不同,records方法将返回一个按分数排序的从主数据库获取的模型实例集合。

$response->records()
    ->map(function ($article) { return $article->title; })
    ->all();
=> ["Fast black dogs", "Quick brown fox"]

返回的对象是数据库返回的模型实例的Collection,即Eloquent实例。

records方法返回你的模型的实际实例,当你想要访问模型方法时很有用 - 当然,这会降低应用程序的运行速度。

在大多数情况下,处理来自Elasticsearch的结果就足够了,而且更快。

当你想要访问数据库records和搜索results时,请使用eachWithHit(或mapWithHit)迭代器。

$lines = [];
$response->records()->eachWithHit(function ($record, $hit) {
    $lines[] = "* {$record->title}: {$hit->_score}";
});

$lines;
=> [ "* Fast black dogs: 0.01125201", "* Quick brown fox: 0.01125201" ]

$lines = $response->records()->mapWithHit(function ($record, $hit) {
    return "* {$record->title}: {$hit->_score}";
})->all();

$lines;
=> [ "* Fast black dogs: 0.01125201", "* Quick brown fox: 0.01125201" ]

请注意,在mapWithHit示例中使用了Collection::all()来转换为常规数组。Collection方法更愿意返回Collection实例而不是常规数组。

records的第一个参数是options数组,第二个参数是一个回调,它传递查询构建器以便即时修改。例如,为了将记录重新排序为不同于结果(如上所述)

$response
    ->records([], function ($query) {
        $query->orderBy('title', 'desc');
    })
    ->map(function ($article) { return $article->title; })
    ->all();

=> [ 'Quick brown fox', 'Fast black dogs' ]

请注意,将orderBy调用添加到查询中会覆盖记录的排序,因此它不再与结果相同。

搜索多个模型

待办事项实现一个用于跨模型搜索的外观。

分页

你可以使用fromsize搜索参数来实现分页。然而,搜索结果可以像Laravel一样自动分页。

# Delegates to the results on page 2 with 20 per page
$response->perPage(20)->page(2);

# Records on page 2 with 20 per page; records ordered the same as results
# Order of the `page` and `perPage` calls doesn't matter
$response->page(2)->perPage(20)->records();

# Results on page 2 with (default) 15 results per page
$response->page(2)->results();

# Records on (default) page 1 with 10 records per page
$response->perPage(10)->records();

你可以访问一个长度感知的分页器(响应在内部委托给results()调用,因此你不需要在链中调用results())。

$response->page(2)->results();
=> object(Illuminate\Pagination\LengthAwarePaginator) ...

$results = response->page(2);

$results->setPath('/articles');
$results->render();
=> <ul class="pagination">
    <li><a href="/articles?page=1" rel="prev">&laquo;</a></li>
    <li><a href="/articles?page=1">1</a></li>
    <li class="active"><span>2</span></li>
    <li><a href="/articles?page=3">3</a></li>
    <li><a href="/articles?page=3" rel="next">&raquo;</a></li>
</ul>

渲染的HTML进行了轻微的整理,以提高可读性。

Elasticsearch DSL

待办事项将其与查询构建器集成。

索引配置

为了正确地搜索功能,通常需要正确配置索引。此包提供类方法来设置索引设置和映射。

Article::settings(['index' => ['number_of_shards' => 1]], function ($s) {
    $s['index'] = array_merge($s['index'], [
        'number_of_replicas' => 4,
    ]);
});

Article::settings->toArray();
=> [ 'index' => [ 'number_of_shards' => 1, 'number_of_replicas' => 4 ] ]

Article::mappings(['dynamic' => false], function ($m) {
    $m->indexes('title', [
        'analyzer' => 'english',
        'index_options' => 'offsets'
    ]);
});

Article::mappings()->toArray();
=> [ "article" => [
    "dynamic" => false,
    "properties" => [
        "title" => [
            "analyzer" => "english",
            "index_options" => "offsets",
            "type" => "string",
        ]
    ]
]]

你可以使用定义的设置和映射来创建具有所需配置的索引。

Article::elasticsearch()->client()->indices()->delete(['index' => Article::indexName()]);
Article::elasticsearch()->client()->indices()->create([
    'index' => Article::indexName(),
    'body' => [
        'settings' => Article::settings()->toArray(),
        'mappings' => Article::mappings()->toArray(),
    ],
]);

此常见操作有快捷方式(例如在测试中方便)。

Article::elasticsearch()->createIndex(['force' => true]);
Article::elasticsearch()->refreshIndex();

默认情况下,索引名称和文档类型将根据你的类名推断,但你也可以明确设置它们。

class Article {
    protected static $indexName = 'article-production';
    protected static $documentType = 'post';
}

或者,你可以使用以下静态方法来设置它们:

Article::indexName('article-production');
Article::documentType('post');

更新索引中的文档

通常,我们需要在数据库中的记录被创建、更新或删除时更新Elasticsearch索引;分别使用index_document、update_document和delete_document方法。

Article::first()->indexDocument();
=> [ 'ok' => true, ... "_version" => 2 ]

Note that this implementation differs from the Ruby one, where the instance has an elasticsearch() method and proxy object. In this package, the instance methods are added directly to the model. Implementing the same pattern in PHP is not easy to do cleanly.

### Automatic callbacks

You can auomatically update the index whenever the record changes, by using the `Datashaman\\Elasticsearch\\Model\\Callbacks` trait in your model:

```php
use Datashaman\Elasticsearch\Model\ElasticsearchModel;
use Datashaman\Elasticsearch\Model\Callbacks;

class Article
{
    use ElasticsearchModel;
    use Callbacks;
}

Article::first()->update([ 'title' => 'Updated!' ]);

Article::search('*')->map(function ($r) { return $r->title; });
=> [ 'Updated!', 'Fast black dogs', 'Swift green Frogs' ]

记录更新的自动回调会跟踪模型中的更改(通过Laravel的getDirty实现),并在支持此功能的情况下执行部分更新。

自动回调是在此包附带的数据库适配器中实现的。你可以轻松实现自己的适配器:请参阅下面的相关章节。

自定义回调

如果你需要更多对索引过程的控制,你可以通过钩入createdsavedupdateddeleted事件来自行实现这些回调。

Article::saved(function ($article) {
    $result = $article->indexDocument();
    Log::debug("Saved document", compact('result'));
});

Article::deleted(function ($article) {
    $result = $article->deleteDocument();
    Log::debug("Deleted document", compact('result'));
});

遗憾的是,在Eloquent中没有像Ruby的ActiveRecord中的committed事件。

异步回调

当然,你在数据库事务期间仍在执行HTTP请求,这对大规模应用程序来说不是最优的。更好的选择是将索引操作在后台处理,使用Laravel的Queue外观。

Article::saved(function ($article) {
    Queue::pushOn('default', new Indexer('index', Article::class, $article->id));
});

Indexer类的示例实现可能如下所示(源代码包含在包中)。

class Indexer implements SelfHandling, ShouldQueue
{
    use InteractsWithQueue, SerializesModels;

    public function __construct($operation, $class, $id)
    {
        $this->operation = $operation;
        $this->class = $class;
        $this->id = $id;
    }

    public function handle()
    {
        $class = $this->class;

        switch ($this->operation) {
        case 'index':
            $record = $class::find($this->id);
            $class::elasticsearch()->client()->index([
                'index' => $class::indexName(),
                'type' => $class::documentType(),
                'id' => $record->id,
                'body' => $record->toIndexedArray(),
            ]);
            $record->indexDocument();
            break;
        case 'delete':
            $class::elasticsearch()->client()->delete([
                'index' => $class::indexName(),
                'type' => $class::documentType(),
                'id' => $this->id,
            ]);
            break;
        default:
            throw new Exception('Unknown operation: '.$this->operation);
        }
    }
}

模型序列化

默认情况下,模型实例将使用包自动定义的 toIndexedArray 方法的输出序列化为JSON。

Article::first()->toIndexedArray();
=> [ 'title' => 'Quick brown fox' ]

如果您想自定义序列化,只需自己实现 toIndexedArray 方法即可,例如使用 toArray 方法。

class Article
{
    use ElasticsearchModel;

    public function toIndexedArray($options = null)
    {
        return $this->toArray();
    }
}

重新定义的方法将在索引方法中使用,例如 indexDocument

归属

原始设计来自 elasticsearch-model,它是

更改包括在PHP中重新编写核心逻辑,以及轻微增强以适应Laravel和Eloquent。

许可证

此包继承了其原始许可证。它使用以下Apache2许可证授权

Copyright (c) 2016 datashaman <marlinf@datashaman.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://apache.ac.cn/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.