スクリプト化されたフィールドとランタイムフィールド

Spring Data Elasticsearch はスクリプトフィールドとランタイムフィールドをサポートしています。これに関する詳細については、スクリプト(www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting.html (英語) )とランタイムフィールド(www.elastic.co/guide/en/elasticsearch/reference/8.9/runtime.html (英語) )に関する Elasticsearch のドキュメントを参照してください。Spring Data Elasticsearch のコンテキストでは、以下を使用できます。

  • 結果ドキュメントで計算され、返されたドキュメントに追加されるフィールドを返すために使用されるスクリプトフィールド。

  • 保存されたドキュメントに基づいて計算され、クエリで使用したり、検索結果で返したりできる実行時フィールド。

次のコードスニペットは、何ができるかを示しています (これらは命令型コードを示していますが、リアクティブ実装も同様に機能します)。

人物エンティティ

これらの例で使用されるエンティティは、Person エンティティです。このエンティティには birthDate プロパティと age プロパティがあります。生年月日は固定ですが、年齢はクエリが発行された時間に依存するため、動的に計算する必要があります。

import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.ScriptedField;
import org.springframework.lang.Nullable;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

import static org.springframework.data.elasticsearch.annotations.FieldType.*;

import java.lang.Integer;

@Document(indexName = "persons")
public record Person(
        @Id
        @Nullable
        String id,
        @Field(type = Text)
        String lastName,
        @Field(type = Text)
        String firstName,
        @Field(type = Keyword)
        String gender,
        @Field(type = Date, format = DateFormat.basic_date)
        LocalDate birthDate,
        @Nullable
        @ScriptedField Integer age                   (1)
) {
    public Person(String id,String lastName, String firstName, String gender, String birthDate) {
        this(id,                                     (2)
            lastName,
            firstName,
            LocalDate.parse(birthDate, DateTimeFormatter.ISO_LOCAL_DATE),
            gender,
            null);
    }
}
1age プロパティが計算され、検索結果に入力されます。
2 テストデータを設定するための便利なコンストラクター。

age プロパティには @ScriptedField というアノテーションが付けられていることに注意してください。これにより、インデックスマッピング内の対応するエントリの書き込みが禁止され、検索レスポンスから計算フィールドを配置するターゲットとしてプロパティがマークされます。

リポジトリインターフェース

この例で使用されるリポジトリ:

public interface PersonRepository extends ElasticsearchRepository<Person, String> {

    SearchHits<Person> findAllBy(ScriptedField scriptedField);

    SearchHits<Person> findByGenderAndAgeLessThanEqual(String gender, Integer age, RuntimeField runtimeField);
}

サービスクラス

サービスクラスには、挿入されたリポジトリと age プロパティを設定して使用するいくつかの方法を示す ElasticsearchOperations インスタンスがあります。説明を入れるためにコードを複数の部分に分割して示します。

import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.RuntimeField;
import org.springframework.data.elasticsearch.core.query.ScriptData;
import org.springframework.data.elasticsearch.core.query.ScriptType;
import org.springframework.data.elasticsearch.core.query.ScriptedField;
import org.springframework.data.elasticsearch.core.query.StringQuery;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class PersonService {
    private final ElasticsearchOperations operations;
    private final PersonRepository repository;

    public PersonService(ElasticsearchOperations operations, SaRPersonRepository repository) {
        this.operations = operations;
        this.repository = repository;
    }

    public void save() { (1)
        List<Person> persons = List.of(
                new Person("1", "Smith", "Mary", "f", "1987-05-03"),
                new Person("2", "Smith", "Joshua", "m", "1982-11-17"),
                new Person("3", "Smith", "Joanna", "f", "2018-03-27"),
                new Person("4", "Smith", "Alex", "m", "2020-08-01"),
                new Person("5", "McNeill", "Fiona", "f", "1989-04-07"),
                new Person("6", "McNeill", "Michael", "m", "1984-10-20"),
                new Person("7", "McNeill", "Geraldine", "f", "2020-03-02"),
                new Person("8", "McNeill", "Patrick", "m", "2022-07-04"));

        repository.saveAll(persons);
    }
1Elasticsearch にデータを格納するユーティリティメソッド。

スクリプト化されたフィールド

次の部分では、スクリプト化されたフィールドを使用して人の年齢を計算して返す方法を示します。スクリプト化されたフィールドは、返されたデータに何かを追加することしかできず、クエリで年齢を使用することはできません (これについては、ランタイムフィールドを参照してください)。

    public SearchHits<Person> findAllWithAge() {

        var scriptedField = ScriptedField.of("age",                               (1)
                ScriptData.of(b -> b
                        .withType(ScriptType.INLINE)
                        .withScript("""
                                Instant currentDate = Instant.ofEpochMilli(new Date().getTime());
                                Instant startDate = doc['birth-date'].value.toInstant();
                                return (ChronoUnit.DAYS.between(startDate, currentDate) / 365);
                                """)));

        // version 1: use a direct query
        var query = new StringQuery("""
                { "match_all": {} }
                """);
        query.addScriptedField(scriptedField);                                    (2)
        query.addSourceFilter(FetchSourceFilter.of(b -> b.withIncludes("*")));    (3)

        var result1 = operations.search(query, Person.class);                     (4)

        // version 2: use the repository
        var result2 = repository.findAllBy(scriptedField);                        (5)

        return result1;
    }
1 人の年齢を計算する ScriptedField を定義します。
2Query を使用する場合は、スクリプト化されたフィールドをクエリに追加します。
3 スクリプト化されたフィールドを Query に追加する場合、ドキュメントソースから通常のフィールドも取得するために追加のソースフィルターが必要になります。
4Person エンティティの値が age プロパティに設定されているデータを取得します。
5 リポジトリを使用する場合、スクリプト化されたフィールドをメソッドパラメーターとして追加するだけです。

ランタイムフィールド

実行時フィールドを使用する場合、計算された値をクエリ自体で使用できます。次のコードでは、これを使用して、特定の性別と人の最大年齢に対するクエリを実行します。

    public SearchHits<Person> findWithGenderAndMaxAge(String gender, Integer maxAge) {

        var runtimeField = new RuntimeField("age", "long", """                    (1)
                                Instant currentDate = Instant.ofEpochMilli(new Date().getTime());
                                Instant startDate = doc['birthDate'].value.toInstant();
                                emit (ChronoUnit.DAYS.between(startDate, currentDate) / 365);
                """);

        // variant 1 : use a direct query
        var query = CriteriaQuery.builder(Criteria
                        .where("gender").is(gender)
                        .and("age").lessThanEqual(maxAge))
                .withRuntimeFields(List.of(runtimeField))                         (2)
                .withFields("age")                                                (3)
                .withSourceFilter(FetchSourceFilter.of(b -> b.withIncludes("*"))) (4)
                .build();

        var result1 = operations.search(query, Person.class);                     (5)

        // variant 2: use the repository                                          (6)
        var result2 = repository.findByGenderAndAgeLessThanEqual(gender, maxAge, runtimeField);

        return result1;
    }
}
1 人の年齢を計算する実行時フィールドを定義します。// 組み込み属性については、asciidoctor.org/docs/user-manual/#builtin-attributes (英語) を参照してください。
2Query を使用する場合は、ランタイムフィールドを追加します。
3 スクリプト化されたフィールドを Query に追加する場合、計算値を返すために追加のフィールドパラメーターが必要です。
4 スクリプト化されたフィールドを Query に追加する場合、ドキュメントソースから通常のフィールドも取得するために追加のソースフィルターが必要になります。
5 クエリでフィルタリングされたデータと、返されたエンティティに age プロパティが設定されているデータを取得します。
6 リポジトリを使用する場合、実行する必要があるのは、ランタイムフィールドをメソッドパラメーターとして追加することだけです。

クエリでランタイムフィールドを定義するだけでなく、ランタイムフィールド定義を含む JSON ファイルを指すように @Mapping アノテーションの runtimeFieldsPath プロパティを設定することで、インデックス内でランタイムフィールドを定義することもできます。