Java文档搜索引擎总结

news/2025/2/12 21:51:58/

Java文档搜索引擎总结

  • 项目介绍
  • 项目使用的技术栈
  • 前端页面展示
  • 后端逻辑部分
    • 索引部分
    • 搜索模块部分
    • Web模块部分

项目介绍

Java文档搜索引擎项目是一个SSM项目,该项目的前端界面部分是由搜索页面和展示页面组成,后端部分索引模块(ScanAnalysis、index)、搜索模块(Searcher)、Web模块(SearcherController)。该项使用ansj第三方分词库进行分词,该项目并没有使用爬虫程序来获取Java文档,而是直接将Java文档下载下来,将Java文档里面的内容进行分词保存到正排索引文件和倒排索引文件中。

项目使用的技术栈

HTML、CSS、JS、Ajax、SpringBoot、SpringMVC

前端页面展示

搜索页面:
在这里插入图片描述
显示页面:
在这里插入图片描述

后端逻辑部分

索引部分

索引部分底层实现了两个类:ScanAnalysis类、Index类
***ScanAnalysis类:***用来扫描Java文档中的所有HTML文件,将HTML文件的标题、url路径、正文保存到正排索引文件和倒排索引文件中。
***Index类:***底层实现了正排索引结构和倒排索引结构,Index类是配合ScanAnalysis类一起使用的,Index将HTML文件内容保存到正排索引和倒排索引结构中,最终保存到正排索引文件和倒排索引文件中。

ScanAnalysis类的底层代码:

public class ScanAnalysis {//要扫描的根路径private static final String PATH_ROOT = "D:\\知识复习思维导图(Java)和Java笔记\\project-warehouse\\jdk-8u351-docs-all\\docs\\api";//Java文档的网络地址 不同部分private static final String JAVA_PATN = "https://docs.oracle.com/javase/8/docs/api/";//索引对象private static Index index = new Index();/*** 启动方法* 我们在进行扫描的时候,我们会发现在进行扫描的时候效率是比较低的。* 该方法使用的是单线程的方式* 我们可以使用多线程的方式来提高效率*/public void run() {long ben1 = System.currentTimeMillis();//保存每一个文档的路径ArrayList<String> arrayList = new ArrayList<>();//1.获取每一个文档的路径scanPath(PATH_ROOT,arrayList);long ben = System.currentTimeMillis();//2.对每一个html文件进行解析for (String pathChild:arrayList) {analysis(pathChild);}long end = System.currentTimeMillis();System.out.println("解析所花费的时间:"+(end - ben)+"ms");//3.将索引保存的索引文档中index.saveFile();long end1 = System.currentTimeMillis();System.out.println("整个程序的时间:"+(end1 - ben1) +"ms");}/*** 启动方法2:我们对解析这个步骤使用多线程的方式来提高效率**/public void run2() {long ben1 = System.currentTimeMillis();//保存每一个文档的路径ArrayList<String> arrayList = new ArrayList<>();//1.获取每一个文档的路径scanPath(PATH_ROOT,arrayList);long ben = System.currentTimeMillis();//2.对每一个html文件进行解析//我们创建一个有时光线程的线程池ExecutorService executorService = Executors.newFixedThreadPool(15);//这个CountDownLatch对象,是用来表明需要等待多少个任务才结束//因为我们要等到解析这个过程完成了在执行下一步CountDownLatch countDownLatch = new CountDownLatch(arrayList.size());for (String pathChild:arrayList) {//将解析的工作提交倒线程池中executorService.submit(new Runnable() {@Overridepublic void run() {analysis(pathChild);//完成一次解析任务就减一countDownLatch.countDown();}});}try {//等待任务结束,如果没结束,就阻塞等待countDownLatch.await();//关闭线程池executorService.shutdown();} catch (InterruptedException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("解析所花费的时间:"+(end - ben)+"ms");//3.将索引保存的索引文档中index.saveFile();long end1 = System.currentTimeMillis();System.out.println("整个程序的时间:"+(end1 - ben1) +"ms");}/*** 对 HTML文件进行解析* 获取到题目、正文、url* @param pathChild*/private void analysis(String pathChild) {File file = new File(pathChild);//1.获取标题String title = getTitle(file);
//        System.out.println(title);//2.获取正文String content = getContents(file);//3.获取urlString url = getUrl(file);System.out.println(url);//4.将标题、正文、url保存到索引中index.saveIndex(title,content,url);}/*** 获取url* @param file* @return*/private String getUrl(File file) {StringBuilder stringBuilder = new StringBuilder();String str = file.getAbsolutePath().substring(PATH_ROOT.length()+1);for (int i = 0; i < str.length(); i++) {char ch = str.charAt(i);if (ch != '\\') {stringBuilder.append(ch);} else {stringBuilder.append('/');}}return JAVA_PATN+stringBuilder.toString();}/*** 获取正文,这个比较麻烦,我们需要去除标签,和<script></script>里面的内容* 这里我们需要使用正则表达式* @param file* @return*/public String getContents(File file) {//获取到HTML里面的内容String content = getcontentHtml(file);//使用正则表达式,将<script></script>标签和里面的内容都替换掉//字符串中的replaceAll方法是支持正则表达式的content = content.replaceAll("<script.*?>(.*?)</script>"," ");//使用正则表达式,去除其他标签content = content.replaceAll("<.*?>"," ");//使用正则表达式,去除连续的空格content = content.replaceAll("\\s+"," ");return content ;}/*** 获取到HTML文件的内容,这人进行文件读取操作,* 使用字符流,进行读取* @param f* @return*/private String getcontentHtml(File f) {try(BufferedReader bufferedReader = new BufferedReader(new FileReader(f),1024*1024)) {StringBuilder content = new StringBuilder();while (true) {int ret = bufferedReader.read();if (ret == -1) {break;}char ch = (char) ret;//去除换行if(ch == '\n' || ch == '\r') {ch = ' ';}content.append(ch);}return content.toString();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return null;}/*** 获取标题* @param file* @return*/private String getTitle(File file) {return file.getName().replace(".html","");}/*** 扫描根路径,获取该目录下的索引HTML文件的路径* 这里要使用的递归 和 文件操作* @param pathRoot* @param arrayList*/private void scanPath(String pathRoot, ArrayList<String> arrayList) {File file = new File(pathRoot);//获取到该目录的以及文件对象File[] files = file.listFiles();//遍历for (File file1:files) {if (file1.isFile()) {//是普通文件//我们要的是html文件,所以还要进行处理if (file1.getAbsolutePath().endsWith("html")) {arrayList.add(file1.getAbsolutePath());System.out.println(file1.getAbsolutePath());}} else {//是目录,进行递归scanPath(file1.getAbsolutePath(),arrayList);}}}public static void main(String[] args) {ScanAnalysis scanAnalysis = new ScanAnalysis();//程序的入口scanAnalysis.run2();}
}

Index类的底层代码:

public class Index {//正排索引的底层,使用顺序表public ArrayList<JavaDocModel> arrayList = new ArrayList<>();//倒排索引的底层,使用HashMappublic HashMap<String,ArrayList<Weight>> map = new HashMap<>();//创建两个锁private Object lock1 = new Object();private Object lock2 = new Object();//正排索引文件 和倒排索引文件保存的 根目录private static final String INDEX_SAVE_PATH ="D:\\知识复习思维导图(Java)和Java笔记\\project-warehouse\\jdk-8u351-docs-all\\";//线上环境 正排索引文件 和倒排索引文件保存的 根目录
//    private static final String INDEX_SAVE_PATH =
//            "/project/java_doc_searcher_ssm/";//进行JSON格式化的 对象private ObjectMapper objectMapper = new ObjectMapper();/*** 1.正排索引:通过文档Id来获取文档对象* @param docId* @return*/public JavaDocModel getForwardIndex(Integer docId) {return arrayList.get(docId);}/*** 2.通过分词来获取相对应的一组文档的id,这里不仅仅获取到了id,还有权重,有利于进行排序* @param terim* @return*/public ArrayList<Weight> getReverseIndex(String terim) {return map.get(terim);}/*** 3.将标题,正文,url* 保存到正排索引,和倒排索引中*/public void saveIndex(String title,String content,String url){JavaDocModel javaDocModel = new JavaDocModel();javaDocModel.setContent(content);javaDocModel.setTitle(title);javaDocModel.setUrl(url);//1.建立正排索引buildForwardIndex(javaDocModel);//2.建立倒排索引buildReverseIndex(javaDocModel);}/*** 建立倒排索引* 我们需要对文档的标题,正文 进行分词* @param javaDocModel*/private void buildReverseIndex(JavaDocModel javaDocModel) {//统计一个分词在标题和内容中出现多少次class Count{public Integer titleCount;public Integer contentCount;}//1.对文档标题 进行分词List<Term> terms = ToAnalysis.parse(javaDocModel.getTitle()).getTerms();//用来统计词频HashMap<String,Count> hashMap = new HashMap<>();//记录总的分词synchronized (lock1) {//遍历分词termsfor (Term term:terms) {//获取到分词结果String termName = term.getName();Count myCount = hashMap.get(termName);if (myCount == null) {//没有Count newCount = new Count();newCount.titleCount = 1;newCount.contentCount = 0;hashMap.put(termName,newCount);} else {//有,titleCount加一myCount.titleCount += 1;}}//2.对文档对象的正文进行分词terms = ToAnalysis.parse(javaDocModel.getContent()).getTerms();//遍历分词termsfor (Term term:terms) {//获取到分词结果String termName = term.getName();Count myCount = hashMap.get(termName);if (myCount == null) {//没有Count newCount = new Count();newCount.contentCount = 1;newCount.titleCount = 0;hashMap.put(termName,newCount);} else {//有,contentCount加一myCount.contentCount += 1;}}//3.将hashMap 里的数据整合到 map 里面//遍历hashMapfor (Map.Entry<String,Count> entry:hashMap.entrySet()) {String key = entry.getKey();Count val = entry.getValue();//从倒排索引中获取value值ArrayList<Weight> weights = map.get(key);if (weights == null) {//没有,创建新的ArrayList<Weight> newWeights = new ArrayList<>();Weight weight = new Weight();//设置文档Idweight.setDocId(javaDocModel.getDocId());//设置权重,titleCount*20+contentCountweight.setWeight(val.contentCount + val.titleCount*20);newWeights.add(weight);map.put(key,newWeights);} else {//有的话,直接添加Weight weight = new Weight();//设置文档Idweight.setDocId(javaDocModel.getDocId());//设置权重,titleCount*20+contentCountweight.setWeight(val.contentCount + val.titleCount*20);weights.add(weight);}}}}/*** 建立正排索引,以顺序表的下标作为文档ID* 直接插入顺序表就行* @param javaDocModel*/private void buildForwardIndex(JavaDocModel javaDocModel) {synchronized (lock2) {//插入docIdjavaDocModel.setDocId(arrayList.size());//直接插入顺序表尾部arrayList.add(javaDocModel);}}/*** 4.将正排索引结构  和 倒排索引结构 保存到 正排索引文件 和倒排索引文件中* 序列化的方法:以JSON的格式保存*/public void saveFile() {//正排索引 和 倒排索引保存的目录File filePath = new File(INDEX_SAVE_PATH);if (!filePath.exists()) {//创建目录filePath.mkdirs();}//正排索引文件对象File fileForwardIndex = new File(INDEX_SAVE_PATH+"forward.txt");//倒排索引文件对象File fileReverseIndex = new File(INDEX_SAVE_PATH+"reverse.txt");if (!fileForwardIndex.exists()) {//不存在,创建正排索引文件try {fileForwardIndex.createNewFile();} catch (IOException e) {e.printStackTrace();}}if (!fileReverseIndex.exists()) {//不存在,创建倒排索引文件try {fileReverseIndex.createNewFile();} catch (IOException e) {e.printStackTrace();}}try {//将正排索引结构转成JSON格式,保存到正排索引文件中objectMapper.writeValue(fileForwardIndex,arrayList);//将倒排索引结构转成JSON格式,保存到倒排索引文件中objectMapper.writeValue(fileReverseIndex,map);} catch (IOException e) {e.printStackTrace();}}/*** 5.加载正排 和 倒排 文件 ,将内容加载倒内存中* 反序列*/public void load() {long ben = System.currentTimeMillis();//正排索引文件对象File fileForwardIndex = new File(INDEX_SAVE_PATH+"forward.txt");//倒排索引文件对象File fileReverseIndex = new File(INDEX_SAVE_PATH+"reverse.txt");try {//这里的 readValue方法用法要注意// 第二个参数是一个匿名内部类,实现了TypeReference,目的就是 我们想要把JSON格式的字符串转成什么类型 告诉了 readValue方法//正排arrayList = objectMapper.readValue(fileForwardIndex, new TypeReference<ArrayList<JavaDocModel>>() {});//倒排map = objectMapper.readValue(fileReverseIndex, new TypeReference<HashMap<String,ArrayList<Weight>>>() {});} catch (IOException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("加载文档的时间:"+(end - ben) +"ms");}
}

搜索模块部分

搜索模块部分底层实现了Searcher类,提供了searcher方法来搜索相关的文档。
Searcher类的底层代码:

public class Searcher {//索引类private Index index = new Index();//保存停用词表的数据结构private Set<String> stopWordsSet = new HashSet<>();//停用词表的存放路径private static final String STOP_WORDS ="D:\\知识复习思维导图(Java)和Java笔记\\project-warehouse\\jdk-8u351-docs-all\\stop_words.txt";//线上环境 停用词表的存放路径
//    private static final String STOP_WORDS =
//            "/project/java_doc_searcher_ssm/stop_words.txt";public Searcher() {//1.创建该类的时候,加载一些索引文档index.load();//2.创建该类的时候,加载停用词表loadStopWords();}/*** 加载停用词表*/private void loadStopWords() {long ben = System.currentTimeMillis();//进行读操作try(BufferedReader bufferedReader = new BufferedReader(new FileReader(STOP_WORDS)) ){while (true) {String str = bufferedReader.readLine();if (str == null) {break;}stopWordsSet.add(str);}} catch (IOException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("加载停用词表的时间:"+(end - ben) + "ms");}public List<ResultReturnModenl> searcher(String word) {//将查询词进行分词、List<Term> terms = ToAnalysis.parse(word).getTerms();//我们通过分词结果可以得出,有些分词是不合理的//我们要排除一些不合理的分词结果//这里我们使用停用词表进行过滤List<Term> newTerms = new ArrayList<>();//保存过滤后的termfor (Term term:terms) {//分词内容String wordName = term.getName();if (!stopWordsSet.contains(wordName)) {//不是停用词newTerms.add(term);}}//遍历newTerms,获取要返回的数据List<ArrayList<Weight>> listList = new ArrayList<>();for (Term term:newTerms) {//获取倒分词的内容String wordName = term.getName();//通过倒排索引,来获取倒相对应的文档对象ArrayList<Weight> reverseIndex = index.getReverseIndex(wordName);//判断是否拿到if (reverseIndex == null) {//没有拿到continue;}//将reverseIndex保存到 listList中listList.add(reverseIndex);}//合并listList中的数组,并且进行去重//类似于合并多个有序数组,并且最后的结果要有序List<Weight> list = sortArray(listList);//对list进行排序,按照权重的大小由高到低排序Collections.sort(list, new Comparator<Weight>() {@Overridepublic int compare(Weight o1, Weight o2) {//降序return o2.getWeight() - o1.getWeight();}});//保存返回的数据List<ResultReturnModenl> results = new ArrayList<>();//将数据进行封装for (Weight weight:list) {//通过正排索引找到文档对象JavaDocModel forwardIndex = index.getForwardIndex(weight.getDocId());ResultReturnModenl resultReturnModenl = new ResultReturnModenl();//设置标题resultReturnModenl.setTitle(forwardIndex.getTitle());//设置urlresultReturnModenl.setUrl(forwardIndex.getUrl());//设置摘要resultReturnModenl.setDesc(getDesc(forwardIndex.getContent(),newTerms));results.add(resultReturnModenl);}return results;}/**生成正文摘要*    由于docInfo对象里面是正文,所以还要做一些处理*    摘要要包含 查询词 或者 查询词的一部分*    生成摘要的思路:可以遍历查询词的分词,找到对应位置*   就针对这个位置,往前截取60个字符,作为描述的开始,然后从描述开始在截取160个字符* @param content* @param newTerms* @return*/public String getDesc(String content, List<Term> terms) {//记录分词出现的位置int termIndex = -1;for (Term term:terms) {//获取到分词内容String wordName = term.getName();//将正文转成小写 使用toLowerCase()//此处需要的是全词匹配,在word前后都加一个空 在进行查找//这里的匹配不严谨,更严谨的方法是使用 正则表达式//indexOf不支持正则表达式//Java提供了 Pattern 和 Matcher 这两个类 来实现正则表达式,自己学习一下//Pattern : 描述一个匹配规则//Matcher 负责进行具体的匹配工作//这里的做法:把不是空格的转成空格content = content.toLowerCase().replaceAll("\\b"+wordName+"\\b"," " + wordName + " ");termIndex = content.toLowerCase().indexOf(" "+wordName+" ");if (termIndex != -1 ) {//存在break;}}if (termIndex == -1) {//所有的分词结果都不存在//返回正文的前160个字符if (content.length() <=160) {return content;}return content.substring(0,160)+"...";}//程序如果到这里,说明正文中有分词结果//判断是否要往前60个字符termIndex = termIndex - 60 >=0?termIndex-60:0;String desc = "";//保存正文摘要if (termIndex+160 >= content.length()) {//从termIndex这个位置截到尾desc = content.substring(termIndex);} else {desc = content.substring(termIndex,160+termIndex)+"...";}//在此处加上替换操作,把描述中的 和 分词结果相同的部分,//加上依次<i>标签,可以使用 replaceAll 的方法来实现//者样在前端显示的时候,可以标红//遍历分词结果for (Term term:terms) {//获取到结果String word = term.getName();//注意此处要进行全字匹配,不区分大小写替换desc = desc.replaceAll("(?i) "+word +" ","<i> "+word+" </i>");}return desc;}/*** 合并listList中的数组,并且进行去重* 类似于合并多个有序数组,并且最后的结果要有序* @param listList* @return*/private List<Weight> sortArray(List<ArrayList<Weight>> listList) {class Pos{public Integer row = 0;//行public Integer col = 0;//列public Pos(Integer row, Integer col) {this.row = row;this.col = col;}}//使用优先级队列,来解决该问题//创建优先级队列PriorityQueue<Pos> pos = new PriorityQueue<>(new Comparator<Pos>() {@Overridepublic int compare(Pos o1, Pos o2) {//小根堆return listList.get(o1.row).get(o1.col).getDocId() - listList.get(o2.row).get(o2.col).getDocId();}});//将每一个数组,按docId的大小,升序排序for (ArrayList<Weight> weights:listList) {Collections.sort(weights, new Comparator<Weight>() {@Overridepublic int compare(Weight o1, Weight o2) {return o1.getDocId() - o2.getDocId();}});}//将每一个数组的第一个元素的位置放进来for (int i = 0; i < listList.size(); i++) {pos.offer(new Pos(i,0));}List<Weight> listResult = new ArrayList<>();//保存最后返回的结果while (!pos.isEmpty()) {//从优先级队列出来的队首元素Pos pos1 = pos.poll();if (listResult.size() == 0) {//插入第一个元素listResult.add(listList.get(pos1.row).get(pos1.col));} else {//不是第一个,要判断是否于前一个相同,相同权重相加if (listResult.get(listResult.size() - 1).getDocId() == listList.get(pos1.row).get(pos1.col).getDocId()) {//文档相同,权重相加listResult.get(listResult.size() - 1).setWeight(listResult.get(listResult.size() - 1).getWeight()+listList.get(pos1.row).get(pos1.col).getWeight());} else {//不相同,添加到listResult中listResult.add(listList.get(pos1.row).get(pos1.col));}}if (pos1.col + 1 >= listList.get(pos1.row).size()) {//这一行处理完了continue;}pos.offer(new Pos(pos1.row, pos1.col+1));}return listResult;}public static void main(String[] args) {Searcher searcher = new Searcher();}
}

Web模块部分

Web模块部分实现前后端的交互。
Web模块的代码:

@RestController
public class SearcherController {@AutowiredSearcher searcher ;@RequestMapping("/searcher")public Object searcher(String word) {if (word == null || word.trim().equals("")) {return -1;}return searcher.searcher(word);}@RequestMapping("/getword")public String getWord(String word) {System.out.println(word);return word;}
}

http://www.ppmy.cn/news/30307.html

相关文章

【懒加载数据 Objective-C语言】

一、咱们就开始进行懒加载 1.懒加载发现,每一个字典,是不是就是四个键值对组成的: 1)answer:String,中国合伙人, 2)icon:String,movie_zghhr, 3)title:String,创业励志电影, 4)options:Array,21 items 前三个都是String类型,最后是不是Array类型, 所…

SVN项目迁移到Git方法

本文记录如何将SVN项目迁移到Git&#xff0c;并保留提交日志信息。 目录Git和SVN差异环境准备Git安装、配置项目迁移1. 将源SVN库转换到Git本地仓库2. 添加Git远程库地址3. 推送代码到Git常见错误参考文档Git和SVN差异 Git是一个开源的分布式版本控制系统&#xff0c;由Linux之…

从零学习python-01helloworld与转义字符

因为记性不好所以开个学习流程方便后面自己复习&#xff0c;也可以系统的整理一下自己敲过的东西python作为一个脚本解释型语言&#xff0c;写起来感觉很清爽&#xff0c;不用定义很多东西。同样语法有点奇特&#xff0c;用缩进来区分语句关系&#xff0c;第一节就先熟悉体验一…

【博学谷学习记录】超强总结,用心分享丨人工智能 自然语言处理 文本特征处理小结

目录文本特征处理作用常见的文本特征处理方法添加n-gram特征说明提取n-gram文本长度规范说明实现导包问题记录心得文本特征处理作用 文本特征处理包括为语料添加具有普适性的文本特征, 如:n-gram特征 以及对加入特征之后的文本语料进行必要的处理, 如: 长度规范. 这些特征处…

React—— hooks(一)

&#x1f9c1;个人主页&#xff1a;个人主页 ✌支持我 &#xff1a;点赞&#x1f44d;收藏&#x1f33c;关注&#x1f9e1; 文章目录⛳React Hooks&#x1f4b8;useState(保存组件状态)&#x1f948;useEffect(处理副作用)&#x1f50b;useCallback&#xff08;记忆函数&#…

算法练习-二分查找(一)

算法练习-二分查找 1 代码实现 1.1 非递归实现 public int bsearch(int[] a, int n, int value) {int low 0;int high n - 1;while (low < high) {int mid (low high) / 2;if (a[mid] value) {return mid;} else if (a[mid] < value) {low mid 1} else {high …

Android Learn

SharedPreference 共享参数用法 SharedPreference 是 Android 的一个轻量级存储工具, 采用的存储结构是Key-Value的键值对方式. 共享参数的存储介质是符合XML规范的配置文件. 保存路径是: /data/data/应用包名/shared_prefs/文件名.xml 利用元数据配置快捷菜单 (1)元数据的meta…

红队APT——邮件钓鱼攻击SwaksOffice漏洞RLO隐藏压缩释放

目录 (一)采用自己搭建Ewomail配合Swaks 0x01 搭建过程 0x02 配置转发信息 (二)网页钓鱼-克隆修改