基于倒排索引的 Java 文档搜索引擎(一)

目录

一、项目简介

二、核心概念

[2.1. 文档](#2.1. 文档)

[2.2. 倒排索引](#2.2. 倒排索引)

[2.3. 分词](#2.3. 分词)

三、核心流程

四、索引模块

[4.1. 标准执行流程](#4.1. 标准执行流程)

[4.2. 核心类设计](#4.2. 核心类设计)

[4.3. Parser 类](#4.3. Parser 类)

[1. 递归枚举文件](#1. 递归枚举文件)

[2. HTML 文档解析](#2. HTML 文档解析)

[4.4. Index 类](#4.4. Index 类)

[4.5. DocInfo 类](#4.5. DocInfo 类)

[4.6. Weight 类](#4.6. Weight 类)


一、项目简介

本项目是基于 Java+SpringBoot 开发的简易 Java API 文档搜索引擎,核心目标是实现对 JDK 17 官方 API 文档的高效检索,用户输入查询词即可获取包含标题、描述、链接的搜索结果,并可跳转至对应官方文档页面。项目摒弃了效率低下的暴力搜索方式,采用搜索引擎核心的倒排索引,搭配正排索引实现文档与关键词的双向映射;整体分为索引、搜索、Web三大核心模块,索引模块负责扫描本地 HTML 格式的 Java API 文档,通过 ansj 分词库完成英文分词,构建并持久化正排、倒排索引,还借助线程池优化索引构建速度,同时用正则清理 HTML 标签、剔除无效脚本内容;搜索模块加载索引后,对查询词分词、过滤停用词,通过多路归并合并多关键词权重并按权重排序,生成带关键词标红的摘要结果;Web 模块通过 SpringBoot 提供搜索接口,搭配简易前端页面实现可视化搜索交互,最终完成轻量化、可交互的Java API文档检索功能。

二、核心概念

2.1. 文档

JDK 8 官方 API 文档的本地 HTML 文件,存储路径:D:\doc_search_index\docs\api\java.base\java。本地每个.html文件,唯一对应 Oracle 官方 Java 8 API 在线文档页面,比如 java.util.ArrayList.html。每一个独立的.html文件,经预处理后即为一个独立文档。

2.2. 倒排索引

倒排索引是现代搜索引擎中最核心的数据结构。它的设计目的非常直接:实现从"词汇"到"文档"的极速映射,从而支持高效的全文本搜索。正向索引以文档为主键,就像是一本书的目录,记录了"哪个文档里包含了哪些内容",其映射关系为文档 ID -> 文档内容 -> 包含的词汇,缺点是如果用户想搜索某个特定的词汇,系统必须遍历所有文档的内容去进行匹配,这在海量数据下是不可接受的,时间复杂度极高;而倒排索引以词汇为主键,就像是一本书最后的索引附录页,记录了"这个词汇出现在了哪些页面上",其映射关系为词汇 -> 包含该词汇的文档 ID 列表,优点是搜索时只需查找词典,直接取出对应的文档 ID 列表,时间复杂度极低。

2.3. 分词

分词是将连续文本拆分为有意义的最小词汇单元(term)的过程,是搜索引擎的核心基础操作。核心作用是为后续的倒排索引构建、词频统计、权重计算、查询匹配提供基础。

使用 ansj_seg 分词库完成分词,英文分词会自动转为小写。并且会覆盖文档标题、文档正文、用户输入的查询词三类文本。

XML 复制代码
<!--分词依赖-->
<dependency>
	<groupId>org.ansj</groupId>
	<artifactId>ansj_seg</artifactId>
	<version>5.0.4</version>
	<scope>compile</scope>
</dependency>

代码示例:

java 复制代码
package com.yang.java_doc_searcher.searcher;

import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.util.List;

public class Parser {
    public static void main(String[] args) {
        String str = "The story of Google began in 1995 " +
                "with the meeting of two Stanford University " +
                "graduate students, Larry Page and Sergey Brin.";
        List<Term> terms = ToAnalysis.parse(str).getTerms();
        for (Term term : terms) {
            System.out.print(term.getName() + "/");
        }
    }
}

三、核心流程

  1. 索引模块:扫描本地 Java API 文档→解析 HTML→构建正排 + 倒排索引→持久化到文件。
  2. 搜索模块:加载索引→查询词分词→查倒排索引→结果排序→返回搜索结果。
  3. Web 模块:提供搜索接口 + 前端页面,展示结果并跳转官方 API 文档。

四、索引模块

4.1. 标准执行流程

  1. 枚举文件 :递归扫描指定目录,收集所有.html文档。
  2. 解析文档:提取每个 HTML 的标题、在线 URL、纯文本正文(去标签)。
  3. 构建索引:先建正排索引,再基于分词结果建倒排索引。
  4. 持久化存储:将索引写入磁盘文件,避免重复构建。

4.2. 核心类设计

  1. Parser:索引构建的入口执行类,负责文件枚举、HTML 解析、调用 Index 完成索引构建与保存。
  2. Index:索引核心管理类,维护正排 / 倒排索引,提供增、查、存、加载的核心方法。
  3. DocInfo:正排索引实体类,存储单篇文档的完整信息。
  4. Weight:倒排索引权重实体类,存储词对应的文档 ID 与权重值。

4.3. Parser 类

1. 递归枚举文件

首先,递归遍历配置的文档根目录 JDK 8 API 的 api 目录,接着仅收集后缀为 .html 的文件,过滤非文档文件与目录。

java 复制代码
package com.yang.java_doc_searcher.searcher;

import java.io.File;
import java.util.ArrayList;

/**
 * @author gao
 * @date 2026/4/20 15:15
 */

public class Parser {

    // 要搜索的根目录路径
    private static final String INPUT_PATH = "D:/doc_search_index/docs/api";

    public void run() {
        ArrayList<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH, fileList);
        System.out.println(fileList);
    }

    /**
     * 递归枚举指定路径下的所有 HTML 路径
     * @param inputPath 要搜索的目录路径
     * @param fileList 将找到的 HTML 文件路径添加到里面
     */
    private void enumFile(String inputPath, ArrayList<File> fileList) {
        // 路径转为 File 对象
        File rootPath = new File(inputPath);
        // 获取目录下的所有文件和子目录
        File[] files = rootPath.listFiles();
        for (File f : files) {
            // 如果是目录,继续递归
            if (f.isDirectory()) {
                enumFile(f.getAbsolutePath(), fileList);
            } else {
                // 如果是 .html 文件,则添加进顺序表中
                if (f.getAbsolutePath().endsWith(".html")) {
                    fileList.add(f);
                }
            }
        }
    }

    public static void main(String[] args) {
        Parser parser = new Parser();
        parser.run();
    }
}

2. HTML 文档解析

java 复制代码
public void run() {
    ArrayList<File> fileList = new ArrayList<>();
    enumFile(INPUT_PATH, fileList);

    for (File f : fileList) {
        // 解析 HTML 文件
        System.out.println("开始解析:" + f.getAbsolutePath());
        parseHTML(f);
    }
}

private void parseHTML(File f) {
    // 1. 解析标题
    String title = parseTitle(f);
    // 2. 解析 URL
    String url = parseURL(f);
    // 3. 解析正文
    String content = parseContent(f);
}
  • 解析标题

我们查看下几个文件里的内容,可以看出 .html 之前的内容就是我们想要获取的标题。我们使用 getName() 方法得到文件名,由于获取的文件都有 .html 后缀,再通过 subString() 去除即可。

java 复制代码
/**
 * 从文件名中解析出标题并去掉文件扩展名
 * @param f 要解析的文件对象
 * @return 返回去掉扩展名的文件名作为标题
 */
private String parseTitle(File f) {
    String tmp = f.getName();
    String title = tmp.substring(0, tmp.length() - 5);
    return title;
}
  • 解析 URL

我们对比本地的文档路径与 Oracle 官网的 URL,可以发现,我们只需将本地文档路径前面的部分替换为 Oracle 官网再拼接即可。

java 复制代码
/**
 * 解析文件路径并构建完整URL链接
 * 该方法将本地文件路径转换为Oracle Java文档API的URL格式
 * @param f 要解析的文件对象
 * @return  完整的URL字符串
 */
private String parseURL(File f) {
    String part1 = "https://docs.oracle.com/javase/8/docs/api/";
    String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());
    return part1 + part2;
}
  • 解析文本
java 复制代码
/**
 * 解析文件内容,跳过HTML标签内的内容
 * @param f 要解析的文件对象
 * @return 解析后的纯文本内容
 */
private String parseContent(File f) {
    try (FileReader fileReader = new FileReader(f)) {
        boolean isCopy = true;
        StringBuilder content = new StringBuilder();
        while (true) {
            int ret = fileReader.read();
            // 到达文件末尾,停止读取
            if (ret == -1) {
                break;
            }
            char ch = (char) ret;
            if (isCopy) {
                // 遇到开始符号
                if (ch == '<') {
                    // 停止复制,跳过当前字符
                    isCopy = false;
                    continue;
                }
                content.append(ch);
            } else {
                // 遇到结束符号恢复复制
                if (ch == '>') {
                    isCopy = true;
                }
            }
        }
        return content.toString();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

4.4. Index 类

Parser 把解析好的文档交给 Index,Index 负责把文档变成可检索的索引;搜索模块需要查数据时,直接调用 Index 的查询方法即可。

我们需要利用正排索引,通过文档索引来获取文档的信息,这里我们可以使用 ArrayList 来进行存储文档,并通过 get() 方法快速通过下标访问。当对输入的查询词进行分词之后,再通过 HashMap 进行快速映射,瞬间找到所有包含这个词的文档。

java 复制代码
package com.yang.java_doc_searcher.searcher;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class Index {
    // 下标 = docId,支持通过文档 ID 快速查文档信息
    private ArrayList<DocInfo> forward = new ArrayList<>();
    // Key 为分词后的词(英文自动小写),Value 为该词对应的文档权重列表
    private HashMap<String, ArrayList<Weight>> forwardIndex = new HashMap<>();

    // 1. 根据 docId 查询正排索引,返回文档详情
    public DocInfo getDocInfo(int docId) {

    }

    // 2. 根据分词查询倒排索引,返回对应文档权重列表
    public List<Weight> getInverted(String term) {

    }

    // 3. 新增文档,触发正排 + 倒排索引构建
    public void addDoc(String title, String url, String content) {

    }

    // 4. 将索引序列化为 JSON,写入磁盘文件
    public void save() {

    }

    // 5. 从磁盘加载索引文件到内存
    public void load() {

    }
}
java 复制代码
/**
 * 根据文档 ID 获取文档信息
 * @param docId 文档索引
 * @return 包含文档的信息
 */
public DocInfo getDocInfo(int docId) {
    return forwardIndex.get(docId);
}

/**
 * 根据给定的词条获取倒排索引列表
 * 从倒排索引中检索与指定词条关联的所有权重信息
 * @param term 给定的词条
 * @return 返回与该词条关联的权重列表
 */
public List<Weight> getInverted(String term) {
    return invertedIndex.get(term);
}

Parser 解析完文档后,调用 addDoc() 方法,把「标题、URL、正文」交给 Index 构建索引。

java 复制代码
/**
 * 新增文档,自动构建正排+倒排索引
 * @param title 标题
 * @param url 在线 URL
 * @param content 纯文本内容
 */
public void addDoc(String title, String url, String content) {
    DocInfo doc = buildForward(title, url, content);
    buildInverted(doc);
}

buildForward() 把文档的标题、URL、正文封装成 DocInfo 对象,加入正排索引,自动分配文档 ID(docId)。

java 复制代码
private DocInfo buildForward(String title, String url, String content) {
    DocInfo doc = new DocInfo();
    doc.setDocId(forwardIndex.size());
    doc.setTitle(title);
    doc.setUrl(url);
    doc.setContent(content);

    forwardIndex.add(doc);
    return doc;
}

buildInverted() 对文档的标题、正文分词 → 统计词频 → 计算权重 → 把「词→文档」写入倒排索引。

java 复制代码
private void buildInverted(DocInfo doc) {
    class WordCount {
        // 词条在标题中出现的次数
        public int titleCount;
        // 词条在文本内容中出现的次数
        public int contentCount;
    }

    HashMap<String, WordCount> wordCountHashMap = new HashMap<>();

    // 对文档标题进行分词处理
    List<Term> terms = ToAnalysis.parse(doc.getTitle()).getTerms();
    for (Term term : terms) {
        String word = term.getName();
        WordCount wordCount = wordCountHashMap.get(word);

        // 如果词不存在,则创建新的 WordCount 对象
        if (wordCount == null) {
            WordCount newWordCount = new WordCount();
            newWordCount.titleCount = 0;
            newWordCount.contentCount = 1;
            wordCountHashMap.put(word, newWordCount);
        } else {
            wordCount.contentCount++;
        }
    }

    // 遍历哈希表中的每个词条
    for (Map.Entry<String, WordCount> entry : wordCountHashMap.entrySet()) {
        List<Weight> invertedList = invertedIndex.get(entry.getKey());
        if (invertedList == null) {
            ArrayList<Weight> newInvertedList = new ArrayList<>();
            Weight weight = new Weight();
            weight.setDocId(doc.getDocId());
            weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
            newInvertedList.add(weight);
            invertedIndex.put(entry.getKey(), newInvertedList);
        } else {
            Weight weight = new Weight();
            weight.setDocId(doc.getDocId());
            weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
            invertedList.add(weight);
        }
    }

分词后统一小写:搜索 String 和 string 能查到相同结果。权重倾斜:标题权重远高于正文,保证标题匹配的文档排前面。

save() 把内存中的正排 / 倒排索引,序列化成 JSON 写入磁盘,下次启动直接加载,不用重新解析文档。

java 复制代码
/**
 * 保存索引信息到文件
 * 将正排索引和倒排索引分别保存到指定路径的文本文件中
 */
public void save() {
    System.out.println("保存索引开始");
    File indexPathFile = new File(INDEX_PATH);
    if (!indexPathFile.exists()) {
        indexPathFile.mkdirs();
    }

    // 正排索引文件对象
    File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
    // 倒排索引文件对象
    File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");

    try {
        // 将 Java 对象(ArrayList 和 HashMap)转换为 JSON 格式的字符串,并写入到指定的文件中
        objectMapper.writeValue(forwardIndexFile, forwardIndex);
        objectMapper.writeValue(invertedIndexFile, invertedIndex);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

load() 在程序启动时,从磁盘读取 JSON 文件,反序列化成内存索引,秒级加载。

java 复制代码
/**
 * 加载索引,从文件中读取正排索引和倒排索引数据
 */
public void load() {
    System.out.println("加载索引开始!");

    // 正排索引文件对象
    File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
    // 倒排索引文件对象
    File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");

    try {
        // 将 JSON 格式的字符串转换为 Java 对象(ArrayList 和 HashMap),并加载到内存中
        forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
        invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
    } catch (IOException e) {
        e.printStackTrace();
    }

    System.out.println("加载索引结束!");
}

4.5. DocInfo 类

java 复制代码
package com.yang.java_doc_searcher.searcher;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class DocInfo {
    private int docId;
    private String title;
    private String url;
    private String content;
}

4.6. Weight 类

java 复制代码
package com.yang.java_doc_searcher.searcher;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Weight {
    private int docId;
    // 表示文档和词之间的关联性
    // 值越大,相关性越强
    private int weight;
}
相关推荐
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第十九期 - 状态模式】状态模式 —— 状态流转与行为切换实现、优缺点与适用场景
java·后端·设计模式·状态模式·软件工程
Han.miracle2 小时前
微服务注册中心实操:Eureka+Zookeeper对比+CAP定理详解
java·spring boot·spring
llm大模型算法工程师weng2 小时前
Java面试核心突破:面向对象与设计模式
java·设计模式·面试
weixin_520649872 小时前
xml json ini 文件语法
xml·java·json
user_admin_god2 小时前
AI编码OpenCode入门到入神
java·人工智能
都说名字长不会被发现2 小时前
多服务节点数据修正方案设计与实现
java·事务性发件箱·数据修正
ch.ju2 小时前
Java程序设计(第3版)第二章——局部变量
java
朱一头zcy2 小时前
Java基础复习10:Java网络编程入门、Junit单元测试、反射基本介绍、注解基本介绍、XML基本介绍
java·笔记
user_admin_god2 小时前
Opencode常见问题与优化排查
java·人工智能·自然语言处理·nlp·idea