网站建设介绍怎么写,铁路建设工程网站,wordpress 视频 slider,金融企业网站php源码一、项目背景 搜索引擎是现代设备中被广泛利用的一种系统软件#xff0c;诸如百度、谷歌、搜索、bing等#xff0c;或者抖音、快手、b站、小红书#xff0c;甚至软件应用市场#xff0c;Windows#xff08;操作系统#xff09;中的各类提供搜索功能的背后都有搜索引擎的影…一、项目背景 搜索引擎是现代设备中被广泛利用的一种系统软件诸如百度、谷歌、搜索、bing等或者抖音、快手、b站、小红书甚至软件应用市场Windows操作系统中的各类提供搜索功能的背后都有搜索引擎的影子。
二、使用技术
Spring SpringMVC Mybatis
Spring 负责提供IoC、AOP
SpringMVC 负责提供Web 业务处理
Mybatis 负责提供方便 SQL 处理
三、项目功能
根据用户检索的内容把检索到的相关信息展现给用户。
四、整体逻辑图 五、具体实现
1.基本流程用户角度
用户输入搜索词一个词或者多个词在已有文档中找到文档包含这些词的所有文档信息再给出搜索后的列表。
2.设计
1初步想法不可行
首先我们可以分析到这实际上就是需要一个文档表里面记录他的 id、标题、内容。然后在数据库中查找select * from 文档表 where 标题 like %搜索词% or 内容 like %搜索词%
但我们不使用这种方式SQL因为上述 SQL 的查找性能非常差。
文档个数记为 m文档的平均长度标题 内容记为 n。O(m*n)。
现实中m 非常大几百亿篇文档所以从性能上这个方案不可行。
2可实行的方法
使用倒排索引inverted index
倒排索引的大概结构key-value 形式。key词value词出现在哪些文档中。
①提前构造好倒排索引倒排索引中有 id、单词、这个单词对应的文章编号、这个单词的权重。
②当我们比如说去搜索 “list” 这个单词的时候根据倒排索引我们可以找到这个单词出现在哪些文档中根据文档的编号取出对应的文档内容。
③我们再维护一个正排索引这里面有 docid文章的编号、文章标题、文章的 URL、文章的内容
④当我们根据倒排索引搜索到单词对应的文章id时比如只取前 20 篇文章那我们就只需要进行 20 次正排索引查找即可。
补充文档是什么文档不重要可以是 html、pdf、图片、视频等等。
我们经常用到的搜索引擎百度、搜狗他背后的数据获取一般使用爬虫自动在互联网上搜集信息将所有内容爬成文档下来然后进行检索和排序等操作。
我们这个项目只针对 JDK API 文档库中的 html 做搜索
3构建两大模块 3.构建索引模块
不需要使用 web 功能只需要执行一次
我们写一个 Indexer 类indexer/command/Indexer这是构建索引的模块是整个程序的逻辑入口。Slf4j ——添加 Spring 日志的使用 Component —— 注册成 Spring 的 bean 我们让这个类实现 CommandLineRunner 接口。
Component
public class Indexer implements CommandLineRunner {Overridepublic void run(String... args) throws Exception {log.info(这里的整个程序的逻辑入口);}
}知识补充 CommandLineRunner 接口 Spring boot的CommandLineRunner接口主要用于实现在应用初始化后去执行一段代码块逻辑这段初始化代码在整个应用生命周期内只会执行一次。 使用CommandLineRunner接口和Component注解一起使用 为什么要使用CommandLineRunner接口 实现在应用启动后去执行相关代码逻辑且只会执行一次spring batch批量处理框架依赖这些执行器去触发执行任务我们可以在run()方法里使用任何依赖因为它们已经初始化好了 构造索引的大概步骤
1.扫描文档目录下的所有文档目录遍历的过程 FileScanner // 1. 扫描出来所有的 html 文件log.debug(开始扫描目录找出所有的 html 文件。{}, properties.getDocRootPath());ListFile htmlFileList fileScanner.scanFile(properties.getDocRootPath(), file - {return file.isFile() file.getName().endsWith(.html);});log.debug(扫描目录结束一共得到 {} 个文件。, htmlFileList.size());
我们把这个类注册成 Spring bean —— Service 知识补充 1、Service注解 是标注在实现类上的因为 Service 是把 spring 容器中的 bean 进行实例化也就是等同于 new操作只有实现类是可以进行 new 实例化的而接口则不能所以是加在实现类上的。 2、要说明Service注解 的使用就得说一下我们经常在 spring 配置文件applicationContext.xml中看到如下图中的配置 !-- 采用扫描 注解的方式进行开发 可以提高开发效率后期维护变的困难了可读性变差了 --
context:component-scan base-packagecom.study.persistent / 在applicationContext.xml配置文件中加上这一行以后将自动扫描指定路径下的包如果一个类带了 Service注解将自动注册到 Spring容器不需要再在applicationContext.xml配置文件中定义 bean 了类似的还包括 Component、Repository、Controller。 具体在 indexer.util.FileScanner 中完成。
以 rootPath 作为根目录开始进行文件的扫描把所有符合条件的 File 对象作为结果以 List 形式返回把这个过程想象成一棵树针对目录树进行遍历深度优先 or 广度优先即可确保每个文件都没遍历到即可我们这里采用深度优先遍历使用递归完成 1. 先通过目录得到该目录下的孩子文件有哪些
File[] files directoryFile.listFiles(); 2. 遍历每个文件检查是否符合条件
for (File file : files) {// 通过 filter.accept(file) 的返回值判断是否符合条件if (filter.accept(file)) {// 说明符合条件需要把该文件加入到结果 List 中resultList.add(file);}} 3. 遍历每个文件针对是目录的情况继续深度优先遍历递归
for (File file : files) {if (file.isDirectory()) {traversal(file, filter, resultList);}} 2.针对每一篇文档进行分析、处理
得到文档的 标题这里就把他的文件名作为标题、最终访问的 URL实际上是一个相对路径、文档下的内容IO 读操作
标题 URL 进行分词后才能得到倒排索引中保存的key也就是你想要搜索的词。
这个分词引入第三方库来做NLP // 2. 针对每个 html 文件得到其 标题、URL、正文信息把这些信息封装成一个对象文档 DocumentFile rootFile new File(properties.getDocRootPath());ListDocument documentList htmlFileList.stream().parallel() // 【注意】由于我们使用了 Stream 用法所以可以通过添加 .parallel()使得整个操作变成并行利用多核增加运行速度.map(file - new Document(file, properties.getUrlPrefix(), rootFile)).collect(Collectors.toList());log.debug(构建文档完毕一共 {} 篇文档, documentList.size());
具体在 indexer.model.Document 中完成
扫描出来所有的 html 文件需要依赖FileScanner 对象构造方法注入的方式让 Spring 容器注入 FileScanner 对象进来
这里最好把要扫描的文件路径放在配置文件src/main/resources/application.yml中这样有利于以后修改会更方便
ListFile htmlFileList fileScanner.scanFile(properties.getDocRootPath(), file - {return file.isFile() file.getName().endsWith(.html);});
searcher:indexer:doc-root-path: E:\java程序\docs\apiurl-prefix: https://docs.oracle.com/javase/8/docs/api/
之后用 properties 的方式来读取
package com.lingqi.searcher.indexer.properties;import lombok.*;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;Component // 是注册到 Spring 的一个 bean
ConfigurationProperties(searcher.indexer)
Data // Getter Setter ToString EqualsAndHashCode
public class IndexerProperties {// 对应 application.yml 配置下的 searcher.indexer.doc-root-pathprivate String docRootPath;
}针对每个 html 文件得到其 标题、URL、正文信息把这些信息封装成一个对象文档 Document——model/Document)
标题从文件名中将 .html 后缀去掉剩余的看作标题 private String parseTitle(File file) {// 从文件名中将 .html 后缀去掉剩余的看作标题String name file.getName();String suffix .html;return name.substring(0, name.length() - suffix.length());}
URL需要得到一个相对路径file 相对于 rootFile 的相对路径 。 比如rootFile 是 E:\java程序\docs\api 。 file 是 E:\java程序\docs\api\javax\sql\DataSource.html 则相对路径就是javax\sql\DataSource.html 把所有反斜杠(\) 变成正斜杠(/)
最终得到 java/sql/DataSource.html
正文信息
随便打开一篇文档 我们需要做的是 1.script... ... /sprict 这是我们不要的 2.所有标签都不要pHello/ppWorld/p 我们要的只是 Hello World 3.所有的换行符替换成 空格 我们只保留纯文本内容用来做分词 因此使用正则表达式完成上述工作 知识补充 正则表达式 . 匹配 任意 字符匹配多少个需要根据后续字符来判断默认就是一个 /s 匹配任意空格包括 \t\r\n\r\n /d 匹配任意数字只有一位0-9 a 匹配 a出现的次数 1次 a* 匹配 a出现的次数 0次 . 匹配任意字符出现次数 1 次 .* 匹配任意字符出现次数 0 次 ? 可选的 先把文章一行一行全部读取到 StringBuilder 中再把 StringBuilder 中不需要的那些内容进行删除或者替换
return contentBuilder.toString()// 首先去掉 script ....../script.replaceAll(script.*?.*?/script, )// 去掉标签.replaceAll(.*?, )// 把最后多出来的空格删除掉.replaceAll(\\s, ).trim(); 3.进行正排索引的保存
拿到分词后我们就知道每一篇文档的 标题、URL、内容、标题和内容的每个词
利用上述信息就可以构建索引了。正排索引、倒排索引 正排索引有1W条数据倒排索引有600W条。如果一条一条插入需要循环600W次才可以插入完成。但我们可以设置成一次插入1000条数据这样只需要循环6000次就可以了。
因此我们只需要两张表存在数据库中
正排索引表docid-pk、title、url、content整体数量级不大只有1W条但是每一条比较大content大——批量插入的时候每次记录不用太多每次插入 10 条
倒排索引id-pk、word、docid、weight整体数量级较大有600W条每一条的记录比较小——批量插入的时候每次记录多插入一些每次插入 1W条
我们docid的生成方式利用 MySQL 的表中的自增机制作为docid MySQL 批量插入语法 insert into forward_indexs(title,url,content) values (1,2,3), (4,5,6), (7,8,9) 既然需要批量插入我就要用到 mybatis 的动态SQL特性 遍历 collectionlist其中下标保存在 index index“index”其中遍历时的每一项保存在itemitem“item” 写sql的
Repository // 注册 Spring bean
Mapper // 是一个 Mybatis 管理的 Mapper
public interface IndexDatabaseMapper {// 正排void batchInsertForwardIndexes(Param(list) ListDocument documentList);//倒排void batchInsertInvertedIndexes(Param(list) ListInvertedRecord recordList);
}
准备一个xml
?xml version1.0 encodingUTF-8 ?
!DOCTYPE mapperPUBLIC -//mybatis.org//DTD Mapper 3.0//ENhttp://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.lingqi.searcher.indexer.mapper.IndexDatabaseMapper在配置文件中也加入。在 Spring 的配置文件中指定 mybatis 查找 mapper xml 文件的路径
classpath代表从 src/main/resources 下进行查找这实际上是错误的理解暂且可以这么简单理解关系不大 index-mapper.xml文件中设置的
mapper namespacecom.lingqi.searcher.indexer.mapper.IndexDatabaseMapper
实际上对应的就是 我们用于写sql 的类 IndexDatabaseMapper。
因为我们这里实际上就是一个插入所以是 insert 语句因此我们在index-mapper.xml中写入insert语句 sql语句写在index-mapper.xml中 最终的到的 sql 就是拼接好的sql。
insert idbatchInsertForwardIndexes useGeneratedKeystrue keyPropertydocId keyColumndocidinsert into forward_indexes (title, url, content) values!-- 一共有多少条记录得根据用户传入的参数来决定所以这里采用动态 SQL 特性 --foreach collectionlist itemdoc separator, (#{doc.title}, #{doc.url}, #{doc.content})/foreach
/insert 我们在 indexer/core/IndexManager 中完成插入
批量生成、保存正排索引 indexManager.saveForwardIndexesConcurrent(documentList); 单线程版本
1. 批量插入时每次插入多少条记录由于每条记录比较大所以这里使用 10 条就够了
int batchSize 10;
2. 一共需要执行多少次 SQL 向上取整(documentList.size() / batchSize)
int listSize documentList.size();
int times (int) Math.ceil(1.0 * listSize / batchSize); // ceil(天花板): 向上取整
3. 开始分批次插入
for (int i 0; i listSize; i batchSize) {// 从 documentList 中截取这批要插入的 文档列表使用 List.subList(int from, int to)int from i;int to Integer.min(from batchSize, listSize);ListDocument subList documentList.subList(from, to);// 针对这个 subList 做批量插入mapper.batchInsertForwardIndexes(subList);
}
多线程版本
前面两步和单线程版本相同在第三步循环插入中把他们放到任务中去执行
我们需要一个线程池定义在 indexer/config/AppConfig 中
Configuration
public class AppConfig {Beanpublic ExecutorService executorService() {ThreadPoolExecutor executor new ThreadPoolExecutor(8, 20, 30, TimeUnit.SECONDS,new ArrayBlockingQueue(5000),(Runnable task) - {Thread thread new Thread(task);thread.setName(批量插入线程);return thread;},new ThreadPoolExecutor.AbortPolicy());return executor;}
}
Timing(构建 保存正排索引 —— 多线程版本)
SneakyThrows
public void saveForwardIndexesConcurrent(ListDocument documentList) {// 1. 批量插入时每次插入多少条记录由于每条记录比较大所以这里使用 10 条就够了int batchSize 10;// 2. 一共需要执行多少次 SQL 向上取整(documentList.size() / batchSize)int listSize documentList.size();int times (int) Math.ceil(1.0 * listSize / batchSize); // ceil(天花板): 向上取整log.debug(一共需要 {} 批任务。, times);CountDownLatch latch new CountDownLatch(times); // 统计每个线程的完全情况初始值是 times(一共多少批)// 3. 开始分批次插入for (int i 0; i listSize; i batchSize) {// 从 documentList 中截取这批要插入的 文档列表使用 List.subList(int from, int to)int from i;int to Integer.min(from batchSize, listSize);Runnable task () - { // 内部类 / lambda 表达式里如果用到了外部变量外部变量必须的 final或者隐式 final 的变量ListDocument subList documentList.subList(from, to);// 针对这个 subList 做批量插入mapper.batchInsertForwardIndexes(subList);latch.countDown(); // 每次任务完成之后countDown()让 latch 的个数减一};executorService.submit(task); // 主线程只负责把一批批的任务提交到线程池具体的插入工作由线程池中的线程完成}// 4. 循环结束只意味着主线程把任务提交完成了但任务有没有做完是不知道的// 主线程等在 latch 上只到 latch 的个数变成 0也就是所有任务都已经执行完了latch.await();
} 4.倒排索引的生成和保存
针对文档进行分词并且分别计算每个词的权重
在倒排索引中我们需要进行分词、处理词的权重问题这里我们统一放到 Document 类中处理 对于每个 Document 进行分词处理需要第三方库的支持
在pom.xml 中添加依赖它支持中文和英文分词
dependencygroupIdorg.ansj/groupIdartifactIdansj_seg/artifactIdversion5.1.6/version
/dependency
用法示例
public void Ansj() {Result result ToAnalysis.parse(我爱北京天安门天安门上太阳升。);ListTerm termList result.getTerms();for (Term term : termList) {System.out.print(term.getName() , );System.out.print(term.getNatureStr() , );System.out.println(term.getRealName());}} 权重的设计
比如说用户要查找 “list” 这个单词找到了好几篇文档都含有 “list” 那么哪篇文章显示在前哪篇在后 第一种按照 docid 从小到大去显示121325273467.... 第二种按照匹配程度去显示比如说13号的 list 在标题出现了10次在正文出现了 100 次1号的 list 在标题出现了1次在正文出现了 1次。那么13号文档更匹配所以排名要在1号的前面。 第三种更加智能的计算根据用户的点击数、点击频率、文档的更新频率、作者的维权程度、文档来源的权威程度等信息更精准的计算。 第四种谁给的钱多谁靠前。 我们这里采用的是第二种方法。 所以我们针对每个单词 --- 每篇文档都伴随一个 weight权重根据这个权重去排序d倒序
权重的计算10 * 单词出现在标题的次数 1 * 单词出现在正文中的次数。 这里我们使用map 来维护key就是某个词value就是该词对应的权重
标题 | 分词
ListString wordInTitle ToAnalysis.parse(title) //对title进行分词.getTerms() .stream().parallel().map(Term::getName) .filter(s - !ignoredWordSet.contains(s)).collect(Collectors.toList());
标题 | 出现次数
MapString, Integer titleWordCount new HashMap();
for (String word : wordInTitle) {int count titleWordCount.getOrDefault(word, 0);titleWordCount.put(word, count 1);
}
正文 | 分词
ListString wordInContent ToAnalysis.parse(content).getTerms().stream().parallel().map(Term::getName).collect(Collectors.toList());
正文 | 出现次数 MapString, Integer contentWordCount new HashMap();
for (String word : wordInContent) {int count contentWordCount.getOrDefault(word, 0);contentWordCount.put(word, count 1);
}
计算权重值
用map某个词对应的权重是多少
MapString, Integer wordToWeight new HashMap();
1. 先计算出有哪些词不重复
就是使用set 把所有的标题中的词初始化就都放入然后再把所有正文的词也放入set 中的元素是不能重复的。
2.遍历set中的元素看这个词在标题中出现多少次在正文中出现多少次最后计算权重
10 * 单词出现在标题的次数 1 * 单词出现在正文中的次数。
3.最后把该词和他的权重放到map中最后返回这个map即可。 上述准备工作处理完成之后我们开始进行倒排索引的生成和保护在IndexManager类中
1.定义一个类 里面的对象用来映射 inverted_indexes 表中的一条记录
// 这个对象映射 inverted_indexes 表中的一条记录我们不关心表中的 id就不写 id 了
Data
public class InvertedRecord {private String word;private int docId;private int weight;public InvertedRecord(String word, int docId, int weight) {this.word word;this.docId docId;this.weight weight;}
}
2.执行 sql 在index-mapper.xml 中
!-- 不关心自增 id --insert idbatchInsertInvertedIndexesinsert into inverted_indexes (word, docid, weight) valuesforeach collectionlist itemrecord separator, (#{record.word}, #{record.docId}, #{record.weight})/foreach/insert
单线程版本
3.设置 批量插入时最多 10000 条
int batchSize 10000;
4.准备一个List recordList里面是 InvertedRecord 类型的然后根据分词不断向里面放入放够10000条了就插入一次。也就是本批次要插入的数据
5.遍历document 文件调用 document.segWordAndCalcWeight() 方法拿到分词结果。
6.遍历每个单词得到单词和权重再得到他的docid。构建出这三个后把他们放入recordList中。
7.如果 recordList.size() batchSize说明够一次插入了够10000条了就进行插入插入完成之后清空 recordList。然后就会重新循环再走。
8. recordList 还剩一些之前放进来但还不够 batchSize 个的所以最后再批量插入一次。执行完成之后就可以认为是所有插入都完成了。
多线程版本
一次插入10000条一次处理50篇文档
int batchSize 10000; // 批量插入时最多 10000 条
int groupSize 50;
提前把线程一批一批分好分好之后交给线程去提交。 static class InvertedInsertTask implements Runnable {private final CountDownLatch latch;private final int batchSize;private final ListDocument documentList;private final IndexDatabaseMapper mapper;InvertedInsertTask(CountDownLatch latch, int batchSize, ListDocument documentList, IndexDatabaseMapper mapper) {this.latch latch;this.batchSize batchSize;this.documentList documentList;this.mapper mapper;}Overridepublic void run() {ListInvertedRecord recordList new ArrayList(); // 放这批要插入的数据for (Document document : documentList) {MapString, Integer wordToWeight document.segWordAndCalcWeight();for (Map.EntryString, Integer entry : wordToWeight.entrySet()) {String word entry.getKey();int docId document.getDocId();int weight entry.getValue();InvertedRecord record new InvertedRecord(word, docId, weight);recordList.add(record);// 如果 recordList.size() batchSize说明够一次插入了if (recordList.size() batchSize) {mapper.batchInsertInvertedIndexes(recordList); // 批量插入recordList.clear(); // 清空 list视为让 list.size() 0}}}// recordList 还剩一些之前放进来但还不够 batchSize 个的所以最后再批量插入一次mapper.batchInsertInvertedIndexes(recordList); // 批量插入recordList.clear();latch.countDown();}}Timing(构建 保存倒排索引 —— 多线程版本)SneakyThrowspublic void saveInvertedIndexesConcurrent(ListDocument documentList) {int batchSize 10000; // 批量插入时最多 10000 条int groupSize 50;int listSize documentList.size();int times (int) Math.ceil(listSize * 1.0 / groupSize);CountDownLatch latch new CountDownLatch(times);for (int i 0; i listSize; i groupSize) {int from i;int to Integer.min(from groupSize, listSize);ListDocument subList documentList.subList(from, to);Runnable task new InvertedInsertTask(latch, batchSize, subList, mapper);executorService.submit(task);}latch.await();} 4.搜索模块
依赖索引构建完成之后才能进行需要 web 功能
使用SpringMVC 实现了Web服务 只针对一个词进行搜索 select docid,weight from inverted_indexes where word list order by weight desc; select * from forward_indexes where docid in (...); 这样排出来是无序的 需要根据 weight 重新再进行一次排序 把上述合并成一条联表 SQL select ii.docid,title,url,content,from inverted_indexes ii join forward_indexes fi on ii.docid fi.docid where word list order by weight desc; 这样排完之后是有序的 前端传过来这个词根据词 去 倒排索引 正排索引中搜索得到文档列表可以做分页 查询很慢只是一个词就需要1.8秒如果搜索的多了时间更久没有人愿意等待这么久
这里我们使用向表中新建索引来解决这个问题针对 word 列去建索引
建索引的过程就是把 word 列作为 keydocid 作为 value新建一棵搜索树B树
从 key 查找 value则时间复杂度变成O(log(n)) 21次 远远小于O(n) 200W 次
建索引的速度很慢而且会导致数据插入很慢所以在表中的数据已经插入完成的情况下再添加索引
给 word 和 weight 都添加索引先用word 查查完之后利用 weight 我们可以利用索引直接进行排序。 建立索引之后查询只需要0.2秒 搜索树的 key索引中的字段 word weight复合索引
左边是word右边weight
左边可以比较大小索引的命中规则遵守靠左原则select ... from 表 where word ... 可以用上索引
随后按weight 进行排序因为 key 里就有 weight 所以 key 本身就是按照 weight 排序好的搜索树的有序原则 不加索引的情况下查询慢因为排序 和 查询都没有用到索引。针对 word 加索引性能有所提升查询使用了索引排序没有用相对快但还不够快。针对word 和 weight 使用索引性能明显提升查询和排序都使用了索引非常快 1.动态资源 /web qurty... 必须填的 page...选填但必须是数字
写一个Controller 叫做 SearchController 通过 Controller 注解修饰代表这是一个控制器。实现 GetMapping(/web)查询的url 是 /web。里面 return search;最后会渲染 search.html 这个模板 GetMapping 注解 GetMapping是一个组合注解等价于RequestMapping (method RequestMethod.GET)它将HTTP Get请求映射到特定的处理方法上。 2.之后就需要进行查询定义一个 Document 类 模板其中需要 title、url、content。
3.这些东西需要根据数据库去查因此我们需要一个接口——SearchMapper用Repository Mapper 这两个注解来修饰。里面返回的是一组 Document 类型的 list
Repository
Mapper
public interface SearchMapper {ListDocument query(Param(word) String word,Param(limit) int limit,Param(offset) int offset);ListDocumentWightWeight queryWithWeight(Param(word) String word,Param(limit) int limit,Param(offset) int offset);
} param 注解 param 标签提供了对某个函数的参数的各项说明包括参数名、参数数据类型、描述等。 param 标签要求您指定要描述参数的名称。 您还可以包含参数的数据类型使用大括号括起来和参数的描述。 参数类型可以是一个内置的JavaScript类型如 string 或 Object 或是你代码中另一个标识符的 JSDoc namepath名称路径 。 如果你已经在这namepath名称路径上为标识符添加了描述JSDoc会自动链接到该标识符的文档。 配置文件
spring:main:log-startup-info: falsebanner-mode: offdatasource:url: jdbc:mysql://127.0.0.1:3306/searcher_refactor?characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghaiusername: rootpassword: 123456mybatis:mapper-locations: classpath:mapper/search-mapper.xmllogging:level:com.lingqi.searcher.web: debug
具体的查询语句这里使用 mybatis 在 search-mapper.xml 语句中写具体的查询语句
!-- #{...} 会添加引号上去; ${...} 不会添加引号 --select idquery resultMapDocumentResultMapselect ii.docid, title, url, contentfrom inverted_indexes iijoin forward_indexes fion ii.docid fi.docidwhere word #{word}order by weight desclimit ${limit}offset ${offset}/select
4. 在 SearchController 中写具体步骤
得到查询的词是哪个词query参数的合法性检查 处理
if (query null) {log.debug(query 为 null重定向到首页);return redirect:/;
}query query.trim().toLowerCase();
if (query.isEmpty()) {log.debug(query 为空字符串重定向到首页);return redirect:/;
}
分词
ListString queryList ToAnalysis.parse(query).getTerms().stream().map(Term::getName).collect(Collectors.toList());如果分词后queryList 为空也就是分词后一个词都没有那么证明没有找到这个词让他重定向到首页处理分页的问题 得到page计算出limit offset
int limit 20;
int offset 0;
int page 1;if (pageString ! null) {pageString pageString.trim();try {page Integer.parseInt(pageString);if (page 0) {page 1;}limit page * 20;} catch (NumberFormatException ignored) {}
}
执行SQL进行查询将数据添加到 model 中是为了在 渲染模板的时候用到这里使用了SpringMVC的模板渲染技术ViewResover这里具体使用的是 ThymeleafModel 添加渲染需要的数据query、docList、page
model.addAttribute(query, query);
model.addAttribute(docList, documentList);
model.addAttribute(page, page);
指定使用哪个模板来进行渲染 return search 对应 resources/templates/search.html
5.搜索出来之后展示基本内容部分
首先我们可以拿到这个文本的内容我们可以前面截取120 个字后面截取120 个字。
public Document build(ListString queryList, Document doc) {// 找到 content 中包含关键字的位置// query list// content ..... hello list go come do ....// desc hello ilist/i go com...String content doc.getContent().toLowerCase();String word ;int i -1;for (String query : queryList) {i content.indexOf(query);if (i ! -1) {word query;break;}}if (i -1) {// 这里中情况如果出现了说明咱的倒排索引建立的有问题log.error(docId {} 中不包含 {}, doc.getDocId(), queryList);throw new RuntimeException();}// 前面截 120 个字后边截 120 个字int from i - 120;if (from 0) {// 说明前面不够 120 个字了from 0;}int to i 120;if (to content.length()) {// 说明后面不够 120 个字了to content.length();}String desc content.substring(from, to);// 这里添加i标签可以使这个单词高亮显示desc desc.replace(word, i word /i);doc.setDesc(desc);return doc;} 针对多个词进行搜索
和单词搜索的区别就是 权重值需要重新计算 多次中就需要有一个权重值聚合的问题
docid 相同的weightsumw1,w2,w3
docid1 weight 13 7 1
docid2 weight 22
把实际业务抽象成如下的简单题给定3个有序数组按照权重从大到排序最终结果的权重sumw1w2w3|docid相同给出第 x 到第 y 个元素 假如需要【020必须把 每个【020找出聚合权重重新排序算出结果中的【020 假如需要【2040必须把 每个【040找出聚合权重重新排序算出结果中的【2040 假如需要【4060必须把 每个【060找出聚合权重重新排序算出结果中的【4060 如果数据量太大这种找法是不可能的内存空间可能不足时间效率也很低
因此实际中我们会舍弃他的准确性比如说某次考试前1000名学生各自的排名多少是非常重要的不能出错。后1000名同学排名略有错误其实也不重要。实际上上就是牺牲正确性来换取性能。
结果必须重新计算所以没办法使用MySQL帮我们排序了
首先我们先对所有的词进行查找查找出来先放到 totalList 中
ListDocumentWightWeight totalList new ArrayList();
for (String s : queryList) {ListDocumentWightWeight documentList mapper.queryWithWeight(s, limit, offset);totalList.addAll(documentList);
}
针对所有文档列表做权重聚合工作
维护:docId - document 的 map
1.我们遍历 totalList 针对每一个docid 我们往里面放。遇到重复的不断累加。 2.此时我们就有了每个单词对应的权重了这放在 documentMap 中我们需要对这个权重进行一个排序首要要拿到这些权重放在 Collection 中
3.但 Collection 没有排序这个概念只有线性结构才有排序的概念所以我们需要一个 List
4. 按照 weight 的从大到小排序
Collections.sort(list, (item1, item2) - {return item2.weight - item1.weight;
}); 5. 从 list 中把分页区间取出来
int from (page - 1) * 20;
int to from 20;ListDocumentWightWeight subList list.subList(from, to);
ListDocument documentList subList.stream().map(DocumentWightWeight::toDocument).collect(Collectors.toList()); 具体的查询语句这里使用 mybatis 在 search-mapper.xml 语句中写具体的查询语句 select idqueryWithWeight resultMapDocumentWithWeightResultMapselect ii.docid, title, url, content, weightfrom inverted_indexes iijoin forward_indexes fion ii.docid fi.docidwhere word #{word}order by weight desclimit ${limit}offset ${offset}/select 完整代码
构造索引搜索引擎_构建索引模块: 写一个搜索引擎的项目
搜索模块https://gitee.com/hlingqi/search-engine-search-module
项目测试[测试] 搜索引擎的相关测试_我要敲代码6400的博客-CSDN博客