DocSearcher:文档搜索引擎

本文档介绍了DocSearcher项目,这是一个针对Java API文档的搜索引擎。项目包括对离线HTML文档的解析、Ansj分词、倒排索引构建、搜索模块和Web模块实现。通过搜索,用户可以快速找到相关API文档,点击结果可直接跳转至在线文档。项目使用SpringBoot、Maven和JDK1.8,涉及的技术栈包括Ansj分词、Servlet、Json和Ajax。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目描述

我们要实现什么样的搜索引擎呢?
该项目主要针对Java API 文档设计出一款文档搜索引擎,当用户在页面上输入查询词后,能够快速的匹配出相关API在线文档,补充了Java在线文档中没有搜索的功能。 每个搜索结果中包含了标题, 描述, 展示 url 和点击 url等信息,便于用户浏览。
Java API 文档线上版本参见: https://docs.oracle.com/javase/8/docs/api/index.html

项目流程

1.对离线版本的HTML文档进行解析,将解析的结果整理为一个行文本文件;
2.读取处理好的行文本文件进行Ansj分词,权重计算等操作,在内存中构造正排索引和倒排索引;
3.根据输入查询词进行分词、触发依据相似性分析以及倒排索引结构对结果进行检索排序,并以Json格式进行包装后序列化为字符串返回;
4.编写简单页面,通过HTTP服务器搭载搜索页面,点击搜索结果可跳转至对应API在线文档。
开发环境:IDEA、Tomcat、Maven、JDK1.8
相关技术栈:Ansj分词、倒排索引、过滤器、HTML、Servlet、Json、Ajax
项目代码https://github.com/xiaoting-hub/Projects-DocSearcher

基础知识

什么是倒排索引?

文档(DOC):经过预处理,用户输入关键字要被检索的页面
正排索引:“一个文档包含了哪些词”。描述一个文档的基本信息, 包括文档标题、 正文,以及文档标题和正文的分词/断句结果。
倒排索引:“一个词被哪些文档引用了”。 描述一个词的基本信息, 包括这个词都被哪些文档引用, 这个词在该文档中的重要程度以及这个词的出现位置等信息。

问题:为什么要用倒排索引?暴力搜索行不行啊?
每次处理搜索请求的时候, 拿着查询词去所有的网页中搜索一遍,检查每个网页是否包含查询词字符串。显然,暴力搜索这种方式随着文档数量的增加开销会线性增加,一般我们对搜索引擎的效率还是比较看重的。所以尽可能的高效才是重点(^ . ^)。

为什么要进行分词?分词的原理,在该项目中分词如何来实现?

用户输入的关键字有时候是多个词/一句话,要搜索准确就必须进行分词,分词原理有两个方面:一种是基于词库,尝试把这些词进行穷举,放到字典文件中,我们可以依次取句子中的内容,每隔一个词进行查找。第二种是基于统计,会有很多官方的语料库进行人工标注/统计,分词技术在NLP中也比较常见。我们在该项目中使用Maven中的第三方库Ansj分词技术。
附带依赖链接:https://mvnrepository.com/artifact/org.ansj/ansj_seg/5.1.6

基本实现

模块划分

项目总共划分为三个模块:
索引模块:扫描下载到的文档,分析数据内容构建正排+倒排索引,并保存到文件中。
搜索模块:加载索引。根据输入的查询词, 基于正排+倒排索引进行检索,得到检索结果。
Web模块:编写一个简单的页面,展示搜索结果。点击其中的搜索结果能跳转到对应的 Java API 文档页面。

创建项目

使用IDEA创建一个SpringBoot项目,具体细节不在详细赘述。项目的目录结结构大致是这样:
在这里插入图片描述

引入分词依赖

使用Ansj分词第三方库,可以看一些简单的示例:java分词-ansj的初次使用
在pom.xml中注入依赖:

<dependency>
    <groupId>org.ansj</groupId>
   	<artifactId>ansj_seg</artifactId>
    <version>5.1.6</version>
</dependency>

注意:当 ansj 对英文分词时,会自动把单词大写转为小写。

实现索引模块

我们要实现的是在本地基于离线文档制作索引,实现检索,当用户在搜素结果页点击具体的搜索结果时,就自动跳转到在线文档的页面。跳转过去的目标页面也称为落地页面。
在这里插入图片描述

1. 实现Parse类

Parse类构建一个可执行程序。
① 根据指定路径,枚举出该路径中的所有文件(html),这个过程中需要把所有子目录的文件获取到;
② 根据文件罗列出的文件路径,打开文件,读取文件内容,解析并构建索引;
a) 标题:直接使用解析操作
b) URL:基于文件路径进行了简单拼接(离线文档和线上文档路径的关系)
c) 正文:核心操作,去标签~简单粗暴的方式实现的。使用<>作为“是否考虑要拷贝数据”的开关
③ 把在文件中构建好的索引数据结构,保存在指定的文件,使用Index类中的addDoc()方法。
Parse类最主要的事情是辅助Index类完成索引的制作过程。详细代码见上述github链接。

public class Parser {
   
    private static final String INPUT_PATH = "E:/IdeaProjects/doc_searcher_index/jdk-8u231-docs-all/docs/api";
    //TODO补充上索引实例
    public static void main(String[] args) throws InterruptedException {
   
        Parser parser = new Parser();
        parser.run();
    }
    
    public void run() {
   
        System.out.println("开始解析!");
        long beg = System.currentTimeMillis();
        // 1. 枚举出这个目录下的所有文件
        ArrayList<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH, fileList);
        for (File f : fileList) {
   
            System.out.println("解析 " + f.getAbsolutePath());
            // 2. 针对每个文件, 打开, 并读取内容, 进行转换
            parseHTML(f);
        }
        System.out.println("解析完成! 开始保存索引!");
        long end = System.currentTimeMillis();
        System.out.println("保存索引完成! 时间: " + (end - beg));
    }

    // 递归完成目录枚举过程
    private void enumFile(String rootPath, ArrayList<File> fileList) {
   
        File rootFile = new File(rootPath);
        File[] files = rootFile.listFiles();
        for (File f : files) {
   
            if (f.isDirectory()) {
   
                enumFile(f.getAbsolutePath(), fileList);
            } else if (f.getAbsolutePath().endsWith(".html")) {
   
                fileList.add(f);
            }
        }
    }
    
    private void parseHTML(File f) {
   
        // 1. 转换出标题
        String title = parseTitle(f);  //得到的文件名字-“.html”
        // 2. 转换出 url
        String url = parseUrl(f);  //网络URL和本地URL进行拼接
        // 3. 转换出正文(正文需要去除 html 标签)
        String content = parseContent(f);  // 先按照一个字符一个字符的方式来读取,以 < 和 > 来控制拷贝数据的开关
        // 4. TODO 添加到索引中
        
    }

    private String parseTitle(File f) {
   
        // 直接使用文件名作为标题
        String name = f.getName();
        return name.substring(0, name.length() - ".html".length());
    }

    private String parseUrl(File f) {
   
        // 这个 url 是指在线文档对应的链接.
        // url 由两个部分构成.
        // 第一部分是 https://docs.oracle.com/javase/8/docs/api
        // 第二部分是 文件路径中 api 之后的部分.
        String part1 = "https://docs.oracle.com/javase/8/docs/api";
        String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());
        return part1 + part2;
    }

    private String parseContent(File f) {
   
        // 读取文件内容, 并去除其中的 html 标签和换行
        try {
   
            FileReader fileReader = new FileReader(f);
            // 是否当前读的字符是正文
            boolean isContent = true;
            StringBuilder output = new StringBuilder();
            while (true) {
   
                int ret = fileReader.read();
                if (ret == -1) {
   
                    break;
                }
                char c = (char)ret;
                if (isContent) {
   
                    if (c == '<') {
   
                        isContent = false;
                        continue;
                    }
                    if (c == '\n' || c == '\r') {
   
                        c = ' ';
                    }
                    output.append(c);
                } else {
   
                    if (c == '>') {
   
                        isContent = true;
                    }
                }
            }
            fileReader.close();
            return output.toString();
        } catch (IOException e) {
   
            e.printStackTrace();
        }
        return "";
    }
}

在这里插入图片描述

2. 实现Index

Index 负责构建索引数据结构。主要提供以下方法:

  • getDocInfo():根据 docId 查正排,返回类型是DocInfo类,包含文档中的详细信息(docId, title, url, content),直接按照下标来取元素
  • getInverted():根据关键词查倒排,返回值类型为List列表;Weight包含了docId和weight权重;按照key取HashMap<String, ArrayList>的value即可
  • addDoc(): 往索引中新增一个文档,包括构建正排索引和构建倒排索引。①构建正排,构造DocInfo对象,添加到正排索引末尾。②构建倒排,先进行标题和正文分词,统计词频。遍历分词结果,去更新倒排索引中对应的倒排拉链即可,同时注意线程安全问题。
  • save():往磁盘中写索引数据,使用ObjectMapper类保存成Json格式;ObjectMapper类是Jackson库的主要类。它能够提供writeValue()和readValue()方法将Java对象和JSON结构相互转换(序列化和反序列化),基于JSON格式把索引数据保存到指定文件中
  • load():从磁盘加载索引数据,基于JSON格式对数据进行解析,将硬盘中的文件读出来,解析到内存中
创建Index类

首先,我们要了解Parse类和Index类的关系,Parse类相当于制作索引的入口,Index类相当于实现了索引的数据结构,可以提供一些API。
所用到的类结构:
在这里插入图片描述

public class Index {
   
    public static final String INDEX_PATH = "E:/IdeaProjects/doc_searcher_index/";
    // 正排索引, 下标对应 docId
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();
    // 倒排索引, key 是分词结果, value 是这个分词 term 对应的倒排拉链(包含一堆 docid)
    private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();

    // 根据 docId 查正排
    public DocInfo getDocInfo(int docId) {
   }
    // 根据 分词结果 查倒排
    public ArrayList<Weight> getInverted(String term) {
   }
    // 向索引中新增一条文档
    public void addDoc(String title, String url, String content) {
   }
    // 加载索引文件
    public void load() {
   }
    // 保存索引文件
    public void save() {
   }
}
创建Weight类

Weight类表示一个文档的权重信息,其中 weight 的值通过词出现的频率来构造。
在代码中使用的是:weight = 10×标题中出现的次数 + 1×正文中出现的次数。

class Weight {
   
    private int docId;
    private int weight;
    public int getDocId() {
   return docId;}
    public void setDocId(int docId) {
   this.docId = docId;}
    public int getWeight() {
   return weight;}
    public void setWeight(int weight) {
   this.weight = weight;}
}
实现 getDocInfo 和 getInverted
// 根据 docId 查正排
public DocInfo getDocInfo(int docId) {
   
    return forwardIndex.get(docId);
}

// 根据 分词结果 查倒排
public ArrayList<Weight> getInverted(String term) {
   
    return invertedIndex.get(term);
}
实现addDoc
DocInfo docInfo = buildForward(title, url, content);
buildInverted(docInfo);
构建正排索引 buildForward
private synchronized 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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值