• SpringBoot 3.2.5 + ElasticSearch 8.12.0 - SpringData 开发指南


     

    目录

    一、SpringData ElasticSearch

    1.1、环境配置

    1.2、创建实体类

    1.3、ElasticSearchTemplate 的使用

    1.3.1、创建索引库,设置映射

    1.3.2、创建索引映射注意事项

    1.3.3、简单的 CRUD

    1.3.4、三种构建搜索条件的方式

    1.3.5、NativeQuery 搜索实战

    1.3.6、completionSuggestion 自动补全


    一、SpringData ElasticSearch


    1.1、环境配置

    a)依赖如下:

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-data-elasticsearchartifactId>
    4. dependency>
    5. <dependency>
    6. <groupId>org.mockitogroupId>
    7. <artifactId>mockito-coreartifactId>
    8. <version>2.23.4version>
    9. <scope>testscope>
    10. dependency>

    b)配置文件如下:

    1. spring:
    2. application:
    3. name: es
    4. elasticsearch:
    5. uris: env-base:9200

    1.2、创建实体类

    a)简单结构如下(后续示例,围绕此结构展开):

    1. import org.springframework.data.annotation.Id
    2. import org.springframework.data.elasticsearch.annotations.Document
    3. import org.springframework.data.elasticsearch.annotations.Field
    4. import org.springframework.data.elasticsearch.annotations.FieldType
    5. @Document(indexName = "album_info", )
    6. data class AlbumInfoDo (
    7. /**
    8. * @Id: 表示文档中的主键,并且会在保存在 ElasticSearch 数据结构中 {"id": "", "userId": "", "title": ""}
    9. */
    10. @Id
    11. @Field(type = FieldType.Keyword)
    12. val id: Long? = null,
    13. /**
    14. * @Field: 描述 Java 类型中的属性映射
    15. * - name: 对应 ES 索引中的字段名. 默认和属性同名
    16. * - type: 对应字段类型,默认是 FieldType.Auto (会根据我们数据类型自动进行定义),但是建议主动定义,避免导致错误映射
    17. * - index: 是否创建索引. text 类型创建倒排索引,其他类型创建正排索引. 默认是 true
    18. * - analyzer: 分词器名称. 中文我们一般都使用 ik 分词器(ik分词器有 ik_smart 和 ik_max_word)
    19. */
    20. @Field(name = "user_id", type = FieldType.Long)
    21. val userId: Long,
    22. @Field(type = FieldType.Text, analyzer = "ik_max_word")
    23. var title: String,
    24. @Field(type = FieldType.Text, analyzer = "ik_smart")
    25. var content: String,
    26. )

    b)复杂嵌套结构如下:

    1. import org.springframework.data.annotation.Id
    2. import org.springframework.data.elasticsearch.annotations.Document
    3. import org.springframework.data.elasticsearch.annotations.Field
    4. import org.springframework.data.elasticsearch.annotations.FieldType
    5. @Document(indexName = "album_list")
    6. data class AlbumListDo(
    7. @Id
    8. @Field(type = FieldType.Keyword)
    9. var id: Long,
    10. @Field(type = FieldType.Nested) // 表示一个嵌套结构
    11. var userinfo: UserInfoSimp,
    12. @Field(type = FieldType.Text, analyzer = "ik_max_word")
    13. var title: String,
    14. @Field(type = FieldType.Text, analyzer = "ik_smart")
    15. var content: String,
    16. @Field(type = FieldType.Nested) // 表示一个嵌套结构
    17. var photos: List,
    18. )
    19. data class UserInfoSimp(
    20. @Field(type = FieldType.Long)
    21. val userId: Long,
    22. @Field(type = FieldType.Text, analyzer = "ik_max_word")
    23. val username: String,
    24. @Field(type = FieldType.Keyword, index = false)
    25. val avatar: String,
    26. )
    27. data class AlbumPhotoSimp(
    28. @Field(type = FieldType.Integer, index = false)
    29. val sort: Int,
    30. @Field(type = FieldType.Keyword, index = false)
    31. val photo: String,
    32. )

    对于一个小型系统来说,一般也不会创建这种复杂程度的文档,因为会涉及到很多一致性问题, 需要通过大量的 mq 进行同步,给系统带来一定的开销. 

    因此,一般会将需要进行模糊查询的字段存 Document 中(es 就擅长这个),而其他数据则可以在 Document 中以 id 的形式进行存储.   这样就既可以借助 es 高效的模糊查询能力,也能减少为保证一致性而带来的系统开销.  从 es 中查到数据后,再通过其他表的 id 从数据库中拿数据即可(这点开销,相对于从大量数据的数据库中进行 like 查询,几乎可以忽略).

    1.3、ElasticSearchTemplate 的使用

    1.3.1、创建索引库,设置映射

    1. @SpringBootTest
    2. class ElasticSearchIndexTests {
    3. @Resource
    4. private lateinit var elasticsearchTemplate: ElasticsearchTemplate
    5. @Test
    6. fun test1() {
    7. //存在索引库就删除
    8. if (elasticsearchTemplate.indexOps(AlbumInfoDo::class.java).exists()) {
    9. elasticsearchTemplate.indexOps(AlbumInfoDo::class.java).delete()
    10. }
    11. //创建索引库
    12. elasticsearchTemplate.indexOps(AlbumInfoDo::class.java).create()
    13. //设置映射
    14. elasticsearchTemplate.indexOps(AlbumInfoDo::class.java).putMapping(
    15. elasticsearchTemplate.indexOps(AlbumInfoDo::class.java).createMapping()
    16. )
    17. }
    18. }

    效果如下: 

    1.3.2、创建索引映射注意事项

    a)在没有创建索引库和映射的情况下,也可以直接向 es 库中插入数据,如下代码:

    1. @Test
    2. fun test2() {
    3. val obj = AlbumListDo(
    4. id = 1,
    5. userinfo = UserInfoSimp(
    6. userId = 1,
    7. username = "cyk",
    8. avatar = "env-base:9200"
    9. ),
    10. title = "今天天气真好",
    11. content = "早上起来,我要好好学习,然去公园散步~",
    12. photos = listOf(
    13. AlbumPhotoSimp(1, "www.photo.com/aaa"),
    14. AlbumPhotoSimp(2, "www.photo.com/bbb")
    15. )
    16. )
    17. val result = elasticsearchTemplate.save(obj)
    18. println(result)
    19. }

    即使上述代码中 AlbumListDo 中有各种注解标记,但是不会生效!!! es 会根据插入的数据,自动转化数据结构(无视你的注解).

    因此,建议先创建索引库和映射,再进行数据插入!

    1.3.3、简单的 CRUD

    1. import jakarta.annotation.Resource
    2. import org.cyk.es.model.AlbumInfoDo
    3. import org.junit.jupiter.api.Test
    4. import org.springframework.boot.test.context.SpringBootTest
    5. import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate
    6. import org.springframework.data.elasticsearch.core.document.Document
    7. import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates
    8. import org.springframework.data.elasticsearch.core.query.UpdateQuery
    9. @SpringBootTest
    10. class ElasticSearchCRUDTests {
    11. @Resource
    12. private lateinit var elasticsearchTemplate: ElasticsearchTemplate
    13. @Test
    14. fun testSave() {
    15. //保存单条数据
    16. val a1 = AlbumInfoDo(
    17. id = 1,
    18. userId = 10000,
    19. title = "今天天气真好",
    20. content = "学习完之后,我要出去好好玩"
    21. )
    22. val result = elasticsearchTemplate.save(a1)
    23. println(result)
    24. //保存多条数据
    25. val list = listOf(
    26. AlbumInfoDo(2, 10000, "西安六号线避雷", "前俯后仰。他就一直在那前后动。他背后是我朋友,我让他不要挤了,他直接就急了,开始故意很大力的挤来挤去。"),
    27. AlbumInfoDo(3, 10000, "字节跳动快上车~", "#内推 #字节跳动内推 #互联网"),
    28. AlbumInfoDo(4, 10000, "连王思聪也变得低调老实了", "如今的王思聪,不仅交女友的质量下降,在网上也不再像以前那样随意喷这喷那。显然,资金的紧张让他低调了许多")
    29. )
    30. val resultList = elasticsearchTemplate.save(list)
    31. resultList.forEach(::println)
    32. }
    33. @Test
    34. fun testDelete() {
    35. //根据主键删除,例如删除主键 id = 1 的文档
    36. elasticsearchTemplate.delete("1", AlbumInfoDo::class.java)
    37. }
    38. @Test
    39. fun testGet() {
    40. //根据主键获取文档
    41. val result = elasticsearchTemplate.get("1", AlbumInfoDo::class.java)
    42. println(result)
    43. }
    44. @Test
    45. fun testUpdate() {
    46. //例如,修改 id = 1 的文档
    47. val id = 1
    48. val title = "今天天气不太好"
    49. val content = "天气不好,只能在家里学习了。。。"
    50. val uq = UpdateQuery.builder(id.toString())
    51. .withDocument(
    52. Document.create()
    53. .append("title", title)
    54. .append("content", content)
    55. ).build()
    56. val result = elasticsearchTemplate.update(uq, IndexCoordinates.of("album_info")).result
    57. println(result.ordinal)
    58. println(result.name)
    59. }
    60. }

    1.3.4、三种构建搜索条件的方式

    关于搜索条件的构建,Spring 官网上给出了三种构建方式:Elasticsearch Operations :: Spring Data Elasticsearch

    a)CriteriaQuery:允许创建查询来搜索数据,而不需要了解 Elasticsearch 查询的语法或基础知识。它们允许用户通过简单地链接和组合 Criteria 对象来构建查询,Criteria 对象指定被搜索文档必须满足的条件。

    1. Criteria criteria = new Criteria("lastname").is("Miller")
    2. .and("firstname").is("James")
    3. Query query = new CriteriaQuery(criteria);

    b)StringQuery:这个类接受 Elasticsearch 查询作为 JSON String。下面的代码显示了一个搜索名为“ Jack”的人的查询:

    1. Query query = new StringQuery("{ \"match\": { \"firstname\": { \"query\": \"Jack\" } } } ");
    2. SearchHits searchHits = operations.search(query, Person.class);

    c)NativeQuery:当您有一个复杂的查询或者一个无法使用 Criteria API 表示的查询时,例如在构建查询和使用聚合时,可以使用 NativeQuery 类。

    d)到底使用哪一种呢?在最新的这一版 SpringDataES 中,NativeQuery 中可以通过大量的 Lambda 来构建条件语句,并且外观上也很符合 ElasticSearch DSL,那么对于比较熟悉原生的 DSL 语句的就建议使用 NativeQuery 啦.  我本人也更倾向 NativeQuery,因此后续的案例都会使用它.

    1.3.5、NativeQuery 搜索实战

    1. import co.elastic.clients.elasticsearch._types.SortOrder
    2. import co.elastic.clients.json.JsonData
    3. import jakarta.annotation.Resource
    4. import org.cyk.es.model.AlbumInfoDo
    5. import org.junit.jupiter.api.Test
    6. import org.springframework.boot.test.context.SpringBootTest
    7. import org.springframework.data.domain.PageRequest
    8. import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate
    9. import org.springframework.data.elasticsearch.client.elc.NativeQuery
    10. import org.springframework.data.elasticsearch.core.query.HighlightQuery
    11. import org.springframework.data.elasticsearch.core.query.highlight.Highlight
    12. import org.springframework.data.elasticsearch.core.query.highlight.HighlightField
    13. import org.springframework.data.elasticsearch.core.query.highlight.HighlightParameters
    14. @SpringBootTest
    15. class SearchTests {
    16. @Resource
    17. private lateinit var elasticsearchTemplate: ElasticsearchTemplate
    18. /**
    19. * 全文检索查询(match_all)
    20. */
    21. @Test
    22. fun testMatchAllQuery() {
    23. val query = NativeQuery.builder()
    24. .withQuery { q -> q
    25. .matchAll { it }
    26. }.build()
    27. val hits = elasticsearchTemplate.search(query, AlbumInfoDo::class.java)
    28. hits.forEach { println(it.content) }
    29. }
    30. /**
    31. * 精确查询(match)
    32. */
    33. @Test
    34. fun testMatchQuery() {
    35. val query = NativeQuery.builder()
    36. .withQuery { q -> q
    37. .match {
    38. it.field("title").query("天气")
    39. }
    40. }.build()
    41. val hits = elasticsearchTemplate.search(query, AlbumInfoDo::class.java)
    42. hits.forEach { println(it.content) }
    43. }
    44. /**
    45. * 精确查询(term)
    46. */
    47. @Test
    48. fun testTerm() {
    49. val query = NativeQuery.builder()
    50. .withQuery { q -> q
    51. .term { t -> t
    52. .field("id").value("2")
    53. }
    54. }.build()
    55. val hits = elasticsearchTemplate.search(query, AlbumInfoDo::class.java)
    56. hits.forEach { println(it.content) }
    57. }
    58. /**
    59. * 范围搜索
    60. */
    61. @Test
    62. fun testRangeQuery() {
    63. val query = NativeQuery.builder()
    64. .withQuery { q -> q
    65. .range { r -> r
    66. .field("id").gte(JsonData.of(1)).lt(JsonData.of(4)) // 大于等于 1,小于 4
    67. }
    68. }.build()
    69. val hits = elasticsearchTemplate.search(query, AlbumInfoDo::class.java)
    70. hits.forEach { println(it.content) }
    71. }
    72. /**
    73. * bool 复合搜索
    74. */
    75. @Test
    76. fun testBoolQuery() {
    77. val query = NativeQuery.builder()
    78. .withQuery { q -> q
    79. .bool { b -> b
    80. .must { m -> m
    81. .range { r -> r
    82. .field("id").gte(JsonData.of(1)).lt(JsonData.of(4)) // 大于等于 1,小于 4
    83. }
    84. }
    85. .mustNot { n -> n
    86. .match { mc -> mc
    87. mc.field("title").query("天气")
    88. }
    89. }
    90. .should { s -> s
    91. .matchAll { it }
    92. }
    93. }
    94. }.build()
    95. val hits = elasticsearchTemplate.search(query, AlbumInfoDo::class.java)
    96. hits.forEach { println(it.content) }
    97. }
    98. /**
    99. * 排序 + 分页
    100. */
    101. @Test
    102. fun testSortAndPage() {
    103. //a) 方式一
    104. // val query = NativeQuery.builder()
    105. // .withQuery { q -> q
    106. // .matchAll { it }
    107. // }
    108. // .withPageable(
    109. // PageRequest.of(0, 3) //页码(从 0 开始),非偏移量
    110. // .withSort(Sort.by(Sort.Order.desc("id")))
    111. // ).build()
    112. //b) 方式二
    113. val query = NativeQuery.builder()
    114. .withQuery { q -> q
    115. .matchAll { it }
    116. }
    117. .withSort { s -> s.field { f->f.field("id").order(SortOrder.Desc) } }
    118. .withPageable(PageRequest.of(0, 3)) //页码(从 0 开始),非偏移量)
    119. .build()
    120. val hits = elasticsearchTemplate.search(query, AlbumInfoDo::class.java)
    121. hits.forEach { println(it.content) }
    122. }
    123. @Test
    124. fun testHighLight() {
    125. //所有需要高亮的字段
    126. val highField = listOf(
    127. HighlightField("title"),
    128. HighlightField("content")
    129. )
    130. val query = NativeQuery.builder()
    131. .withQuery { q ->
    132. q.multiMatch { ma -> ma
    133. .fields("title", "content").query("天气")
    134. }
    135. }
    136. .withHighlightQuery(
    137. HighlightQuery(
    138. Highlight(
    139. HighlightParameters.builder()
    140. .withPreTags("") //前缀标签
    141. .withPostTags("") //后缀标签
    142. .withFragmentSize(10) //高亮的片段长度(多少个几个字需要高亮,一般会设置的大一些,让匹配到的字段尽量都高亮)
    143. .withNumberOfFragments(1) //高亮片段的数量
    144. .build(),
    145. highField
    146. ),
    147. String::class.java
    148. )
    149. ).build()
    150. val hits = elasticsearchTemplate.search(query, AlbumInfoDo::class.java)
    151. //hits.content 本身是没有高亮数据的,因此这里需要手动处理
    152. hits.forEach {
    153. val result = it.content
    154. //根据高亮字段名称,获取高亮数据集合
    155. val titleList = it.getHighlightField("title")
    156. val contentList = it.getHighlightField("content")
    157. if (titleList.size > 0) result.title = titleList[0]
    158. if (contentList.size > 0) result.content = contentList[0]
    159. println(result)
    160. }
    161. }
    162. }

    1.3.6、completionSuggestion 自动补全

    a)自动补全的字段必须是 completion 类型. 
    这里自动补全的字段为 title,通过 copyTo 将其拷贝到 suggestion 字段中,实现自动补全.
    1. import org.springframework.data.annotation.Id
    2. import org.springframework.data.elasticsearch.annotations.CompletionField
    3. import org.springframework.data.elasticsearch.annotations.Document
    4. import org.springframework.data.elasticsearch.annotations.Field
    5. import org.springframework.data.elasticsearch.annotations.FieldType
    6. import org.springframework.data.elasticsearch.core.suggest.Completion
    7. @Document(indexName = "album_doc")
    8. data class AlbumSugDo (
    9. @Id
    10. @Field(type = FieldType.Keyword)
    11. val id: Long,
    12. @Field(name = "user_id", type = FieldType.Long)
    13. val userId: Long,
    14. @Field(type = FieldType.Text, analyzer = "ik_max_word", copyTo = ["suggestion"]) //注意,copyTo 的字段一定是 var 类型
    15. val title: String,
    16. @Field(type = FieldType.Text, analyzer = "ik_smart")
    17. val content: String,
    18. @CompletionField(maxInputLength = 100, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
    19. var suggestion: Completion? = null, //注意,被 copyTo 的字段一定要是 var 类型
    20. )

    Ps:被 copyTo 的字段一定要是 var 类型

    b)需求:在搜索框中输入 “今天”,对其进行自动补全.

    1. import co.elastic.clients.elasticsearch.core.search.FieldSuggester
    2. import co.elastic.clients.elasticsearch.core.search.FieldSuggesterBuilders
    3. import co.elastic.clients.elasticsearch.core.search.Suggester
    4. import jakarta.annotation.Resource
    5. import org.cyk.es.model.AlbumSugDo
    6. import org.junit.jupiter.api.Test
    7. import org.springframework.boot.test.context.SpringBootTest
    8. import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate
    9. import org.springframework.data.elasticsearch.client.elc.NativeQuery
    10. import org.springframework.data.elasticsearch.core.suggest.response.Suggest
    11. @SpringBootTest
    12. class SuggestTests {
    13. @Resource
    14. private lateinit var elasticsearchTemplate: ElasticsearchTemplate
    15. @Test
    16. fun init() {
    17. if(elasticsearchTemplate.indexOps(AlbumSugDo::class.java).exists()) {
    18. elasticsearchTemplate.indexOps(AlbumSugDo::class.java).delete()
    19. }
    20. elasticsearchTemplate.indexOps(AlbumSugDo::class.java).create()
    21. elasticsearchTemplate.indexOps(AlbumSugDo::class.java).putMapping(
    22. elasticsearchTemplate.indexOps(AlbumSugDo::class.java).createMapping()
    23. )
    24. elasticsearchTemplate.save(
    25. listOf(
    26. AlbumSugDo(1, 10000, "今天发现西安真美", "西安真美丽啊,来到了钟楼...."),
    27. AlbumSugDo(2, 10000, "今天六号线避雷", "前俯后仰。他就一直在那前后动。他背后是我朋友,我让他不要挤了,他直接就急了,开始故意很大力的挤来挤去。"),
    28. AlbumSugDo(3, 10000, "字节跳动快上车~", "#内推 #字节跳动内推 #互联网"),
    29. AlbumSugDo(4, 10000, "连王思聪也变得低调老实了", "如今的王思聪,不仅交女友的质量下降,在网上也不再像以前那样随意喷这喷那。显然,资金的紧张让他低调了许多")
    30. )
    31. )
    32. }
    33. @Test
    34. fun suggestTest() {
    35. //模拟客户端输入的需要自动补全的字段
    36. val input = "今天"
    37. val limit = 10
    38. val fieldSuggester = FieldSuggester.Builder()
    39. .text(input) //用户输入
    40. .completion(
    41. FieldSuggesterBuilders.completion()
    42. .field("suggestion") //对哪个字段自动补全
    43. .skipDuplicates(true) //如果有重复的词条,自动跳过
    44. .size(limit) //最多显示 limit 条数据
    45. .build()
    46. )
    47. .build()
    48. val query = NativeQuery.builder()
    49. .withSuggester(Suggester.of { s -> s.suggesters("sug1", fieldSuggester) }) //参数一: 自定义自动补全名
    50. .build()
    51. val hits = elasticsearchTemplate.search(query, AlbumSugDo::class.java)
    52. val suggestList = hits.suggest
    53. ?.getSuggestion("sug1")
    54. ?.entries?.get(0)
    55. ?.options?.map(::map) ?: emptyList()
    56. println(suggestList)
    57. }
    58. private fun map(hit: Suggest.Suggestion.Entry.Option): String {
    59. return hit.text
    60. }
    61. }

    上述代码中的 hits 结构如下:

    运行结果:

  • 相关阅读:
    Spring AOP aspect切面指北
    在线制作作息时间表
    Vite3 + Vue2.7 环境搭建(TS)
    mysql varchar和bigint比较的坑
    React Redux 如何更新购物车中的产品数量
    C#/VB.NET 将PDF转为Excel
    C#的关于窗体的类库方案 - 开源研究系列文章
    Simulink 最基础教程(三)常用模块
    智能网联汽车终端T-BOX应用方案——主控芯片ACM32F403、安全芯片S6A/S6B
    DRF的认证组件(源码分析)
  • 原文地址:https://blog.csdn.net/CYK_byte/article/details/138721140