phoenix-lib/elasticsearch-model

Laravel/Eloquent 集成 Elasticsearch

1.3.3 2020-03-06 12:11 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。

目标是保持对 Ruby on Rails 实现的相对忠实,但放在 Laravel 中。

安装

使用 composer 安装该包

composer require phoenix-lib/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 ]);

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

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

搜索

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

$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 有一个动态获取器

  • indextypeidscoresourcehit 的顶层提取。例如,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.