【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.rhkb.cn/news/331104.html

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

相关文章

Ajax异步删除

在页面上定义一个按钮 <button type"button" class"btn"><a href"JavaScript:;" class"id" b_id"{{$attachment[id]}}">删除</a></button> js代码 <script>$(.id).click(function (){va…

[读论文]精读Self-Attentive Sequential Recommendation

论文链接&#xff1a;https://arxiv.org/abs/1808.09781 其他解读文章&#xff1a;https://mp.weixin.qq.com/s/cRQi3FBi9OMdO7imK2Y4Ew 摘要 顺序动态是许多现代推荐系统的一个关键特征&#xff0c;这些系统试图根据用户最近执行的操作来捕获用户活动的“上下文”。为了捕捉…

ES基础概念

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

区块链钱包如果丢失了私钥或助记词,资产还能恢复吗?

如果你丢失了区块链钱包的私钥或助记词&#xff08;通常是用于恢复钱包的短语或种子&#xff09;&#xff0c;那么你的资产在大多数情况下是无法恢复的。私钥是访问和控制你在区块链上资产的唯一凭证&#xff0c;而助记词&#xff08;如BIP39标准中的12、18、24个单词的短语&am…

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

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

无人机监测系统:天空之眼,精准掌握地球脉动

在当今信息化快速发展的时代&#xff0c;无人机技术以其独特的优势&#xff0c;正在成为资源调查、环境监测和规划支持的重要工具。无人机监测系统通过搭载多种传感器和设备&#xff0c;能够快速、高效地获取地表信息&#xff0c;为决策提供科学依据。 项目背景 随着全球环境…

SpringMVC接收请求参数的方式:

接收简单变量的请求参数 直接使用简单变量作为形参进行接收&#xff08;这里简单变量名称需要与接收的参数名称保持一致&#xff0c;否则需要加上RequestParam注解&#xff09;&#xff1a; 细节&#xff1a; 1&#xff1a;SpringMVC会针对常见类型&#xff08;八种基本类型及…

二叉排序树的创建

二叉排序树就是节点经过排序构建起的二叉树&#xff0c;其有以下性质&#xff1a; 1. 若它的左子树不为空&#xff0c;则左子树上所有节点的值均小于它的根节点的值。 2. 若它的右子树不为空&#xff0c;则右子树上所有节点的值均大于它的根节点的值。 3. 它的左、右子树也分…

python期末作业:批量爬取站长之家的网站排行榜数据并保存,数据分析可视化

爬虫作业,含python爬取数据和保存文件,数据分析使用pyecharts做数据可视化 整体上分析网站的排名,直观看各个网站的热度。 数据分析之后大致的效果: 整个项目分为两个大的部分,第一部分就是抓取网站排名数据,然后保存为Excel、csv等格式,其次就是从文件中…

下一代Docker会让部署更丝滑吗

下一代Docker会让部署更丝滑吗 如何通俗易懂的理解DockerDocker有什么缺点Docker与AI结合&#xff0c;会让部署更加丝滑吗 随着互联网技术的不断发展&#xff0c;单机系统已经无法满足日益正常的用户量以及正常处理用户请求&#xff0c;这个时候就需要进行多机部署&#xff0c;…

设计新境界:大数据赋能UI的创新美学

设计新境界&#xff1a;大数据赋能UI的创新美学 引言 随着大数据技术的蓬勃发展&#xff0c;它已成为推动UI设计创新的重要力量。大数据不仅为界面设计提供了丰富的数据资源&#xff0c;还赋予了设计师以全新的视角和工具来探索美学的新境界。本文将探讨大数据如何赋能UI设计…

使用Datav,echarts开发各种地图

一、功能描述 在实际中&#xff0c;有时候需要针对不同的地图进行开发&#xff0c;而能在网上找到现成&#xff0c;与需要匹配度高的&#xff0c;几乎很难&#xff0c;而且找起对应的资源也相对麻烦。所以结合DataV提供的地图数据&#xff0c;就能开发出各种地图&#xff0c;然…

英语学习笔记25——Mrs. Smith‘s kitchen

Mrs. Smith’s kitchen 史密斯太太的厨房 词汇 Vocabulary Mrs. 夫人【已婚】 复习&#xff1a;Mr. 先生 全名 / 姓    Mrs. 夫人 全名 / 丈夫的姓    Miss 小姐&#xff08;未婚&#xff09; 全名 / 姓    Ms. 女士 全名 / 姓 查看婚姻状况&#xff0c;可以观察…

神经网络的工程基础(零)——PyTorch基础

相关说明 这篇文章的大部分内容参考自我的新书《解构大语言模型&#xff1a;从线性回归到通用人工智能》&#xff0c;欢迎有兴趣的读者多多支持。 本文涉及到的代码链接如下&#xff1a;regression2chatgpt/ch06_optimizer/gradient_descent.ipynb 本文将介绍PyTorch的基础。…

速看!!!24上软考-信息系统项目管理师真题回忆,考点已更新

整理了24上半年软考高级信息系统项目管理师的考试真题&#xff0c;软考一个批次一套题&#xff0c;现在都是机考&#xff0c;收集题目比较困难&#xff0c;希望能给个小小的赞支持一下。 注意&#xff1a;当天考试的宝子们可以对答案预估分数&#xff01;后面场次的宝子可以提…

WordPress搭建流程

1. 简介 WordPress 是一个 PHP 编写的网站制作平台。WordPress 本身免费,并且拥有众多的主题可以使用,适合用于搭建个人博客、公司官网、独立站等。 2. 环境准备 2.1 WordPress 下载 WordPress 可以在 Worpress中文官网 下载(如果后续要将后台调成中文的话,一定要从中文…

idea中显示git的Local Changes

1. 第一打开idea中的Settings文件 2. 找到Version Contro中的commint 3. 取消勾选应用即可 4. 本地提交就会显示出来

堆和堆排序

目录 1.二叉树的顺序存储2.堆的性质3.堆的实现3.1 堆的插入&#xff08;向上调整算法&#xff09;3.2 堆向下调整算法3.3 堆的创建3.4 堆的删除3.5 全套代码 4.堆排序5.Top-K问题 1.二叉树的顺序存储 顺序存储就是数组存储&#xff0c;一般使用数组只适合完全二叉树&#xff0…

AI革命:生活无处不智能

AI革命&#xff1a;生活无处不智能 &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f3c6; 博客首页 怒放吧德德 To记录领地 &#x1f31d;分享学习心得&#xff0…

回见,那果园

记不得何时开始骑行&#xff0c;何时开始爬山&#xff0c;何时偶遇洛师傅&#xff0c;何时进了那半山腰的果园。 似乎很远&#xff0c;又很近。 昨天打电话给果园的师傅&#xff0c;本意问问杏是否熟了&#xff0c;周末骑行过去、进山聊天顺道吃个新鲜。 洛师傅呵呵的笑…