【Spring Boot】深度复盘在开发搜索引擎项目中重难点的整理,以及遇到的困难和总结

💓 博客主页:从零开始的-CodeNinja之路

⏩ 收录文章:【Spring Boot】深度复盘在开发搜索引擎项目中重难点的整理,以及遇到的困难和总结

🎉欢迎大家点赞👍评论📝收藏⭐文章

目录

  • 什么是搜索引擎?
      • 搜索引擎的核心思路
    • 一、解析模块
        • 1.1 枚举所有文件
        • 1.2 解析每个文件的标题,URL以及正文
          • 1.2.1 解析标题
          • 1.2.2 解析URL
          • 1.2.3 解析正文
        • 1.3 线程池优化代码
    • 二 、创建排序模块
        • 2.1 构建正排索引
        • 2.2 构建倒排索引
        • 2.3 序列化
        • 2.4 反序列化
    • 三、搜索模块
        • 3.1 引入停用词
        • 3.2 优化正文内容
        • 3.3 权重合并
    • 四、全部代码
  • 遇到的困难
  • 总结

在这里插入图片描述
在这里插入图片描述

什么是搜索引擎?

简单点来说就是模拟实现一个属于自己的小百度,通过这前端网页输入关键字,后端返回响应的结果,由于百度的搜索极其复杂,我们在模拟时只用实现返回文章的标题,链接以及部分包含关键字内容的正文即可。
如下图中:搜索ArrayList关键字,点击搜索一下,就会出现如下的页面。
在这里插入图片描述
搜索引擎的本质就是输入一个查询词,得到若干个搜索结果,其中包含了标题,展示url、点击url以及部分正文内容。

搜索引擎的核心思路

因为百度是包含了很多很多信息,我们无法取到(说实话,哪怕取到了自己的电脑也会跑宕机,在这里插入图片描述

),所以我们来实现范围搜索,就是在所给的固定的范围内进行搜索,这里我采用的是JDK21的辅助文档,
前提是必须把这个压缩包给下载下来哈,不下载可没法操作嘞,这里我就把关于22版本的链接放在这了,直接下载解压就好,链接:https://www.oracle.com/java/technologies/javase-jdk22-doc-downloads.html,当然了使用其他的版本也都一样的。
首先我将项目分为了四大模块哈:

  1. 解析模块
  2. 排序模块
  3. 搜索模块
    其中两大最重要的模块的总体实现思路如下图:
    在这里插入图片描述

一、解析模块

在这里插入图片描述

在本篇文章中,解析文件模块所要创建的类名为----Parser,整体的思路是先创建一个文件集合(List)用来保存从引入压缩包解析后的所有以.html结尾的文件,在遍历每一个文件进行解析他们的标题,URL以及响应正文,并且将每个文件解析好的结果传给创建排序模块进行排序(只是后话了,为了节省时间进行的 ,就是每解析一个以.html结尾的文件,就将其进行排序)。

1.1 枚举所有文件

首先将下载好的压缩包解压过后,将文件的路径以字符串的形式写入IDEA,创建方法名为enumFile的方法来解析文件,应为一个文件下有许多文件夹,我们要将他们全部遍历进行存储,这里我采用的是递归的方法来读取该路径下的所有文件,创建数组来保存该一级目录下的文件,其中肯定也包含文件夹,在遍历该数组,如果该文件的文件是以.html结尾的,那么直接保存到集合中即可,如果是文件夹那么通过递归进行再次遍历,直到将引入的压缩包下的所有以.html结尾的文件全部保存到集合中,然后返回集合,实现的代码如下。
在这里插入图片描述

 private void enumFile(String inputFile, List<File> fileList) {File file=new File(inputFile);File[]files=file.listFiles();for(File file1:files){if(file1.isFile()){if(file1.getAbsolutePath().endsWith(".html")){fileList.add(file1);}}else{enumFile(file1.getAbsolutePath(),fileList);}}}

代码实现的结果如下:
在这里插入图片描述
从这里可以看出,该压缩包中的所有以.html的文件已经全部被我们枚举出来了,其全部数量为2311

1.2 解析每个文件的标题,URL以及正文

这里我们创建一个方法名为parseHtml,该方法内包含三个分别用来解析标题,URL以及正文的方法
该方法为:

 private void parseHtml(File file) {//解析html的标题String title=parseHtmlTitle(file);//解析html的urlString url= parseHtmlUrl(file);//解析html的正文//String content= parseHtmlContent(file);//解析html的正文通过正则表达式String content=parseHtmlContentByRegex(file);//每解析一个文件就创造一个正排索引和倒排索引index.createIndex(title,url,content);}

其中title字符串用来接收parseHtmlTitle方法解析回来的文件标题
其中url字符串用来接收parseHtmlUrl方法解析回来的文件标题
其中content字符串用来接收parseHtmlContentByRegex方法解析回来的文件标题
(这里的解析文件正文是最难的一部分,其它两个极其简单)
然后将解析的三部分传入index类用来创建索引(这个我们接下来说)

1.2.1 解析标题

解析文件标题很简单,因为前面读取文件时,每个文件不是以.html结尾的吗,那我们直接选取该文件名再去掉它的后缀,例如文件名:arraylist.html,我们直接去掉.html只要前面的arraylist即可
代码如下:

 private String parseHtmlTitle(File file) {return file.getName().replaceAll(".html","");}

我就说 很简单吧,我直接把字符串中的.html用空串替代就完了

1.2.2 解析URL

其实解析URL也是极其简单的,就是要考验眼力,别给看错了就行,就是把官网上的该页面的链接截取前半段,
在把你下载解析好的该文件的路径的后半段截下来,两端一填充就完美了,不够在拼接好以后要自己先试试看能否访问哈,访问不了就不怪我辽在这里插入图片描述

代码如下:

 private String parseHtmlUrl(File file) {//C:\Users\xia\IdeaProjects\SearchProject \docs\api\java.base\java\\util//file:///C:/Users/xia/IdeaProjects/SearchProject/docs/api/java.base/java/util/ArrayList.html//https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/util/ArrayList.htmlString s=file.getAbsolutePath().substring("C:\\Users\\xia\\IdeaProjects\\SearchProject".length());return  "https://docs.oracle.com/en/java/javase/22"+s;}
1.2.3 解析正文

首先老样子我们还是先介绍一下关于解析正文的思路哈,读取一个文件的内容是不是首先要运用到学过的读取数据流里的FileReader(这个是读取字节流),回顾一下还有一个是读取字符流的为OutPut…啥的,跑题了哈,然后将读取的数据存在一个字符串中,在遇到换行以及大空格后将其替换成空字符串。
代码如下

 private String readFile(File file) {StringBuilder stringBuilder=new StringBuilder();try(BufferedReader bufferedReader=new BufferedReader(new FileReader(file),1024*1024)){while(true){int c=bufferedReader.read();if(c==-1){break;}char ch=(char)c;if(ch=='\n'||ch=='\t'){ch=' ';}stringBuilder.append(ch);}}catch (IOException e){e.printStackTrace();}return  stringBuilder.toString();}

这里解释一下为什么不直接用FileReader而是将其嵌套在StringBuilde中,因为直接使用FileReader表示每次是从硬板中读取数据,这样以来读取速度就会非常之慢,而采用StringBUilder则是在内存中开辟一块空间,这里我们开辟空间用来保存从硬盘中读来的数据,在接下来的使用中直接从内存中读取就会比从硬盘中读取快10倍不止

 private String parseHtmlContentByRegex(File file ){String content=readFile(file);//通过正则表达式去掉正文中的<script>标签content=content.replaceAll("<script.*?>(.*?)</script>"," ");//通过正则表达式去掉正文中的其它标签content=content.replaceAll("<.*?>"," ");通过正则表达式合并多个空格content = content.replaceAll("\\s+", " ");return  content;}

然后通过正则表达式将该字符串中所有的以

1.3 线程池优化代码

因为之前的代码都是有一个线程来进行解析,会很慢,这里我们采用多线程来解决,首先就是创建一个拥有10个线程的池子,以方便后面在用的时候直接从池子里拿就行,然后创建一个计数器用来判断是否全部执行完(每解析一个文件,计数器就会+1),在计数器等于我们解析我文件数量后就停止线程,销毁线程池,然后调用index类中将结果进行字符化保存在本地文件中

代码如下;

 public void runByThread() throws InterruptedException {List<File> fileList=new ArrayList<>();//枚举所有以.html结尾的文件enumFile(INPUT_FILE,fileList);long start=System.currentTimeMillis();//创建一个包含10个线程的线程池ExecutorService executorService= Executors.newFixedThreadPool(10);//创建一个计数器来表示文件的数量CountDownLatch countDownLatch=new CountDownLatch(fileList.size());for(File file:fileList){executorService.submit(new Runnable() {@Overridepublic void run() {parseHtml(file);log.info("文件名:"+file.getName()+"文件路径:"+file.getAbsolutePath());countDownLatch.countDown();}});}countDownLatch.await();executorService.shutdown();index.save();long end=System.currentTimeMillis();log.info("多线程所消耗的时间:"+(end-start)+"ms");}

整体的Parser的代码放在在文章最后了~

二 、创建排序模块

总体的思路为:

  1. 构建正排索引、
  2. 倒排索引、
  3. 序列化,
  4. 反序列
    四大方法

其中构建
正排索引:就是根据每篇文章的id来搜索该文章,并将该文件章的所有信息查找出来,正排索引就是使用一个集合来保存所有文章的id,这里我命名为forwordIndex
在这里插入图片描述
倒排索引:通过输入的关键词搜索到与其全部有关的文章,这里使用Map来实现,通过一个词来获取一个与其相关的集合,这个集合内包含的是每篇与这个关键词有联系的文章id
在这里插入图片描述

2.1 构建正排索引

构建正排很简单,直接把从parse传过来的标题、URl以及正文进行封装成一个类放在存储的集合中就行
代码如下:

private DocInfo CreateForwardIndex(String title, String url, String content) {DocInfo docInfo=new DocInfo();docInfo.setTitle(title);docInfo.setUrl(url);docInfo.setContent(content);synchronized (lock1){docInfo.setId(forwardIndex.size());forwardIndex.add(docInfo);}return  docInfo;}
2.2 构建倒排索引

构建倒排索引的总体思路是:首先将传进来的文章的标题以及正文进行分词,就是根据我们大众认识的分成多个组合在一块的词组,然后将每篇文章的分词结果进行权重比较(权重:该文章出现的次数越多,权重越大),权重最大的放在该词集合的最前面,方便用户直接看到。
引入Ansj分词库
我们在将单词存入倒排索引表中的时候,其实是将正排索引表中存储的标题还有内容进行分词,统计权重后才存入表中的,而分词的操作中,我们需要引入分词库ansj

        <!-- Java 版本的一个分词库,本身是支持中文分词的,只是咱的文档中没有中文。但英文分词它也支持 --><!-- https://github.com/NLPchina/ansj_seg --><dependency><groupId>org.ansj</groupId><artifactId>ansj_seg</artifactId><version>5.1.6</version></dependency>

代码如下:

 private void createInvertedIndex(DocInfo docInfo) {class WordCount{public  int titleCount;public  int contentCount;public WordCount(){};}Map<String,WordCount> wordCountMap=new HashMap<>();//先对标题进行分词List<Term>terms=ToAnalysis.parse( docInfo.getTitle()).getTerms();for(Term term:terms){String temp=term.getName();WordCount wordCount=wordCountMap.get(temp);if(wordCount==null){WordCount newWordCount=new WordCount();newWordCount.titleCount=10;newWordCount.contentCount=0;wordCountMap.put(temp,newWordCount);}else {wordCount.titleCount+=10;}}//对正文进行分词List<Term>terms1=ToAnalysis.parse( docInfo.getContent()).getTerms();for(Term term:terms1){String temp=term.getName();WordCount wordCount=wordCountMap.get(temp);if(wordCount==null){WordCount newWordCount=new WordCount();newWordCount.titleCount=0;newWordCount.contentCount=1;wordCountMap.put(temp,newWordCount);}else {wordCount.contentCount+=1;}}//统计完成,开始合并Set<Map.Entry<String, WordCount>>entrySet= wordCountMap.entrySet();for(Map.Entry<String, WordCount> entry:entrySet){synchronized (lock2){String s=entry.getKey();Integer sum=entry.getValue().contentCount+entry.getValue().titleCount;Weight weight=new Weight(sum,docInfo.getId());List<Weight>weightList=invertedIndex.get(s);if(weightList==null){List<Weight>newList=new ArrayList<>();newList.add(weight);invertedIndex.put(s,newList);}else {invertedIndex.get(s).add(weight);}}}}
2.3 序列化

序列化简单来说就是游戏里的存档,这里我们是先创建两个文件用来保存正排索引和倒排索引的结果,然后使用内置的函数将我们的数据转为字符串,然后存储在提前创建好的文档中、
内置函数如下:

private ObjectMapper objectMapper=new ObjectMapper();

代码如下:

  /*** 加载到文件*/public  void save(){long start=System.currentTimeMillis();File indexPathFile=new File(SAVE_LOAD_FILE);if(!indexPathFile.exists()){indexPathFile.mkdirs();}File forwordFile=new File(SAVE_LOAD_FILE+"forword.txt");File invertedFile=new File(SAVE_LOAD_FILE+"inverted.txt");try{objectMapper.writeValue(forwordFile,forwardIndex);objectMapper.writeValue(invertedFile,invertedIndex);}catch (IOException e){e.printStackTrace();}long end=System.currentTimeMillis();log.info("保存文件成功,消耗时间:"+(end-start)+"ms");};
2.4 反序列化

序列化是将内容转字符串写入文件中,那么反序列化就是将该文件中存储的数据以一定的格式再次读取到原来的形式中。
代码如下:

  public  void load(){long start=System.currentTimeMillis();try {File forwordFile = new File(SAVE_LOAD_FILE + "forword.txt");File invertedFile = new File(SAVE_LOAD_FILE + "inverted.txt");forwardIndex= objectMapper.readValue(forwordFile, new TypeReference<List<DocInfo>>() {});invertedIndex = objectMapper.readValue(invertedFile, new TypeReference<Map<String, List<Weight>>>() {});}catch (IOException e){e.printStackTrace();}long end=System.currentTimeMillis();log.info("加载文件成功,消耗时间:"+(end-start)+"ms");};

三、搜索模块

其实搜索模块主要分为两大部分:

  1. 引入停用词,将正文中无关紧要的数据给屏蔽掉
  2. 优化正文内容,由于正文过长,我们定位其中的关键字进行部分输出
  3. 权重合并,将不同权重的文章进行排序
    我们在前端输入一个词,然后根据词去倒排+正排索引中去搜索,然后就可以获得文档列表在这里插入图片描述
3.1 引入停用词

首先停用词是一个文档,我们将该文档读取后保存在一个Map中,在后面的正文筛选中如果包含该词则直接忽略掉即可.
代码如下:

  private void loadStopWords(String stopWordPath) {try {BufferedReader bufferedReader=new BufferedReader(new FileReader(stopWordPath));while (true){String line=bufferedReader.readLine();if(line==null){break;}stopWords.add(line);}} catch (IOException e) {throw new RuntimeException(e);}}
3.2 优化正文内容

因为一篇文章的正文内容非常多,在搜索中也不是全部输出,而是输出其中一部分包含标题的部分正文,这里我们定位输入的关键词在正文中查找下标,然后以查找到的下标为中心进行左右范围截取进行输出,这里我采取的是下标中心词的前后个80个词作为正文输出.
代码如下:

private String updateContent(String content, List<Term> termList) {int index=-1;for(Term term:termList){String word=term.getName();index=content.toLowerCase().indexOf(" "+word+" ");if(index>=0){break;}}if(index==-1){if(content.length()<160){return  content;}return  content.substring(0,160)+"...";}int start=index<60?0:index-60;String desc="";if(start+160>content.length()){desc=content.substring(start);}else{desc=content.substring(start,start+160)+"...";}for(Term term:termList){String word=term.getName();//(?i)表示不区分大小写进行替换desc=desc.replaceAll("(?i) "+word+" ","<i> "+word+" </i>");//自己加的desc=desc.replaceAll("\\s"," ");}return desc;}
3.3 权重合并

通过对于不同的权重进行排序,将权重比较大的文章id放在搜索的前面,方便用户在搜索显示时的页面上最先出现的就是关键字最多的一篇文章
在这里插入图片描述
实现代码如下:

 public List<Result> search(String query){List<Term> oldTerm=ToAnalysis.parse(query).getTerms();//用于存储去掉停用词后的分词结果List<Term> termList=new ArrayList<>();for(Term term:oldTerm){if(stopWords.contains(term.getName())){continue;}termList.add(term);}List<List<Weight>> allResultList=new ArrayList<>();for(Term term:termList){String s=term.getName();List<Weight> temp=index.checkByInverted(s);if(temp==null){continue;}allResultList.add(temp);}//进行权重合并List<Weight> weightList=myselfMergeResult(allResultList);weightList.sort(new Comparator<Weight>() {@Overridepublic int compare(Weight o1, Weight o2) {return o2.getWeight()-o1.getWeight();}});List<Result> resultList=new ArrayList<>();for(Weight weight:weightList){DocInfo docInfo=index.checkByForward(weight.getId());Result result=new Result();result.setTitle(docInfo.getTitle());result.setUrl(docInfo.getUrl());String content=updateContent(docInfo.getContent(),termList);result.setContent(content);resultList.add(result);}return  resultList;}@Datastatic class Pos{public int row;public  int col;}

到此为止,我们的核心功能就以全部实现了。

四、全部代码

SpringBoot于前端进行交互的代码:

package com.example.searchproject.controller;import com.example.searchproject.Search.DocSearcher;
import com.example.searchproject.Search.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.xml.ws.Action;
import org.nlpcn.commons.lang.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController
public class DocSearcherController {private DocSearcher docSearcher=new DocSearcher();private ObjectMapper objectMapper=new ObjectMapper();@RequestMapping(value = "/searcher",produces = "application/json;charset=utf-8")@ResponseBodypublic String search(@RequestParam("query") String query) throws JsonProcessingException {List<Result> resultList=docSearcher.search(query);return objectMapper.writeValueAsString(resultList);//return StringUtil.joiner(resultList,",");}
}

Parser解析文件类的代码如下:

package com.example.searchproject.Search;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;@Slf4j
public class Parser {private Index index=new Index();private  final static String INPUT_FILE="C:\\Users\\xia\\IdeaProjects\\SearchProject\\docs";public void run(){List<File> fileList=new ArrayList<>();//枚举所有以.html结尾的文件enumFile(INPUT_FILE,fileList);//解析每一个html文件for(File file:fileList){//解析每一个html文件parseHtml(file);}index.save();}public void runByThread() throws InterruptedException {List<File> fileList=new ArrayList<>();//枚举所有以.html结尾的文件enumFile(INPUT_FILE,fileList);long start=System.currentTimeMillis();//创建一个包含10个线程的线程池ExecutorService executorService= Executors.newFixedThreadPool(10);//创建一个计数器来表示文件的数量CountDownLatch countDownLatch=new CountDownLatch(fileList.size());for(File file:fileList){executorService.submit(new Runnable() {@Overridepublic void run() {parseHtml(file);log.info("文件名:"+file.getName()+"文件路径:"+file.getAbsolutePath());countDownLatch.countDown();}});}countDownLatch.await();executorService.shutdown();index.save();long end=System.currentTimeMillis();log.info("多线程所消耗的时间:"+(end-start)+"ms");}private void parseHtml(File file) {//解析html的标题String title=parseHtmlTitle(file);//解析html的urlString url= parseHtmlUrl(file);//解析html的正文//String content= parseHtmlContent(file);//解析html的正文通过正则表达式String content=parseHtmlContentByRegex(file);//每解析一个文件就创造一个正排索引和倒排索引index.createIndex(title,url,content);}private String readFile(File file) {StringBuilder stringBuilder=new StringBuilder();try(BufferedReader bufferedReader=new BufferedReader(new FileReader(file),1024*1024)){while(true){int c=bufferedReader.read();if(c==-1){break;}char ch=(char)c;if(ch=='\n'||ch=='\t'){ch=' ';}stringBuilder.append(ch);}}catch (IOException e){e.printStackTrace();}return  stringBuilder.toString();}private String parseHtmlContentByRegex(File file ){String content=readFile(file);//通过正则表达式去掉正文中的<script>标签content=content.replaceAll("<script.*?>(.*?)</script>"," ");//通过正则表达式去掉正文中的其它标签content=content.replaceAll("<.*?>"," ");通过正则表达式合并多个空格content = content.replaceAll("\\s+", " ");return  content;}private String parseHtmlContent(File file)  {StringBuilder stringBuilder=new StringBuilder();try{BufferedReader bufferedReader=new BufferedReader(new FileReader(file),1024*1024);int flag=0;while (true){int n=bufferedReader.read();if(n==-1){break;}char ch=(char)n;if(ch=='<'){flag=1;}else {if(ch=='>'){flag=0;continue;}if(ch=='\n'||ch=='\r'){ch=' ';}stringBuilder.append(ch);}}}catch (IOException e){e.printStackTrace();}return  stringBuilder.toString();}private String parseHtmlUrl(File file) {//C:\Users\xia\IdeaProjects\SearchProject \docs\api\java.base\java\\util//file:///C:/Users/xia/IdeaProjects/SearchProject/docs/api/java.base/java/util/ArrayList.html//https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/util/ArrayList.htmlString s=file.getAbsolutePath().substring("C:\\Users\\xia\\IdeaProjects\\SearchProject".length());return  "https://docs.oracle.com/en/java/javase/22"+s;}private String parseHtmlTitle(File file) {return file.getName().replaceAll(".html","");}private void enumFile(String inputFile, List<File> fileList) {File file=new File(inputFile);File[]files=file.listFiles();for(File file1:files){if(file1.isFile()){if(file1.getAbsolutePath().endsWith(".html")){fileList.add(file1);}}else{enumFile(file1.getAbsolutePath(),fileList);}}}public static void main(String[] args) throws InterruptedException {Parser parser=new Parser();parser.run();}
}

index创建索引模块的代码如下:

package com.example.searchproject.Search;import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;
import org.springframework.stereotype.Component;import java.io.File;
import java.io.IOException;
import java.util.*;
@Slf4j
public class Index {private static  final String SAVE_LOAD_FILE="C:\\Users\\xia\\IdeaProjects\\SearchProject\\";private ObjectMapper objectMapper=new ObjectMapper();//正排索引private List<DocInfo> forwardIndex=new ArrayList<>();//倒排索引1private Map<String,List<Weight>> invertedIndex=new HashMap<>();private  Object lock1=new Object();private  Object lock2=new Object();public  DocInfo checkByForward(Integer id){return  forwardIndex.get(id);}public  List<Weight> checkByInverted(String query){return  invertedIndex.get(query);}/*** 创建正排索引和倒排索引*/public void createIndex(String title,String url,String content){//创建正排索引DocInfo docInfo= CreateForwardIndex( title, url, content);//创建倒排索引createInvertedIndex(docInfo);}private void createInvertedIndex(DocInfo docInfo) {class WordCount{public  int titleCount;public  int contentCount;public WordCount(){};}Map<String,WordCount> wordCountMap=new HashMap<>();//先对标题进行分词List<Term>terms=ToAnalysis.parse( docInfo.getTitle()).getTerms();for(Term term:terms){String temp=term.getName();WordCount wordCount=wordCountMap.get(temp);if(wordCount==null){WordCount newWordCount=new WordCount();newWordCount.titleCount=10;newWordCount.contentCount=0;wordCountMap.put(temp,newWordCount);}else {wordCount.titleCount+=10;}}//对正文进行分词List<Term>terms1=ToAnalysis.parse( docInfo.getContent()).getTerms();for(Term term:terms1){String temp=term.getName();WordCount wordCount=wordCountMap.get(temp);if(wordCount==null){WordCount newWordCount=new WordCount();newWordCount.titleCount=0;newWordCount.contentCount=1;wordCountMap.put(temp,newWordCount);}else {wordCount.contentCount+=1;}}//统计完成,开始合并Set<Map.Entry<String, WordCount>>entrySet= wordCountMap.entrySet();for(Map.Entry<String, WordCount> entry:entrySet){synchronized (lock2){String s=entry.getKey();Integer sum=entry.getValue().contentCount+entry.getValue().titleCount;Weight weight=new Weight(sum,docInfo.getId());List<Weight>weightList=invertedIndex.get(s);if(weightList==null){List<Weight>newList=new ArrayList<>();newList.add(weight);invertedIndex.put(s,newList);}else {invertedIndex.get(s).add(weight);}}}}private DocInfo CreateForwardIndex(String title, String url, String content) {DocInfo docInfo=new DocInfo();docInfo.setTitle(title);docInfo.setUrl(url);docInfo.setContent(content);synchronized (lock1){docInfo.setId(forwardIndex.size());forwardIndex.add(docInfo);}return  docInfo;};/*** 加载到文件*/public  void save(){long start=System.currentTimeMillis();File indexPathFile=new File(SAVE_LOAD_FILE);if(!indexPathFile.exists()){indexPathFile.mkdirs();}File forwordFile=new File(SAVE_LOAD_FILE+"forword.txt");File invertedFile=new File(SAVE_LOAD_FILE+"inverted.txt");try{objectMapper.writeValue(forwordFile,forwardIndex);objectMapper.writeValue(invertedFile,invertedIndex);}catch (IOException e){e.printStackTrace();}long end=System.currentTimeMillis();log.info("保存文件成功,消耗时间:"+(end-start)+"ms");};/*** 从文件中加载到idea*/public  void load(){long start=System.currentTimeMillis();try {File forwordFile = new File(SAVE_LOAD_FILE + "forword.txt");File invertedFile = new File(SAVE_LOAD_FILE + "inverted.txt");forwardIndex= objectMapper.readValue(forwordFile, new TypeReference<List<DocInfo>>() {});invertedIndex = objectMapper.readValue(invertedFile, new TypeReference<Map<String, List<Weight>>>() {});}catch (IOException e){e.printStackTrace();}long end=System.currentTimeMillis();log.info("加载文件成功,消耗时间:"+(end-start)+"ms");};}

DOSearcher搜索模块的代码如下:

package com.example.searchproject.Search;import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;@Slf4j
public class DocSearcher {private  Index index=new Index();public DocSearcher(){index.load();loadStopWords(STOP_WORD_PATH);log.info("文件加载成功");}String STOP_WORD_PATH= "C:\\Users\\xia\\IdeaProjects\\SearchProject\\stop_word.txt" ;HashSet<String >stopWords=new HashSet<>();public List<Result> search(String query){List<Term> oldTerm=ToAnalysis.parse(query).getTerms();//用于存储去掉停用词后的分词结果List<Term> termList=new ArrayList<>();for(Term term:oldTerm){if(stopWords.contains(term.getName())){continue;}termList.add(term);}List<List<Weight>> allResultList=new ArrayList<>();for(Term term:termList){String s=term.getName();List<Weight> temp=index.checkByInverted(s);if(temp==null){continue;}allResultList.add(temp);}//进行权重合并List<Weight> weightList=myselfMergeResult(allResultList);weightList.sort(new Comparator<Weight>() {@Overridepublic int compare(Weight o1, Weight o2) {return o2.getWeight()-o1.getWeight();}});List<Result> resultList=new ArrayList<>();for(Weight weight:weightList){DocInfo docInfo=index.checkByForward(weight.getId());Result result=new Result();result.setTitle(docInfo.getTitle());result.setUrl(docInfo.getUrl());String content=updateContent(docInfo.getContent(),termList);result.setContent(content);resultList.add(result);}return  resultList;}@Datastatic class Pos{public int row;public  int col;}private List<Weight> myselfMergeResult(List<List<Weight>> source) {PriorityQueue<Weight> queue=new PriorityQueue<>(new Comparator<Weight>() {@Overridepublic int compare(Weight o1, Weight o2) {return o1.getId()-o2.getId();}});for(List<Weight> list:source){for(Weight weight:list){queue.offer(weight);}}List<Weight> target=new ArrayList<>();while (!queue.isEmpty()){Weight curWeight=queue.poll();if(!target.isEmpty()){Weight oldWeight=target.get(target.size()-1);if(curWeight.getId()==oldWeight.getId()){oldWeight.setWeight(oldWeight.getWeight()+curWeight.getWeight());}else {target.add(curWeight);}}else {target.add(curWeight);}}return  target;}private void loadStopWords(String stopWordPath) {try {BufferedReader bufferedReader=new BufferedReader(new FileReader(stopWordPath));while (true){String line=bufferedReader.readLine();if(line==null){break;}stopWords.add(line);}} catch (IOException e) {throw new RuntimeException(e);}}private String updateContent(String content, List<Term> termList) {int index=-1;for(Term term:termList){String word=term.getName();index=content.toLowerCase().indexOf(" "+word+" ");if(index>=0){break;}}if(index==-1){if(content.length()<160){return  content;}return  content.substring(0,160)+"...";}int start=index<60?0:index-60;String desc="";if(start+160>content.length()){desc=content.substring(start);}else{desc=content.substring(start,start+160)+"...";}for(Term term:termList){String word=term.getName();//(?i)表示不区分大小写进行替换desc=desc.replaceAll("(?i) "+word+" ","<i> "+word+" </i>");//自己加的desc=desc.replaceAll("\\s"," ");}return desc;}public static void main(String[] args) {DocSearcher docSearcher=new DocSearcher();List<Result> resultList=docSearcher.search("arraylist");for(Result result:resultList){System.out.println(result.toString());}}}

正排索引中包含DOInfo类的代码:

package com.example.searchproject.Search;import lombok.Data;@Data
public class DocInfo {private Integer id;private String title;private  String url;private String content;public DocInfo(){};
}

每个文件的基本构成的Rusult类的代码:

package com.example.searchproject.Search;import lombok.Data;@Data
public class Result {private String title;private String url;private String content;public Result(){};public Result(String title, String url, String content) {this.title = title;this.url = url;this.content = content;}
}

权重类的代码:

package com.example.searchproject.Search;import lombok.Data;@Data
public class Weight {private Integer weight;private  Integer id;public Weight(){};public Weight(Integer weight, Integer id) {this.weight = weight;this.id = id;}
}

遇到的困难

在本次项目中遇到的这个困难困扰了我整整一天,最后终于在电脑仅剩10%电量时给解决了,说多了都是泪…
在这里插入图片描述
刚开始fiddle抓包试了,postman试了,前端就是有响应内容但是不显示页面,而且报的不是平时那种一眼就知道的异常

然后就是上网查看文章,有的说这时运行时异常,就是编译时有这个方法运行时由于版本不同就无法调用这个方法体,应该是idea运行时的版本和我下载的版本不一样,我就去上网查如何看两个版本,简单学了使用命令框看版本和端口号,最后发现我的版本是一样的

在这里插入图片描述

又有文章说方法调用的包名不同,这也不是我那个错误,那时认为错误出在了前端代码的页面渲染上,又硬着头皮把copy过来的前端代码给看了,里面有好多在资源上没有的,又去自己查这个代码有啥作用,然后发现简单看懂了前端代码,但是我的前端是渲染有问题

然后受不了了,在csdn上把代码贴给了一起写文章的大佬们,让他们看看,然后他们说让我去调试前端代码,那时我也认为是前端代码的错,可我又不会前端的调试啊,平时都是搞后端的,然后去csdn上查如何调试,他们说用浏览器提供的有说用vs code的,我学了一下调试浏览器感觉不习惯,又去学了vs code 如何调试,然后就是没有问题

最后我有把objectMapper方法换成了StringUtils方法,然后就是不报错了但是前端还是不显示页面,最后没办法了,又去看了一遍报错日志,用翻译软件给它全翻译过来,还是不明白,然后晚上看csdn常见出错的地方后,文章突然提到了还有一个细小且不容易发现的地方就是依赖冲突,但是pom.xml里没有报错,日志里只显示了引用的依赖,我就想,算了试着注掉试试,注释掉后用maven更新以后跑了一遍,发现突然显示出来了,当时电脑还剩有10%的电量差点就回寝了,那一刻感觉值了,成就感拉满了

总结

  1. 在使用ObjectMapper的方法时,将文件或字符串等类型转为类对象或包含类对象时,该类必须包含无参构造方法,若写的有含参的构造方法则Spring就不会在提供无参构造方法,会导致程序报错

  2. 应为Spring MVC中以内置了Object Mapper方法,在使用时直接创建调用即可,调用后生成一个JSON类型的字符串, 再在RequestMpping中添加Produces来指定返回数据的类型,这样传递出去的是JSON对象格式,传递结果图片如下:
    在这里插入图片描述

然而若是引入该依赖
在这里插入图片描述

则会发生依赖冲突,当我引入SpringWeb(Spring Web MVC)框架时就已经引入了ObjectMapper,在次引入依赖就是多此一举,我再次引入的依赖和SpringWeb框架内置的依赖版本不同,在运行加载配置文件时从我引入的低版本依赖中找objectMapper方法,发现找不到就报异常

在这里插入图片描述

StringUtils则是生成字符串,在不指定返回类型时默认的是text/html格式
在这里插入图片描述

在指定返回类型是json后,因为不是json字符串转json对象,而是由字符串转json对象,则结果如下:
在这里插入图片描述
此时看前端接收处理数据对应的格式了,如不同则报错,例:搜索引擎中前端接收JSON对象而我引入了jackson依赖则导致类型不同,前端无法解析数据而报错

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/15376.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AJAX初级

AJAX的概念&#xff1a; 使用浏览器的 XMLHttpRequest 对象 与服务器通信 浏览器网页中&#xff0c;使用 AJAX技术&#xff08;XHR对象&#xff09;发起获取省份列表数据的请求&#xff0c;服务器代码响应准备好的省份列表数据给前端&#xff0c;前端拿到数据数组以后&#xf…

TENT: FULLY TEST-TIME ADAPTATION BY ENTROPY MINIMIZATION--论文笔记

论文笔记 资料 1.代码地址 https://github.com/DequanWang/tent 2.论文地址 https://arxiv.org/abs/2006.10726 1论文摘要的翻译 在这种完全测试时适应的情况下&#xff0c;模型只有测试数据和自身参数。我们建议通过测试熵最小化&#xff08;tent&#xff09;进行适应&…

虚拟化技术[2]之存储虚拟化

存储虚拟化 存储虚拟化简介存储虚拟化一般模型存储虚拟化实现方式基于主机存储虚拟化基于存储设备存储虚拟化基于网络存储虚拟化 案例分析&#xff1a;VMFSVMFS功能 存储虚拟化简介 存储虚拟化&#xff1a;将存储网络中的各个分散且异构的存储设备按照一定的策略映射成一个统一…

C++学习笔记(19)——模板

目录 模板参数与非类型模板参数 模板参数 类型模板参数——传递类型 非类型模板参数——传递数量 C11希望array替代静态数组&#xff0c;但实际上vector包揽了一切 模板总结 优点&#xff1a; 缺点&#xff1a; 模板特化&#xff1a;针对某些类型进行特殊化处理 特化…

P451 try-Catch异常处理

//基本使用演示代码 public static void main(String[] args) { int num1 10; int num2 0; try { int res num1 / num2; }catch (Exception e) { System.out.println(e.getMessage()); } } public class TryCatchDetail { public static void main(String[] args) { //1. 如…

ubuntu20.04 终端 设置字体大小

##ubuntu20.04 Terminal 终端 设置字体大小 ##打开Terminal 终端&#xff0c;点击Preferences设置字体大小 ##点击unnamed选项卡&#xff0c;打钩Custom font 设置 字体大小

三前奏:获取/ 读取/ 评估数据【数据分析】

各位大佬好 &#xff0c;这里是阿川的博客 &#xff0c; 祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 前面的博客 数据分析—技术栈和开发环境搭建 …

二叉树——堆详解

目录 前言&#xff1a; 一、堆的结构 二、向上调整和向下调整 2.1 向上调整 2.2 向下调整 2.3 向上调整和向下调整时间复杂度比较 三、堆的实现 3.1 堆的初始化 3.2 堆的销毁 3.3 堆的插入 3.4堆的删除 3.5 取堆顶元素 3.6 对堆判空 四、堆排序 五、TOP-K 问题 六、代码总…

你真正了解 Java 中的 Date 类吗?以及如何正确使用它

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

pygame raycasting纹理

插值原理 原理 color&#xff08;x&#xff09;(x-x1)/(x2-x1)(color2-color1)color1 x1<x<x2 假如说x伪3 那么color&#xff08;3-x1&#xff09;/(x2-x1)(color2-color1)color 可是图片纹理 这里不需要两种颜色&#xff0c;只需要获得碰撞点坐标后&#xff0c;如果…

安卓玩机搞机技巧综合资源----自己手机制作证件照的几种方法 免费制作证件照

接上篇 安卓玩机搞机技巧综合资源------如何提取手机分区 小米机型代码分享等等 【一】 安卓玩机搞机技巧综合资源------开机英文提示解决dm-verity corruption your device is corrupt. 设备内部报错 AB分区等等【二】 安卓玩机搞机技巧综合资源------EROFS分区格式 小米红…

ES基础概念

本文不介绍如何使用ES&#xff08;使用ES见&#xff1a;&#xff09; 1.ES生态圈 ES&#xff1a; Logstash&#xff1a;数据处理服务程序&#xff0c;解析转换加工数据&#xff1b; Kibana&#xff1a;数据展示、集群管理&#xff0c;数据可视化、ES管理与监控、报表等&#xf…

Nature期刊的等级和分类

Nature期刊不用过多介绍&#xff0c;学术界人员都对其有所了解&#xff0c;可以和Science&#xff0c;Cell比肩&#xff0c;Nature旗下创办了很多子刊&#xff0c;系列期刊有一百多种&#xff0c;当然其含金量各有不同&#xff0c;nature旗下的期刊等级你是否都了解了。 Nature…

设计模式——概述

1.设计模式定义 ​ 设计模式是软件设计中常见问题的典型解决方案,可用于解决代码中反复出现的设计问题。设计模式的出现可以让我们站在前人的肩膀上&#xff0c;通过一些成熟的设计方案来指导新项目的开发和设计&#xff0c;以便于我们开发出具有更好的灵活性和可扩展性&#…

JAVAEE之线程(10)_线程池、线程池的创建、实现线程池

一 线程池 1.1为什么要有线程池&#xff1f; 线程池顾名思义是由多个线程所组成&#xff0c;作用就是减少线程的建立与销毁&#xff0c;与数据库连接池相同概念&#xff0c;为了减少连接与释放&#xff0c;从而降低消耗提升效率。 1.2 线程池的优势 总体来说&#xff0c;线程…

【数据分析面试】53.推送消息的分布情况(SQL)

题目 我们有两个表&#xff0c;一个是 notification_deliveries 表&#xff0c;另一个是包含 created 和购买 conversion dates 的 users 表。如果用户没有购买&#xff0c;那么 conversion_date 列为 NULL。 编写一个查询&#xff0c;以获取用户转换前的推送通知总数的分布情…

Java反射角度简单理解spring IOC容器

概述 Java反射&#xff08;Reflection&#xff09;是Java编程语言的一个特性&#xff0c;它允许在运行时对类、接口、字段和方法进行动态查询和操作。反射提供了一种在运行时查看和修改程序行为的能力&#xff0c;这通常用于实现一些高级功能&#xff0c;如框架(Spring)、ORM&…

算法:树状数组

文章目录 面试题 10.10. 数字流的秩327. 区间和的个数315. 计算右侧小于当前元素的个数 树状数组可以理解一种数的存储格式。 面试题 10.10. 数字流的秩 假设你正在读取一串整数。每隔一段时间&#xff0c;你希望能找出数字 x 的秩(小于或等于 x 的值的个数)。 请实现数据结构…

网络信息安全

目录 引言 网络信息安全的基本概念 定义 主要目标 网络信息安全的范围 主要威胁 恶意软件 黑客攻击 拒绝服务攻击 社交工程 内部威胁 常用技术和防护措施 加密技术 防火墙 入侵检测和防御系统 访问控制 多因素认证 安全审计和监控 安全培训和意识提升 未来发…

无人机支持下的自然灾害风险评估技术应用

张老师&#xff08;副教授&#xff09;&#xff0c;长期从事无人机遥感技术与应用&#xff0c;主持多项国家级科研项目&#xff0c;编写著作2部&#xff0c;第一作者发表科研论文20余篇。对无人机遥感的多平台、多传感应用现状以及涉及的核心技术具有很深的理解&#xff0c;精通…