b2pweb/bdf-prime-indexer

Prime 的索引插件

2.0.0 2024-02-26 14:13 UTC

README

build codecov Packagist Version Total Downloads Type Coverage

通过 Prime 索引实体,并从 Elasticsearch 索引中请求数据。

安装

使用 composer 安装

composer require b2pweb/bdf-prime-indexer

config/bundles.php 中注册

<?php

return [
    // ...
    Bdf\Prime\Indexer\Bundle\PrimeIndexerBundle::class => ['all' => true],
    Bdf\PrimeBundle\PrimeBundle::class => ['all' => true],
];

config/packages/prime_indexer.yaml 中配置索引

prime_indexer:
  elasticsearch:
    # Define elasticsearch hosts
    hosts: ['127.0.0.1:9222']

  # Define indexes in form [Entity class]: [Index configuration class]
  # This is not mandatory if autoconfiguration is enabled
  indexes:
    App\Entities\City: App\Entities\CityIndex
    App\Entities\User: App\Entities\UserIndex

使用

声明索引

要声明索引,您首先需要声明配置

<?php
class CityIndex implements ElasticsearchIndexConfigurationInterface
{
    // Declare the index name
    public function index(): string
    {
        return 'test_cities';
    }

    // Get the mapped entity type
    public function entity(): string
    {
        return City::class;
    }

    // Build properties
    public function properties(PropertiesBuilder $builder): void
    {
        $builder
            ->string('name')
            ->integer('population')
            ->string('zipCode')
            ->string('country')->notAnalyzed()
            ->boolean('enabled')
        ;
    }

    // The id accessor
    public function id(): ?PropertyAccessorInterface
    {
        return new SimplePropertyAccessor('id');
    }

    // Declare analyzers
    public function analyzers(): array
    {
        return [
            'default' => [
                'type'      => 'custom',
                'tokenizer' => 'standard',
                'filter'    => ['lowercase', 'asciifolding'],
            ],
        ];
    }

    // Scopes
    public function scopes(): array
    {
        return [
            // "default" scope is always applied to the query
            'default' => function (ElasticsearchQuery $query) {
                $query
                    ->wrap(
                        (new FunctionScoreQuery())
                            ->addFunction('field_value_factor', [
                                'field' => 'population',
                                'factor' => 1,
                                'modifier' => 'log1p'
                            ])
                            ->scoreMode('multiply')
                    )
                    ->filter('enabled', true)
                ;
            },

            // Other scope : can be used as custom filter on query
            // Or using $index->myScope()
            'matchName' => function (ElasticsearchQuery $query, string $name) {
                $query
                    ->where(new Match('name', $name))
                    ->orWhere(
                        (new QueryString($name.'%'))
                            ->and()
                            ->defaultField('name')
                            ->analyzeWildcard()
                            ->useLikeSyntax()
                    )
                ;
            }
        ];
    }
}

可以通过实现接口添加一些额外配置

  • CustomEntitiesConfigurationInterface:用于定义实体加载方法
  • ShouldBeIndexedConfigurationInterface:用于定义判断实体是否应该被索引的谓词

之后,可以将索引添加到 "prime_indexer.indexes" 配置中,或者让自动配置完成工作。

查询索引

查询系统使用 Prime 接口,所以用法几乎相同

<?php
// Get the City index
$index = $container->get(\Bdf\Prime\Indexer\IndexFactory::class)->for(City::class);

// Get the query
$query = $index->query();

$query
    ->where('country', 'FR') // Simple where works as expected
    ->where('name', ':like', 'P%') // "like" operator is supported
    ->orWhere(new QueryString('my complete query')) // Operator object can be used for more powerful filters
;

// Get all cities who match with filters
$query->all();

// First returns the first matching element, wrapped into an Optional
$query->first()->get();

// Get the raw result of the elasticsearch query
$query->execute();

// Use scope directly
$index->matchName('Paris')->all();

// Same as above, but with scope as filter
$index->query()->where('matchName', 'Paris')->all();

更新索引

可以手动在索引上执行更新操作

<?php
// Get the City index
$index = $container->get(\Bdf\Prime\Indexer\IndexFactory::class)->for(City::class);

// Create the index, and insert all cities from database
$index->create(City::walk());

$paris = new City([
    'name' => 'Paris',
    'population' => 2201578,
    'country' => 'FR',
    'zipCode' => '75000'
]);

// Indexing the city
$index->add($paris);

// The "id" property is filled after insertion
echo $paris->id();

// Make sure that index is up to date
// !!! Do not use on production !!!
$index->refresh();

$index->contains($paris); // true

// Update one attribute
$paris->setPopulation(2201984);
$index->update($paris, ['population']); 

// Remove the entity
$index->remove($paris);
$index->contains($paris); // false

// Drop index
$index->drop();

使用 CLI

创建索引并索引实体

bin/console.php prime:indexer:create App\Entities\City

将显示进度条以跟踪索引进度。

注意:实体的完全限定类名必须用作参数。

用于管理 Elasticsearch 索引

bin/console.php elasticsearch:show
bin/console.php elasticsearch:delete test_cities

测试

因为测试是更重要的事情之一,为此添加了一个实用程序类

注意:索引名称将前缀为 "test_" 以确保它不会影响真实索引。

<?php
class MyTest extends \Bdf\PHPUnit\WebTestCase
{
    /**
     * @var TestingIndexer
     */
    private $indexTester;
    
    protected function setUp(): void
    {
        parent::setUp();
        
        $this->indexTester = new TestingIndexer($this->app);
        $this->indexTester->index(City::class); // Declare the city index
    }
    
    protected function tearDown() : void
    {
        parent::tearDown();
        
        $this->indexTester->destroy();
    }
    
    public function test_city_index()
    {
        // Push entities to index
        $this->indexTester->push([
            new City(...),
            new City(...),
            new City(...),
        ]);
        
        // Remove from index
        $this->indexTester->remove(new City(...));
        
        // Querying to the index
        $query = $this->indexTester->index(City::class)->query();
    }
}

与 Prime 的交互和差异

  • 使用索引系统不需要注册 Prime。一些实体可以进入索引,但不在数据库中。
  • 与 Prime 不同,映射是面向索引而不是面向模型
    • PropertiesBuilder 定义索引属性,并将其映射到模型属性
    • 允许计算属性(即未存储在实体中的属性)
    • 查询过滤器列未映射,并使用索引的列
  • 查询使用流(来自 b2pweb/bdf-collections),因此 first() 返回 OptionalInterface,并且转换是在流上完成的