Elasticsearch 杂项操作支持
本章节介绍对无法直接通过 repository 接口访问的 Elasticsearch 操作的额外支持。建议将这些操作作为自定义实现添加,如自定义 Repository 实现中所述。
索引设置
使用 Spring Data Elasticsearch 创建 Elasticsearch 索引时,可以使用 @Setting
注解定义不同的索引设置。以下参数可用
-
useServerConfiguration
不发送任何设置参数,因此由 Elasticsearch 服务器配置确定它们。 -
settingPath
指向一个定义设置的 JSON 文件,该文件必须可在 classpath 中解析 -
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...
}
1 | 定义排序字段时,使用 Java 属性的名称(firstField),而不是可能为 Elasticsearch 定义的名称(first_field) |
2 | sortModes 、sortOrders 和 sortMissingValues 是可选的,但如果设置了,则条目数量必须与 sortFields 元素的数量匹配 |
索引映射
当 Spring Data Elasticsearch 使用 IndexOperations.createMapping()
方法创建索引映射时,它使用映射注解概述中描述的注解,特别是 @Field
注解。除此之外,还可以向类添加 @Mapping
注解。此注解具有以下属性
-
mappingPath
classpath 中的 JSON 格式资源;如果非空,则将其用作映射,不进行其他映射处理。 -
enabled
设置为 false 时,此标志将写入映射,并且不进行进一步处理。 -
dateDetection
和numericDetection
当未设置为DEFAULT
时,设置映射中的相应属性。 -
dynamicDateFormats
当此字符串数组非空时,它定义用于自动日期检测的日期格式。 -
runtimeFieldsPath
classpath 中的 JSON 格式资源,包含运行时字段的定义,这些定义将写入索引映射,例如
{
"day_of_week": {
"type": "keyword",
"script": {
"source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
}
}
}
过滤器构建器
过滤器构建器提高了查询速度。
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);
使用 Scroll 处理大结果集
Elasticsearch 提供了一个 Scroll API,用于分块获取大结果集。Spring Data Elasticsearch 内部使用它来提供 <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();
SearchOperations
API 中没有方法可以访问 scroll 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);
要将 Scroll API 与 repository 方法一起使用,返回类型必须在 Elasticsearch Repository 中定义为 Stream
。然后,方法的实现将使用 ElasticsearchTemplate 中的 scroll 方法。
interface SampleEntityRepository extends Repository<SampleEntity, String> {
Stream<SampleEntity> findBy();
}
排序选项
除了分页和排序中描述的默认排序选项外,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
类,可用于按地理距离对搜索操作的结果进行排序。
如果要检索的类具有名为 location 的 GeoPoint
属性,以下 Sort
将按与给定点的距离对结果进行排序
Sort.by(new GeoDistanceOrder("location", new GeoPoint(48.137154, 11.5761247)))
运行时字段
从版本 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 文件,例如
{
"day_of_week": {
"type": "keyword",
"script": {
"source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
}
}
}
然后必须在实体的 @Mapping
注解中设置 classpath 中存在的此 JSON 文件的路径
@Document(indexName = "runtime-fields")
@Mapping(runtimeFieldsPath = "/runtime-fields.json")
public class RuntimeFieldEntity {
// properties, getter, setter,...
}
在 Query 上设置运行时字段定义
定义运行时字段的第二种方法是将定义添加到搜索查询中(参见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
}
以下查询使用了一个运行时字段,该字段通过在价格上加 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);
这适用于 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)
1 | 为索引(可以是多个名称)和 keep-alive 持续时间创建一个时间点,并检索其 id |
2 | 将该 id 传递到查询中,与下一个 keep-alive 值一起搜索 |
3 | 对于下一个查询,使用上一次搜索返回的 id |
4 | 完成后,使用最后返回的 id 关闭时间点 |
搜索模板支持
支持使用搜索模板 API。要使用此功能,首先需要创建一个存储的脚本。ElasticsearchOperations
接口扩展了 ScriptOperations
,后者提供了必要的功能。此处使用的示例假定我们有一个名为 firstName
属性的 Person
实体。可以这样保存搜索模板脚本
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()
);
1 | 使用 putScript() 方法存储搜索模板脚本 |
2 | 脚本的名称/id |
3 | 在搜索模板中使用的脚本必须是 mustache 语言。 |
4 | 脚本源 |
5 | 脚本中的搜索参数 |
6 | 分页请求偏移量 |
7 | 分页请求大小 |
要在搜索查询中使用搜索模板,Spring Data Elasticsearch 提供了 SearchTemplateQuery
,它是 org.springframework.data.elasticsearch.core.query.Query
接口的一个实现。
在以下代码中,我们将添加一个使用搜索模板查询的调用到一个自定义 repository 实现中(参见自定义 Repository 实现),以此作为如何将其集成到 repository 调用中的示例。
我们首先定义自定义 repository 片段接口
interface PersonCustomRepository {
SearchPage<Person> findByFirstNameWithSearchTemplate(String firstName, Pageable pageable);
}
此 repository 片段的实现如下所示
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);
}
}
1 | 创建一个 SearchTemplateQuery |
2 | 提供搜索模板的 id |
3 | 参数以 Map<String,Object> 形式传递 |
4 | 以与其他查询类型相同的方式执行搜索。 |
嵌套排序
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));
关于过滤器查询:此处无法使用 CriteriaQuery
,因为此查询将被转换为 Elasticsearch 嵌套查询,这在过滤器上下文中不起作用。因此只能在此处使用 StringQuery
或 NativeQuery
。使用其中一个时,如上面的术语查询,必须使用 Elasticsearch 字段名称,因此当使用 @Field(name="…")
定义重新定义它们时,请注意。
对于 order path 和 nested paths 的定义,应使用 Java 实体属性名称。