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

专栏:基于正倒排 Java 文档搜索引擎

个人主页:手握风云

目录

[一、Web 模块](#一、Web 模块)

[1.1. 整体架构](#1.1. 整体架构)

[1.2. 后端 Web 接口实现](#1.2. 后端 Web 接口实现)

[1.3. 处理停用词](#1.3. 处理停用词)

[1.4. 多路归并](#1.4. 多路归并)


一、Web 模块

Web 模块的核心作用是将后端搜索能力封装为Web 接口,并提供可视化前端页面,让用户通过浏览器完成搜索、查看结果、跳转官方 Java API 文档的完整操作,是整个搜索引擎的交互入口。核心流程:前端发起搜索请求 → 后端 Controller 接收并调用搜索模块 → 返回 JSON 结果 → 前端渲染展示。

1.1. 整体架构

  • 后端:SpringBoot Controller 层,提供搜索接口,对接搜索模块
  • 交互:GET 请求传参,JSON 格式返回数据
  • 优化:搜索结果关键词标红、样式美化、新窗口跳转文档

1.2. 后端 Web 接口实现

核心基于 SpringBoot Web 场景,使用 @RestController 声明控制器(返回 JSON / 字符串,非页面跳转);接口:/searcher,GET 请求,参数query(搜索关键词)。

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

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yang.java_doc_searcher.searcher.DocSearcher;
import com.yang.java_doc_searcher.searcher.Result;
import org.springframework.web.bind.annotation.GetMapping;
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 DocSearchController {
    private static DocSearcher searcher = new DocSearcher();
    private static ObjectMapper objectMapper = new ObjectMapper();

    @GetMapping(value = "/searcher", produces = "application/json;charset=utf-8")
    @ResponseBody
    public String search(@RequestParam("query") String query) throws JsonProcessingException {
        List<Result> results = searcher.search(query);
        return objectMapper.writeValueAsString(results);
    }
}

1.3. 处理停用词

如果我们输入的 query 为"Array List",我们会发现查询的摘要里面空格也被算进了分词结果集中来执行倒排索引查询。有些无意义高频词,如 is、the、a、have 进行剔除,可以提升搜索速度与结果精准度。因此我们需要在网上找到现有的停用词表,再通过程序把停用词表加载到内存中。我们可以使用哈希表来存储这些停用词,再针对分词结果在哈希表中进行筛选。

java 复制代码
private static final String STOP_WORD_PATH ="D:\\doc_search_index\\stop_word.txt";
private HashSet<String> stopWords = new HashSet<>();

// 停用词加载方法
private void loadStopWord() {
    try (BufferedReader bufferedReader = new BufferedReader(new FileReader(STOP_WORD_PATH))) {
        while (true) {
            String line = bufferedReader.readLine();
            if (line == null) {
                // 读取文件完毕
                break;
            }
            stopWords.add(line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}    

1.4. 多路归并

当输入多词查询时,同一文档被多次匹配」的问题,将多个词的权重累加,让同时包含多个查询词的文档排名更靠前。原理类似N 个有序数组的归并,用优先队列实现高效归并。

java 复制代码
private List<Weight> mergeResult(List<List<Weight>> source) {
// 对每个内部列表按DocId进行排序
    for (List<Weight> curRow : source) {
        curRow.sort(new Comparator<Weight>() {
            @Override
            public int compare(Weight o1, Weight o2) {
            // 比较两个Weight对象的DocId,返回差值用于排序
                return o1.getDocId() - o2.getDocId();
            }
        });
    }

// 初始化目标列表和优先队列
    List<Weight> target = new ArrayList<>();
// 创建优先队列,用于存储每个列表当前最小的元素位置
    PriorityQueue<Pos> queue = new PriorityQueue<>(new Comparator<Pos>() {
        @Override
        public int compare(Pos o1, Pos o2) {
            // 比较两个位置对应的Weight对象的DocId
            Weight w1 = source.get(o1.row).get(o1.col);
            Weight w2 = source.get(o2.row).get(o2.col);
            return w1.getDocId() - w2.getDocId();
        }
    });

    for (int row = 0; row < source.size(); row++) {
        queue.offer(new Pos(row, 0));
    }

    while (!queue.isEmpty()) {
        Pos minPos = queue.poll();
        Weight curWeight = source.get(minPos.row).get(minPos.col);

        // 处理相同DocId的Weight合并
        if (target.size() > 0) {
            Weight lastWeight = target.get(target.size() - 1);
            if (lastWeight.getDocId() == curWeight.getDocId()) {
                // 如果DocId相同,合并权重
                lastWeight.setWeight(lastWeight.getWeight() + curWeight.getWeight());
            } else {
                // 如果DocId不同,添加到目标列表
                target.add(curWeight);
            }
        } else {
            // 如果目标列表为空,直接添加当前元素
            target.add(curWeight);
        }

        // 将当前元素的下一个元素加入优先队列
        Pos newPos = new Pos(minPos.row, minPos.col + 1);
        if (newPos.col >= source.get(newPos.row).size()) {
            // 如果当前列表已处理完,跳过
            continue;
        }
        queue.offer(newPos);
    }
    return target;
}
相关推荐
Elastic 中国社区官方博客1 天前
Elasticsearch Vector DiskBBQ 过滤搜索现已提升 3 – 5 倍速度
大数据·人工智能·elasticsearch·搜索引擎·全文检索
skilllite作者1 天前
Evotown——开启本地化、可验证的AI智能体进化新时代
人工智能·分布式·安全·搜索引擎·agentskills
逸Y 仙X1 天前
文章三十:Elasticsearch SQL实战案例
java·大数据·sql·elasticsearch·搜索引擎·全文检索
有梦想的小何1 天前
Cursor AI 编程实战(篇二):Rules、速查与 Adapter/App 全文
java·大数据·elasticsearch·搜索引擎·ai·ai编程
老陈头聊SEO2 天前
长尾关键词如何提升SEO效能的实用指南与创新策略
其他·搜索引擎·seo优化
Elastic 中国社区官方博客2 天前
jina-embeddings-v5-omni:用于文本、图像、音频和视频的 embeddings
大数据·人工智能·elasticsearch·搜索引擎·ai·音视频·jina
泓博2 天前
Openclaw-Ubuntu常用命令
大数据·elasticsearch·搜索引擎·ai
技术路线图2 天前
教学智慧的数字围城:当专业积累遭遇人工智能认知屏蔽
人工智能·搜索引擎
Elastic 中国社区官方博客2 天前
Elasticsearch ES|QL “读取时模式”:你的未映射字段一直都在那里
大数据·数据库·sql·elasticsearch·搜索引擎·全文检索
Elastic 中国社区官方博客2 天前
Elasticsearch 查询日志:每个查询一行协调器级别日志,适用于 ES|QL、DSL、SQL 和 EQL
大数据·数据库·sql·elasticsearch·搜索引擎·全文检索·可用性测试