【Spring Boot】手撕搜索引擎项目,深度复盘在开发中的重难点和总结(长达两万6千字的干货,系好安全带,要发车了......)

目录

  • 搜索引擎
      • 搜索引擎的核心思路
    • 一、解析模块
        • 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.rhkb.cn/news/389334.html

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

相关文章

86. UE5 RPG 技能面板实现监听数据

在上一篇文章里&#xff0c;我们创建了技能面板的控制器&#xff0c;接下来&#xff0c;我们将实现通过控制器绑定委托&#xff0c;来更新显示内容。 更新技能面板应用的技能 我们首先更新技能面板上面已经应用的技能&#xff0c;让其和WBP_Overlay上面一样&#xff0c;可以更…

ELK+filebeat

ELKfilebeat 一、filebeat概述 1、filebeat概念&#xff1a; filebeat日志收集工具和logstash相同 filebeat是一款轻量级的日志收集工具&#xff0c;可以在非JAVA环境下运行。 因此&#xff0c;filebeat常被用在非JAVAf的服务器上用于替代Logstash&#xff0c;收集日志信息。…

产品经理NPDP好考吗?

NPDP是新产品开发专业人员的资格认证&#xff0c;对于希望在产品管理领域取得认可的专业人士来说&#xff0c;NPDP认证是一项重要的资格。 那么&#xff0c;产品经理考取NPDP资格认证究竟难不难呢&#xff1f; 首先&#xff0c;NPDP考试的难易程度取决于考生的背景和准备情况…

谷粒商城实战笔记-105~107-全文检索-ElasticSearch-入门

文章目录 一&#xff0c;105-全文检索-ElasticSearch-入门-_cat二&#xff0c;106-全文检索-ElasticSearch-入门-put&post新增数据三&#xff0c;107-全文检索-ElasticSearch-入门-get查询数据&乐观锁字段1&#xff0c;过时的乐观锁-version2&#xff0c;Elasticsearch…

计算机网络知识点面试总结4

#来自ウルトラマンゼロ&#xff08;赛罗&#xff09; 1 传输层提供的服务 1.1 功能 传输层向它上面的应用层提供通信服务&#xff0c;它属于面向部分的最高层&#xff0c;同时也是用户功能中的最底层。 为运行在不同主机上的进程之间提供了逻辑通信。 传输层的功能&#xff1…

【教程】从零开始用QT简易实现modbus通信

前言&#xff1a;本文旨在让读者了解在qt6中实现modbus通信主要使用哪些函数&#xff0c;需要引用哪些库和头文件&#xff0c;不对modbus协议进行介绍&#xff0c;仅在代码层面简单实现一个modbus通信案例 实现效果&#xff1a;点击读取按钮可以读取从机中的十个寄存器&#x…

【QT】鼠标按键事件 - QMouseEvent QKeyEvent

qt 事件 事件1. 事件概念2. 事件的处理3. 按键事件&#xff08;1&#xff09;单个按键&#xff08;2&#xff09;组合按键 4. 鼠标事件&#xff08;1&#xff09;鼠标单击事件&#xff08;2&#xff09;鼠标释放事件&#xff08;3&#xff09;鼠标双击事件&#xff08;4&#x…

【数据分析】统计学基础及Python具体实现

各位大佬好 &#xff0c;这里是阿川的博客&#xff0c;祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 Python 初阶 Python–语言基础与由来介绍 Python–…

【Python】已解决:ERROR: Could not install packages due to an OSError: [WinError 5] 拒绝访问。: ‘e:\anaconda\in

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决&#xff1a;ERROR: Could not install packages due to an OSError: [WinError 5] 拒绝访问。: ‘e:\anaconda\install_root\scripts\pip.exe’ Consider using the --user o…

C语言详解(结构体)

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#xff0c;在这里撰写成文一…

【MAVEN】如何解决“Error unmarshaling return header; nested exception is: java.io.EOFException“?

目录标题 异常现场分析解决Chat GPT出场一下增大【Build process heap size (Mbytes) 】试试&#x1f64f;增大【Maven->importing->VM options for importer】试试✅Idea的所有配置说明 异常现场 Error unmarshaling return header; nested exception is: java.io.EOFEx…

C++内存管理(区别C语言)深度对比

欢迎来到我的Blog&#xff0c;点击关注哦&#x1f495; 前言 前面已经介绍了类和对象&#xff0c;对C面向对象编程已经有了全面认识&#xff0c;接下来要学习对语言学习比较重要的是对内存的管理。 一、内存的分区 代码区&#xff1a;存放程序的机器指令&#xff0c;通常是可…

IntelliJ IDEA安装教程(超详细)

✅作者简介&#xff1a;CSDN内容合伙人、阿里云专家博主、51CTO专家博主、新星计划第三季python赛道Top1&#x1f3c6; &#x1f4c3;个人主页&#xff1a; IDEA的使用 IDEA的简单介绍IDEA的主要优势IDEA的卸载IDEA的安装第一个程序&#xff1a;HelloWorld结束语 IDEA的简单介绍…

JAVA (Springboot) i18n国际化语言配置

JAVA i18n国际化语言配置 一、简介二、功能三、Java配置国际化步骤四、Java国际化配置工具类五、Spring Boot配置六、测试 一、简介 在Java中&#xff0c;国际化&#xff08;Internationalization&#xff0c;通常简称为i18n&#xff09;是一个过程&#xff0c;它允许应用程序…

【C语言项目】实现一个通讯录,一步一步详细讲解,小白也能看

目录 设计思路 代码实现 代码改造1 代码改造2 完整代码 代码仓库 设计思路 1. 通讯录存放的信息 这个通讯录保存的信息包括&#xff1a;名字&#xff0c;年龄&#xff0c;性别&#xff0c;电话&#xff0c;住址。 2. 通讯录的功能 1. 通讯录可以存放100个人的信息。 2…

2024年 Java 面试八股文(20w字)

> &#x1f345;我是小宋&#xff0c; 一个只熬夜但不秃头的Java程序员。 > &#x1f345;关注我&#xff0c;带你**过面试,读源码**。提升简历亮点&#xff08;14个demo&#xff09; > &#x1f345;我的面试集已有12W 浏览量。 > &#x1f30f;号…

[C++] 深度剖析C_C++内存管理机制

文章目录 内存分布内存分布图解 C语言中动态内存管理方式malloc:callocrealloc C内存管理方式内置类型**自定义类型** operator new & operator deleteoperator new & operator delete函数operator newoperator delete **new T[N]** 与**delete[]** **定位new表达式(pl…

【C语言】指针由浅入深全方位详解!!!

目录 指针 野指针 二级指针 指针数组 字符指针 数组指针 数组参数&#xff0c;指针参数 函数指针 函数指针数组 回调函数 练习题 代码仓库 指针 1. 指针定义 1. 指针是内存中一个最小单元的编号&#xff0c;也就是地址。 2. 平时口语中说的指针&#xff…

【C++】如何巧妙运用C++命名空间:初学者必备指南

C语法相关知识点可以通过点击以下链接进行学习一起加油&#xff01; 本篇将带领大家走进C的旅途&#xff0c;为了更好地学习C这门语言&#xff0c;我们需要了解它的前世今生。在了解完C如何诞生后&#xff0c;将开始我们C之旅第一站"命名空间"。(老早说是C/C博主&…

Java 集合框架:HashMap 的介绍、使用、原理与源码解析

大家好&#xff0c;我是栗筝i&#xff0c;这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 020 篇文章&#xff0c;在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验&#xff0c;并希望进…