东营免费建网站,商业性质网站建设步骤,租个国内服务器做网站多少钱,商业网站开发 流程#x1f493; 博客主页#xff1a;从零开始的-CodeNinja之路
⏩ 收录文章#xff1a;【Spring Boot】深度复盘在开发搜索引擎项目中重难点的整理#xff0c;以及遇到的困难和总结
#x1f389;欢迎大家点赞#x1f44d;评论#x1f4dd;收藏⭐文章 目录 什么是搜索引… 博客主页从零开始的-CodeNinja之路
⏩ 收录文章【Spring Boot】深度复盘在开发搜索引擎项目中重难点的整理以及遇到的困难和总结
欢迎大家点赞评论收藏⭐文章 目录 什么是搜索引擎搜索引擎的核心思路 一、解析模块1.1 枚举所有文件1.2 解析每个文件的标题URL以及正文1.2.1 解析标题1.2.2 解析URL1.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当然了使用其他的版本也都一样的。 首先我将项目分为了四大模块哈
解析模块排序模块搜索模块 其中两大最重要的模块的总体实现思路如下图
一、解析模块 在本篇文章中解析文件模块所要创建的类名为----Parser整体的思路是先创建一个文件集合List用来保存从引入压缩包解析后的所有以.html结尾的文件在遍历每一个文件进行解析他们的标题URL以及响应正文并且将每个文件解析好的结果传给创建排序模块进行排序只是后话了为了节省时间进行的 就是每解析一个以.html结尾的文件就将其进行排序。
1.1 枚举所有文件
首先将下载好的压缩包解压过后将文件的路径以字符串的形式写入IDEA,创建方法名为enumFile的方法来解析文件应为一个文件下有许多文件夹我们要将他们全部遍历进行存储这里我采用的是递归的方法来读取该路径下的所有文件创建数组来保存该一级目录下的文件其中肯定也包含文件夹在遍历该数组如果该文件的文件是以.html结尾的那么直接保存到集合中即可如果是文件夹那么通过递归进行再次遍历直到将引入的压缩包下的所有以.html结尾的文件全部保存到集合中然后返回集合实现的代码如下。 private void enumFile(String inputFile, ListFile fileList) {File filenew File(inputFile);File[]filesfile.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 titleparseHtmlTitle(file);//解析html的urlString url parseHtmlUrl(file);//解析html的正文//String content parseHtmlContent(file);//解析html的正文通过正则表达式String contentparseHtmlContentByRegex(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 sfile.getAbsolutePath().substring(C:\\Users\\xia\\IdeaProjects\\SearchProject.length());return https://docs.oracle.com/en/java/javase/22s;}1.2.3 解析正文
首先老样子我们还是先介绍一下关于解析正文的思路哈读取一个文件的内容是不是首先要运用到学过的读取数据流里的FileReader(这个是读取字节流)回顾一下还有一个是读取字符流的为OutPut…啥的跑题了哈然后将读取的数据存在一个字符串中在遇到换行以及大空格后将其替换成空字符串。 代码如下 private String readFile(File file) {StringBuilder stringBuildernew StringBuilder();try(BufferedReader bufferedReadernew BufferedReader(new FileReader(file),1024*1024)){while(true){int cbufferedReader.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 contentreadFile(file);//通过正则表达式去掉正文中的script标签contentcontent.replaceAll(script.*?(.*?)/script, );//通过正则表达式去掉正文中的其它标签contentcontent.replaceAll(.*?, );通过正则表达式合并多个空格content content.replaceAll(\\s, );return content;}
然后通过正则表达式将该字符串中所有的以
1.3 线程池优化代码
因为之前的代码都是有一个线程来进行解析会很慢这里我们采用多线程来解决首先就是创建一个拥有10个线程的池子以方便后面在用的时候直接从池子里拿就行然后创建一个计数器用来判断是否全部执行完每解析一个文件计数器就会1在计数器等于我们解析我文件数量后就停止线程销毁线程池,然后调用index类中将结果进行字符化保存在本地文件中
代码如下; public void runByThread() throws InterruptedException {ListFile fileListnew ArrayList();//枚举所有以.html结尾的文件enumFile(INPUT_FILE,fileList);long startSystem.currentTimeMillis();//创建一个包含10个线程的线程池ExecutorService executorService Executors.newFixedThreadPool(10);//创建一个计数器来表示文件的数量CountDownLatch countDownLatchnew 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 endSystem.currentTimeMillis();log.info(多线程所消耗的时间:(end-start)ms);}整体的Parser的代码放在在文章最后了~
二 、创建排序模块
总体的思路为
构建正排索引、倒排索引、序列化反序列 四大方法
其中构建 正排索引就是根据每篇文章的id来搜索该文章并将该文件章的所有信息查找出来正排索引就是使用一个集合来保存所有文章的id这里我命名为forwordIndex 倒排索引通过输入的关键词搜索到与其全部有关的文章这里使用Map来实现通过一个词来获取一个与其相关的集合这个集合内包含的是每篇与这个关键词有联系的文章id
2.1 构建正排索引
构建正排很简单直接把从parse传过来的标题、URl以及正文进行封装成一个类放在存储的集合中就行 代码如下
private DocInfo CreateForwardIndex(String title, String url, String content) {DocInfo docInfonew 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 --dependencygroupIdorg.ansj/groupIdartifactIdansj_seg/artifactIdversion5.1.6/version/dependency
代码如下 private void createInvertedIndex(DocInfo docInfo) {class WordCount{public int titleCount;public int contentCount;public WordCount(){};}MapString,WordCount wordCountMapnew HashMap();//先对标题进行分词ListTermtermsToAnalysis.parse( docInfo.getTitle()).getTerms();for(Term term:terms){String tempterm.getName();WordCount wordCountwordCountMap.get(temp);if(wordCountnull){WordCount newWordCountnew WordCount();newWordCount.titleCount10;newWordCount.contentCount0;wordCountMap.put(temp,newWordCount);}else {wordCount.titleCount10;}}//对正文进行分词ListTermterms1ToAnalysis.parse( docInfo.getContent()).getTerms();for(Term term:terms1){String tempterm.getName();WordCount wordCountwordCountMap.get(temp);if(wordCountnull){WordCount newWordCountnew WordCount();newWordCount.titleCount0;newWordCount.contentCount1;wordCountMap.put(temp,newWordCount);}else {wordCount.contentCount1;}}//统计完成,开始合并SetMap.EntryString, WordCountentrySet wordCountMap.entrySet();for(Map.EntryString, WordCount entry:entrySet){synchronized (lock2){String sentry.getKey();Integer sumentry.getValue().contentCountentry.getValue().titleCount;Weight weightnew Weight(sum,docInfo.getId());ListWeightweightListinvertedIndex.get(s);if(weightListnull){ListWeightnewListnew ArrayList();newList.add(weight);invertedIndex.put(s,newList);}else {invertedIndex.get(s).add(weight);}}}}
2.3 序列化
序列化简单来说就是游戏里的存档这里我们是先创建两个文件用来保存正排索引和倒排索引的结果然后使用内置的函数将我们的数据转为字符串然后存储在提前创建好的文档中、 内置函数如下
private ObjectMapper objectMappernew ObjectMapper();代码如下 /*** 加载到文件*/public void save(){long startSystem.currentTimeMillis();File indexPathFilenew File(SAVE_LOAD_FILE);if(!indexPathFile.exists()){indexPathFile.mkdirs();}File forwordFilenew File(SAVE_LOAD_FILEforword.txt);File invertedFilenew File(SAVE_LOAD_FILEinverted.txt);try{objectMapper.writeValue(forwordFile,forwardIndex);objectMapper.writeValue(invertedFile,invertedIndex);}catch (IOException e){e.printStackTrace();}long endSystem.currentTimeMillis();log.info(保存文件成功,消耗时间:(end-start)ms);};
2.4 反序列化
序列化是将内容转字符串写入文件中那么反序列化就是将该文件中存储的数据以一定的格式再次读取到原来的形式中。 代码如下 public void load(){long startSystem.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 TypeReferenceListDocInfo() {});invertedIndex objectMapper.readValue(invertedFile, new TypeReferenceMapString, ListWeight() {});}catch (IOException e){e.printStackTrace();}long endSystem.currentTimeMillis();log.info(加载文件成功,消耗时间:(end-start)ms);};三、搜索模块
其实搜索模块主要分为两大部分
引入停用词将正文中无关紧要的数据给屏蔽掉优化正文内容,由于正文过长我们定位其中的关键字进行部分输出权重合并,将不同权重的文章进行排序 我们在前端输入一个词然后根据词去倒排正排索引中去搜索然后就可以获得文档列表
3.1 引入停用词
首先停用词是一个文档,我们将该文档读取后保存在一个Map中,在后面的正文筛选中如果包含该词则直接忽略掉即可. 代码如下: private void loadStopWords(String stopWordPath) {try {BufferedReader bufferedReadernew BufferedReader(new FileReader(stopWordPath));while (true){String linebufferedReader.readLine();if(linenull){break;}stopWords.add(line);}} catch (IOException e) {throw new RuntimeException(e);}}3.2 优化正文内容
因为一篇文章的正文内容非常多,在搜索中也不是全部输出,而是输出其中一部分包含标题的部分正文,这里我们定位输入的关键词在正文中查找下标,然后以查找到的下标为中心进行左右范围截取进行输出,这里我采取的是下标中心词的前后个80个词作为正文输出. 代码如下:
private String updateContent(String content, ListTerm termList) {int index-1;for(Term term:termList){String wordterm.getName();indexcontent.toLowerCase().indexOf( word );if(index0){break;}}if(index-1){if(content.length()160){return content;}return content.substring(0,160)...;}int startindex60?0:index-60;String desc;if(start160content.length()){desccontent.substring(start);}else{desccontent.substring(start,start160)...;}for(Term term:termList){String wordterm.getName();//(?i)表示不区分大小写进行替换descdesc.replaceAll((?i) word ,i word /i);//自己加的descdesc.replaceAll(\\s, );}return desc;}3.3 权重合并
通过对于不同的权重进行排序,将权重比较大的文章id放在搜索的前面,方便用户在搜索显示时的页面上最先出现的就是关键字最多的一篇文章 实现代码如下: public ListResult search(String query){ListTerm oldTermToAnalysis.parse(query).getTerms();//用于存储去掉停用词后的分词结果ListTerm termListnew ArrayList();for(Term term:oldTerm){if(stopWords.contains(term.getName())){continue;}termList.add(term);}ListListWeight allResultListnew ArrayList();for(Term term:termList){String sterm.getName();ListWeight tempindex.checkByInverted(s);if(tempnull){continue;}allResultList.add(temp);}//进行权重合并ListWeight weightListmyselfMergeResult(allResultList);weightList.sort(new ComparatorWeight() {Overridepublic int compare(Weight o1, Weight o2) {return o2.getWeight()-o1.getWeight();}});ListResult resultListnew ArrayList();for(Weight weight:weightList){DocInfo docInfoindex.checkByForward(weight.getId());Result resultnew Result();result.setTitle(docInfo.getTitle());result.setUrl(docInfo.getUrl());String contentupdateContent(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 docSearchernew DocSearcher();private ObjectMapper objectMappernew ObjectMapper();RequestMapping(value /searcher,produces application/json;charsetutf-8)ResponseBodypublic String search(RequestParam(query) String query) throws JsonProcessingException {ListResult resultListdocSearcher.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 indexnew Index();private final static String INPUT_FILEC:\\Users\\xia\\IdeaProjects\\SearchProject\\docs;public void run(){ListFile fileListnew ArrayList();//枚举所有以.html结尾的文件enumFile(INPUT_FILE,fileList);//解析每一个html文件for(File file:fileList){//解析每一个html文件parseHtml(file);}index.save();}public void runByThread() throws InterruptedException {ListFile fileListnew ArrayList();//枚举所有以.html结尾的文件enumFile(INPUT_FILE,fileList);long startSystem.currentTimeMillis();//创建一个包含10个线程的线程池ExecutorService executorService Executors.newFixedThreadPool(10);//创建一个计数器来表示文件的数量CountDownLatch countDownLatchnew 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 endSystem.currentTimeMillis();log.info(多线程所消耗的时间:(end-start)ms);}private void parseHtml(File file) {//解析html的标题String titleparseHtmlTitle(file);//解析html的urlString url parseHtmlUrl(file);//解析html的正文//String content parseHtmlContent(file);//解析html的正文通过正则表达式String contentparseHtmlContentByRegex(file);//每解析一个文件就创造一个正排索引和倒排索引index.createIndex(title,url,content);}private String readFile(File file) {StringBuilder stringBuildernew StringBuilder();try(BufferedReader bufferedReadernew BufferedReader(new FileReader(file),1024*1024)){while(true){int cbufferedReader.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 contentreadFile(file);//通过正则表达式去掉正文中的script标签contentcontent.replaceAll(script.*?(.*?)/script, );//通过正则表达式去掉正文中的其它标签contentcontent.replaceAll(.*?, );通过正则表达式合并多个空格content content.replaceAll(\\s, );return content;}private String parseHtmlContent(File file) {StringBuilder stringBuildernew StringBuilder();try{BufferedReader bufferedReadernew BufferedReader(new FileReader(file),1024*1024);int flag0;while (true){int nbufferedReader.read();if(n-1){break;}char ch(char)n;if(ch){flag1;}else {if(ch){flag0;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 sfile.getAbsolutePath().substring(C:\\Users\\xia\\IdeaProjects\\SearchProject.length());return https://docs.oracle.com/en/java/javase/22s;}private String parseHtmlTitle(File file) {return file.getName().replaceAll(.html,);}private void enumFile(String inputFile, ListFile fileList) {File filenew File(inputFile);File[]filesfile.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 parsernew 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_FILEC:\\Users\\xia\\IdeaProjects\\SearchProject\\;private ObjectMapper objectMappernew ObjectMapper();//正排索引private ListDocInfo forwardIndexnew ArrayList();//倒排索引1private MapString,ListWeight invertedIndexnew HashMap();private Object lock1new Object();private Object lock2new Object();public DocInfo checkByForward(Integer id){return forwardIndex.get(id);}public ListWeight 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(){};}MapString,WordCount wordCountMapnew HashMap();//先对标题进行分词ListTermtermsToAnalysis.parse( docInfo.getTitle()).getTerms();for(Term term:terms){String tempterm.getName();WordCount wordCountwordCountMap.get(temp);if(wordCountnull){WordCount newWordCountnew WordCount();newWordCount.titleCount10;newWordCount.contentCount0;wordCountMap.put(temp,newWordCount);}else {wordCount.titleCount10;}}//对正文进行分词ListTermterms1ToAnalysis.parse( docInfo.getContent()).getTerms();for(Term term:terms1){String tempterm.getName();WordCount wordCountwordCountMap.get(temp);if(wordCountnull){WordCount newWordCountnew WordCount();newWordCount.titleCount0;newWordCount.contentCount1;wordCountMap.put(temp,newWordCount);}else {wordCount.contentCount1;}}//统计完成,开始合并SetMap.EntryString, WordCountentrySet wordCountMap.entrySet();for(Map.EntryString, WordCount entry:entrySet){synchronized (lock2){String sentry.getKey();Integer sumentry.getValue().contentCountentry.getValue().titleCount;Weight weightnew Weight(sum,docInfo.getId());ListWeightweightListinvertedIndex.get(s);if(weightListnull){ListWeightnewListnew ArrayList();newList.add(weight);invertedIndex.put(s,newList);}else {invertedIndex.get(s).add(weight);}}}}private DocInfo CreateForwardIndex(String title, String url, String content) {DocInfo docInfonew 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 startSystem.currentTimeMillis();File indexPathFilenew File(SAVE_LOAD_FILE);if(!indexPathFile.exists()){indexPathFile.mkdirs();}File forwordFilenew File(SAVE_LOAD_FILEforword.txt);File invertedFilenew File(SAVE_LOAD_FILEinverted.txt);try{objectMapper.writeValue(forwordFile,forwardIndex);objectMapper.writeValue(invertedFile,invertedIndex);}catch (IOException e){e.printStackTrace();}long endSystem.currentTimeMillis();log.info(保存文件成功,消耗时间:(end-start)ms);};/*** 从文件中加载到idea*/public void load(){long startSystem.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 TypeReferenceListDocInfo() {});invertedIndex objectMapper.readValue(invertedFile, new TypeReferenceMapString, ListWeight() {});}catch (IOException e){e.printStackTrace();}long endSystem.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 indexnew Index();public DocSearcher(){index.load();loadStopWords(STOP_WORD_PATH);log.info(文件加载成功);}String STOP_WORD_PATH C:\\Users\\xia\\IdeaProjects\\SearchProject\\stop_word.txt ;HashSetString stopWordsnew HashSet();public ListResult search(String query){ListTerm oldTermToAnalysis.parse(query).getTerms();//用于存储去掉停用词后的分词结果ListTerm termListnew ArrayList();for(Term term:oldTerm){if(stopWords.contains(term.getName())){continue;}termList.add(term);}ListListWeight allResultListnew ArrayList();for(Term term:termList){String sterm.getName();ListWeight tempindex.checkByInverted(s);if(tempnull){continue;}allResultList.add(temp);}//进行权重合并ListWeight weightListmyselfMergeResult(allResultList);weightList.sort(new ComparatorWeight() {Overridepublic int compare(Weight o1, Weight o2) {return o2.getWeight()-o1.getWeight();}});ListResult resultListnew ArrayList();for(Weight weight:weightList){DocInfo docInfoindex.checkByForward(weight.getId());Result resultnew Result();result.setTitle(docInfo.getTitle());result.setUrl(docInfo.getUrl());String contentupdateContent(docInfo.getContent(),termList);result.setContent(content);resultList.add(result);}return resultList;}Datastatic class Pos{public int row;public int col;}private ListWeight myselfMergeResult(ListListWeight source) {PriorityQueueWeight queuenew PriorityQueue(new ComparatorWeight() {Overridepublic int compare(Weight o1, Weight o2) {return o1.getId()-o2.getId();}});for(ListWeight list:source){for(Weight weight:list){queue.offer(weight);}}ListWeight targetnew ArrayList();while (!queue.isEmpty()){Weight curWeightqueue.poll();if(!target.isEmpty()){Weight oldWeighttarget.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 bufferedReadernew BufferedReader(new FileReader(stopWordPath));while (true){String linebufferedReader.readLine();if(linenull){break;}stopWords.add(line);}} catch (IOException e) {throw new RuntimeException(e);}}private String updateContent(String content, ListTerm termList) {int index-1;for(Term term:termList){String wordterm.getName();indexcontent.toLowerCase().indexOf( word );if(index0){break;}}if(index-1){if(content.length()160){return content;}return content.substring(0,160)...;}int startindex60?0:index-60;String desc;if(start160content.length()){desccontent.substring(start);}else{desccontent.substring(start,start160)...;}for(Term term:termList){String wordterm.getName();//(?i)表示不区分大小写进行替换descdesc.replaceAll((?i) word ,i word /i);//自己加的descdesc.replaceAll(\\s, );}return desc;}public static void main(String[] args) {DocSearcher docSearchernew DocSearcher();ListResult resultListdocSearcher.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%的电量差点就回寝了,那一刻感觉值了,成就感拉满了
总结 在使用ObjectMapper的方法时,将文件或字符串等类型转为类对象或包含类对象时,该类必须包含无参构造方法,若写的有含参的构造方法则Spring就不会在提供无参构造方法,会导致程序报错 应为Spring MVC中以内置了Object Mapper方法,在使用时直接创建调用即可,调用后生成一个JSON类型的字符串, 再在RequestMpping中添加Produces来指定返回数据的类型,这样传递出去的是JSON对象格式,传递结果图片如下:
然而若是引入该依赖
则会发生依赖冲突,当我引入SpringWeb(Spring Web MVC)框架时就已经引入了ObjectMapper,在次引入依赖就是多此一举,我再次引入的依赖和SpringWeb框架内置的依赖版本不同,在运行加载配置文件时从我引入的低版本依赖中找objectMapper方法,发现找不到就报异常 StringUtils则是生成字符串,在不指定返回类型时默认的是text/html格式
在指定返回类型是json后,因为不是json字符串转json对象,而是由字符串转json对象,则结果如下: 此时看前端接收处理数据对应的格式了,如不同则报错,例:搜索引擎中前端接收JSON对象而我引入了jackson依赖则导致类型不同,前端无法解析数据而报错