alirzaj/laravel-elasticsearch-builder

elasticsearch数据库的查询构建器

V4.2.2 2023-12-10 15:01 UTC

README

Latest Version on Packagist Total Downloads

此包可以为elasticsearch数据库构建查询,并提供了一种简单的方式来将Eloquent模型添加到elasticsearch索引中。

它并不试图提供类似于Eloquent的API来与elasticsearch交互。相反,它将提供与在elasticsearch中编写查询类似的方法,但以一种更面向对象和更优雅的方式。

此包包含我在项目中需要的查询。所以如果你觉得需要某种方法或功能,请提出问题,我将尽快实现。

安装

要安装此包,请通过composer要求

composer require alirzaj/laravel-elasticsearch-builder

使用

定义索引

首先,您需要定义索引类。索引类必须扩展 Alirzaj\ElasticsearchBuilder\Index 类。以下是一个示例

<?php

namespace Alirzaj\ElasticsearchBuilder\Tests\Indices;

use Alirzaj\ElasticsearchBuilder\Index;

class Users extends Index
{
    public string $name = 'users_index';

    public array $propertyTypes = [
        'text' => 'text',
        'user_id' => 'keyword',
        'ip' => 'ip',
        'created_at' => 'date',
    ];

    public array $fields = [
        'text' => [
            'hashtags' => [
                'type' => 'text',
                'analyzer' => 'hashtag',
            ],
        ],
    ];
    
    public array $dateFormats = [
        'created_at' => 'strict_date_optional_time||strict_date_optional_time_nanos||yyyy-MM-dd HH:mm:ss',
    ];

    public array $analyzers = [
        'hashtag' => [
            'type' => 'custom',
            'tokenizer' => 'hashtag_tokenizer',
            'filter' => ['lowercase'],
        ],
        'hashtag_2' => [
            'type' => 'custom',
            'tokenizer' => 'hashtag_tokenizer',
            'filter' => ['lowercase'],
        ],
    ];

    public array $tokenizers = [
        'hashtag_tokenizer' => [
            'type' => 'pattern',
            'pattern' => '#\S+',
            'group' => 0,
        ],
    ];

    public array $propertyAnalyzers = [
        'text' => 'hashtag',
    ];

    public array $searchAnalyzers = [
        'text' => 'hashtag_2',
    ];

    public array $normalizers = [
        'my_normalizer' => [
            'type' => 'custom',
            'char_filter' => ['special_character_strip'],
            'filter' => ['lowercase',]
        ],
    ];

    public array $propertyNormalizers = [
        'ip' => 'my_normalizer'
    ];

    public array $characterFilters = [
        'special_character_strip' => [
            'type' => 'pattern_replace',
            'pattern' => '[._]',
        ],
    ];

    public array $tokenFilters = [
        '4_7_edge_ngram' => [
            'min_gram' => '4',
            'max_gram' => '7',
            'type' => 'edge_ngram',
            'preserve_original' => 'true',
        ],
    ];
    
    public array $staticIndexSettings = [
        'number_of_shards' => 1,
    ];

    public array $dynamicIndexSettings = [
        'number_of_replicas' => 1,
    ];
}

如果您没有定义 $name 索引名称将与类名称相同。

$propertyTypes 数组是属性名称及其数据类型的映射。键是字段名称,值是数据类型。

$fields 是字段的其它定义。例如,您想要保存文本字段的另一个版本以匹配标签(使用另一个分析器)。

$analyzers 是索引的自定义定义分析器。

$tokenizers 包含分词器的配置。要了解 $staticIndexSettings$dynamicIndexSettings,您可以在此处查看: https://elastic.ac.cn/guide/en/elasticsearch/reference/current/index-modules.html

您必须定义的唯一属性是 $properties

如果您不知道其他属性的功能,请参阅每个属性的文档注释来了解更多信息。

创建索引

要创建前一步定义的索引,运行 php artisan elastic:create-indices

删除索引

要删除通过此包定义的所有索引,运行 php artisan elastic:delete-indices

配置

使用 php artisan vendor:publish --provider="Alirzaj\\ElasticsearchBuilder\\ElasticsearchBuilderServiceProvider" 命令发布包的配置文件。所有选项都有文件中的描述。

使模型可搜索

您可以在 Eloquent 模型中使用 Alirzaj\ElasticsearchBuilder\Searchable 特性。此特性将自动在相应的索引中添加和更新文档。您可以在模型上重写 toIndex 方法来控制将保存到 Elasticsearch 的属性。默认行为是模型的数组表示形式(toArray)。

不使用Eloquent模型和可搜索特性索引文档

在某些情况下,您可能需要在 Eloquent 模型上不使用可搜索特性来索引或更新文档。此包提供了两个用于索引和更新的作业。

IndexDocument::dispatch(
                'name_of_index',
                'id',
                ['name' => 'alirzaj'] //an array that you want indexed in elasticsearch
            );

UpdateDocument::dispatch(
                'name_of_index',
                'id',
               ['name' => 'alirzaj'] //an array that you want to add in your existing elasticsearch document
            );

批量索引文档

BulkIndexDocuments::dispatchSync(
        'blogs',
        [
            ['id' => 1, 'title' => 'abcd'],
            ['id' => 2, 'title' => 'efgh'],
        ]
    );

根据条件更新文档

UpdateDocumentsByCondition::dispatchSync(
        'blogs',
        [
            'condition-field' => 'condition-value',
            'condition-field-2' => null,
        ],
        ['my-field' => 'new-value'],
    );

如果您要更新满足某些条件的 大字段 文档,请务必使用 UpdateDocumentsByCondition 作业的第四个参数。

UpdateDocumentsByCondition::dispatchSync(
        'blogs',
        [
            'condition-field' => 'condition-value',
            'condition-field-2' => null,
        ],
        ['text' => 'large-value'],
        ['text']
    );

向嵌套字段添加项目

AddItemToNestedField::dispatchSync(
        'blogs',
        10,
        'tags',
        ['id' => 20, 'name' => 'php'],
    );

向数组字段添加项目

AddItemToArrayField::dispatchSync(
        'blogs',
        10,
        'tags',
        'php',
    );

根据条件更新文档的嵌套字段项目

UpdateNestedItemByCondition::dispatchSync(
        'blogs',
        10,
        'tags',
        ['id' => 20], // in document, we have a [nested] tags field. now we are looking for the ones with id of 20
        /**
         * we want all of those items having above condition to be updated to this item
         * note that if you have id key in conditions, and id key in document parameter, the values must be the same
         * in other words condition's value must not change in update.
         * in this example we find the tag via id and update its name. we couldn't find it via old name and set a new name
         */
        ['id' => 20, 'name' => 'new-php']
    );

根据条件更新所有文档的嵌套字段项目

 UpdateNestedItemByQuery::dispatchSync(
        'blogs',
        'tags',
        ['id' => 20], // in documents, we have a [nested] tags field. now we are looking for all documents with this criteria
        /**
         * we want all of those items having above condition to be updated to this item
         * note that if you have id key in conditions, and id key in document parameter, the values must be the same
         * in other words condition's value must not change in update.
         * in this example we find the tag via id and update its name. we couldn't find it via old name and set a new name
         */
        ['id' => 20, 'name' => 'new-php']
    );

从特定文档的嵌套字段中删除项目

 /**
     * In tags field, remove all sub-fields with the key of id and value of 20
     */
    RemoveItemFromNestedField::dispatch('blogs', 10, 'tags', 'id', 20);

根据条件从嵌套字段中删除项目

 /**
     * find documents that have id:20 in their tags field and delete id:20 from them
     */
    DeleteNestedFieldByCondition::dispatch(
        'blogs',
        'tags',
        ['id' => 20]
    );

删除文档

 DeleteDocument::dispatchSync('blogs',10);

删除满足某些条件的所有文档

DeleteDocumentsByCondition::dispatchSync(
        'blogs',
        [
            'condition-field' => 'condition-value',
            'condition-field-2' => null,
        ],
    );

查询索引

如果您有可搜索的模型,可以开始像这样查询相应的索引

Model::elasticsearchQuery()

您也可以通过实例化 Query 类来开始查询索引

new \Alirzaj\ElasticsearchBuilder\Query();

将索引包含在查询中

您可以将索引添加到正在查询的索引中

Blog::elasticsearchQuery()->addIndex(Users::class)->addIndex('blogs');

提高某些索引的得分

Blog::elasticsearchQuery()->addIndex('blogs')->addIndex('posts')->boost(['blogs' => 2]);

确定搜索类型

Blog::elasticsearchQuery()->addIndex('blogs')->searchType('dfs_query_then_fetch');

有关更多信息,请访问 https://elastic.ac.cn/guide/en/elasticsearch/reference/current/search-search.html

通过其ID查找文档

Blog::elasticsearchQuery()->find(150);

匹配

您可以使用命名参数来仅传递所需的选项。

Blog::elasticsearchQuery()->match('field', 'value', 'analyzer', 'fuzziness');

匹配全部

Blog::elasticsearchQuery()->matchAll(1.7);

多匹配

Blog::elasticsearchQuery()->multiMatch(['field1', 'field2'], 'value', 'analyzer', 'fuzziness', 'type');

嵌套

Blog::elasticsearchQuery()->nested(
    fn (Query $query) => $query->match('field', 'value'), //query
    'driver.vehicle', //path
    'sum',//score mode
    true //ignore_unmapped
);

存在

Blog::elasticsearchQuery()->exists('title');

布尔

您可以将闭包传递给布尔方法,并指定您想要的查询类型

Blog::elasticsearchQuery()
    ->boolean(
        fn (Must $must) => $must
            ->match('a', 'b')
            ->exists('description'),
        fn (MustNot $mustNot) => $mustNot
            ->match('a', 'b')
            ->exists('description'),
        fn (Filter $filter) => $filter
            ->match('a', 'b')
            ->exists('z'),
        fn (Should $should) => $should
            ->match('a', 'b')
            ->match('z', 'x', analyzer: 'custom-analyzer')
            ->multiMatch(['c', 'd'], 'e')
        fn(BooleanOptions $booleanOptions) => $booleanOptions->minimumShouldMatch(1)
    );

Blog::elasticsearchQuery()->term('field', 'value', 1.5);

项集

Blog::elasticsearchQuery()->terms('field', ['value-1', 'value-2']);

范围

Blog::elasticsearchQuery()->range(field: 'field', gte: 10, lte: 20);

最大值

Blog::elasticsearchQuery()
    ->disjunctionMax(
        fn (Query $query) => $query->match('a', 'b'),
        fn (Query $query) => $query->boolean(
            fn (Should $should) => $should
                 ->exists('f')
                 ->term('g', 'h')
          ),
    );

聚合(aggs)

您可以像这样构建聚合查询:(请注意,目前,当您使用聚合时,您将获得原始的 Elasticsearch 结果)

(new Query())
        ->addIndex('posts')
        ->addIndex('users')
        ->size(0)
        ->boolean(
            fn(Must $must) => $must->term('published', true),
            fn(Should $should) => $should
                ->term('title', 'aaaa')
                ->term('title', 'bbbb'),
        )
        ->aggregation('types', (new Terms('morph_type'))
            ->aggregation('latest', (new TopHits(source: ['title', 'morph_type', 'created_at'], size: 3))
                ->sort(field: 'created_at', direction: 'desc')
            )
        )
        ->get();

处理数组字段

此包提供了两个用于更新/从数组字段中删除项的作业

RemoveArrayItem::dispatch('index_name', 'array_field_name', 'value_to_remove');
UpdateArrayItem::dispatch('index_name', 'array_field_name', 'old_value', 'new_value');

获取结果

在编写查询后,您可以调用 get() 来获取结果集合。

Blog::elasticsearchQuery()->match('title', 'ttt')->get(keyResultsBy: '_id'); //a collection including _source of the resulting documents

您还可以将结果转换为 Eloquent 模型

Blog::elasticsearchQuery()->match('title', 'ttt')->hydrate(); //an Eloquent collection containing models filled with attributes from elasticsearch documents

请注意,结果集合的键是文档的 _id。

确定结果的大小限制

Blog::elasticsearchQuery()->match('title', 'ttt')->size(15)->get();

确定获取结果时的from选项(分页)

Blog::elasticsearchQuery()->match('title', 'ttt')->from(10)->get();

有关更多信息,请访问 https://elastic.ac.cn/guide/en/elasticsearch/reference/current/paginate-search-results.html

选择特定字段

only() 方法将在您的查询中添加 "_source"。

Blog::elasticsearchQuery()->match('title', 'ttt')->only(['title'])->get();

根据条件构建查询

查询构建器在底层使用 Laravel 的 Conditionable 特性,这意味着您可以这样做

use Alirzaj\ElasticsearchBuilder\Query\Query;

Blog::elasticsearchQuery()
    ->match('title', 'ttt')
    ->when(isset($select), fn(Query $query) => $query->only(['title']))
    ->get(); 

调试

您可以转储或终止查询

Blog::elasticsearchQuery()->match('title', 'ttt')->dump()->exists('field')->dump();
Blog::elasticsearchQuery()->match('title', 'ttt')->dd();

使用低级elasticsearch客户端

此包将 Elastic\Elasticsearch\Client 类绑定到服务容器作为单例,因此您可以在需要直接使用它时从容器中解析它。

记录

当环境为测试或本地时,此包将在 storage/logs/elasticsearch.log 文件中记录执行的查询。

测试助手

刷新索引状态

此包提供了一个 RefreshElasticsearchDatabase 特性,您可以在每个测试后使用它来清理 Elasticsearch 索引。

首先,您必须在测试用例中使用此特性。

abstract class TestCase extends BaseTestCase
{
    use RefreshElasticsearchDatabase;
}

然后,您应该调用两个方法。一个在 setUp() 方法中,一个在 tearDown()

public function setUp(): void
{
    parent::setUp();

    $this->createIndices();
}
public function tearDown(): void
{
    $this->clearElasticsearchData();

    parent::tearDown();
}

断言

此包提供了一个 InteractsWithElasticsearch 特性,您可以在测试用例中使用它来对 Elasticsearch 索引中的数据进行断言。

abstract class TestCase extends BaseTestCase
{
    use InteractsWithElasticsearch;
}

您可以在 Elasticsearch 索引中断言某个文档是否存在

 $this->assertElasticsearchHas(
    'blogs',
    15,
    ['title' => 'my title']
 );

或确保某个文档未在 Elasticsearch 中索引

 $this->assertElasticsearchMissing(
    'blogs',
    15,
    ['title' => 'my title']
 );

致谢

许可

MIT 许可证(MIT)。有关更多信息,请参阅 许可文件