飞道的博客

springboot 搜索引擎项目使用到的技术

344人阅读  评论(0)

目录

构建索引模块

SSM框架

stream流

分词模块

 mybaits

搜索模块

 前端

后端


构建索引模块

SSM框架

        首先我们用到了很多 SSM 框架的注解简化了很多重复的工作,同时我们不必关心对象的管理,这些都由 IoC 容器(Spring) 完成了。

Spring 中常用注解:

存取对象相关的:


  
  1. // 存对象用到的注解
  2. // 五大类注解
  3. // 控制器
  4. @Controller
  5. // 配置相关的
  6. @Configuration
  7. // 组件,经常用到的工具类
  8. @Component
  9. // 服务层,对数据进行组合转换等处理
  10. @Service
  11. // 仓库,数据持久层,主要是和数据库有关的类
  12. @Repository
  13. // 方法注解,对象的类型由方法返回类型决定
  14. // 方法注解必须配合五大类注解进行使用
  15. @Bean
  16. // 取对象用到的注解
  17. // 对象注入有三种方法 1.字段注入 2.构造方法注入(官方推荐) 3.setter注入
  18. // spring 框架提供的
  19. @Autowired
  20. // JDK 提供的
  21. @Resource
  22. // 配合 @Autowired 一起使用的
  23. @Qualifier

spring MVC 相关的:

路由相关的:


  
  1. // Spring Web 应用程序中最常被用到的注解之一,它是用来注册接口的路由映射的。
  2. @RequestMapping
  3. // 等效于 @RequestMapping(Method=RequestMethod.POST)
  4. @PostMapping
  5. // 等效于 @RequestMapping(Method=RequestMethod.GET)
  6. @GetMapping

获取前端参数相关的:


  
  1. // 后端参数映射(重命名) 前端参数和后端参数名不一样时使用
  2. // 这个注解默认参数必传,如果非必传 需设置 required = false
  3. @RequestMapping
  4. // 接受 JSON 对象
  5. @RequestBody
  6. // 获取路径中的参数 配合 @**Mapping 中的 ${} 一起使用
  7. @PathVariable
  8. // 上传文件
  9. @RequestPart
  10. // 获取 cookie
  11. @CookieValue
  12. // 获取 header
  13. @RequestHeader
  14. // 获取 session
  15. @SessionAttribute

获取非静态页面数据:


  
  1. // 返回非静态页面,如果没有那一般返回的就是静态页面的 url html,js这类的
  2. // @ResponseBody 返回的值如果是字符会转换成 text/html,如果返回的是对象会转换成
  3. application/json 返回给前端。
  4. @ResponseBody

组合注解


  
  1. // 等于 @Controller + @ResponseBody
  2. @RestController

日志相关的:


  
  1. // 获取日志对象
  2. // 等价于 Logger log = LoggerFactory.getLogger(类.class);
  3. @Slf4j

stream流

        由于构建索引时,我们需要用到很多的 集合 ,而Java 8 中的 Stream 是对集(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。配合lambda表达式,可以使得代码更精简。


  
  1. @Override
  2. public void run (String... args) throws Exception {
  3. ToAnalysis.parse( "随便分个什么,进行预热,避免优化的时候计算第一次特别慢的时间");
  4. log.info( "这里的整个程序的逻辑入口");
  5. // 1. 扫描出来所有的 html 文件
  6. log.debug( "开始扫描目录,找出所有的 html 文件。{}", docRootPath);
  7. List<File> htmlFileList = fileScanner.scanFile(docRootPath);
  8. log.debug( "扫描目录结束,一共得到 {} 个文件。", htmlFileList.size());
  9. // 2. 针对每个 html 文件,得到其 标题、URL、正文信息,把这些信息封装成一个对象(文档 Document)
  10. File rootFile = new File(docRootPath);
  11. List<Document> documentList = htmlFileList
  12. .stream()
  13. .parallel()
  14. .map(file -> new Document(file,urlPrefix,rootFile))
  15. .collect(Collectors.toList());
  16. log.debug( "构建文档完毕,一共 {} 篇文档", documentList.size());
  17. // 3. 进行正排索引的保存
  18. indexManager.saveForwardIndexesConcurrent(documentList);
  19. log.debug( "正排索引保存成功。");
  20. // 4. 进行倒排索引的生成核保存
  21. indexManager.saveInvertedIndexesConcurrent(documentList);
  22. log.debug( "倒排索引保存成功。");
  23. // 5. 关闭线程池
  24. executorService.shutdown();
  25. }

        在这个代码中,我们需要扫描出根目录中(包含所有子文件夹)所有的 html 文件,并且将这些文件转变成我们需要的 Document 对象以便后续进行分词和权重的计算。stream 像是一个管道。stream()将集合变成一个流,parallel()表示使用并行流,使用多核cpu时可以显著的提升速度,map()表示一个映射,将原来集合的 File 类型 映射成 我们需要的 Document 类型。collect()就是一个收集器,将管道中的内容收集到我们需要的集合中。

分词模块

使用了第三方的分词库 ansj

添加如下依赖


  
  1. <dependency>
  2. <groupId>org.ansj </groupId>
  3. <artifactId>ansj_seg </artifactId>
  4. <version>5.1.6 </version>
  5. </dependency>

导包

import org.ansj.splitWord.analysis.ToAnalysis;

使用 ToAnalysis.parse() 进行分词,保存到集合中


  
  1. //对正文进行分词,的到一个 contentWordList
  2. Result parseResultOfContent = ToAnalysis.parse(content);
  3. List<String> contentWordList = parseResultOfContent
  4. .getTerms()
  5. .stream()
  6. .parallel()
  7. .map(Term::getName)
  8. .filter(s -> !ignoredWordSet.contains(s))
  9. .collect(Collectors.toList());

 mybaits

        MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型。简单来说 MyBatis 是更简单完成程序和数据库交互的工具,也就是更简单的操作和读取数据库工具。mybatis 是介于 java 和 数据库之间的一个框架,帮我们干了 jdbc 的很多事:

1. 创建数据库连接池 DataSource
2. 通过 DataSource 获取数据库连接 Connection
3. 编写要执行带 ? 占位符的 SQL 语句
4. 通过 Connection 及 SQL 创建操作命令对象 Statement
5. 替换占位符:指定要替换的数据库字段类型,占位符索引及要替换的值
6. 使用 Statement 执行 SQL 语句
7. 查询操作:返回结果集 ResultSet,更新操作:返回更新的数量
8. 处理结果集
9. 释放资源

我们只需要关注如何写好 **Mapper.xml、sql语句和接口就好了       

 在 Mapper.xml 中进行如下配置:


  
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.yukuanyan.indexer.mapper.IndexMapper">
  4. <insert id="batchInsertForwardIndexes" useGeneratedKeys="true" keyProperty="docId" keyColumn="docid">
  5. insert into forward_indexes (title, url, content) values
  6. <!-- 一共有多少条记录,得根据用户传入的参数来决定,所以这里采用动态 SQL 特性 -->
  7. <foreach collection="list" item="doc" separator=", ">
  8. (#{doc.title}, #{doc.url}, #{doc.content})
  9. </foreach>
  10. </insert>
  11. <!-- 不关心自增 id -->
  12. <insert id="batchInsertInvertedIndexes">
  13. insert into inverted_indexes (word, docid, weight) values
  14. <foreach collection="list" item="record" separator=", ">
  15. (#{record.word}, #{record.docId}, #{record.weight})
  16. </foreach>
  17. </insert>
  18. </mapper>

insert 标签对应插入操作 id 对应 接口中方法的名字 useGeneratedKeys 表示数据库表中的主键使用生成的 id

keyProperty 对应 方法中插入对象中的属性 keyColumn 对应 表中的 列属性.

#{} 和 ${} 都是 参数占位符。前者预编译处理。后者字符直接替换。

因为 ${} 是字符直接替换,会有 sql注入问题,因此能不用我们尽量不用。如果一定要使用,要在 controller 层对参数进行安全校验

但是 #{} 也不是万能的,它会将被替换的字符串加上单引号,因此如果参数是sql关键字(sql命令时)例如里面是定义排序的内容(asc / desc)就会出错,我们尽量在被替换内容是数据的时候使用

IndexerMapper 接口


  
  1. package com.yukuanyan.indexer.mapper;
  2. import com.yukuanyan.indexer. module.Document;
  3. import com.yukuanyan.indexer. module.InvertedRecord;
  4. import org.apache.ibatis.annotations.Mapper;
  5. import org.apache.ibatis.annotations.Param;
  6. import org.springframework.stereotype.Repository;
  7. import java.util.List;
  8. @Mapper
  9. @Repository
  10. public interface IndexMapper {
  11. //批量插入正排索引
  12. public void batchInsertForwardIndexes (@Param("list") List<Document> documentList);
  13. //批量插入倒排索引
  14. public void batchInsertInvertedIndexes (@Param("list") List<InvertedRecord> recordsList);
  15. }

插入操作:首先在需要的类中 注入 IndexerMapper 类,再使用里面的方法就可以进行插入了


  
  1. Runnable task = new Runnable() {
  2. @Override
  3. public void run () {
  4. List<Document> subList = documentList.subList(from, to);
  5. //对 subList 进行批量操作
  6. indexMapper.batchInsertForwardIndexes(subList);
  7. //每次批量插入操作完成之后,latch 的个数就减一
  8. latch.countDown();
  9. }
  10. };

搜索模块

 前端

只有两个页面,一个是搜索的主页,一个是显示搜索结果的页面。搜索的主页是一个静态资源,写在index.html内

首页的设计借鉴了青柠起始页的设计,具体细节如下:甲方你请说:仿青柠搜索页模态搜索栏(HTML+CSS+JS)_哔哩哔哩_bilibili

搜索页再主页输入搜索词,点击搜索之后进入。,使用了 thmeleaf 模板技术

后端

搜索结果展示对应后端 controller 下的一个 方法。设置路径为 ”/web"

首先对参数进行合法性校验


  
  1. log.debug( "查询: query = {}", query);
  2. // 参数的合法性检查 + 处理
  3. if (query == null) {
  4. log.debug( "query 为 null,重定向到首页");
  5. return "redirect:/";
  6. }
  7. query = query.trim().toLowerCase();
  8. if (query.isEmpty()) {
  9. log.debug( "query 为空字符串,重定向到首页");
  10. return "redirect:/";
  11. }

对查询字段进行分词


  
  1. List<String> queryList = ToAnalysis.parse(query)
  2. .getTerms()
  3. .stream()
  4. .map(Term::getName)
  5. .collect(Collectors.toList());
  6. if (queryList.isEmpty()) {
  7. log.debug( "query 分词后一个词都没有,重定向到首页");
  8. return "redirect:/";
  9. }
  10. log.debug( "进行查询的词: {}", query);

对所有的查询词进行查询,将所有结果保存到集合中


  
  1. List<DocumentWithWeight> totalList = new ArrayList<>();
  2. for (String s : queryList) {
  3. List<DocumentWithWeight> documentList = mapper.queryWithWeight(s, limit, offset);
  4. totalList.addAll(documentList);
  5. }

由于可能有多个查询词,需要对不同的查询词进行权重聚合


  
  1. Map<Integer, DocumentWithWeight> documentMap = new HashMap<>();
  2. for (DocumentWithWeight documentWithWeight : totalList) {
  3. int docId = documentWithWeight.getDocId();
  4. if (documentMap.containsKey(docId)) {
  5. DocumentWithWeight item = documentMap.get(docId);
  6. item.weight += documentWithWeight.weight;
  7. continue;
  8. }
  9. DocumentWithWeight item = new DocumentWithWeight(documentWithWeight);
  10. documentMap.put(docId, item);
  11. }

对聚合后的结果进行排序,由于集合没有排序的概念,我们需要转变为线性结构才能进行排序


  
  1. Collection<DocumentWithWeight> values = documentMap.values();
  2. // Collection 没有排序这个概念(只有线性结构才有排序的概念),所以我们需要一个 List
  3. List<DocumentWithWeight> list = new ArrayList<>(values);
  4. // 按照 weight 的从大到小排序了
  5. Collections.sort(list, (item1, item2) -> {
  6. return item2.weight - item1.weight;
  7. });

将结果交给模板,由模板去渲染


  
  1. model.addAttribute( "query", query);
  2. model.addAttribute( "docList", documentList);
  3. model.addAttribute( "page", page);


转载:https://blog.csdn.net/m0_53653818/article/details/128512573
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场