当前位置: 首页 > news >正文

搜索引擎项目

认识搜索引擎

1、有一个主页、有搜索框。在搜索框中输入的内容 称为“查询词”

2、还有搜索结果页,包含了若干条搜索结果

3、针对每一个搜索结果,都会包含查询词或者查询词的一部分或者和查询词具有一定的相关性

4、每个搜索结果包含好几个部分:

        a)标题 

        b)描述      通常是页面的摘要信息

        c)子链(子链接)

        d)展示 url

        e)图片

        f)点击 url        点击“点击url”浏览器将跳转到“落地页”

 搜索引擎的功能,就是“搜索”=>“查找”;

查找用户输入的查询词在哪些网页(自然搜索结果:网页数据通常是通过爬虫来获取的。广告搜索结果:广告主把物料提交给广告平台)中出现过或者出现过一部分;

把结果展示到网页上。点击结果就能跳转到该页面。

像百度,搜狗这样的搜索引擎,全网搜索。处理的数据量级是非常庞大的。PB级(1PB=1024TB)

咱们现在不具有搞全网搜索的条件(没有那么多服务器),可以搞一个数据量小得多的,站内搜索。

当前所做的搜索引擎,Java API文档

为啥要搜索Java API文档?

1、官方文档上没有一个好用的搜索框

2、Java API 文档数量比较少,当前有限的硬件资源足以处理。(几万个页面)

3、文档内容不需要使用爬虫来获取,可以直接在官网上下载。

搜索引擎是如何工作的?

需要在很多网页数据中找到你输入的查询词(部分查询词)

搜索引擎后台当前已经获取了很多很多的网页数据。每个网页数据都是一个html。称为一个”文档/Document“想要知道的是该查询词在哪些文档中出现过。

1、暴力搜索

一次遍历每个文件,查看当前文件是否包含查询词。(文档数目非常多,依次遍历的话效率就非常低)  

2、倒排索引(这是一种特殊的数据结构)

正排索引:docId => 文档内容        

根据正排索引制作倒排索引,需要先针对文档内容进行分词(对计算机来说,中文分词是一件很CPU疼的事)

词 => docId  倒排索引 相当于是个键值对的结构(乔布斯这个词在文档1和文档2中都出现过)

正排:根据文档id找到对应文档相关信息

倒排:根据词找到这个词在哪些文档id中存在

这是项目中的最核心的部分。也是一个搜索引擎中的最核心部分。

当我们有了倒排序之后,针对用户输入的查询词就可以快速找到相关联。

实现分词的基本思路:

1、基于词典。几十万个

2、基于概率,统计很多的文本信息

哪两个/三个/四个汉字在一起的概率特别高,那么就说明是一个词。

当前商业公司分词库都能达到99%以上的准确率,分词算法一般都是公司的商业机密。

可以使用现成的第三方库。虽然达不到那么高的准确率,但是也够用。

项目的目标:

实现一个Java API文档的站内搜索引擎 在线API文档

Java API文档有一个线上版本,还有一个可以离线下载的版本 API下载地址。

下载好API文档,分析里面的内容,在搜索结果中填上线上文档的连接地址。 

线上文档和线下文档具有一定的对应关系。

后面分析线下文档的时候,就可以根据当前文件的路径拼接出对应的线上文档的url。

项目的模块划分:

1、预处理模块:把下载好的 html 文档进行一次初步的处理(简单分析结构并且干掉其中的 html 标签) 

2、索引模块:预处理得到的结果,构建正排+倒排索引

3、搜索模块:完成一次搜索过程基本流程(从用户输入查询词,到得到最终的搜索结果)

4、前端模块:有一个页面,展示结果并且让用户输入数据

1、预处理模块

把 api 目录中所有的html进行处理 => 得到一个单个的文件(为了后面制作索引更方便)。使用行文本的方式来进行组织(组织方式其实也有很多)。 

这个得到的临时文件中,每一行对应到一个html文档,每一行中又包含3列。

第一列:表示这个文档的标题

第二列:表示这个文档的url(线上版本文档的url)

第三列:表示这个文档的正文(去掉html格式)

过滤掉标签是为了让搜索结果只集中到正文上

1、创建一个common 包表示每个模块都可能用到的公共信息

创建一个DocInfo类对象

2、创建一个Parser包,表示预处理

创建一个Parser类,遍历文档目录,递归的方式读取所有的html文档内容,把结果解析成一个行文本文件

每一行都对应一个文档,每一行中都包含 文档标题,文档的URL,文档的正文(去掉html标签的内容,去除换行符)

1、枚举出INPUT_PATH 下所有的 html 文件(递归)

2、针对枚举出来的html文件路径进行遍历,依次打开每个文件,并读取内容

        把内容转换成需要的结构化的数据(DocInfo对象)

3、把DocInfo对象写入到最终的输出文件中(OUTPUT_PATH)。写成行文本的形式 

convertcontent这个方法两件事情:
// 1. 把 html 中的标签去掉
// 2. 把 \n 也去掉
eg:  <body> <h1>hehe</h1>  <div>我是一句话</div>  </body>
一个一个字符读取并判定

boolean isContent 值为true当前读取的内容是html正文(不是标签)

为false当前读取的内容是标签。

当当前字符为 < 把isContent置为false。把读到的字符直接忽略

当当前字符为 > 把isContent职位true。

如果当前 isContent 为 true 的时候,读到的字符就放到一个StringBuilder中即可。

这里使用字符流

补充:文件是二进制文件,就是用字节流

           文件是文本文件,就是用字符流(用记事本打开能看懂的是文本文件)

public class Parser {// 下载好的 Java API 文档在哪private static final String INPUT_PATH = "E:\\project\\docs\\api";// 预处理模块输出文件存放的目录private static final String OUTPUT_PATH = "E:\\project\\raw_data.txt";public static void main(String[] args) throws IOException {FileWriter resultFileWriter = new FileWriter(new File(OUTPUT_PATH));// 通过 main 完成整个预处理的过程// 1. 枚举出 INPUT_PATH 下所有的 html 文件(递归)ArrayList<File> fileList = new ArrayList<>();enumFile(INPUT_PATH, fileList);// 2. 针对枚举出来的html文件路径进行遍历, 依次打开每个文件, 并读取内容.//    把内容转换成需要的结构化的数据(DocInfo对象)for (File f : fileList) {System.out.println("converting " + f.getAbsolutePath() + " ...");// 最终输出的 raw_data 文件是一个行文本文件. 每一行对应一个 html 文件.// line 这个对象就对应到一个文件.String line = convertLine(f);// 3. 把得到的结果写入到最终的输出文件中(OUTPUT_PATH). 写成行文本的形式resultFileWriter.write(line);}resultFileWriter.close();}// 此处咱们的 raw_data 文件使用行文本来表示只是一种方式而已.// 完全也可以使用 json 或者 xml 或者其他任何你喜欢的方式来表示都行private static String convertLine(File f) throws IOException {// 1. 根据 f 转换出 标题String title = convertTitle(f);// 2. 根据 f 转换出 urlString url = convertUrl(f);// 3. 根据 f 转换成正文, a) 去掉 html 标签; b) 去掉换行符String content = convertContent(f);// 4. 把这三个部分拼成一行文本//    \3 起到分割三个部分的效果. \3 ASCII 值为 3 的字符//    在一个 html 这样的文本文件中是不会存在 \3 这种不可见字符//    类似的, 使用 \1 \2 \4 \5....来分割效果也是一样return title + "\3" + url + "\3" + content + "\n";}private static String convertContent(File f) throws IOException {// 这个方法做两件事情:// 1. 把 html 中的标签去掉// 2. 把 \n 也去掉// 一个一个字符读取并判定FileReader fileReader = new FileReader(f);boolean isContent = true;StringBuilder output = new StringBuilder();while (true) {int ret = fileReader.read();if (ret == -1) {// 读取完毕了break;}char c = (char)ret;if (isContent) {// 当前这部分内容是正文if (c == '<') {isContent = false;continue;}if (c == '\n' || c == '\r') {c = ' ';}output.append(c);} else {// 当前这个部分内容是标签// 不去写 outputif (c == '>') {isContent = true;}}}fileReader.close();return output.toString();}private static String convertUrl(File f) {// URL 线上文档对应的 URL// 线上文档 URL 形如:// https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html// 本地目录文档路径形如:// E:\jdk1.8\docs\api\java\util\Collection.html// 线上文档的 URL 由两个部分构成.// part1: https://docs.oracle.com/javase/8/docs/api  固定的// part2: /java/util/Collection.html 和本地文件的路径密切相关.// 此处对于 浏览器 来说, / 或者 \ 都能识别.String part1 = "https://docs.oracle.com/javase/8/docs/api";String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());return part1 + part2;}private static String convertTitle(File f) {// 把文件名(不是全路径, 去掉.html后缀)作为标题就可以了// 文件名: EntityResolver.html// 全路径: D:\jdk1.8\docs\api\org\xml\sax\EntityResolver.htmlString name = f.getName();return name.substring(0, name.length() - ".html".length());}// 当这个方法递归完毕后, 当前 inputPath 目录下所有子目录中的 html 的路径就都被收集到// fileList 这个 List 中了private static void enumFile(String inputPath, ArrayList<File> fileList) {// 递归的把 inputPath 对应的全部目录和文件都遍历一遍File root = new File(inputPath);// listFiles 相当于 Linux 上的 ls 命令.// 就把当前目录下所有的文件和目录都罗列出来了. 当前目录就是 root 对象所对应的目录File[] files = root.listFiles();// System.out.println(Arrays.toString(files));// 遍历当前这些目录和文件路径, 分别处理for (File f : files) {if (f.isDirectory()) {// 如果当前这个 f 是一个目录. 递归调用 enumFileenumFile(f.getAbsolutePath(), fileList);} else if (f.getAbsolutePath().endsWith(".html")) {// 如果当前 f 不是一个目录, 看文件后缀是不是 .html。 如果是就把这个文件的对象// 加入到 fileList 这个 List 中fileList.add(f);}}}
}

运行: 文件很大,直接打开,加载不出来。使用less命令

less 特点就是打开大文件速度很快。

很多文本编辑器都是尝试把所有文件内容都加载到内存中

less 只加载一小块。显示哪部分就加载哪部分(懒加载) 

在 less /Vim 如何显示不可见字符呢?

\1 => ^A

\2 => ^B

\3 => ^C

也可以用head - n 1 xxx

补充:测试项目,测试用例

针对这个预处理程序:

1、验证文件整体格式是否是行文本格式

2、验证每一行是否对应一个html文件

3、验证每一行中是不是都包含3个字段,是否用\3分割

4、验证标题是否和html文件名一直

5、验证url是否是正确的,是否能跳转到线上文档页面

6、验证正文格式是否正确,html标签是否去掉是否把 \n 去掉 

当前文件内容很多,如何验证所有行都是包含3个字段?=> 写一个程序来验证,自动化测试 

2、索引模块

索引分词

根据 raw_data.txt 得到正排索引和倒排索引 => 分词

分词第三方库有很多 .ansj

安装对应 jar 包

创建Index索引类

引用权重:该词和该文档之间的相关程度。相关程度越高,权重就越大。

实际的搜索引擎或根据查询词和文档之间的相关性进行降序排序,把相关程度越高的文档排到越靠前。相关程度越低的,就排到越靠后。

相关性——有专门的算法工程师团队来做这个事情。(人工智能)

此处就使用简单粗暴的方式来相关性的衡量。

就看这个词在文档中的出现次数,出现次数越多,相关性就越强。

词在标题中出现,就应该相关性比在正文中出现更强一些。

此处设定一个简单粗暴的公式来描述权重。

weight = 标题中出现的次数 * 10 + 正文中出现的次数

正排索引,就是一个数组。docId作为下标,知道docId就可以找到DocInfo。

 倒排索引:根据词找到这个词在哪些文档中出现过。


Index 类需要提供的方法

查正排

查倒排 

构建索引,把raw_data.txt 文件内容读取出来,加载到内存上面的数据结构中

索引模块中最复杂的部分

// 构建索引, 把 raw_data.txt 文件内容读取出来, 加载到内存上面的数据结构中// raw_data.txt 是一个行文本文件. 每一行都有三个部分. 每个部分之间使用 \3 分割// 索引模块中最复杂的部分public void build(String inputPath) throws IOException {class Timer {public long readFileTime;public long buildForwardTime;public long buildInvertedTime;public long logTime;}Timer timer = new Timer();long startTime = System.currentTimeMillis();System.out.println("build start!");// 1. 打开文件, 并且按行读取文件内容BufferedReader bufferedReader = new BufferedReader(new FileReader(new File(inputPath)));// 2. 读取到的每一行.// 加时间是有技巧的. 不应该在比较大的循环中加入过多的打印信息. 打印信息会很多, 看起来很不方便.// 打印函数本身也是有时间消耗的.while (true) {long t1 = System.currentTimeMillis();String line = bufferedReader.readLine();if (line == null) {break;}long t2 = System.currentTimeMillis();// 3. 构造正排的过程: 按照 \3 来切分, 切分结果构造成一个 DocInfo 对象, 并加入到正排索引中DocInfo docInfo = buildForward(line);long t3 = System.currentTimeMillis();// 4. 构造倒排的过程: 把 DocInfo 对象里面的内容进一步处理, 构造出倒排buildInverted(docInfo);long t4 = System.currentTimeMillis();// System.out.println("Build " + docInfo.getTitle() + " done!");long t5 = System.currentTimeMillis();timer.readFileTime += (t2 - t1);timer.buildForwardTime += (t3 - t2);timer.buildInvertedTime += (t4 - t3);timer.logTime += (t5 - t4);}bufferedReader.close();long finishTime = System.currentTimeMillis();System.out.println("build finish! time: " + (finishTime - startTime) + " ms");System.out.println("readFileTime: " + timer.readFileTime+ " buildForwardTime: " + timer.buildForwardTime+ " buildInvertedTime: " + timer.buildInvertedTime+ " logTime: " + timer.logTime);}

正排索引:

private DocInfo buildForward(String line) {// 把这一行按照 \3 切分// 分出来的三个部分就是一个文档的 标题, URL, 正文String[] tokens = line.split("\3");if (tokens.length != 3) {// 发现文件格式有问题~~// 当前场景下咱们有 1w 多个文件. 如果某个文件出现格式问题,// 不应该让某个问题影响到整体的索引构建// 此处我们的处理方式仅仅就是打印了日志.System.out.println("文件格式存在问题: " + line);return null;}// 把新的 docInfo 插入到 数组末尾.// 如果数组是 0 个元素, 新的元素所处在的下标就是 0DocInfo docInfo = new DocInfo();// id 就是正排索引数组下标docInfo.setDocId(forwardIndex.size());docInfo.setTitle(tokens[0]);docInfo.setUrl(tokens[1]);docInfo.setContent(tokens[2]);forwardIndex.add(docInfo);return docInfo;}

 倒排索引:

private void buildInverted(DocInfo docInfo) {class WordCnt {public int titleCount;public int contentCount;public WordCnt(int titleCount, int contentCount) {this.titleCount = titleCount;this.contentCount = contentCount;}}HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();// 前 4 个操作都是为了给后面的计算权重做铺垫// 针对 DocInfo 中的 title 和 content 进行分词, 再根据分词结果构建出 Weight 对象, 更新倒排索引// 1. 先针对标题分词List<Term> titleTerms = ToAnalysis.parse(docInfo.getTitle()).getTerms();// 2. 遍历分词结果, 统计标题中的每个词出现的次数for (Term term : titleTerms) {// 此处 word 已经是被转成小写了String word = term.getName();WordCnt wordCnt = wordCntHashMap.get(word);if (wordCnt == null) {// 当前这个词在哈希表中不存在wordCntHashMap.put(word, new WordCnt(1, 0));} else {// 当前这个词已经在哈希表中存在, 直接修改 titleCount 即可wordCnt.titleCount++;}}// 3. 再针对正文分词List<Term> contentTerms = ToAnalysis.parse(docInfo.getContent()).getTerms();// 4. 遍历分词结果, 统计正文中的每个词出现的次数for (Term term : contentTerms) {String word = term.getName();WordCnt wordCnt = wordCntHashMap.get(word);if (wordCnt == null) {// 当前这个词在哈希表中不存在wordCntHashMap.put(word, new WordCnt(0, 1));} else {wordCnt.contentCount++;}}// 5. 遍历 HashMap, 依次构建 Weight 对象并更新倒排索引的映射关系for (HashMap.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {Weight weight = new Weight();weight.word = entry.getKey();weight.docId = docInfo.getDocId();// weight = 标题中出现次数 * 10 + 正文中出现次数weight.weight = entry.getValue().titleCount * 10 + entry.getValue().contentCount;// weight 加入到倒排索引中. 倒排索引是一个 HashMap, value 就是 Weight 构成的 ArrayList// 先根据这个词, 找到 HashMap 中对应的这个 ArrayList, 称为 "倒排拉链"ArrayList<Weight> invertedList = invertedIndex.get(entry.getKey());if (invertedList == null) {// 当前这个键值对不存在, 就新加入一个键值对就可以了invertedList = new ArrayList<>();invertedIndex.put(entry.getKey(), invertedList);}// 到了这一步, invertedIndex 已经是一个合法的 ArrayList 了, 就可以把 weight 直接加入即可invertedList.add(weight);}}

3、搜索模块

把刚才这些docId所对应的DocInfo信息查找到,组装成一个响应数据. 

 

public class Result {private String title;// 当前这个场景中, 这两个 URL 就填成一样的内容了private String showUrl;private String clickUrl;private String desc; // 描述. 网页正文的摘要信息, 一般要包含查询词(查询词的一部分)public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}public String getShowUrl() {return showUrl;}public void setShowUrl(String showUrl) {this.showUrl = showUrl;}public String getClickUrl() {return clickUrl;}public void setClickUrl(String clickUrl) {this.clickUrl = clickUrl;}public String getDesc() {return desc;}public void setDesc(String desc) {this.desc = desc;}@Overridepublic String toString() {return "Result{" +"title='" + title + '\'' +", showUrl='" + showUrl + '\'' +", clickUrl='" + clickUrl + '\'' +", desc='" + desc + '\'' +'}';}
}

 通过Searcher这个类来完成核心的搜索过程

思路:

1、【分词】针对查询词进行分词

2、【触发】针对查询词的分词结果依次查找倒排索引,得到一大堆相关的 docId

3、【排序】按照权重进行降序排序

4、【包装结果】根据刚才查找到的docId在正排中查找DocInfo,包装成 Result 对象

用户输入的内容到底是啥?

很可能输入的内容中就包含某个词,这个词在所有的文档中都不存在。

当我们针对一个集合类进行排序的时候,往往需要指定比较规则。尤其是集合类内部包含是引用类型的时候。 

Comparable:让需要被比较的类实现这个接口,重写compareTo方法

Comparator:创建一个比较器类,实现这个接口,实现的这个接口内部重写compare方法。

class WeightComparator implements Comparator<Index.Weight> {@Overridepublic int compare(Index.Weight o1, Index.Weight o2) {// 如果 o1 < o2 返回一个 < 0// 如果 o1 > o2 返回一个 > 0// 如果 o1 == o2 返回 0return o2.weight - o1.weight;}
}/*** 通过这个类来完成核心的搜索过程*/
public class Searcher {private Index index = new Index();public Searcher() throws IOException {index.build("d:/raw_data.txt");}public List<Result> search(String query) {// 1. [分词] 针对查询词进行分词List<Term> terms = ToAnalysis.parse(query).getTerms();// 2. [触发] 针对查询词的分词结果依次查找倒排索引, 得到一大堆相关的 docId// 这个 ArrayList 中就保存 每个分词结果 得到的倒排拉链的整体结果ArrayList<Index.Weight> allTokenResult = new ArrayList<>();for (Term term : terms) {// 此处得到的 word 就已经是全小写的内容了. 索引中的内容也是小写的String word = term.getName();List<Index.Weight> invertedList = index.getInverted(word);if (invertedList == null) {// 用户输入的这部分词很生僻, 在所有文档中都不存在continue;}allTokenResult.addAll(invertedList);}// 3. [排序] 按照权重进行降序排序//    匿名内部类allTokenResult.sort(new WeightComparator());// 4. [包装结果] 根据刚才查找到的 docId 在正排中查找 DocInfo, 包装成 Result 对象ArrayList<Result> results = new ArrayList<>();for (Index.Weight weight : allTokenResult) {// 根据 weight 中包含的 docId 找到对应的 DocInfo 对象DocInfo docInfo = index.getDocInfo(weight.docId);Result result = new Result();result.setTitle(docInfo.getTitle());result.setShowUrl(docInfo.getUrl());result.setClickUrl(docInfo.getUrl());// GenDesc 做的事情是从正文中摘取一段摘要信息. 根据当前的这个词, 找到这个词在正文中的位置// 再把这个位置周围的文本都获取到. 得到了一个片段result.setDesc(GenDesc(docInfo.getContent(), weight.word));results.add(result);}return results;}// 这个方法根据当前的词, 提取正文中的一部分内容作为描述.// 以下的实现逻辑还是咱们拍脑门出来的.private String GenDesc(String content, String word) {// 查找 word 在 content 中出现的位置.// word 里内容已经是全小写了. content 里头还是大小写都有.int firstPos = content.toLowerCase().indexOf(word);if (firstPos == -1) {// 极端情况下, 某个词只在标题中出现, 而没在正文中出现, 在正文中肯定找不到了// 这种情况非常少见, 咱们暂时不考虑return "";}// 从 firstPos 开始往前找 60 个字符, 作为描述开始. 如果前面不足 60 个, 就从正文头部开始;int descBeg = firstPos < 60 ? 0 : firstPos - 60;// 从描述开始往后找 160 个字符作为整个描述内容. 如果后面不足 160 个, 把剩下的都算上.if (descBeg + 160 > content.length()) {return content.substring(descBeg);}return content.substring(descBeg, descBeg + 160) + "...";}
}

针对搜索模块的测试

1、针对某个查询词,搜索出来的结构是否都包含指定查询词

2、针对搜索结果,是否按照权重排序

3、针对搜索结果,里面包含的标题,url,描述是否正确

4、URL是否能跳转

5、描述是否包含指定的词

6、描述前面比较短的情况

7、描述后面比较短的情况

8、描述中是否带。。。

9、搜索结果的数目是否符合预期

10、搜索时间是否符合预期

……

搜索模块中核心的搜索类,已经完成了。把搜索类放到一个服务器中,通过服务器来进行搜索过程。

HTTP Servlet。

前后端交互接口

请求:GET /search?query=ArrayList

响应(通过 Json 来组织):

{

        {

                tiltle:“我是标题”,

                showUrl:“我是展示URL”,

                clickUrl:“我是点击URL”,

                desc:“我是描述信息”,

        },

        {

        }

项目部署到云服务器

maven -> package 打包到 target 目录下

当前这个 war 包不能直接放到 Linux 上执行。

Tomcat 肯定会创建 Servlet 实例。Servlet 实例里面包含了 Searcher 对象的实例,又包含了 Index 的实例并且需要进行 build。

build 依赖了一个 raw_data.txt 数据文件。

光把 war 包部署到 Linux 上是不行的,需要把 raw_data.txt 也部署上去,并且要把build 对应的路径给写对。

把 war 包部署到 tomcat 上之后,发现收到第一次请求的时候,才触发了索引构建(Searcher => DocSearcherServlet)

DocSearcherServlet 并不是在 Tomcat 一启动的时候就实例化,而是收到第一个请求的时候才实例化。

问题1第一次请求响应时间太长了。应该让服务器一启动就创建Servlet 的实例。实例创建好了,索引构造完了,那么后面的响应其实就快了。

让服务器一启动就创建对应 Servlet 的实例,如果这个值 <= 0还是表示收到请求才实例化

如果这个值 >0 表示服务器启动就立刻实例化。

当 >0 的时候,这个值越小,就越先加载。

问题2如果在加载过程中,用户的请求就来了咋办

 实际的搜索引擎中,会把整个系统拆分成很多不同的服务器。HTTP服务器 + 搜索服务器(需要加载索引启动速度比较慢)这样的情况下 搜索服务器 如果在启动过程中,HTTP请求就来了,HTTP服务器就可以告诉用户“我们还没准备好”

事实上,搜索服务器通常有多台。重启不至于把所有的都重启。总会留下一些可以正常工作的服务器。就能保证HTTP服务器始终能获取到想要的结果。

4、前端模块

写了一个简单的前面页面,用到的技术栈有:

HTML,CSS,JavaScript

Bootstrap:一个用来更好的显示界面的组件库。

JQuery:方便的使用 ajax 和 服务器进行数据交互

Vue:前端框架,方便数据同步展示到界面上

性能优化

运行预处理模块发现执行时间挺长的。

如果要进行性能优化,怎么办?

1、先看这里有没有必要优化,必要性不高。预处理操作其实执行一次就够了

     API文档内容是很稳定,内容不变,预处理结果就不变

2、先找到性能瓶颈。枚举目录,转换标题,转换正文,转换URL,写文件??

猜测是这两个,因为涉及到IO操作

3、根据性能瓶颈分析原因,指定解决方案


索引模块有必要进行优化的,构建索引过程,每次服务器启动都需要构建索引

思路还是一样,需要找到性能瓶颈才能优化!

给核心步骤加上时间

构建倒排代码中,性能瓶颈在于分词操作。

 分析原因:分词本身就是一个很复杂的操作。

解决方案:文档内容固定,分词结果一定也固定。完全可以在预处理阶段就把标题和正文都分词分好,存到 raw_data 中。后续服务器构建索引,直接加载分词结果即可,不需要重复进行分词。


总时间 = 读文件时间 + 正排索引时间 + 倒排索引时间 + 打印时间

之前IDEA里面执行,log时间就很短。全缓冲,打印换行不会影响刷新缓冲区

在Tomcat上执行,log时间就比较久,行缓冲,每次打印换行都会刷新缓冲区,频繁访问磁盘。

取决于这里打印日志的缓冲策略。

 优化手段:把日志去掉~

可以每循环处理1000个文档,打印1条日志。

结论:不要在一个频繁循环中加日志。 

相关文章:

搜索引擎项目

认识搜索引擎 1、有一个主页、有搜索框。在搜索框中输入的内容 称为“查询词” 2、还有搜索结果页&#xff0c;包含了若干条搜索结果 3、针对每一个搜索结果&#xff0c;都会包含查询词或者查询词的一部分或者和查询词具有一定的相关性 4、每个搜索结果包含好几个部分&…...

7.外部存储器,Cache,虚拟存储器

目录 一. 外部存储器 &#xff08;1&#xff09;磁盘存储器 1.磁盘的组成 2.磁盘的性能指标 3.磁盘地址 4.硬盘的工作过程 5.磁盘阵列 &#xff08;2&#xff09;固态硬盘&#xff08;SSD&#xff09; 二. Cache基本概念与原理 三. Cache和主存的映射方式 &#xff…...

UITableView的style是UITableViewStyleGrouped

一般情况下&#xff0c;UITableViewStylePlain和UITableViewStyleGrouped是UITableView常用到的style&#xff0c; 之前都是用到的时候&#xff0c;遇到问题直接用度娘&#xff0c;差不多就够用了&#xff0c;今天在修复UI提出的间隙问题&#xff0c;来回改&#xff0c;总觉得…...

Java17新增特性

前言 前面的文章&#xff0c;我们对Java9、Java10、Java11、Java12 、Java13、Java14、Java15、Java16 的特性进行了介绍&#xff0c;对应的文章如下 Java9新增特性 Java10新增特性 Java11新增特性 Java12新增特性 Java13新增特性 Java14新增特性 Java15新增特性 Java16新增特…...

VR全景技术在城市园区发展中有哪些应用与帮助

引言&#xff1a; 在数字化时代的浪潮中&#xff0c;虚拟现实&#xff08;VR&#xff09;全景技术逐渐融入各个领域&#xff0c;也为城市园区展示带来了全新的可能性。 一&#xff0e;VR全景技术简介 虚拟现实全景技术是一种通过全景图像和视频模拟真实环境的技术。通过相关设…...

在 SQL 中,当复合主键成为外键时应该如何被其它表引用

文章目录 当研究一个问题慢慢深入时&#xff0c;一个看起来简单的问题也暗藏玄机。在 SQL 中&#xff0c;主键成为外键这是一个很平常的问题&#xff0c;乍一看没啥值得注意的。但如果这个主键是一种复合主键&#xff0c;而另一个表又引用这个键作为它的复合主键&#xff0c;问…...

Ps:通过显示大小了解图像的打印尺寸

在 Photoshop 中&#xff0c;如果想了解文档窗口中的图像打印出来之后的实质大小&#xff0c;只要知道两个数值即可。 第一个数值是图像分辨率&#xff08;也称“文档分辨率”&#xff09;的大小&#xff0c;可在Ps菜单&#xff1a;图像/图像大小 Image Size对话框中查询或设置…...

Linux - 驱动开发 - watchdog - SMP机制下多核确活

说明 理论上&#xff1a;不管IC是单核还是多核&#xff0c;只要watchdog有被循环feed&#xff0c;就不会触发超时重启&#xff0c;因此watchdog在SMP机制下的多核环境显得比较宽松&#xff0c;只要任意核存活&#xff08;喂狗&#xff09;就不会重启设备。 实际情况 有客户反…...

概念解析 | LoRA:低秩矩阵分解在神经网络微调中的魔力

注1:本文系“概念解析”系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:基于低秩矩阵分解的神经网络微调方法LoRA LoRA:低秩矩阵分解在神经网络微调中的魔力 Low-Rank Adaptation of Large Language Models LoRA由如下论文提出,详细信息请参见论文原…...

量子计算和量子通信技术:引领潜力无限的未来

近年来&#xff0c;随着量子计算和量子通信技术的迅速发展&#xff0c;它们在各个领域的广泛应用前景引起了人们的极大兴趣。本文将深入探讨量子计算和量子通信技术的普遍应用&#xff0c;以及它们预示的未来&#xff0c;同时提出业内人士需要注意的事项。 介绍&#xff1a;量子…...

nodejs+vue+python+PHP+微信小程序-安卓- 电影在线订票系统的设计与实现-计算机毕业设计推荐

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…...

LightDB23.4支持mysql aes_encrypt/aes_decrypt/hex/unhex函数

背景介绍 为了兼容mysql数据库的功能&#xff0c;在LightDB23.4版本上支持hex/unhex/aes_encrypt/aes_decrypt函数。 函数原型如下&#xff1a; hex(data bytea) returns textunhex(data varchar) returns byteaaes_encrypt(data bytea, key bytea) returns byteaaes_encryp…...

uniapp——项目day04

购物车页面——商品列表区域 渲染购物车商品列表的标题区域 1. 定义如下的 UI 结构&#xff1a; 2.美化样式 渲染商品列表区域的基本结构 1. 通过 mapState 辅助函数&#xff0c;将 Store 中的 cart 数组映射到当前页面中使用&#xff1a; import badgeMix from /mixins/tab…...

7-爬虫-中间件和下载中间件(加代理,加请求头,加cookie)、scrapy集成selenium、源码去重规则(布隆过滤器)、分布式爬虫

0 持久化(pipelines.py)使用步骤 1 爬虫中间件和下载中间件 1.1 爬虫中间件(一般不用) 1.2 下载中间件&#xff08;代理&#xff0c;加请求头&#xff0c;加cookie&#xff09; 1.2.1 加请求头(加到请求对象中) 1.2.2 加cookie 1.2.3 加代理 2 scrapy集成selenium 3 源码去重…...

创建自己的nas服务,从远端拉取所需文件

一、前言 创建一个nas文件存储&#xff0c;然后需要的时候随时从远端或者其他终端拉取所需文件是不是一件很帅气的工作。 二、准备工作 一台服务器&#xff08;云的更好&#xff09;&#xff0c;没了。 首先安装docker和docker-compose 此处省略docker的安装(改天更新)&…...

智慧化城市内涝的预警,万宾科技内涝积水监测仪

随着城市化进程的加速&#xff0c;伴随的是城市内涝问题日益凸显。频繁的暴雨和积水给市民的生活带来了诸多不便&#xff0c;也给城市的基础设施带来了巨大压力。如何解决这一问题&#xff0c;成为智慧城市建设的重要课题和政府管理的工作主题&#xff0c;只要内涝问题得到缓解…...

7-18 调用一个函数

分数 2 作者 Yiping 单位 广东东软学院 现有如下程序&#xff0c;请将注释后带??的代码补充完整&#xff1a; import mathdef normalize(normal):x normal[0]y normal[1]z normal[2]s math.sqrt(x**2 y**2 z**2)x / sy / sz / sreturn (x, y, z)if __name__ __mai…...

VB.net TCP服务端监听端口接收客户端RFID网络读卡器上传的读卡数据

本 示例使用设备介绍&#xff1a;WIFI/TCP/UDP/HTTP协议RFID液显网络读卡器可二次开发语音播报POE-淘宝网 (taobao.com) Imports System.Threading Imports System.Net Imports System.Net.Sockets Public Class Form1Dim ListenSocket As SocketDim Dict As New Dictionary(Of…...

Springboot 集成 MongoDB

在SpringBoot项目中集成MongoDB后的一些基本操作。 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 本文介绍的内容是Springboot如何集成MongoDB&#xff0c;以及对MongoDB进行基本的增加、查询数据的操作。 提示&#xff1a;以下是本篇…...

AM@定积分的定义求某些类型的极限

文章目录 定积分定义求极限步骤例 定积分表示为极限 定积分定义求极限 容易从定积分的定义: ∫ a b f ( x ) d x \int_{a}^{b}f(x)\mathrm{d}x ∫ab​f(x)dx lim ⁡ λ → 0 ∑ i 1 n f ( ξ i ) Δ x i \lim\limits_{\lambda\to{0}}\sum_{i1}^{n}f{(\xi_{i})}\Delta{x_i} λ→…...

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

测试微信模版消息推送

进入“开发接口管理”--“公众平台测试账号”&#xff0c;无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息&#xff1a; 关注测试号&#xff1a;扫二维码关注测试号。 发送模版消息&#xff1a; import requests da…...

Ubuntu系统下交叉编译openssl

一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机&#xff1a;Ubuntu 20.04.6 LTSHost&#xff1a;ARM32位交叉编译器&#xff1a;arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...

K8S认证|CKS题库+答案| 11. AppArmor

目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、切换节点 3&#xff09;、切换到 apparmor 的目录 4&#xff09;、执行 apparmor 策略模块 5&#xff09;、修改 pod 文件 6&#xff09;、…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

三维GIS开发cesium智慧地铁教程(5)Cesium相机控制

一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点&#xff1a; 路径验证&#xff1a;确保相对路径.…...

苍穹外卖--缓存菜品

1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得&#xff0c;如果用户端访问量比较大&#xff0c;数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据&#xff0c;减少数据库查询操作。 缓存逻辑分析&#xff1a; ①每个分类下的菜品保持一份缓存数据…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南

&#x1f680; C extern 关键字深度解析&#xff1a;跨文件编程的终极指南 &#x1f4c5; 更新时间&#xff1a;2025年6月5日 &#x1f3f7;️ 标签&#xff1a;C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言&#x1f525;一、extern 是什么&#xff1f;&…...

分布式增量爬虫实现方案

之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面&#xff0c;避免重复抓取&#xff0c;以节省资源和时间。 在分布式环境下&#xff0c;增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路&#xff1a;将增量判…...

【Java学习笔记】BigInteger 和 BigDecimal 类

BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点&#xff1a;传参类型必须是类对象 一、BigInteger 1. 作用&#xff1a;适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...