跳到主要内容

其他 Elasticsearch 操作支持

DeepSeek V3 中英对照 Miscellaneous Elasticsearch Operation Support

本章涵盖了无法通过仓库接口直接访问的 Elasticsearch 操作的其他支持。建议按照自定义仓库实现中的描述,将这些操作添加为自定义实现。

索引设置

在使用 Spring Data Elasticsearch 创建 Elasticsearch 索引时,可以通过使用 @Setting 注解定义不同的索引设置。以下是可用的参数:

  • useServerConfiguration 不会发送任何设置参数,因此由 Elasticsearch 服务器配置决定这些参数。

  • settingPath 指向一个 JSON 文件,该文件定义了必须在类路径中解析的设置。

  • shards 使用的分片数量,默认为 1

  • replicas 副本的数量,默认为 1

  • refreshIntervall 默认值为 "1s"

  • indexStoreType 默认值为 "fs"

同样可以定义 索引排序(请查看链接的 Elasticsearch 文档以了解可能的字段类型和值):

@Document(indexName = "entities")
@Setting(
sortFields = { "secondField", "firstField" }, 1
sortModes = { Setting.SortMode.max, Setting.SortMode.min }, 2
sortOrders = { Setting.SortOrder.desc, Setting.SortOrder.asc },
sortMissingValues = { Setting.SortMissing._last, Setting.SortMissing._first })
class Entity {
@Nullable
@Id private String id;

@Nullable
@Field(name = "first_field", type = FieldType.Keyword)
private String firstField;

@Nullable @Field(name = "second_field", type = FieldType.Keyword)
private String secondField;

// getter and setter...
}
java
  • 在定义排序字段时,使用 Java 属性的名称(firstField),而不是可能为 Elasticsearch 定义的名称(first_field

  • sortModessortOrderssortMissingValues 是可选的,但如果设置了它们,条目的数量必须与 sortFields 元素的数量匹配

索引映射

当 Spring Data Elasticsearch 使用 IndexOperations.createMapping() 方法创建索引映射时,它会使用 映射注解概述 中描述的注解,特别是 @Field 注解。除此之外,还可以在类上添加 @Mapping 注解。该注解具有以下属性:

  • mappingPath 一个 JSON 格式的类路径资源;如果此字段不为空,则将其用作映射,不再进行其他映射处理。

  • enabled 当设置为 false 时,此标志会被写入映射中,并且不再进行进一步处理。

  • dateDetectionnumericDetection 当未设置为 DEFAULT 时,会在映射中设置相应的属性。

  • dynamicDateFormats 当此字符串数组不为空时,它定义了用于自动日期检测的日期格式。

  • runtimeFieldsPath 一个 JSON 格式的类路径资源,包含运行时字段的定义,这些定义会被写入索引映射中,例如:

{
"day_of_week": {
"type": "keyword",
"script": {
"source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
}
}
}
json

过滤器构建器

筛选器构建器提高了查询速度。

private ElasticsearchOperations operations;

IndexCoordinates index = IndexCoordinates.of("sample-index");

Query query = NativeQuery.builder()
.withQuery(q -> q
.matchAll(ma -> ma))
.withFilter( q -> q
.bool(b -> b
.must(m -> m
.term(t -> t
.field("id")
.value(documentId))
)))
.build();

SearchHits<SampleEntity> sampleEntities = operations.search(query, SampleEntity.class, index);
java

使用 Scroll 处理大数据集

Elasticsearch 提供了一个滚动 API,用于分块获取大型结果集。Spring Data Elasticsearch 在内部使用这个 API 来实现 <T> SearchHitsIterator<T> SearchOperations.searchForStream(Query query, Class<T> clazz, IndexCoordinates index) 方法。

IndexCoordinates index = IndexCoordinates.of("sample-index");

Query searchQuery = NativeQuery.builder()
.withQuery(q -> q
.matchAll(ma -> ma))
.withFields("message")
.withPageable(PageRequest.of(0, 10))
.build();

SearchHitsIterator<SampleEntity> stream = elasticsearchOperations.searchForStream(searchQuery, SampleEntity.class,
index);

List<SampleEntity> sampleEntities = new ArrayList<>();
while (stream.hasNext()) {
sampleEntities.add(stream.next());
}

stream.close();
java

SearchOperations API 中没有访问滚动 ID 的方法,如果需要访问滚动 ID,可以使用 AbstractElasticsearchTemplate 的以下方法(这是不同 ElasticsearchOperations 实现的基础实现):

@Autowired ElasticsearchOperations operations;

AbstractElasticsearchTemplate template = (AbstractElasticsearchTemplate)operations;

IndexCoordinates index = IndexCoordinates.of("sample-index");

Query query = NativeQuery.builder()
.withQuery(q -> q
.matchAll(ma -> ma))
.withFields("message")
.withPageable(PageRequest.of(0, 10))
.build();

SearchScrollHits<SampleEntity> scroll = template.searchScrollStart(1000, query, SampleEntity.class, index);

String scrollId = scroll.getScrollId();
List<SampleEntity> sampleEntities = new ArrayList<>();
while (scroll.hasSearchHits()) {
sampleEntities.addAll(scroll.getSearchHits());
scrollId = scroll.getScrollId();
scroll = template.searchScrollContinue(scrollId, 1000, SampleEntity.class);
}
template.searchScrollClear(scrollId);
java

要在存储库方法中使用 Scroll API,返回类型必须在 Elasticsearch 存储库中定义为 Stream。然后,该方法的实现将使用 ElasticsearchTemplate 中的滚动方法。

interface SampleEntityRepository extends Repository<SampleEntity, String> {

Stream<SampleEntity> findBy();

}
java

排序选项

除了 分页与排序 中描述的默认排序选项外,Spring Data Elasticsearch 还提供了 org.springframework.data.elasticsearch.core.query.Order 类,该类继承自 org.springframework.data.domain.Sort.Order。它提供了在指定结果排序时可以发送到 Elasticsearch 的额外参数(参见 www.elastic.co/guide/en/elasticsearch/reference/7.15/sort-search-results.html)。

还有一个 org.springframework.data.elasticsearch.core.query.GeoDistanceOrder 类,可以用来根据地理距离对搜索结果进行排序。

如果要检索的类具有名为 locationGeoPoint 属性,以下 Sort 将根据与给定点的距离对结果进行排序:

Sort.by(new GeoDistanceOrder("location", new GeoPoint(48.137154, 11.5761247)))
java

运行时字段

从 Elasticsearch 7.12 版本开始,Elasticsearch 增加了运行时字段的功能(www.elastic.co/guide/en/elasticsearch/reference/7.12/runtime.html)。Spring Data Elasticsearch 以两种方式支持这一功能:

索引映射中的运行时字段定义

定义运行时字段的第一种方法是将定义添加到索引映射中(参见 www.elastic.co/guide/en/elasticsearch/reference/7.12/runtime-mapping-fields.html)。要在 Spring Data Elasticsearch 中使用此方法,用户必须提供一个包含相应定义的 JSON 文件,例如:

示例 1. runtime-fields.json

{
"day_of_week": {
"type": "keyword",
"script": {
"source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
}
}
}
json

然后,必须在实体的 @Mapping 注解中设置此 JSON 文件的路径,该文件必须存在于类路径中:

@Document(indexName = "runtime-fields")
@Mapping(runtimeFieldsPath = "/runtime-fields.json")
public class RuntimeFieldEntity {
// properties, getter, setter,...
}
java

在查询上设置的运行时字段定义

定义运行时字段的第二种方法是将定义添加到搜索查询中(参见 www.elastic.co/guide/en/elasticsearch/reference/7.12/runtime-search-request.html)。以下代码示例展示了如何使用 Spring Data Elasticsearch 实现这一点:

所使用的实体是一个具有 price 属性的简单对象:

@Document(indexName = "some_index_name")
public class SomethingToBuy {

private @Id @Nullable String id;
@Nullable @Field(type = FieldType.Text) private String description;
@Nullable @Field(type = FieldType.Double) private Double price;

// getter and setter
}
java

以下查询使用了一个运行时字段,该字段通过将价格增加 19% 来计算 priceWithTax 值,并在搜索查询中使用此值来查找所有 priceWithTax 大于或等于给定值的实体:

RuntimeField runtimeField = new RuntimeField("priceWithTax", "double", "emit(doc['price'].value * 1.19)");
Query query = new CriteriaQuery(new Criteria("priceWithTax").greaterThanEqual(16.5));
query.addRuntimeField(runtimeField);

SearchHits<SomethingToBuy> searchHits = operations.search(query, SomethingToBuy.class);
java

这适用于 Query 接口的每个实现。

点时间 (PIT) API

ElasticsearchOperations 支持 Elasticsearch 的点时间 API(参见 www.elastic.co/guide/en/elasticsearch/reference/8.3/point-in-time-api.html)。以下代码片段展示了如何在一个虚构的 Person 类中使用此功能:

ElasticsearchOperations operations; // autowired
Duration tenSeconds = Duration.ofSeconds(10);

String pit = operations.openPointInTime(IndexCoordinates.of("person"), tenSeconds); 1

// create query for the pit
Query query1 = new CriteriaQueryBuilder(Criteria.where("lastName").is("Smith"))
.withPointInTime(new Query.PointInTime(pit, tenSeconds)) 2
.build();
SearchHits<Person> searchHits1 = operations.search(query1, Person.class);
// do something with the data

// create 2nd query for the pit, use the id returned in the previous result
Query query2 = new CriteriaQueryBuilder(Criteria.where("lastName").is("Miller"))
.withPointInTime(
new Query.PointInTime(searchHits1.getPointInTimeId(), tenSeconds)) 3
.build();
SearchHits<Person> searchHits2 = operations.search(query2, Person.class);
// do something with the data

operations.closePointInTime(searchHits2.getPointInTimeId()); 4
java
  • 为索引(可以是多个名称)创建一个时间点,并设置一个保持活动状态的持续时间,然后检索其 ID

  • 将该 ID 与下一个保持活动状态的值一起传递到查询中进行搜索

  • 对于下一个查询,使用从上一个搜索返回的 ID

  • 完成后,使用最后返回的 ID 关闭时间点

搜索模板支持

支持使用搜索模板 API。要使用此功能,首先需要创建一个存储的脚本。ElasticsearchOperations 接口扩展了 ScriptOperations,它提供了必要的函数。这里的示例假设我们有一个 Person 实体,其中包含一个名为 firstName 的属性。可以像这样保存一个搜索模板脚本:

import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.script.Script;

operations.putScript( 1
Script.builder()
.withId("person-firstname") 2
.withLanguage("mustache") 3
.withSource(""" 4
{
"query": {
"bool": {
"must": [
{
"match": {
"firstName": "{{firstName}}" 5
}
}
]
}
},
"from": "{{from}}", 6
"size": "{{size}}" 7
}
""")
.build()
);
java
  • 使用 putScript() 方法存储搜索模板脚本

  • 脚本的名称 / ID

  • 用于搜索模板的脚本必须使用 mustache 语言。

  • 脚本源

  • 脚本中的搜索参数

  • 分页请求的偏移量

  • 分页请求的大小

要在搜索查询中使用搜索模板,Spring Data Elasticsearch 提供了 SearchTemplateQuery,它是 org.springframework.data.elasticsearch.core.query.Query 接口的一个实现。

在以下代码中,我们将使用搜索模板查询添加一个调用到自定义仓库实现(参见自定义仓库实现),以此为例展示如何将其集成到仓库调用中。

我们首先定义自定义的存储库片段接口:

interface PersonCustomRepository {
SearchPage<Person> findByFirstNameWithSearchTemplate(String firstName, Pageable pageable);
}
java

这个仓库片段的实现如下所示:

public class PersonCustomRepositoryImpl implements PersonCustomRepository {

private final ElasticsearchOperations operations;

public PersonCustomRepositoryImpl(ElasticsearchOperations operations) {
this.operations = operations;
}

@Override
public SearchPage<Person> findByFirstNameWithSearchTemplate(String firstName, Pageable pageable) {

var query = SearchTemplateQuery.builder() 1
.withId("person-firstname") 2
.withParams(
Map.of( 3
"firstName", firstName,
"from", pageable.getOffset(),
"size", pageable.getPageSize()
)
)
.build();

SearchHits<Person> searchHits = operations.search(query, Person.class); 4

return SearchHitSupport.searchPageFor(searchHits, pageable);
}
}
java
  • 创建一个 SearchTemplateQuery

  • 提供搜索模板的 ID

  • 参数通过 Map<String,Object> 传递

  • 以与其他查询类型相同的方式进行搜索。

嵌套排序

Spring Data Elasticsearch 支持在嵌套对象中进行排序(www.elastic.co/guide/en/elasticsearch/reference/8.9/sort-search-results.html#nested-sorting)。

以下示例取自 org.springframework.data.elasticsearch.core.query.sort.NestedSortIntegrationTests 类,展示了如何定义嵌套排序。

var filter = StringQuery.builder("""
{ "term": {"movies.actors.sex": "m"} }
""").build();
var order = new org.springframework.data.elasticsearch.core.query.Order(Sort.Direction.DESC,
"movies.actors.yearOfBirth")
.withNested(
Nested.builder("movies")
.withNested(
Nested.builder("movies.actors")
.withFilter(filter)
.build())
.build());

var query = Query.findAll().addSort(Sort.by(order));
java

关于过滤器查询:这里不能使用 CriteriaQuery,因为该查询会被转换为 Elasticsearch 嵌套查询,而嵌套查询在过滤器上下文中无法正常工作。因此,这里只能使用 StringQueryNativeQuery。当使用这些查询时,比如上面的 term 查询,必须使用 Elasticsearch 字段名称,因此请注意,当这些字段名称通过 @Field(name="…​") 重新定义时,需要确保使用正确的字段名称。

对于路径顺序和嵌套路径的定义,应使用 Java 实体属性名称。