アクティブレコードを使う

Yii のアクティブレコードの使用方法に関する一般的な情報については、ガイド を参照してください。

Elasticsearch のアクティブレコードを定義するためには、あなたのレコード・クラスを yii\elasticsearch\ActiveRecord から拡張して、 最低限、レコードの属性を定義するための attributes() メソッドを実装する必要があります。 Elasticsearch ではプライマリ・キーの扱いが通常と異なります。 というのは、プライマリ・キー (elasticsearch の用語では _id フィールド) が、デフォルトでは属性のうちに入らないからです。 ただし、_id フィールドを属性に含めるための パス・マッピング を定義することは出来ます。 パス・マッピングの定義の仕方については、elasticsearch のドキュメント を参照してください。 document または record の _id フィールドは、getPrimaryKey() および setPrimaryKey() を使ってアクセスすることが出来ます。 パス・マッピングが定義されている場合は、primaryKey() メソッドを使って属性の名前を定義することが出来ます。

以下は Customer と呼ばれるモデルの例です。

class Customer extends \yii\elasticsearch\ActiveRecord
{
    /**
     * @return array このレコードの属性のリスト
     */
    public function attributes()
    {
        // '_id' に対するパス・マッピングは 'id' フィールドに設定される
        return ['id', 'name', 'address', 'registration_date'];
    }

    /**
     * @return ActiveQuery Order レコード へのリレーションを定義 (Order は他のデータベース、例えば、redis や通常の SQLDB にあっても良い)
     */
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('id');
    }

    /**
     * `$query` を修正してアクティブ (status = 1) な顧客だけを返すスコープを定義
     */
    public static function active($query)
    {
        $query->andWhere(['status' => 1]);
    }
}

index()type() をオーバーライドして、 このレコードが表すインデックスとタイプを定義することが出来ます。

elasticsearch のアクティブレコードの一般的な使用方法は、ガイド で説明されたデータベースのアクティブレコードの場合と非常によく似ています。 以下の制限と拡張 (!) があることを除けば、同じインタフェイスと機能をサポートしています。

  • elasticsearch は SQL をサポートしていないため、クエリの API は join()groupBy()having() および union() をサポートしません。 並べ替え、リミット、オフセット、条件付き WHERE は、すべてサポートされています。
  • from() はテーブルを選択しません。 そうではなく、クエリ対象の インデックスタイプ を選択します。
  • select()yii\elasticsearch\ActiveQuery::fields() に置き換えられています。 基本的には同じことをするものですが、fields の方が elasticsearch の用語として相応しいでしょう。 ドキュメントから取得するフィールドを定義します。
  • Elasticsearch にはテーブルがありませんので、テーブルを通じての yii\elasticsearch\ActiveQuery::via() リレーションは定義することが出来ません。
  • Elasticsearch はデータ・ストレージであると同時に検索エンジンでもありますので、当然ながら、レコードの検索に対するサポートが追加されています。 Elasticsearch のクエリを構成するための query()yii\elasticsearch\ActiveQuery::filter() そして yii\elasticsearch\ActiveQuery::addFacet() というメソッドがあります。 これらがどのように働くかについて、下の使用例を見てください。 また、queryfilter の部分を構成する方法については、クエリ DSL を参照してください。
  • Elasticsearch のアクティブレコードから通常のアクティブレコード・クラスへのリレーションを定義することも可能です。また、その逆も可能です。

Note: デフォルトでは、elasticsearch は、どんなクエリでも、返されるレコードの数を 10 に限定しています。 もっと多くのレコードを取得することを期待する場合は、リレーションの定義で上限を明示的に指定しなければなりません。 このことは、via() を使うリレーションにとっても重要です。 なぜなら、via のレコードが 10 までに制限されている場合は、リレーションのレコードも 10 を超えることは出来ないからです。

使用例:

$customer = new Customer();
$customer->primaryKey = 1; // この場合は、$customer->id = 1 と等価
$customer->attributes = ['name' => 'test'];
$customer->save();

$customer = Customer::get(1); // PK によってレコードを取得
$customers = Customer::mget([1,2,3]); // PK によって複数のレコードを取得
$customer = Customer::find()->where(['name' => 'test'])->one(); // クエリによる取得。レコードを正しく取得するためにはこのフィールドにマッピングを構成する必要があることに注意。
$customers = Customer::find()->active()->all(); // クエリによって全てを取得 (`active` スコープを使って)

// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html
$result = Article::find()->query(["match" => ["title" => "yii"]])->all(); // articles whose title contains "yii"

// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html#query-dsl-match-query-fuzziness
$query = Article::find()->query([
    'match' => [
        'title' => [
            'query' => 'このクエリは、このテキストに似た記事を返します :-)',
            'operator' => 'and',
            'fuzziness' => 'AUTO'
        ]
    ]
]);

$query->all(); // 全てのドキュメントを取得
// 検索に facets を追加できる
$query->addStatisticalFacet('click_stats', ['field' => 'visit_count']);
$query->search(); // 全てのレコード、および、visit_count フィールドに関する統計 (例えば、平均、合計、最小、最大など) を取得

複雑なクエリ

どのようなクエリでも、Elasticsearch のクエリ DSL を使って作成して ActiveRecord::query() メソッドに渡すことが出来ます。しかし、ES のクエリ DSL は冗長さで悪名高いものです。長すぎるクエリは、すぐに管理できないものになってしまいます。 クエリをもっと保守しやすくする方法があります。SQL ベースの ActiveRecord のために定義されているようなクエリクラスを定義することから始めましょう。

class CustomerQuery extends ActiveQuery
{
    public static function name($name)
    {
        return ['match' => ['name' => $name]];
    }

    public static function address($address)
    {
        return ['match' => ['address' => $address]];
    }

    public static function registrationDateRange($dateFrom, $dateTo)
    {
        return ['range' => ['registration_date' => [
            'gte' => $dateFrom,
            'lte' => $dateTo,
        ]]];
    }
}

こうすれば、これらのクエリ・コンポーネントを、結果となるクエリやフィルタを組み上げるために使用することが出来ます。

$customers = Customer::find()->filter([
    CustomerQuery::registrationDateRange('2016-01-01', '2016-01-20'),
])->query([
    'bool' => [
        'should' => [
            CustomerQuery::name('John'),
            CustomerQuery::address('London'),
        ],
        'must_not' => [
            CustomerQuery::name('Jack'),
        ],
    ],
])->all();

集合 (Aggregations)

集合フレームワーク が、検索クエリに基づいた集合データを提供するのを助けてくれます。これは集合 (aggregation) と呼ばれる単純な構成要素に基づくもので、複雑なデータの要約を構築するために作成することが出来るものです。

以前に定義された Customer クラスを使って、毎日何人の顧客が登録されているかを検索しましょう。そうするために terms 集合を使います。

$aggData = Customer::find()->addAggregate('customers_by_date', [
    'terms' => [
        'field' => 'registration_date',
        'order' => ['_count' => 'desc'],
        'size' => 10, // 登録日の上位 10
    ],
])->search(null, ['search_type' => 'count']);

この例では、集合の結果だけを特にリクエストしています。データを更に処理するために次のコードを使います。

$customersByDate = ArrayHelper::map($aggData['aggregations']['customers_by_date']['buckets'], 'key', 'doc_count');

これで $customersByDate に、ユーザー登録数の最も多い日付け上位 10 個が入ります。

オブジェクトにマップされた属性の異常な振る舞いについて

このエクステンションは _update エンドポイントを使ってレコードを更新します。このエンドポイントはドキュメントの部分更新をするように設計されているため、Elasticsearch で "オブジェクト" マップ型を持つ全ての属性は既存のデータとマージされます。例示しましょう。

$customer = new Customer();
$customer->my_attribute = ['foo' => 'v1', 'bar' => 'v2'];
$customer->save();
// この時点で Elasticsearch における my_attribute の値は {"foo": "v1", "bar": "v2"}

$customer->my_attribute = ['foo' => 'v3', 'bar' => 'v4'];
$customer->save();
// Elasticsearch における my_attribute の値は {"foo": "v3", "bar": "v4"} となる

$customer->my_attribute = ['baz' => 'v5'];
$customer->save();
// Elasticsearch における my_attribute の値は {"foo": "v3", "bar": "v4", "baz": "v5"} となる
// しかし $customer->my_attribute は ['baz' => 'v5'] に等しいままである

このロジックはオブジェクトに対してのみ適用されるので、オブジェクトを単一要素の配列に包むことが解決策になります。Elasticsearch にとっては単一要素の配列は要素自体と同じものであるため、それ以外のコードを修正する必要はありません。

$customer->my_attribute = [['new' => 'value']]; // 二重括弧に注意
$customer->save();
// Elasticsearch における my_attribute の値は {"new": "value"} になる
$customer->my_attribute = $customer->my_attribute[0]; // 一貫性のためにこうしてもよい

詳細については次の議論を参照して下さい。 https://discuss.elastic.co/t/updating-an-object-field/110735