WebFlux 下的 ReactiveElasticSearch 高级查询

非 Reactive 的SpringData-ElasticSearch 可利用ESRepository达到高级查询的目的,但 Reative因支持较晚
,目前缺乏 search( criteria ) 等高级查询实现,暂时无法利用 Reacitve-JPA 的 Repository 实现高级查询,很尴尬的鸡肋存在

不过好在ReactiveElasticsearchTemplate支持相对较为"全面"

目的:在reactive 的 webflux 体系下实现 es 查询多字段并高亮查询命中结果

了解到 JPA 后遂拉入响应式的依赖,调用接口实现,一顿操作猛如虎...然后开心的在IDEA 敲下. 体验 JPA 的魔法

多次google,百度相关资料全网无果,被逼无奈那就自己走源码实现吧👩‍🦯

1. 解决构造查询丢失条件问题

查阅 JPA 相关资料了解到了Template这个相对底层的封装,发现ReactiveElasticsearchTemplate提供了传入查询构造器的 find () 方法

通过debug观察其运行流程发现了查询请求前置hook 方法prepareSearchRequest,看到这个 hook 后,问题已经解决了80%

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 仅重写 hook
class CustomReactiveESTemplate(client: ReactiveElasticsearchClient?, converter: ElasticsearchConverter?, resultsMapper: ResultsMapper?) : ReactiveElasticsearchTemplate(client, converter, resultsMapper) {
//ES 查询前置Hook
override fun prepareSearchRequest(request: SearchRequest): SearchRequest {
if (request.preference().isNotEmpty()) {
val sourceQuery = request.source()
// 利用相对来说用不上的(项目功能单一)preference属性存取多字段,实现多字段高亮查询
val fields = request.preference().split("#")
//高亮构建器
val highlightBuilder = HighlightBuilder()
.preTags("<em>").postTags("</em>")
//遍历解析到的高亮域至高亮构建
fields.forEach {
highlightBuilder.field(it)
}
sourceQuery.highlighter(highlightBuilder)
//还原查询
request.preference("")
}
return super.prepareSearchRequest(request)
}
}

你也许会说既然允许自己构造查询了,何必大费周章还带有破坏性地写hook 函数,直接构造高级查询传入不行吗?

我一开始也这样想的且这样做的,不过行不通,位于ReactiveElasticsearchTemplate的find() 会调用到buildSearchRequest(),该函数只读取特定查询,高亮查询并不在此列,只能自行通过预置在buildSearchRequest()之后的 hook 函数,加入高级查询

2. 向 SpringBoot注入自定义hook的ReactiveESTemplate

在注入后并不意味着结束,还会有映射问题,因返回高亮结果不能和 pojo 自动映射,故还需自行扩展 DefaultResultMapper()修改映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Configuration(proxyBeanMethods = false)
class ReactiveESTemplateConfiguration() {
@Bean
fun reactiveElasticsearchTemplate(client: ReactiveElasticsearchClient?, converter: ElasticsearchConverter?): CustomReactiveESTemplate? {
//处理高亮字段映射
val resultsMapper = object : DefaultResultMapper() {
override fun <T : Any?> mapSearchHit(searchHit: SearchHit?, type: Class<T>?): T? {
if (!searchHit!!.hasSource())
return null

val source = searchHit.sourceAsMap
if (!source.containsKey("id") || source["id"] == null)
source["id"] = searchHit.id
val highlightFields = searchHit.highlightFields

// fragments 因所写项目功能单一故处理不是很恰当
// 将原本与高亮域对应的原生字段替换为高亮后的结果
source["q"] = highlightFields.get("q")
?.fragments
?.get(0)?.toString()
?: source["a"]
source["a"] =
highlightFields.get("a")
?.fragments
?.get(0)?.toString()
?: source["a"]
// 映射到 pojo
val mappedResult: Any = entityMapper.readObject(source, type) as Any
return if (type!!.isInterface)
projectionFactory.createProjection(type, mappedResult)
else
type.cast(mappedResult)
}
}

//注入构造参数
val template = CustomReactiveESTemplate(client, converter,
resultsMapper)
template.setIndicesOptions(IndicesOptions.strictExpandOpenAndForbidClosed())
template.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)
return template
}
}

3. 在 WebFlux 调用查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
class ItemsESRepository {
@Autowired
private lateinit var customReactiveESTemplate: CustomReactiveESTemplate

/**
* 分页
*/
private val pageRequest = PageRequest.of(0, 5)

/**
* 最少搜索输入字符长度
*/
private val minSearchLength = 5

/**
* 通过问题和选项搜索
*/
suspend fun searchByQuestionAndOptions(questions: String): Flux<TestEntity> {
/* if (questions.length < minSearchLength)
return Flux.empty()*/
val boolBuilder = QueryBuilders.boolQuery()
//多个字段匹配 属性值 must query
val matchQueryBuilder = QueryBuilders.multiMatchQuery(questions, "q", "a")
boolBuilder.must(matchQueryBuilder)
val query = NativeSearchQueryBuilder()
.withQuery(boolBuilder)
.withPageable(pageRequest)
.build()
query.preference = "q#a"

return customReactiveESTemplate
.find(query, TestEntity::class.java)
}
}

结语

至此,在webflux 中进行es的高亮多字段查询完成

对了 SpringBoot 版本为 2.2.6.RELEASE, ES 版本为6.8

Spring WebFlux 目前生态算不上太好,总的来说Rective还是大势所趋的,期待loom的早日完工