微网站O2O平台平台开发,推广网上国网的意义,珠海网站建设防,湖北神润建设工程有限公司网站文章目录 4.保存到磁盘中为什么要保存在磁盘中怎么保存操作步骤1. 前期准备2. 主要操作 5. 将磁盘中的数据加载到内存中Parser 类完整源码Index 类完整源码 4.保存到磁盘中
为什么要保存在磁盘中
索引本来是存储在内存中的#xff0c;为什么要将其保存在硬盘中#xff1f; … 文章目录 4.保存到磁盘中为什么要保存在磁盘中怎么保存操作步骤1. 前期准备2. 主要操作 5. 将磁盘中的数据加载到内存中Parser 类完整源码Index 类完整源码 4.保存到磁盘中
为什么要保存在磁盘中
索引本来是存储在内存中的为什么要将其保存在硬盘中
因为创建索引是比较耗时的
因此我们不应该在服务器启动的时候才构建索引启动服务器就可能会拖慢很多很多
通常的做法是把这些耗时的操作单独去进行执行单独执行完了之后再让线上服务器直接加载这个构造好的索引
怎么保存
文本实质上就是字符串我们就可以把字符串直接保存在文件中。我们就需要把内存中的索引结构变成一个“字符串”然后写文件即可
变成字符串的过程就是——序列化对应的特定结构的字符串反向解析成一些结构化数据类/对象/基础数据结构——反序列化
序列化和反序列化有很多现成的通用方法此处咱们就直接使用 JSON 格式来进行序列化/反序列化——jackson
通过 Maven 仓库引入依赖
!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --
dependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-databind/artifactIdversion2.18.2/version
/dependency操作步骤
1. 前期准备
引入一个 jackson 里面会用到的核心对象
private ObjectMapper objectMapper new ObjectMapper();之后就通过这个对象完成后续的序列化和反序列化操作
创建一个文件指定存放的目录
private static final String INDEX_PATH
/Users/yechiel/Desktop/Byte/code_world/Gitee/java_doc_searcher;2. 主要操作
使用两个文件分别保存正排和倒排
先判定一下索引对应的目录是否存在不存在就创建然后在索引中分别创建两个文件forwardIndexFile (正排文件)、invertedIndexFile (倒排文件)使用 writeValue 方法将文件进行写入
public void save(){ // 使用两个文件分别保存正排和倒排 // 1. 先判断一下索引对应的目录是否存在不存在就创建 File indexPathFile new File(INDEX_PATH); if(!indexPathFile.exists()){ indexPathFile.mkdirs(); } File forwardIndexFile new File(INDEX_PATH fordword.txt); File invertedIndexFile new File(INDEX_PATH inverted.txt); try { // 第一个参数写到哪个文件里 第二个对哪个对象进行写入 objectMapper.writeValue(forwardIndexFile, forwardIndex); objectMapper.writeValue(invertedIndexFile, invertedIndex); }catch (IOException e) { e.printStackTrace(); }
}mkdirs() 可以一次嵌套创建多级目录writeValue 方法会报错要在两个操作外面加上 try-catch。这里调用这个方法就不用我们再将文件变成字符串然后再写入文件这里直接进行写入就方便了很多
5. 将磁盘中的数据加载到内存中
public void load(){ System.out.println(加载索引开始); // 1. 设置加载索引的路径和前面保存的路径一样 File forwardIndexFile new File(INDEX_PATH forward.txt); File invertedIndexFile new File(INDEX_PATH inverted.txt); try{ // 第一个参数从哪里读 第二个参数当前读到的数据按照什么类型进行解析 forwardIndex objectMapper.readValue(forwardIndexFile, new TypeReferenceArrayListDocInfo() {}); invertedIndex objectMapper.readValue(invertedIndexFile, new TypeReferenceHashMapString, ArrayListWeight() {});}catch (IOException e){ e.printStackTrace(); } System.out.println(加载索引结束);
}readValue 就会直接读取到文件内容并且把文件内容按照这里指定的类型进行解析 看见这个类型是 ArrayList然后就预期文件里面的 jason 也是代大括号的数组然后看到每一个元素又是 DocInfo我们的 readValue 就期望我们的数据里面的大括号里面的每一个字段都得和 DocInfo 是相对应的 这个对应关系我们是可以保证的因为前面存入磁盘的时候就是用 objectMapper 的 writeValue() 来去把对象生成 JSON 然后保存的生成的时候就是按照每一个属性名为 key 来去存的所以下面解析的时候也是和上面相对应的根据得到的 JSON 中的每一个 key 的值来去找到对应对象中的属性然后给其赋值 这里需要将这个这个结构的字符串转换成一个 ArrayListDocInfo 类型的对象jakson 专门提供了一个辅助工具类—— TypeReference
这是一个带有泛型参数的类我们通过这个类的泛型参数来指定我们实际要转换的类型
forwardIndex objectMapper.readValue
(forwardIndexFile, new TypeReferenceArrayListDocInfo() {});这里相当于创建了一个匿名内部类的实例后面 new 的部分 创建一个匿名内部类这个类实现了 TypeReference同时再创建一个这个匿名内部类的实例创建这个实例的最主要目的就是为了把 ArrayListDocInfo 这个类型信息告诉 readValue 方法
在 java 中并不能直接把一个类型作为方法的参数而是必须得传一个具体的对象正因为这个语法限制我们就必须得绕一个弯。通过一个专门的泛型类再搭配泛型参数才能完成这个过程
Parser 类完整源码
package com.glg.javadoc_searcher;import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;public class Parser {// 先指定一个加载文档的路径private static final String INPUT_PATH /Users/yechiel/Desktop/Byte/code_world/docs;// 创建一个 Index 实例private Index index new Index();public void run(){// 整个 Parser 的入口// 1. 根据指定的路径枚举出该路径中所有的文件(HTML)这个过程需要把所有子目录中的文件都获取到ArrayListFile fileList new ArrayList();enumFile(INPUT_PATH, fileList);/*for(File file : fileList){System.out.println(file);}System.out.println(fileList.size());
*/// 2. 针对上面罗列出的文件路径打开路径读取文件内容进行解析并构建索引for(File f : fileList) {// 通过这个方法来解析单个 HTML 文件System.out.println(开始解析 f.getAbsolutePath());parseHTML(f);}// 3. 把在内存中构造好的索引数据结构保存到指定的文件中index.save();}private void parseHTML(File f) {// 1. 解析出 HTML 的标题String title parseTitle(f);// 2. 解析出 HTML 对应的 URLString url parseUrl(f);// 3. 解析出 HTML 对应的正文有了正文才有后续的描述String content parseContent(f);// 4. 将解析出来的这些信息加入到索引当中index.addDoc(title,url,content);}// 用来解析 HTML 里面的标题信息private String parseTitle(File f) {String name f.getName();return name.substring(0, name.length() - .html.length());}// 用来解析 HTML 里面的 URL 信息private String parseUrl(File f) {String part1 https://docs.oracle.com/javase/8/docs/;String part2 f.getAbsolutePath().substring(INPUT_PATH.length());return part1 part2;}// 用来解析 HTML 里面的正文信息public String parseContent(File f) {//先按照一个字符一个字符的方式来读取以 和 来控制拷贝数据的开关StringBuilder content new StringBuilder();try {FileReader fileReader new FileReader(f);// 加上一个是否要进行拷贝的开关boolean isCopy true;// 还得准备一个保存结果的 StringBuilder//StringBuilder content new StringBuilder();while (true) {// 注意此处的 read() 返回值是 int不是 char// 按理说应该是依次读一个字符返回 char 就够了呀// 此处使用 int 作为返回值主要是为了表示一些非法情况// 比如说读到了文件末尾继续读就会返回 -1// 我们就可以根据返回的 -1 判断读完了int ret fileReader.read();if(ret -1) {// 表示文件读完了break;}// 这个结果不是 -1那么就是一个合法的字符了char c (char)ret;if(isCopy){// 开关打开的状态遇到普通字符就应该拷贝到 StringBuilder 中if(c ){// 关闭开关isCopy false;continue;}if(c \n || c \r){// 为了去掉换行把换行/回车替换成空格c ;}// 其他字符直接进行拷贝即可把结果拷贝到最终的 StringBuilder 中content.append(c);}else {// 开关关闭的状态暂时不拷贝直到遇到 if(c ){isCopy true;}}}fileReader.close();} catch (IOException e) {e.printStackTrace();}return content.toString();}// 第一个参数表示我们从哪个参数开始进行递归遍历// 第二个参数表示递归得到的结果private void enumFile(String inputPath, ArrayListFile fileList) {File rootPath new File(inputPath);// 把当前目录中所包含的目录名全部获取到// listFiles 能够获取到 rootPath 当前目录下所包含的文件/目录一层目录不会进入子文件File[] files rootPath.listFiles();for(File f : files) {// 此时我们就根据当前 f 的类型来决定是否要进行递归// 若 f 是一个普通文件就把 f 加入到 fileList 结果中// 若 f 是一个目录就递归调用 enumFile 方法来进一步地获取子目录中的内容if(f.isDirectory()) {enumFile(f.getAbsolutePath(),fileList);}else {if (f.getAbsolutePath().endsWith(.html))fileList.add(f);}}}public static void main(String[] args) {// 通过 main 方法来实现整个制作索引的过程Parser parser new Parser();parser.run();}
}
Index 类完整源码
package com.glg.javadoc_searcher;import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;// 通过这个类在内存中构造索引结构
public class Index {private static final String INDEX_PATH /Users/yechiel/Desktop/Byte/code_world/Gitee/java_doc_searcher/;private ObjectMapper objectMapper new ObjectMapper();// 使用数组下标表示 docIdprivate ArrayListDocInfo forwardIndex new ArrayList();// 使用一个 哈希表 来表示倒排索引// key 就是词 value 就是一簇和这个词相关的文章private HashMapString, ArrayListWeight invertedIndex new HashMap();// 这个类要提供的方法// 1. 给定一个 docId在正排索引中查询文档的详细信息public DocInfo getDocInfo(int docId){return forwardIndex.get(docId);}// 2. 给定一个词在倒排索引中查询哪些文档和这个词关联// 仔细思考这里的返回值单纯的返回一个整数的 List 是否可行呢这样不太好返回整数是因为 List 里面存的是文档 id// 词和文档之间是存在一定的“相关性”的文档和词的相关性有强有弱不是单一的依次排列// 所以我们再创建一个 Weight 类来处理 文档id 和 文档与词 的相关性权重public ListWeight getInverted(String term){return invertedIndex.get(term);}// 3. 往索引中新增一个文档public void addDoc(String title, String url, String content){// 新增文档操作需要同时给正排索引和倒排索引新增信息// 构建正排索引DocInfo docInfo buildForward(title, url, content);// 构建倒排索引buildInverted(docInfo);}// 实现倒排索引private void buildInverted(DocInfo docInfo) {// 直接使用内部类词频统计class WordCnt {public int titleCount;public int contentCount;}// 通过一个内部类将两个数据装到一起了变成一个 HashMap更方便遍历// 这个数据结构用来统计词频HashMapString, WordCnt wordCntHashMap new HashMap();// 3.1 针对文档标题进行分词ListTerm terms ToAnalysis.parse(docInfo.getTitle()).getTerms();// 3.2 遍历分词结果统计每个词出现的次数for(Term term : terms){// 先判断一下 term 是否存在String word term.getName();WordCnt wordCnt wordCntHashMap.get(word);if(wordCnt null) {// 如果不存在就创建一个新的键值对插入进去titleCount 设为 1WordCnt newWordCnt new WordCnt();newWordCnt.titleCount 1;newWordCnt.contentCount 0;wordCntHashMap.put(word, newWordCnt);}// 如果存在就找到之前的值然后把对应的 titleCount 1wordCnt.titleCount;}// 3.3 针对正文页进行分词terms ToAnalysis.parse(docInfo.getContent()).getTerms();// 3.4 遍历分词结果统计每个词出现的次数for(Term term : terms) {String word term.getName();WordCnt wordCnt wordCntHashMap.get(word);if(wordCnt null) {WordCnt newWordCnt new WordCnt();newWordCnt.titleCount 0;newWordCnt.contentCount 1;wordCntHashMap.put(word, newWordCnt);}else{wordCnt.contentCount;}}// 3.5 把上面的结果汇总到一个 HashMap 里面// 最终文档的权重就设定成标题中出现的次数 * 10 正文中出现的次数// 3.6 遍历刚才这个 HashMap依次来更新倒排索引中的结构// 将 Map 转换成 Set 进行遍历Map 不能直接进行遍历for(Map.EntryString, WordCnt entry : wordCntHashMap.entrySet()) {// 先根据这里的词去倒排索引中查一查// 倒排索引中的一个值——倒排拉链ListWeight invertedList invertedIndex.get(entry.getKey());// 判断是不是存在的空的if(invertedList null) {// 如果为空就插入一个新的键值对ArrayListWeight newInvertedList new ArrayList();// 把新的文档当前的 DocInfo构造成 Weight 对象插入进来Weight weight new Weight();weight.setDocId(docInfo.getDocId());// 权重计算公式标题中出现的次数 * 10 正文中出现的次数weight.setWeight(entry.getValue().titleCount * 10 entry.getValue().contentCount);newInvertedList.add(weight);invertedIndex.put(entry.getKey(), newInvertedList);}else{// 如果非空就把当前这个文档构造出一个 Weight 对象插入到倒排拉链的后面Weight weight new Weight();weight.setDocId(docInfo.getDocId());// 权重计算公式标题中出现的次数 * 10 正文中出现的次数weight.setWeight(entry.getValue().titleCount * 10 entry.getValue().contentCount);invertedList.add(weight);}}}private DocInfo buildForward(String title, String url, String content) {DocInfo docInfo new DocInfo();docInfo.setDocId(forwardIndex.size());docInfo.setTitle(title);docInfo.setUrl(url);docInfo.setContent(content);forwardIndex.add(docInfo);return docInfo;}// 4. 把内存中的索引结构保存到磁盘中public void save(){long beg System.currentTimeMillis();// 使用两个文件分贝保存正排和倒排System.out.println(保存索引开始);// 先判断一下索引对应的目录是否存在不存在就创建File indexPathFile new File(INDEX_PATH);if(!indexPathFile.exists()){indexPathFile.mkdirs();}File forwardIndexFile new File(INDEX_PATH fordword.txt);File invertedIndexFile new File(INDEX_PATH inverted.txt);try {// 第一个参数写到哪个文件里 第二个对哪个对象进行写入objectMapper.writeValue(forwardIndexFile, forwardIndex);objectMapper.writeValue(invertedIndexFile, invertedIndex);}catch (IOException e) {e.printStackTrace();}long end System.currentTimeMillis();System.out.println(保存索引完成消耗时间为 (end - beg) ms);}// 5. 把磁盘中的索引数据加载到内存中public void load(){long beg System.currentTimeMillis();System.out.println(加载索引开始);// 设置加载索引的路径和前面保存的路径一样File forwardIndexFile new File(INDEX_PATH forward.txt);File invertedIndexFile new File(INDEX_PATH inverted.txt);try{// 第一个参数从哪里读 第二个参数当前读到的数据按照什么类型进行解析forwardIndex objectMapper.readValue(forwardIndexFile, new TypeReferenceArrayListDocInfo() {});invertedIndex objectMapper.readValue(invertedIndexFile, new TypeReferenceHashMapString, ArrayListWeight() {});}catch (IOException e){e.printStackTrace();}long end System.currentTimeMillis();System.out.println(加载索引结束消耗时间为 (end - beg) ms);}}