fadymounir966/wmatchory

缺失的Laravel Elasticsearch ORM!

这个包的官方仓库似乎已经消失,因此该包已被冻结。

安装: 0

依赖: 0

建议者: 0

安全: 0

类型:package

1.0.0 2024-02-28 09:44 UTC

This package is not auto-updated.

Last update: 2024-07-26 13:29:49 UTC


README

Latest Stable Version Total Downloads Latest Unstable Version License

Laravel Elasticsearch集成

这是基于@basemkhirat编写的优秀库的分支,遗憾的是他似乎已经放弃了它。
由于我们非常依赖这个库,我们将努力保持其更新并与新的Laravel和Elasticsearch版本兼容。

分支中的更改

  • [x] 支持Elasticsearch 7.10及更高版本
  • [x] 支持PHP 7.3及更高版本(包括PHP 8)
  • [x] 扩展了对Laravel库的支持,允许您与几乎所有版本的Laravel一起使用
  • [x] 在所有支持的位置添加类型提示,对所有参数都有信心
  • [x] 为高级自动完成添加Docblock注释,提供详尽的内联文档
  • [x] 将连接管理清晰分离到ConnectionManager中,同时保留向后兼容性
  • [x] 支持大多数Eloquent模型行为(见下文)
  • [x] 移除了对Laravel内部机制的依赖

如果您有兴趣贡献,请提交PR或打开一个问题!

特性

  • 具有优雅语法的流畅Elasticsearch查询构建器
  • 受Laravel Eloquent启发的Elasticsearch模型
  • 使用简单的artisan命令进行索引管理
  • Lumen框架有限的支持
  • 可以作为Laravel Scout驱动程序使用
  • 并行使用多个Elasticsearch连接
  • 基于Laravel分页的内置分页
  • 使用基于laravel缓存的缓存层缓存查询

目录

要求

  • PHP >= 7.3
    查看Travis CI构建
  • laravel/laravel >= 5.laravel/lumen >= 5. 或任何其他使用 composer 的应用

安装

本节描述了所有受支持应用类型的安装过程。

使用composer安装包

无论您使用 Laravel、Lumen 还是其他框架,首先通过 composer 安装该包

composer require matchory/elasticsearch

Laravel安装

如果您已禁用包自动发现,请将服务提供者和外观添加到您的 config/app.php

    'providers' => [
        // ...

        Matchory\Elasticsearch\ElasticsearchServiceProvider::class,

        // ...
    ],

    // ...

    'aliases' => [
        // ...

        'ES' => Matchory\Elasticsearch\Facades\ES::class,

        // ...
    ],

最后,将服务提供者发布到您的配置目录

php artisan vendor:publish --provider="Matchory\Elasticsearch\ElasticsearchServiceProvider"

Lumen安装

通过 composer 安装包后,请将包服务提供者添加到 bootstrap/app.php

$app->register(Matchory\Elasticsearch\ElasticsearchServiceProvider::class);

将包配置目录 vendor/matchory/elasticsearch/src/config/ 复制到您的项目根目录,与您的 app/ 目录并列

cp -r ./vendor/matchory/elasticsearch/src/config ./config

如果您尚未这样做,通过在 bootstrap/app.php 中取消注释此行,使 Lumen 与外观协同工作

$app->withFacades();

如果您不想在 Lumen 中启用外观,可以使用 app("es") 访问查询构建器

app("es")->index("my_index")->type("my_type")->get();

# This is similar to:
ES::index("my_index")->type("my_type")->get();

通用应用程序安装

您可以使用任何基于 composer 的应用安装包。虽然我们无法提供通用说明,但以下示例应能给您一个大致的工作原理

require "vendor/autoload.php";

use Matchory\Elasticsearch\ConnectionManager;
use Matchory\Elasticsearch\Factories\ClientFactory;

$connectionManager = new ConnectionManager([
 'servers' => [
        [
            "host" => '127.0.0.1',
            "port" => 9200,
            'user' => '',
            'pass' => '',
            'scheme' => 'http',
        ],
    ],

	// Custom handlers
	// 'handler' => new MyCustomHandler(),

    'index' => 'my_index',
], new ClientFactory());

$connection = $connectionManager->connection();

// Access the query builder using created connection
$documents = $connection->search("hello")->get();

配置(Laravel & Lumen)

发布服务提供者后,将在 config/es.php 中创建一个配置文件。在此,您可以添加一个或多个 Elasticsearch 连接,每个连接包含多个服务器。请看以下示例

# Here you can define the default connection name.
'default' => env('ELASTIC_CONNECTION', 'default'),

# Here you can define your connections.
'connections' => [
	'default' => [
	    'servers' => [
	        [
	            "host" => env("ELASTIC_HOST", "127.0.0.1"),
	            "port" => env("ELASTIC_PORT", 9200),
	            'user' => env('ELASTIC_USER', ''),
	            'pass' => env('ELASTIC_PASS', ''),
	            'scheme' => env('ELASTIC_SCHEME', 'http'),
	        ]
	    ],
	    
		// Custom handlers
		// 'handler' => new MyCustomHandler(),
		'index' => env('ELASTIC_INDEX', 'my_index')
	]
],
 
# Here you can define your indices.
'indices' => [
	'my_index_1' => [
	    "aliases" => [
	        "my_index"
	    ],
	    'settings' => [
	        "number_of_shards" => 1,
	        "number_of_replicas" => 0,
	    ],
	    'mappings' => [
	        'posts' => [
                'properties' => [
                    'title' => [
                        'type' => 'string'
                    ]
                ]
	        ]
	    ]
	]
]

如果您想使用 Laravel Scout 来使用 Elasticsearch,您可以在 config/scout.php 中找到特定的 scout 设置。

Artisan命令(Laravel & Lumen)

本包包含的 artisan 命令可创建或更新设置、映射和别名。请注意,所有命令默认使用默认连接。您可以通过传递 --connection <your_connection_name> 选项来更改此设置。

以下命令可用

es:indices:list:列出服务器上的所有索引

$ php artisan es:indices:list
+----------------------+--------+--------+----------+------------------------+-----+-----+------------+--------------+------------+----------------+
| configured (es.php)  | health | status | index    | uuid                   | pri | rep | docs.count | docs.deleted | store.size | pri.store.size |
+----------------------+--------+--------+----------+------------------------+-----+-----+------------+--------------+------------+----------------+
| yes                  | green  | open   | my_index | 5URW60KJQNionAJgL6Q2TQ | 1   | 0   | 0          | 0            | 260b       | 260b           |
+----------------------+--------+--------+----------+------------------------+-----+-----+------------+--------------+------------+----------------+

es:indices:create:创建在config/es.php中定义的索引

请注意,创建操作会跳过已存在的索引。

# Create all indices in config file.
php artisan es:indices:create

# Create only 'my_index' index in config file
php artisan es:indices:create my_index 

es:indices:update:更新在config/es.php中定义的索引

请注意,更新操作更新索引设置、别名和映射,但不会删除索引数据。

# Update all indices in config file.
php artisan es:indices:update

# Update only 'my_index' index in config file
php artisan es:indices:update my_index 

es:indices:drop:删除索引

使用此命令时要小心,因为您将丢失索引数据!
使用带 --force 选项的 drop 命令将跳过所有确认消息。

# Drop all indices in config file.
php artisan es:indices:drop

# Drop specific index on sever. Not matter for index to be exist in config file or not.
php artisan es:indices:drop my_index 

数据重新索引(零停机时间)

首先,为什么需要重新索引?
更改索引映射不进行数据重新索引是不会反映出来的,否则您的搜索结果将无法正确工作。
为了避免停机时间,您的应用程序应该使用索引 alias 而不是索引 name
索引 alias 是一个应用程序应该与之协同工作的常量名称,以避免更改索引名称。

假设我们想更改 my_index 的映射,以下是操作方法

  1. alias 作为示例 my_index_alias 添加到 my_index 配置,并确保您的应用程序正在使用它。

    "aliases" => [
        "my_index_alias"
    ]       
    
  2. 使用命令更新索引

    php artisan es:indices:update my_index
    
  3. 创建一个新索引,例如 my_new_index,并在配置文件中包含您的新映射。

    $ php artisan es:indices:create my_new_index
    
  4. 使用命令将 my_index 中的数据重新索引到 my_new_index

    php artisan es:indices:reindex my_index my_new_index
       
    # Control bulk size. Adjust it with your server.
    php artisan es:indices:reindex my_index my_new_index --bulk-size=2000
       
    # Control query scroll value.
    php artisan es:indices:reindex my_index my_new_index --bulk-size=2000 --scroll=2m
       
    # Skip reindexing errors such as mapper parsing exceptions.
    php artisan es:indices:reindex my_index my_new_index --bulk-size=2000 --skip-errors 
       
    # Hide all reindexing errors and show the progres bar only.
    php artisan es:indices:reindex my_index my_new_index --bulk-size=2000 --skip-errors --hide-errors
    
  5. 在配置文件中从 my_index 中移除 my_index_alias 别名,并将其添加到 my_new_index,然后使用命令更新

    php artisan es:indices:update
    

作为Laravel Scout驱动程序的使用

首先,遵循 Laravel Scout 安装
您需要做的就是更新 config/scout.php 中的以下行

# change the default driver to 'es'
'driver' => env('SCOUT_DRIVER', 'es'),

# link `es` driver with default elasticsearch connection in config/es.php
'es' => [
    'connection' => env('ELASTIC_CONNECTION', 'default'),
],

也请查看 Laravel Scout 文档

Elasticsearch模型

每个索引类型都有一个相应的 "模型",用于与该类型交互。模型允许您查询类型或索引中的数据,以及将新文档插入到类型中。Elasticsearch 模型尽可能模仿 Eloquent 模型:您可以使用模型事件、路由绑定、高级属性方法等。 如果您缺少任何 Eloquent 功能,请提交一个问题,我们将很高兴地添加它!

支持的功能

  • 属性
  • 事件
  • 路由绑定
  • 全局和局部查询作用域
  • 复制模型

一个最小模型可能看起来像这样

namespace App\Models;

use Matchory\Elasticsearch\Model;

class Post extends Model
{
    // ...
}

索引名称

此模型未与任何特定索引绑定,它将直接使用为给定Elasticsearch连接配置的索引。要针对特定索引进行操作,您可以在模型上定义一个index属性。

namespace App\Models;

use Matchory\Elasticsearch\Model;

class Post extends Model
{
    protected $index = 'posts';
}

连接名称

默认情况下,所有Elasticsearch模型都将使用应用程序配置的默认连接。如果您想指定与特定模型交互时应使用的不同连接,您应该在模型上定义一个$connection属性。

namespace App\Models;

use Matchory\Elasticsearch\Model;

class Post extends Model
{
    protected $connection = 'blag';
}

映射类型

如果您仍在使用映射类型,您可以在模型上添加一个type属性来指示用于查询的映射_type

映射类型已被弃用
请注意,Elastic已弃用映射类型,并将从下一个主要版本中删除它们。您不应依赖于它们以保持工作状态。

namespace App;

use Matchory\Elasticsearch\Model;

class Post extends Model
{
    protected $type = 'posts';
}

默认属性值

默认情况下,新实例化的模型实例将不包含任何属性值。如果您想为模型的一些属性定义默认值,您可以在模型上定义一个attributes属性。

namespace App\Models;

use Matchory\Elasticsearch\Model;

class Post extends Model
{
    protected $attributes = [
        'published' => false,
    ];
}

检索模型

一旦创建了模型及其相关的索引类型,您就可以开始从索引中检索数据。您可以认为Elasticsearch模型是一个功能强大的查询构建器,允许您流畅地查询与模型关联的索引。模型的all方法将从与模型关联的Elasticsearch索引检索所有文档。

use App\Models\Post;

foreach (Post::all() as $post) {
    echo $post->title;
}

添加额外的约束

all方法将返回模型索引中的所有结果。然而,由于每个Elasticsearch模型都充当查询构建器,您可以在查询中添加额外的约束,然后调用get()方法来检索结果。

use App\Models\Post;

$posts = Post::where('status', 1)
             ->orderBy('created_at', 'desc')
             ->take(10)
             ->get();

集合

如我们所见,Elasticsearch方法如allget从索引中检索多个文档。然而,这些方法不返回一个普通的PHP数组。相反,返回一个Matchory\Elasticsearch\Collection的实例。

Elasticsearch Collection类扩展了Laravel的基本Illuminate\Support\Collection类,它为与数据集合交互提供了各种有用的方法。例如,可以使用reject方法根据调用的闭包结果从集合中删除模型。

use App\Models\Post;

$posts = Post::where('sponsored', true)->get();
$posts = $posts->reject($post => $post->in_review);

除了Laravel基本集合类提供的方法外,Elasticsearch集合类还提供了一些专门用于与Elasticsearch模型集合交互的额外方法。

结果元数据

Elasticsearch在查询的命中结果之外还提供了一些额外的字段,例如总结果数量或查询执行时间。Elasticsearch集合提供了这些属性的getter。

use App\Models\Post;

$posts = Post::all();
$total = $posts->getTotal();
$maxScore = $posts->getMaxScore();
$duration = $posts->getDuration();
$isTimedOut = $posts->isTimedOut();
$scrollId = $posts->getScrollId();
$shards = $posts->getShards();

迭代

由于所有Laravel的集合都实现了PHP的iterable接口,因此您可以像数组一样遍历集合。

foreach ($title as $title) {
    echo $post->title;
}

分块结果

Elasticsearch索引可能会变得非常大。如果尝试通过allget方法加载数十万个Elasticsearch文档(没有设置上限),则应用程序可能会耗尽内存。因此,默认的文档检索量设置为10。要更改此值,请使用take方法。

use App\Models\Post;

$posts = Post::take(500)->get();

检索单个模型

除了检索与给定查询匹配的所有文档外,您还可以使用findfirstfirstWhere方法检索单个文档。这些方法返回单个模型实例,而不是模型集合。

use App\Models\Post;

// Retrieve a model by its ID...
$posts = Post::find('AVp_tCaAoV7YQD3Esfmp');

// Retrieve the first model matching the query constraints...
$post = Post::where('published', 1)->first();

// Alternative to retrieving the first model matching the query constraints...
$post = Post::firstWhere('published', 1);```

有时你可能希望检索查询的第一个结果或在没有找到结果时执行其他操作。`firstOr` 方法将返回与查询匹配的第一个结果,如果没有找到结果,将执行给定的闭包。闭包返回的值将被视为 `firstOr` 方法的返回结果。

use App\Models\Post;

$model = Post::where('tags', '>', 3)->firstOr(function () {
    // ...
});

未找到异常

有时你可能希望在找不到模型时抛出异常。这在路由或控制器中特别有用。`findOrFail` 和 `firstOrFail` 方法将检索查询的第一个结果;然而,如果没有找到结果,将抛出一个 Matchory\Elasticsearch\Exceptions\DocumentNotFoundException

$post = Post::findOrFail('AVp_tCaAoV7YQD3Esfmp');

$post = Post::where('published', true)->firstOrFail();

如果没有捕获到 `DocumentNotFoundException`,将自动向客户端发送一个 404 HTTP 响应。

use App\Models\Post;

Route::get('/api/posts/{id}', function ($id) {
    return Post::findOrFail($id);
});

插入和更新模型

插入

要向索引中插入新的文档,你应该实例化一个新的模型实例并设置模型上的属性。然后,在模型实例上调用 `save` 方法。

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Controllers\Controller;

class PostController extends Controller
{
    /**
     * Create a new post instance.
     *
     * @param  Request  $request
     * @return Response
     */
    public function store(Request $request): Response
    {
        // Validate the request...

        $post = new Post;
        $post->title = $request->title;

        $post->save();
    }
}

在这个例子中,我们将传入的 HTTP 请求中的 `name` 字段赋值给 `App\Models\Post` 模型实例的 `name` 属性。当我们调用 `save` 方法时,文档将被插入到索引中。

或者,你也可以使用 `create` 方法通过单个 PHP 语句“保存”新的模型。`create` 方法将返回插入的模型实例。

use App\Models\Post;

$post = Post::create([
'title' => 'Searching efficiently',
]);

但是,在使用 `create` 方法之前,你需要在你的模型类上指定 `fillable` 或 `guarded` 属性。这些属性是必需的,因为默认情况下所有 Elasticsearch 模型都受到防止批量赋值漏洞的保护。有关批量赋值的更多信息,请参阅 批量赋值文档

更新

`save` 方法也可以用来更新索引中已经存在的模型。要更新一个模型,你应该检索它并设置你想要更新的任何属性。然后,你应该调用模型的 `save` 方法。

`save()` 方法也可以用来更新已经存在于索引中的模型。要更新一个模型,你应该检索它,设置你想要更新的任何属性,然后调用保存方法。

use App\Models\Post;

$post = Post::find('AVp_tCaAoV7YQD3Esfmp');

$post->title = 'Modified Post Title';

$post->save();

检查属性更改

Elasticsearch 提供了 `isDirty`、`isClean` 和 `wasChanged` 方法来检查模型的内部状态,并确定其属性自从最初检索以来是如何变化的。

`isDirty` 方法确定自模型检索以来模型是否更改了任何属性。你可以向 `isDirty` 方法传递一个特定的属性名称来确定某个特定的属性是否已被更改。`isClean` 将确定自模型检索以来属性是否保持不变。此方法还接受一个可选的属性参数。

use App\Models\Author;

$author = Author::create([
'first_name' => 'Moritz',
'last_name' => 'Friedrich',
'title' => 'Developer',
]);

$author->title = 'Painter';

$author->isDirty(); // true
$author->isDirty('title'); // true
$author->isDirty('first_name'); // false

$author->isClean(); // false
$author->isClean('title'); // false
$author->isClean('first_name'); // true

$author->save();

$author->isDirty(); // false
$author->isClean(); // true

`wasChanged` 方法确定在当前请求周期中模型上次保存时是否更改了任何属性。如果需要,你可以传递一个属性名称以查看特定的属性是否更改。

use App\Models\Author;

$author = Author::create([
'first_name' => 'Taylor',
'last_name' => 'Otwell',
'title' => 'Developer',
]);

$author->title = 'Painter';

$author->save();

$author->wasChanged(); // true
$author->wasChanged('title'); // true
$author->wasChanged('first_name'); // false

`getOriginal` 方法返回一个数组,包含模型的原生属性,无论模型自检索以来是否发生变化。如果需要,你可以传递一个特定的属性名称以获取特定属性的原始值。

use App\Models\Author;

$author = Author::find(1);

$author->name; // John
$author->email; // john@example.com

$author->name = "Jack";
$author->name; // Jack

$author->getOriginal('name'); // John
$author->getOriginal(); // Array of original attributes...

批量赋值

你可以使用 `create` 方法通过单个 PHP 语句“保存”新的模型。该方法将返回插入的模型实例。

use App\Models\Post;

$post = Post::create([
    'title' => 'Searching effectively',
]);

但是,在使用 `create` 方法之前,你需要在你的模型类上指定 `fillable` 或 `guarded` 属性。这些属性是必需的,因为默认情况下所有 Elasticsearch 模型都受到防止批量赋值漏洞的保护。

当用户传递一个意外的 HTTP 请求字段并且该字段更改了你没有期望在索引中更改的字段时,就会发生批量赋值漏洞。

要开始使用,您需要定义您想要设置为可批量赋值的模型属性。您可以使用模型上的fillable属性来完成此操作。例如,让我们使我们的Post模型的title属性可批量赋值

namespace App\Models;

use Matchory\Elasticsearch\Model;

class Post extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['title'];
}

一旦您指定了哪些属性是可批量赋值的,您就可以使用create方法在索引中插入一个新的文档。create方法返回新创建的模型实例

$post = Post::create(['title' => 'Searching effectively']);

如果您已经有一个模型实例,您可以使用fill方法用属性数组填充它

$post->fill(['title' => 'Searching more effectively']);

允许批量赋值

如果您希望使所有属性都可批量赋值,您可以定义模型的guarded属性为一个空数组。如果您选择取消保护模型,您应特别注意始终手动制作传递给Elasticsearch的fillcreateupdate方法的数组

/**
 * The attributes that aren't mass assignable.
 *
 * @var array
 */
protected $guarded = [];

Upserts

目前还没有用于更新文档(根据模型是否存在插入或更新)的便利包装器。如果您对此类功能感兴趣,请提交一个问题。

删除模型

要删除一个模型,请在模型实例上调用delete方法

use App\Models\Post;

$post = Post::find('AVp_tCaAoV7YQD3Esfmp');

$post->delete();

通过ID删除现有模型

在上面的例子中,我们在调用delete方法之前从索引中检索模型。但是,如果您知道模型的ID,您可以通过调用destroy方法来删除模型而无需显式检索它。除了接受单个ID外,destroy方法还将接受多个ID、ID数组或ID集合

use App\Models\Post;

Post::destroy(1);

Post::destroy(1, 2, 3);

Post::destroy([1, 2, 3]);

Post::destroy(collect([1, 2, 3]));

重要
destroy方法会单独加载每个模型并调用delete方法,以确保为每个模型正确地派发deletingdeleted事件。

查询作用域

查询作用域的实现方式与Eloquent中的实现方式完全相同。

全局作用域

全局作用域允许您为给定模型的查询添加约束。编写自己的全局作用域可以提供一种方便、简单的方法,以确保对给定模型的每个查询都接收某些约束。

编写全局作用域

编写全局作用域很简单。首先,定义一个实现Matchory\Elasticsearch\Interfaces\ScopeInterface接口的类。Laravel没有推荐的位置来放置作用域类,因此您可以将此类放在您希望的任何目录中。

ScopeInterface要求您实现一个方法:apply。根据需要,apply方法可以添加约束或其他类型的子句到查询中。

namespace App\Scopes;

use Matchory\Elasticsearch\Query;
use Matchory\Elasticsearch\Model;
use Matchory\Elasticsearch\Interfaces\ScopeInterface;

class AncientScope implements ScopeInterface
{
    /**
     * Apply the scope to a given Elasticsearch query builder.
     *
     * @param  \Matchory\Elasticsearch\Query  $query
     * @param  \Matchory\Elasticsearch\Model  $model
     * @return void
     */
    public function apply(Query $query, Model $model)
    {
        $query->where('created_at', '<', now()->subYears(2000));
    }
}
应用全局作用域

要将全局作用域分配给模型,您应覆盖模型的booted方法并调用模型的addGlobalScope方法。addGlobalScope方法接受作用域实例作为其唯一参数

namespace App\Models;

use App\Scopes\AncientScope;
use Matchory\Elasticsearch\Model;

class Post extends Model
{
    /**
     * The "booted" method of the model.
     *
     * @return void
     */
    protected static function booted()
    {
        static::addGlobalScope(new AncientScope);
    }
}
匿名全局作用域

Elasticsearch还允许您使用闭包来定义全局作用域,这对于不需要单独类的简单作用域特别有用。当使用闭包定义全局作用域时,您应将自定义的作用域名称作为addGlobalScope方法的第一个参数提供

namespace App\Models;

use Matchory\Elasticsearch\Query;
use Matchory\Elasticsearch\Model;

class Post extends Model
{
    /**
     * The "booted" method of the model.
     *
     * @return void
     */
    protected static function booted(): void
    {
        static::addGlobalScope('ancient', function (Query $query) {
            $query->where('created_at', '<', now()->subYears(2000));
        });
    }
}
移除全局作用域

如果您希望从给定查询中移除全局作用域,您可以使用withoutGlobalScope方法。此方法接受全局作用域的类名作为其唯一参数

Post::withoutGlobalScope(AncientScope::class)->get();

或者,如果您使用闭包定义了全局作用域,应传递分配给全局作用域的字符串名称

Post::withoutGlobalScope('ancient')->get();

如果您想移除多个或所有查询的全局作用域,可以使用withoutGlobalScopes方法

// Remove all of the global scopes...
Post::withoutGlobalScopes()->get();
// Remove some of the global scopes...
Post::withoutGlobalScopes([
    FirstScope::class,
    SecondScope::class
])->get();

局部作用域

局部作用域允许您定义可以轻松在整个应用程序中重用的常见查询约束集。例如,您可能需要频繁检索所有被认为是“流行”的帖子。

编写局部作用域

为了定义作用域,请使用scope前缀来标记Elasticsearch模型方法。作用域应该始终返回一个查询构建器实例。

namespace App\Models;

use Matchory\Elasticsearch\Model;

class Post extends Model
{
    /**
     * Scope a query to only include popular posts.
     *
     * @param  \Matchory\Elasticsearch\Query  $query
     * @return \Matchory\Elasticsearch\Query
     */
    public function scopePopular(Query $query): Query
    {
        return $query->where('votes', '>', 100);
    }

    /**
     * Scope a query to only include published posts.
     *
     * @param  \Matchory\Elasticsearch\Query  $query
     * @return \Matchory\Elasticsearch\Query
     */
    public function scopePublished(Query $query): Query
    {
        return $query->where('published', 1);
    }
}
使用局部作用域

一旦定义了作用域,在查询模型时可以调用作用域方法。但是,在调用方法时不应包括作用域前缀。您甚至可以链接调用不同的作用域。

use App\Models\Post;

$posts = Post::popular()->published()->orderBy('created_at')->get();

动态作用域

有时您可能希望定义一个接受参数的作用域。为了开始,只需将您的额外参数添加到作用域方法的签名中。作用域参数应定义在$query参数之后。

namespace App\Models;

use Matchory\Elasticsearch\Model;

class Post extends Model
{
    /**
     * Scope a query to only include posts of a given type.
     *
     * @param  \Matchory\Elasticsearch\Query  $query
     * @param  mixed  $type
     * @return \Matchory\Elasticsearch\Query
     */
    public function scopeOfType(Query $query, $type): Query
    {
        return $query->where('type', $type);
    }
}

一旦将预期的参数添加到作用域方法的签名中,在调用作用域时可以传递这些参数。

$posts = Post::ofType('news')->get();

比较模型

有时您可能需要确定两个模型是否是“相同”的。可以使用is方法快速验证两个模型是否具有相同的ID、索引、类型和连接。

if ($post->is($anotherPost)) {
    //
}

事件

Elasticsearch模型会分发多个事件,允许您挂钩模型生命周期的以下时刻:retrievedcreatingcreatedupdatingupdatedsavingsaveddeletingdeletedrestoringrestoredreplicating

当从索引中检索现有模型时,将分发retrieved事件。当新模型第一次保存时,将分发creatingcreated事件。当现有模型被修改并调用save方法时,将分发updating / updated事件。当模型创建或更新时(即使模型的属性未更改),将分发save / saved事件。

要开始监听模型事件,请在您的Elasticsearch模型上定义一个dispatchesEvents属性。该属性将Elasticsearch模型的生命周期中的各个点映射到您的自己的事件类。每个模型事件类都应该期望通过其构造函数接收受影响的模型实例。

namespace App\Models;

use Matchory\Elasticsearch\Model;
use App\Events\UserDeleted;
use App\Events\UserSaved;

class Post extends Model
{
    /**
     * The event map for the model.
     *
     * @var array
     */
    protected $dispatchesEvents = [
        'saved' => PostSaved::class,
        'deleted' => PostDeleted::class,
    ];
}

定义和映射事件后,您可以使用事件监听器来处理事件。

使用闭包

除了使用自定义事件类之外,您还可以注册在分发各种模型事件时执行的闭包。通常,您应在模型的booted方法中注册这些闭包。

namespace App\Models;

use Matchory\Elasticsearch\Model;

class Post extends Model
{
    /**
     * The "booted" method of the model.
     *
     * @return void
     */
    protected static function booted(): void
    {
        static::created(function ($post) {
            //
        });
    }
}

如果需要,在注册模型事件时可以使用可排队匿名事件监听器。这将指示Laravel在后台使用您的应用程序的队列来执行模型事件监听器。

use function Illuminate\Events\queueable;

static::created(queueable(function ($post): void {
    //
}));
访问器 & 修改器
定义访问器

要定义访问器,请在您的模型上创建一个getFooAttribute方法,其中Foo是您希望访问的字段的“大写化”名称。在这个例子中,我们将定义一个对title属性的访问器。访问器将被模型自动调用,当尝试检索title属性的值时。


namespace App;

use Matchory\Elasticsearch\Model;

class post extends Model
{
    /**
     * Get the post title.
     *
     * @param  string  $value
     * @return string
     */
    public function getTitleAttribute(string $value): string
    {
        return ucfirst($value);
    }
}

如您所见,字段的原始值传递给访问器,允许您操纵并返回值。要访问访问器的值,您可以直接访问模型实例上的title属性。

$post = App\Post::find(1);

$title = $post->title;

偶尔,您可能需要添加没有在索引中对应字段的数组属性。为此,只需为该值定义一个访问器。

public function getIsPublishedAttribute(): bool
{
    return $this->attributes['status'] === 1;
}

一旦创建了访问器,只需将其添加到模型的appends属性上。

protected $appends = ['is_published'];

一旦将属性添加到appends列表中,它就会包含在模型的数组中。

定义修改器

要定义修改器,请定义一个setFooAttribute方法,其中Foo是您希望访问的字段的“大写化”名称。所以,让我们再次为title属性定义一个修改器。当我们尝试在模型上设置title属性的值时,这个修改器将被自动调用。

namespace App;

use Matchory\Elasticsearch\Model;

class post extends Model
{
    /**
     * Set the post title.
     *
     * @param  string  $value
     * @return string
     */
    public function setTitleAttribute(string $value): string
    {
        return strtolower($value);
    }
}

修改器将接收设置的属性值,允许您操作该值并将操作后的值设置在模型内部的 $attributes 属性上。例如,如果我们尝试将 title 属性设置为 Awesome post to read

$post = App\Post::find(1);

$post->title = 'Awesome post to read';

在这个例子中,setTitleAttribute 函数将使用值 Awesome post to read 被调用。修改器随后将对名称应用 strtolower 函数,并将结果值设置在内部 $attributes 数组中。

静音事件

有时您可能需要临时“静音”模型触发的所有事件。您可以使用 withoutEvents 方法实现此目的。withoutEvents 方法接受一个闭包作为其唯一参数。在这个闭包内执行的任何代码都不会触发模型事件。例如,以下示例将获取并删除一个 App\Models\Post 实例而不会触发任何模型事件。闭包返回的任何值都将由 withoutEvents 方法返回。

use App\Models\Post;

$post = Post::withoutEvents(function () use () {
Post::findOrFail(1)->delete();

    return Post::find(2);
});

不触发事件保存单个模型

有时您可能希望“保存”一个给定的模型而不触发任何事件。您可以使用 saveQuietly 方法完成此操作。

$post = Post::findOrFail(1);

$post->title = 'Other search strategies';

$post->saveQuietly();

复制模型

您可以使用 replicate 方法创建现有模型实例的非保存副本。当您有许多模型实例共享许多相同属性时,此方法尤其有用。

use App\Models\Address;

$shipping = Address::create([
    'type' => 'shipping',
    'line_1' => '123 Example Street',
    'city' => 'Victorville',
    'state' => 'CA',
    'postcode' => '90001',
]);

$billing = $shipping->replicate()->fill([
    'type' => 'billing'
]);

$billing->save();

修改器和类型转换

访问器、修改器和属性转换允许您在获取或设置模型实例上的 Elasticsearch 属性值时转换属性值。例如,您可能想使用 Laravel 加密器 在索引中存储值时对其进行加密,并在访问 Elasticsearch 模型上的属性时自动解密属性。或者,您可能想将存储在您的索引中的 JSON 字符串转换为数组,当它通过您的 Elasticsearch 模型访问时。

访问器 & 修改器

定义访问器

访问器将访问时的 Elasticsearch 属性值进行转换。要定义一个访问器,在您的模型上创建一个 get{Attribute}Attribute 方法,其中 {Attribute} 是您要访问的字段的“骆驼”命名。

在这个例子中,我们将为 first_name 属性定义一个访问器。Elasticsearch 在尝试获取 first_name 属性的值时将自动调用该访问器。

namespace App\Models;

use Matchory\Elasticsearch\Model;

class User extends Model
{
    /**
     * Get the user's first name.
     *
     * @param  string  $value
     * @return string
     */
    public function getFirstNameAttribute(string $value): string
    {
        return ucfirst($value);
    }
}

如你所见,字段的原始值被传递给访问器,允许你操作并返回值。要访问访问器的值,你只需在模型实例上访问 first_name 属性。

use App\Models\User;

$user = User::find(1);

$firstName = $user->first_name;

你不仅限于在你的访问器中与单个属性交互。你也可以使用访问器从现有属性返回新的、计算值。

/**
 * Get the user's full name.
 *
 * @return string
 */
public function getFullNameAttribute(): string
{
    return "{$this->first_name} {$this->last_name}";
}
定义修改器

修改器在设置时转换 Elasticsearch 属性值。要定义一个修改器,在您的模型上定义一个 set{Attribute}Attribute 方法,其中 {Attribute} 是您要访问的字段的“骆驼”命名。

让我们为 first_name 属性定义一个修改器。当尝试在模型上设置 first_name 属性的值时,此修改器将被自动调用。

namespace App\Models;

use Matchory\Elasticsearch\Model;

class User extends Model
{
    /**
     * Set the user's first name.
     *
     * @param  string  $value
     * @return void
     */
    public function setFirstNameAttribute(string $value): void
    {
        $this->attributes['first_name'] = strtolower($value);
    }
}

修改器将接收设置的属性值,允许您操作该值并在 Elasticsearch 模型的内部 $attributes 属性上设置操作后的值。要使用我们的修改器,我们只需在 Elasticsearch 模型上设置 first_name 属性。

use App\Models\User;

$user = User::find(1);

$user->first_name = 'Sally';

在这个例子中,将调用 setFirstNameAttribute 函数,并传递值 Sally。修改器将随后对名称应用 strtolower 函数,并将结果值设置在内部 $attributes 数组中。

属性转换

属性转换提供了与访问器和修改器类似的功能,而无需在您的模型上定义任何额外的函数。相反,您的模型 $casts 属性提供了一个方便的方法来将属性转换为常见的数据类型。

属性 $casts 应该是一个数组,其中键是要转换的属性名,值是要将该字段转换到的类型。支持的转换类型有

  • 数组
  • 布尔值
  • 集合
  • 日期
  • 日期时间
  • 十进制:
  • 双精度浮点数
  • 加密
  • 加密:数组
  • 加密:集合
  • 加密:对象
  • 浮点数
  • 整数
  • 对象
  • 实数
  • 字符串
  • 时间戳

为了演示属性转换,让我们将 is_admin 属性转换,它在我们的索引中以整数(01)的形式存储,转换为布尔值

namespace App\Models;

use Matchory\Elasticsearch\Model;

class User extends Model
{
    /**
     * The attributes that should be cast.
     *
     * @var array
     */
    protected $casts = [
        'is_admin' => 'boolean',
    ];
}

定义转换后,当您访问 is_admin 属性时,它将始终转换为布尔值,即使其基础值在索引中以整数形式存储

$user = App\Models\User::find(1);

if ($user->is_admin) {
    //
}

注意:值为 null 的属性不会进行转换。

日期转换

您可以通过在模型中定义 $cast 属性数组来转换日期属性。通常,日期应使用 datetime 转换进行转换。

在定义 datedatetime 转换时,您还可以指定日期的格式。该格式将在模型序列化为数组或 JSON 时使用

/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'created_at' => 'datetime:Y-m-d',
];

当字段被转换为日期时,您可以将其值设置为 UNIX 时间戳、日期字符串(Y-m-d)、日期时间字符串或 DateTime / Carbon 实例。日期的值将被正确转换并存储在您的索引中

您可以通过在模型上定义 serializeDate 方法来自定义模型的所有日期的默认序列化格式。此方法不会影响您的日期在索引中的存储格式

/**
 * Prepare a date for array / JSON serialization.
 *
 * @param  \DateTimeInterface  $date
 * @return string
 */
protected function serializeDate(DateTimeInterface $date)
{
    return $date->format('Y-m-d');
}

为了指定在索引中实际存储模型日期时应使用的格式,您应该在模型上定义 $dateFormat 属性

/**
 * The storage format of the model's date fields.
 *
 * @var string
 */
protected $dateFormat = 'U';

自定义转换

Laravel 有多种内置的、有用的转换类型;然而,您有时可能需要定义自己的转换类型。您可以通过定义一个实现了 CastsAttributes 接口的类来实现这一点。

实现此接口的类必须定义一个 getset 方法。get 方法负责将索引中的原始值转换为转换值,而 set 方法应该将转换值转换为可以存储在索引中的原始值。以下是一个示例,我们将重新实现内置的 json 转换类型作为自定义转换类型

注意:由于类型不兼容,您将需要为 Eloquent 和 Elasticsearch 模型使用不同的转换,或者省略参数类型。

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Json implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Matchory\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return array
     */
    public function get($model, $key, $value, $attributes)
    {
        return json_decode($value, true);
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Matchory\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  array  $value
     * @param  array  $attributes
     * @return string
     */
    public function set($model, $key, $value, $attributes)
    {
        return json_encode($value);
    }
}

一旦您定义了自定义转换类型,您就可以使用其类名将其附加到模型的属性上

    namespace App\Models;

    use App\Casts\Json;
    use Matchory\Elasticsearch\Model;

    class User extends Model
    {
        /**
         * The attributes that should be cast.
         *
         * @var array
         */
        protected $casts = [
            'options' => Json::class,
        ];
    }
值对象转换

您不仅可以将值转换为原始类型。您还可以将值转换为对象。定义将值转换为对象的自定义转换与转换为原始类型非常相似;然而,set 方法应返回一个键/值对数组,该数组将用于在模型上设置原始的、可存储的值。

以下是一个示例,我们将定义一个自定义转换类,该类将多个模型值转换为单个 Address 值对象。我们假设 Address 值有两个公共属性:lineOnelineTwo

namespace App\Casts;

use App\Models\Address as AddressModel;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use InvalidArgumentException;

class Address implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Matchory\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  mixed  $value
     * @param  array  $attributes
     * @return \App\Models\Address
     */
    public function get($model, $key, $value, $attributes)
    {
        return new AddressModel(
            $attributes['address_line_one'],
            $attributes['address_line_two']
        );
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Matchory\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  \App\Models\Address  $value
     * @param  array  $attributes
     * @return array
     */
    public function set($model, $key, $value, $attributes)
    {
        if (! $value instanceof AddressModel) {
            throw new InvalidArgumentException('The given value is not an Address instance.');
        }

        return [
            'address_line_one' => $value->lineOne,
            'address_line_two' => $value->lineTwo,
        ];
    }
}

在转换到值对象时,对值对象所做的任何更改都将自动在模型保存之前同步回模型

use App\Models\User;

$user = User::find(1);

$user->address->lineOne = 'Updated Address Value';

$user->save();

提示:如果您计划将包含值对象的 Elasticsearch 模型序列化为 JSON 或数组,则应在值对象上实现 Illuminate\Contracts\Support\ArrayableJsonSerializable 接口。

数组/JSON序列化

当使用 toArraytoJson 方法将 Elasticsearch 模型转换为数组或 JSON 时,只要自定义的值对象实现了 Illuminate\Contracts\Support\ArrayableJsonSerializable 接口,通常也会进行序列化。然而,当使用第三方库提供的值对象时,可能无法将这两个接口添加到对象中。

因此,您可以指定自定义转换类负责序列化值对象。为此,您的自定义类转换应该实现 Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes 接口。该接口声明您的类应该包含一个 serialize 方法,该方法应返回值对象的序列化形式。

/**
 * Get the serialized representation of the value.
 *
 * @param  \Illuminate\Database\Eloquent\Model|\Matchory\Elasticsearch\Model  $model
 * @param  string  $key
 * @param  mixed  $value
 * @param  array  $attributes
 * @return mixed
 */
public function serialize($model, string $key, $value, array $attributes)
{
    return (string) $value;
}
传入转换

有时,您可能需要编写一个自定义转换,它只转换在模型上设置的值,而不会在从模型检索属性时执行任何操作。一个只进行传入转换的典型示例是“哈希”转换。只进行传入的自定义转换应实现 CastsInboundAttributes 接口,该接口只需要定义一个 set 方法。

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;

class Hash implements CastsInboundAttributes
{
    /**
     * The hashing algorithm.
     *
     * @var string
     */
    protected $algorithm;

    /**
     * Create a new cast class instance.
     *
     * @param  string|null  $algorithm
     * @return void
     */
    public function __construct($algorithm = null)
    {
        $this->algorithm = $algorithm;
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model|\Matchory\Elasticsearch\Model  $model
     * @param  string  $key
     * @param  array  $value
     * @param  array  $attributes
     * @return string
     */
    public function set($model, $key, $value, $attributes)
    {
        return is_null($this->algorithm)
                    ? bcrypt($value)
                    : hash($this->algorithm, $value);
    }
}
转换参数

当将自定义转换附加到模型时,可以通过使用冒号字符将它们从类名分隔开,并使用逗号分隔多个参数来指定转换参数。这些参数将传递到转换类的构造函数。

/**
 * The attributes that should be cast.
 *
 * @var array
 */
protected $casts = [
    'secret' => Hash::class.':sha256',
];
可转换对象

您可能希望允许应用程序的值对象定义它们自己的自定义转换类。您可以选择不将自定义转换类附加到模型,而是将实现 Illuminate\Contracts\Database\Eloquent\Castable 接口的价值对象类附加到模型。

use App\Models\Address;

protected $casts = [
    'address' => Address::class,
];

实现 Castable 接口的对象必须定义一个 castUsing 方法,该方法返回负责将数据转换到和从 Castable 类的自定义转换类。

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\Address as AddressCast;

class Address implements Castable
{
    /**
     * Get the name of the caster class to use when casting from / to this cast target.
     *
     * @param  array  $arguments
     * @return string
     */
    public static function castUsing(array $arguments): string
    {
        return AddressCast::class;
    }
}

当使用 Castable 类时,您仍然可以在 $casts 定义中提供参数。这些参数将传递给 castUsing 方法。

use App\Models\Address;

protected $casts = [
    'address' => Address::class.':argument',
];
可转换对象与匿名转换类

通过结合“可转换对象”和 PHP 的 匿名类,您可以定义一个值对象及其转换逻辑作为单个可转换对象。为此,从值对象的 castUsing 方法返回一个匿名类。该匿名类应该实现 CastsAttributes 接口。

namespace App\Models;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class Address implements Castable
{
    // ...

    /**
     * Get the caster class to use when casting from / to this cast target.
     *
     * @param  array  $arguments
     * @return object|string
     */
    public static function castUsing(array $arguments)
    {
        return new class implements CastsAttributes
        {
            public function get($model, $key, $value, $attributes)
            {
                return new Address(
                    $attributes['address_line_one'],
                    $attributes['address_line_two']
                );
            }

            public function set($model, $key, $value, $attributes)
            {
                return [
                    'address_line_one' => $value->lineOne,
                    'address_line_two' => $value->lineTwo,
                ];
            }
        };
    }
}

路由模型绑定

当向路由或控制器操作注入模型 ID 时,您通常会查询 Elasticsearch 索引以检索与该 ID 对应的模型。Laravel 路由模型绑定提供了一种方便的方法,可以自动将模型实例直接注入到路由中。例如,您可以用整个匹配给定 ID 的 User 模型实例替换用户 ID。

隐式绑定

Laravel 会自动解析在路由或控制器操作中定义的 Elasticsearch 模型,这些模型的类型提示变量名与路由段名称匹配。例如

use App\Models\Post;

Route::get('/posts/{post}', function (Post $post) {
    return $post->content;
});

由于 $post 变量被类型提示为 App\Models\Post Elasticsearch 模型,并且变量名与 {post} URI 段匹配,Laravel 将自动注入具有与请求 URI 中相应值匹配的 ID 的模型实例。如果数据库中没有找到匹配的模型实例,将自动生成 404 HTTP 响应。

当然,在控制器方法中也可以实现隐式绑定。请注意,{post} URI 段与控制器中的 $post 变量匹配,该变量包含一个 App\Models\Post 类型提示。

use App\Http\Controllers\PostController;
use App\Models\Post;

// Route definition...
Route::get('/posts/{post}', [PostController::class, 'show']);

// Controller method definition...
public function show(Post $post): View
{
    return view('post.full', ['post' => $post]);
}

自定义键

有时您可能希望使用除 _id 字段之外的字段来解决 Elasticsearch 模型。为了实现这一点,您可以在路由参数定义中指定该字段。

use App\Models\Post;

Route::get('/posts/{post:slug}', fn(Post $post): Post => $post);

如果您希望模型绑定在检索给定的模型类时始终使用除 _id 之外的索引字段,您可以在 Elasticsearch 模型上重写 getRouteKeyName 方法。

/**
 * Get the route key for the model.
 *
 * @return string
 */
public function getRouteKeyName(): string
{
    return 'slug';
}

自定义缺失模型行为

通常,如果未找到隐式绑定的模型,将生成 404 HTTP 响应。但是,您可以通过在定义路由时调用缺失方法来自定义此行为。缺失方法接受一个闭包,该闭包将在无法找到隐式绑定的模型时被调用。

use App\Http\Controllers\LocationsController;
use Illuminate\Http\Request;

Route::get('/locations/{location:slug}', [LocationsController::class, 'show'])
    ->missing(fn(Request $request) => Redirect::route('locations.index')
    ->name('locations.view');

显式绑定

您不需要使用 Laravel 的隐式、基于约定的模型解析来使用模型绑定。您还可以显式定义路由参数与模型之间的对应关系。要注册显式绑定,请使用路由器的模型方法为给定参数指定类。您应该在 RouteServiceProvider 类的 boot 方法的开头定义您的显式模型绑定。

use App\Models\Post;
use Illuminate\Support\Facades\Route;

/**
 * Define your route model bindings, pattern filters, etc.
 *
 * @return void
 */
public function boot():void
{
    Route::model('post', Post::class);

    // ...
}

接下来,定义一个包含 {post} 参数的路由。

use App\Models\Post;

Route::get('/posts/{post}', function (Post $post) {
    // ...
});

由于我们已经将所有 {post} 参数绑定到 App\Models\Post 模型,因此将注入该类的实例。例如,对 posts/1 的请求将注入具有 ID 为 1 的索引中的 Post 实例。

如果在索引中找不到匹配的模型实例,将自动生成 404 HTTP 响应。

自定义解析逻辑

如果您想定义自己的模型绑定解析逻辑,可以使用 Route::bind 方法。您传递给 bind 方法的闭包将接收 URI 段的值,并应返回应注入到路由中的类的实例。同样,这种自定义应发生在应用程序的 RouteServiceProviderboot 方法中。

use App\Models\Post;
use Illuminate\Support\Facades\Route;

/**
 * Define your route model bindings, pattern filters, etc.
 *
 * @return void
 */
public function boot(): void
{
    Route::bind('post', function (string $value): Post {
        return Post::where('title', $value)->firstOrFail();
    });

    // ...
}

或者,您可以在 Elasticsearch 模型上重写 resolveRouteBinding 方法。此方法将接收 URI 段的值,并应返回应注入到路由中的类的实例。

/**
 * Retrieve the model for a bound value.
 *
 * @param  mixed  $value
 * @param  string|null  $field
 * @return \Matchory\Elasticsearch\Model|null
 */
public function resolveRouteBinding($value, ?string $field = null): ?self
{
    return $this->where('name', $value)->firstOrFail();
}

如果路由使用隐式绑定作用域,将使用 resolveChildRouteBinding 方法来解决父模型的子绑定。

/**
 * Retrieve the child model for a bound value.
 *
 * @param  string  $childType
 * @param  mixed  $value
 * @param  string|null  $field
 * @return \Matchory\Elasticsearch\Model|null
 */
public function resolveChildRouteBinding(string $childType, $value, ?string $field): ?self
{
    return parent::resolveChildRouteBinding($childType, $value, $field);
}

作为查询构建器的使用

您可以使用 ES 门面从应用程序的任何位置直接访问查询构建器。

创建新的索引

ES::create('my_index');
    
# or 
    
ES::index('my_index')->create();

使用自定义选项创建索引(可选)

use Matchory\Elasticsearch\Facades\ES;
use Matchory\Elasticsearch\Index;

ES::index('my_index')->create(function(Index $index) {
    $index->shards(5)->replicas(1)->mapping([
        'my_type' => [
            'properties' => [
                'first_name' => [
                    'type' => 'string',
                ],
                'age' => [
                    'type' => 'integer'
                ]
            ]
        ]
    ])
});
    
# or
    
ES::create('my_index', function(Index $index){
  
      $index->shards(5)->replicas(1)->mapping([
          'my_type' => [
              'properties' => [
                  'first_name' => [
                      'type' => 'string',
                  ],
                  'age' => [
                      'type' => 'integer'
                  ]
              ]
          ]
      ])
});

删除索引

ES::drop("my_index");
    
# or

ES::index("my_index")->drop();

运行查询

要运行查询,首先(可选)选择连接和索引。

$documents = ES::connection("default")
                ->index("my_index")
                ->type("my_type")
                ->get();    # return a collection of results

您可以缩短上述查询为:

$documents = ES::type("my_type")->get();    # return a collection of results

在查询中显式设置连接或索引名称将覆盖 config/es.php 中的配置。

通过 ID 获取文档

ES::type("my_type")->id(3)->first();
    
# or
    
ES::type("my_type")->_id(3)->first();

排序

ES::type("my_type")->orderBy("created_at", "desc")->get();
    
# Sorting with text search score
    
ES::type("my_type")->orderBy("_score")->get();

限制和偏移量

ES::type("my_type")->take(10)->skip(5)->get();

仅选择特定字段

ES::type("my_type")->select("title", "content")->take(10)->skip(5)->get();

WHERE 子句

ES::type("my_type")->where("status", "published")->get();

# or

ES::type("my_type")->where("status", "=", "published")->get();

WHERE 大于

ES::type("my_type")->where("views", ">", 150)->get();

WHERE 大于等于

ES::type("my_type")->where("views", ">=", 150)->get();

WHERE 小于

ES::type("my_type")->where("views", "<", 150)->get();

WHERE 小于等于

ES::type("my_type")->where("views", "<=", 150)->get();

WHERE LIKE

ES::type("my_type")->where("title", "like", "foo")->get();

WHERE 字段存在

ES::type("my_type")->where("hobbies", "exists", true)->get(); 

# or 

ES::type("my_type")->whereExists("hobbies", true)->get();

WHERE IN 子句

ES::type("my_type")->whereIn("id", [100, 150])->get();

WHERE BETWEEN 子句

ES::type("my_type")->whereBetween("id", 100, 150)->get();

# or 

ES::type("my_type")->whereBetween("id", [100, 150])->get();

WHERE NOT 子句

ES::type("my_type")->whereNot("status", "published")->get(); 

# or

ES::type("my_type")->whereNot("status", "=", "published")->get();

WHERE NOT 大于

ES::type("my_type")->whereNot("views", ">", 150)->get();

WHERE NOT 大于等于

ES::type("my_type")->whereNot("views", ">=", 150)->get();

WHERE NOT 小于

ES::type("my_type")->whereNot("views", "<", 150)->get();

WHERE NOT 小于等于

ES::type("my_type")->whereNot("views", "<=", 150)->get();

WHERE NOT LIKE

ES::type("my_type")->whereNot("title", "like", "foo")->get();

WHERE NOT 字段存在

ES::type("my_type")->whereNot("hobbies", "exists", true)->get(); 

# or

ES::type("my_type")->whereExists("hobbies", true)->get();

WHERE NOT IN 子句

ES::type("my_type")->whereNotIn("id", [100, 150])->get();

WHERE NOT BETWEEN 子句

ES::type("my_type")->whereNotBetween("id", 100, 150)->get();

# or

ES::type("my_type")->whereNotBetween("id", [100, 150])->get();

从地理点搜索距离

ES::type("my_type")->distance("location", ["lat" => -33.8688197, "lon" => 151.20929550000005], "10km")->get();

# or

ES::type("my_type")->distance("location", "-33.8688197,151.20929550000005", "10km")->get();

# or

ES::type("my_type")->distance("location", [151.20929550000005, -33.8688197], "10km")->get();  

使用数组查询搜索

ES::type("my_type")->body([
    "query" => [
         "bool" => [
             "must" => [
                 [ "match" => [ "address" => "mill" ] ],
                 [ "match" => [ "address" => "lane" ] ]
             ]
         ]
     ]
])->get();

# Note that you can mix between query builder and array queries.
# The query builder will will be merged with the array query.

ES::type("my_type")->body([
	"_source" => ["content"]
	
	"query" => [
	     "bool" => [
	         "must" => [
	             [ "match" => [ "address" => "mill" ] ]
	         ]
	     ]
	],
	   
	"sort" => [
		"_score"
	]
     
])->select("name")->orderBy("created_at", "desc")->take(10)->skip(5)->get();

# The result query will be
/*
Array
(
    [index] => my_index
    [type] => my_type
    [body] => Array
        (
            [_source] => Array
                (
                    [0] => content
                    [1] => name
                )
            [query] => Array
                (
                    [bool] => Array
                        (
                            [must] => Array
                                (
                                    [0] => Array
                                        (
                                            [match] => Array
                                                (
                                                    [address] => mill
                                                )
                                        )
                                )
                        )
                )
            [sort] => Array
                (
                    [0] => _score
                    [1] => Array
                        (
                            [created_at] => desc
                        )
                )
        )
    [from] => 5
    [size] => 10
    [client] => Array
        (
            [ignore] => Array
                (
                )
        )
)
*/

搜索整个文档

ES::type("my_type")->search("hello")->get();
    
# search with Boost = 2
    
ES::type("my_type")->search("hello", 2)->get();

# search within specific fields with different weights

ES::type("my_type")->search("hello", function($search){
	$search->boost(2)->fields(["title" => 2, "content" => 1])
})->get();

使用高亮字段搜索

$doc = ES::type("my_type")->highlight("title")->search("hello")->first();

# Multiple fields Highlighting is allowed.

$doc = ES::type("my_type")->highlight("title", "content")->search("hello")->first();

# Return all highlights as array using $doc->getHighlights() method.

$doc->getHighlights();

# Also you can return only highlights of specific field.

$doc->getHighlights("title");

仅返回第一个文档

ES::type("my_type")->search("hello")->first();

仅返回计数

ES::type("my_type")->search("hello")->count();

Scan-and-Scroll 查询

# These queries are suitable for large amount of data. 
# A scrolled search allows you to do an initial search and to keep pulling batches of results
# from Elasticsearch until there are no more results left.
# It’s a bit like a cursor in a traditional database
    
$documents = ES::type("my_type")->search("hello")
                 ->scroll("2m")
                 ->take(1000)
                 ->get();

# Response will contain a hashed code `scroll_id` will be used to get the next result by running

$documents = ES::type("my_type")->search("hello")
                 ->scroll("2m")
                 ->scrollID("DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAFMFlJQOEtTdnJIUklhcU1FX2VqS0EwZncAAAAAAAABSxZSUDhLU3ZySFJJYXFNRV9laktBMGZ3AAAAAAAAAU4WUlA4S1N2ckhSSWFxTUVfZWpLQTBmdwAAAAAAAAFPFlJQOEtTdnJIUklhcU1FX2VqS0EwZncAAAAAAAABTRZSUDhLU3ZySFJJYXFNRV9laktBMGZ3")
                 ->get();

# And so on ...
# Note that you don't need to write the query parameters in every scroll. All you need the `scroll_id` and query scroll time.
    
# To clear `scroll_id` 
  
ES::type("my_type")->scrollID("DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAFMFlJQOEtTdnJIUklhcU1FX2VqS0EwZncAAAAAAAABSxZSUDhLU3ZySFJJYXFNRV9laktBMGZ3AAAAAAAAAU4WUlA4S1N2ckhSSWFxTUVfZWpLQTBmdwAAAAAAAAFPFlJQOEtTdnJIUklhcU1FX2VqS0EwZncAAAAAAAABTRZSUDhLU3ZySFJJYXFNRV9laktBMGZ3")
        ->clear();

每页 5 个文档进行分页

$documents = ES::type("my_type")->search("hello")->paginate(5);
    
# Getting pagination links
    
$documents->links();

# Bootstrap 4 pagination

$documents->links("bootstrap-4");

# Simple bootstrap 4 pagination

$documents->links("simple-bootstrap-4");

# Simple pagination

$documents->links("simple-default");

这些都是您可以使用的方法进行分页。

$documents->count()
$documents->currentPage()
$documents->firstItem()
$documents->hasMorePages()
$documents->lastItem()
$documents->lastPage()
$documents->nextPageUrl()
$documents->perPage()
$documents->previousPageUrl()
$documents->total()
$documents->url($page)

获取未执行的查询数组

ES::type("my_type")->search("hello")->where("views", ">", 150)->query();

获取原始 Elasticsearch 响应

ES::type("my_type")->search("hello")->where("views", ">", 150)->response();

忽略不良 HTTP 响应

ES::type("my_type")->ignore(404, 500)->id(5)->first();

查询缓存(Laravel & Lumen)

该软件包自带基于Laravel缓存的内置缓存层。

ES::type("my_type")->search("hello")->remember(10)->get();

# Specify a custom cache key
ES::type("my_type")->search("hello")->remember(10, "last_documents")->get();

# Caching using other available driver
ES::type("my_type")->search("hello")->cacheDriver("redis")->remember(10, "last_documents")->get();

# Caching with cache key prefix
ES::type("my_type")->search("hello")->cacheDriver("redis")->cachePrefix("docs")->remember(10, "last_documents")->get();

执行Elasticsearch原生查询

ES::raw()->search([
    "index" => "my_index",
    "type"  => "my_type",
    "body" => [
        "query" => [
            "bool" => [
                "must" => [
                    [ "match" => [ "address" => "mill" ] ],
                    [ "match" => [ "address" => "lane" ] ]
                ]
            ]
        ]
    ]
]);

插入新文档

ES::type("my_type")->id(3)->insert([
    "title" => "Test document",
    "content" => "Sample content"
]);
     
# A new document will be inserted with _id = 3.
# [id is optional] if not specified, a unique hash key will be generated.

一次性批量插入多个文档。

# Main query
ES::index("my_index")->type("my_type")->bulk(function ($bulk){

    # Sub queries
	$bulk->index("my_index_1")->type("my_type_1")->id(10)->insert(["title" => "Test document 1","content" => "Sample content 1"]);
	$bulk->index("my_index_2")->id(11)->insert(["title" => "Test document 2","content" => "Sample content 2"]);
	$bulk->id(12)->insert(["title" => "Test document 3", "content" => "Sample content 3"]);
	
});

# Notes from the above query:

# As index and type names are required for insertion, Index and type names are extendable. This means that: 

# If index() is not specified in subquery:
# -- The builder will get index name from the main query.
# -- if index is not specified in main query, the builder will get index name from configuration file.

# And

# If type() is not specified in subquery:
# -- The builder will get type name from the main query.
# you can use old bulk code style using multidimensional array of [id => data] pairs
 
ES::type("my_type")->bulk([
 
	10 => [
		"title" => "Test document 1",
		"content" => "Sample content 1"
	],

	11 => [
		"title" => "Test document 2",
		"content" => "Sample content 2"
	]
 
]);
 
# The two given documents will be inserted with its associated ids

更新现有文档

ES::type("my_type")->id(3)->update([
   "title" => "Test document",
   "content" => "sample content"
]);
    
# Document has _id = 3 will be updated.
    
# [id is required]
# Bulk update

ES::type("my_type")->bulk(function ($bulk){
    $bulk->id(10)->update(["title" => "Test document 1","content" => "Sample content 1"]);
    $bulk->id(11)->update(["title" => "Test document 2","content" => "Sample content 2"]);
});

增加字段

ES::type("my_type")->id(3)->increment("views");
    
# Document has _id = 3 will be incremented by 1.

ES::type("my_type")->id(3)->increment("views", 3);

# Document has _id = 3 will be incremented by 3.

# [id is required]

减少字段

ES::type("my_type")->id(3)->decrement("views");
    
# Document has _id = 3 will be decremented by 1.
    
ES::type("my_type")->id(3)->decrement("views", 3);
    
# Document has _id = 3 will be decremented by 3.

# [id is required]

使用脚本更新

# increment field by script
ES::type("my_type")->id(3)->script(
    "ctx._source.$field += params.count",
    ["count" => 1]
);
    
# add php tag to tags array list
ES::type("my_type")->id(3)->script(
    "ctx._source.tags.add(params.tag)",
    ["tag" => "php"]
);
    
# delete the doc if the tags field contain mongodb, otherwise it does nothing (noop)
ES::type("my_type")->id(3)->script(
    "if (ctx._source.tags.contains(params.tag)) { ctx.op = 'delete' } else { ctx.op = 'none' }",
    ["tag" => "mongodb"]
);

删除文档

ES::type("my_type")->id(3)->delete();

# Document has _id = 3 will be deleted.
# [id is required]
# Bulk delete
ES::type("my_type")->bulk(function ($bulk){
    $bulk->id(10)->delete();
    $bulk->id(11)->delete();
});

版本

查看发布页面

作者

Basem Khirat - basemkhirat@gmail.com - @basemkhirat
Moritz Friedrich - moritz@matchory.com

错误、建议和贡献

感谢所有为原始项目做出贡献的,以及为这个分支做出贡献的其他人
请使用GitHub来报告错误、发表评论或提出建议。

如果您有兴趣帮忙,最紧迫的问题是对查询构建器进行现代化,以提供对Elasticsearch功能的更好支持,以及完成测试套件!

许可

MIT

祝您搜索愉快。