返回 导航

SpringBoot / Cloud

hangge.com

SpringBoot - 实现基于Elasticsearch+HBase全文检索系统教程3(建立索引及搜索数据)

作者:hangge | 2025-05-31 09:34

五、通过 Elasticsearch 对 HBase 中的数据建立索引

1,数据字段分析

(1)通过 ElasticsearchHBase 中的数据建立索引在开发数据索引功能之前,需要先根据需求设计 Elasticsearch 索引库的 settingmapping 信息:
  • setting 信息主要包括索引库的分片和副本参数。 
  • mapping 信息主要包括字段的类型、是否存储、是否建立索引等参数。 
    • 字段的类型:根据字段内容的具体类型进行选择,常见的是字符串、数字和日期类型。
    • 是否存储:如果需要从 Elasticsearch 中返回这个字段的内容,则需要存储。
    • 是否建立索引:如果需要在 Elasticsearch 中根据这个字段进行查询,则需要建立索引。

(2)文章数据主要包括文章 ID、标题、来源、正文和时间这些字段。
  • 文章数据的核心内容主要在“标题”“正文”这 2 个字段中,所以在查询时需要用到这 2 个字段。
  • 而在全文搜索系统的列表页面中,需要显示“标题”“来源”和“时间”这 3 个字段。具体显示哪些字段,可以根据工作中的具体需求而定,这些字段信息是需要通过 Elasticsearch 直接返回的。
  • 文章 ID 在这里直接作为 Elasticsearch 中数据的 IDElasticsearch 中的 ID 是必须要存储和建立索引的。

(3)通过上述分析,我们可以总结文章相关字段的索引存储情况如下:
  • 文章 ID:需要建立素引并且存储,这是 ElasticsearchID 字段必须具备的特性。
  • 标题:因为查询时会用到,所以需要建立索引;在返回结果列表信息时需要直接从 Elasticsearch 中返回,所以需要存储。
  • 来源:查询时用不到,所以不需要建立索引。但是需要在返回结果列表信息时一起返回,所以需要存储。
  • 正文:查询时会用到,所以需要建立索引。但是在返回结果列表信息时不需要返回这个字段,所以不需要存储。其实还有一个很重要的原因:这个字段的内容太长了,如果在 Elasticsearch 中存储会额外占用很多的存储空间,最终影响 Elasticsearch 的性能。
  • 时间:查询时用不到,所以不需要建立索引。但是需要在返回结果列表信息时一起返回,所以需要存储。
比较项 是否建立索引 是否存储
文章 IDid)
标题(title)
来源(src)
正文(content)
时间(time)

2,创建索引库 article。

(1)我们创建一个 article.json 文件,在文件中指定索引库的 settingmapping 配置信息。
内容说明如下:
  • dynamic:此参数有 4 个选项值。
    • true:默认值,表示开启动态映射,此时 Elasticsearch 会自定识别字段的数据类型。
    • false:忽略在 mapping 中没有定义的字段。
    • strict:在遇到在 mapping 中没有指定的未知字段时抛出异常。
    • runtime:在遇到在 mapping 中没有指定的未知字段时,将它作为运行时字段。运行时字段是在 Elasticsearch 7.11 版本中增加的。运行时字段不会被索引,但是可以从 _source 中获取运行时字段的内容。所以,运行时字段可以适合公共字段已知且想兼容未知扩展字段的场景。
  • _sourceElasticsearch 中的特殊字段,默认会存储 Elasticsearch 中所有字段的内容。由于 content 字段不需要从 Elasticsearch 中返回,所以通过 excludes 参数将 content 字段排除掉,不在 _source 中存储 content 字段的内容。
  • index:是否在 Elasticsearch 中建立索引,默认为 true。如果此参数的值是 true,则在 mapping 中可以省略不写。
  • analyzer:对于需要建立索引的字段指定分词器。这里使用 IK 分词器。
  • store:是否在 Elasticsearch 中存储,默认为 false。如果此参数的值为 false,则在 mapping 中可以省略不写。因为所有的字段的内容都会在 _source 字段中存储,所以就不需要再单独存储每个字段的内容了,否则会造成重复存储。
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1
  },
  "mappings": {
    "dynamic": "false",
    "_source": {
      "excludes": ["content"]
    },
    "properties": {
      "title": {
        "type": "text", "analyzer": "ik_max_word"
      },
      "src": {
        "type": "text", "index": false
      },
      "content": {
        "type": "text", "analyzer": "ik_max_word"
      },
      "time": {
        "type": "date", "index": false, "format": "yyyy-MM-dd'T'HH:mm:ss"
      }
    }
  }
}

(2)接着执行如下命令创建索引库 article
注意:需要确认在 Elasticsearch 集群中已经集成了 IK 分词器,否则执行时会报错。关于 IK 分词器的安装配置,可以查看我之前写的文章(点击访问
curl -H "Content-Type:application/json" -XPUT 'http://node1:9200/article' -d @article.json

(3)创建完毕后,执行如下命令确认索引库 articlemapping 信息:
curl -XGET 'http://node1:9200/article/_mapping?pretty'

3,代码实现

(1)这里我们使用 Spring Data Elasticsearch 来操作 Elasticsearch。因此首先需要使用注解来定义实体类,从而告诉 Spring Data Elasticsearch 如何映射数据到 Elasticsearch 中的文档。下面我们创建一个表示新闻文章信息的 Article 实体类。
注意:书籍的标题字段 title 和简介字段 introduction 配置了 IK 分词器以支持后续的中文检索。ik_max_wordik_smart 区别:
  • ik_max_word 会将文本做最细粒度的拆分。
  • ik_smart 会做最粗粒度的拆分。
@Document(indexName = "article", createIndex = false)
@Data
public class Article {
  @Id
  private String id; // 文章的唯一标识符

  @Field(type = FieldType.Text, analyzer = "ik_max_word")
  private String title; // 书籍的标题

  @Field(type = FieldType.Text, index = false)
  private String src; // 文章的来源

  @Field(type = FieldType.Text, analyzer = "ik_max_word")
  private String content; // 文章的内容

  @Field(type = FieldType.Date, index = false, format = DateFormat.date_hour_minute_second)
  @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
  private Date time; // 文章的时间
}

(2)接着需要编写 Elasticsearch Repository 接口 在 Spring Data Elasticsearch 中,可以使用 Repository 接口来定义 Elasticsearch 的增删改查操作。下面我们定义一个 ArticleRepository 接口,继承自 ElasticsearchRepository,并指定实体类(Article)和主键的类型(String
public interface ArticleRepository extends ElasticsearchRepository<Article, String> {
}

(3)最后则是建立索引的业务代码,具体逻辑是不断从 Redis 列表中获取文章的 ID,然后通过该 IDHBase 数据库中获取文章的详细信息,将获取到的数据转换为一个 Article 对象,并将其保存到 Elasticsearch 中建立索引。循环直到 Redis 列表为空,完成对所有文章的处理。
@GetMapping("/index")
public void index() throws ParseException {
  // 循环直到列表为空
  while (true) {
    // 从Redis获取一个Rowkey
    String id = stringRedisTemplate.opsForList().rightPop("l_article_ids");

    if (id != null) {
      // HBase表名为"article",列族为"info"
      String tableName = "article";
      String cf = "info";

      // 根据Rowkey从HBase获取数据的详细信息
      Map<String, String> data = hbaseUtils.getData(tableName, id);

      // 在ES中对数据建立索引
      if (data != null) {
        Article article = new Article();
        article.setId(id);
        article.setTitle(data.get(cf + ":title"));
        article.setSrc(data.get(cf + ":src"));
        article.setContent(data.get(cf + ":content"));
        // 将日期字符串转换为日期对象
        LocalDateTime localDateTime = LocalDateTime.parse(data.get(cf + ":time")+"T00:00:00");
        ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Shanghai"));
        Date date = Date.from(zonedDateTime.toInstant());
        article.setTime(date);

        // 记录索引数据
        articleRepository.save(article);
        System.out.println(article.getTitle() + " - 保存成功");
      } else {
        System.out.println("未找到ID为" + id + "的数据");
      }
    } else {
      // 如果列表为空,则中断循环
      System.out.println("Redis中没有更多的文章ID。");
      break;
    }
  }
}

4,运行测试

(1)我们启动项目,然后访问项目的 /index 接口开始建立索引:

(2)然后执行如下命令验证 Elasticsearch 中的数据是否生成成功,重点关注 _source 字段中星否包台 content 字段,如果包含此字段的内容则说明前面的 mapping 配置有问题。如果不包台此字段的内容则说明是正确的,我们只是索引 content 字段,并没有在 ES 存储该字段内容。

六、通过 Elasticsearch 搜索数据

1,代码实现

(1)首先我们对 ArticleRepository 接口进行修改,增加了一个 findByTitleOrContent 接口方法,作用是根据关键词在文章标题或内容中进行全文检索,并返回匹配的文章列表。
public interface ArticleRepository extends ElasticsearchRepository<Article, String> {

  @Query(value =
        "{"
          + "\"bool\": {"
            + "\"should\": ["
              + "{ \"match\": { \"title\": \"?0\" } },"
              + "{ \"match\": { \"content\": \"?0\" } }"
            + "]"
          + "}"
        + "}"
  )
  List<Article> findByTitleOrContent(String keyword);
}

(2)接着是搜索数据的业务代码,调用 ArticleRepository 中的全文检索方法根据客户端输入的关键词在文章标题或内容中查询匹配的文章列表,然后通过 HBase 数据库获取每篇文章的详细内容,并返回包含完整内容的文章列表。
@GetMapping("/search/{keyword}")
public List<Article> search(@PathVariable("keyword") String keyword) {
  // 通过articleRepository根据标题或内容查询文章列表
  List<Article> articles = articleRepository.findByTitleOrContent(keyword);
  // 遍历查询到的文章列表
  for (Article article : articles) {
    // HBase表名为"article",列族为"info"
    String tableName = "article";
    String cf = "info";
    // 从HBase中获取content正文数据
    String content = hbaseUtils.getData(tableName, article.getId(), cf, "content");
    // 将获取到的content设置到文章对象的content属性中
    article.setContent(content);
  }
  // 返回查询到的文章列表
  return articles;
}

2,运行测试

(1)我们启动项目,然后访问项目的 /search 接口,并在接口后面带上关键词进行搜索,例如:
http://localhost:8080/search/黄岩岛菲律宾

(2)该接口便会返回匹配的文章数据:

评论

全部评论(0)

回到顶部