基于学院网站的搜索引擎 课程设计博客

基于学院网站的搜索引擎 课程设计博客

网安2411 于鸿硕 202421336018


一、前言

在课程设计第一次提交时,在我的博客中写明了课程设计的来源、背景调查、参考项目/代码、基本功能框架预期设计,这里不过多赘述。在后续的尝试中发现,该项目的难度来到了前所未有的高度,所要学习的新知识量极大。虽然会尽力去做好整个设计,但总归还是一个人的工作,在后续设计中有纰漏或不足之处,还望更多见谅。以下将开始正式记录本次设计过程。


二、项目代码设计与包设计

本项目包与类设计如下:

核心搜索流程及关键类:

graph TD 网页抓取crawler.SiteCrawler-->HTML解析crawler.PageParser HTML解析crawler.PageParser-->内容储存storage.ArticleRepository 内容储存storage.ArticleRepository-->索引构建index.IndexBuilder+InvertedIndex 索引构建index.IndexBuilder+InvertedIndex-->查询服务search.SearchService 查询服务search.SearchService-->Web展示servlet.SearchServlet

根据搜索引擎工作路径创建包结构

具体类方向与方法:

classDiagram class ServerBootstrap { +main() } class SiteCrawler { +crawl() } class PageParser { +parse() } class ArticleRepository { +save(Article) +findAll() } class IndexBuilder { +build(List~Article~) } class InvertedIndex { +add(token, articleId) +search(token) } class SearchService { +search(keyword) } class SearchServlet { +doGet() } class Article { id title content summary url } class SearchResult { title summary url category score } ServerBootstrap --> SiteCrawler SiteCrawler --> PageParser SiteCrawler --> ArticleRepository SiteCrawler --> Article ServerBootstrap --> IndexBuilder IndexBuilder --> InvertedIndex IndexBuilder --> ArticleRepository ServerBootstrap --> SearchService SearchService --> InvertedIndex SearchService --> ArticleRepository SearchService --> SearchResult SearchServlet --> SearchService SearchServlet --> SearchResult

三、关键技术要点

1.倒排索引的数据结构

核心思想:关键词-->文档集

在index.InvertedIndex中:

private final Map<String, Set<String>> index=new HashMap<> ();
    public void add(String token, String articleId) {
        index.computeIfAbsent(token, k -> new HashSet<>()).add(articleId);
    }
    public Set<String> search(String token) {
        return index.getOrDefault(token, Collections.emptySet());
    }
    public Map<String, Set<String>> getIndex() {
        return index;
    }

数据结构分为两层,外层为Map 键为关键字token、值为子HashSet,内层的HashSet储存文章ID,主要方法为通过search方法找到目标子Set和返回首索引地址。

2.中文分词

使用jieba分词库进行分词,在index.Tokenizer中:

//import com.huaban.analysis.jieba.JiebaSegmenter;

private final JiebaSegmenter segmenter = new JiebaSegmenter();

    public List<String> tokenize(String text) {
        List<String> tokens = new ArrayList<>();
        if (text == null || text.isBlank()) return tokens;

        // 按段落分割
        String[] paragraphs = text.split("\n");
        for (String paragraph : paragraphs) {
            if (paragraph == null || paragraph.isBlank()) continue;
            tokens.addAll(segmenter.sentenceProcess(paragraph));
        }

        return tokens;

先自行按换行符划分,然后使用jieba分词把中文分装成小字符串装入List。

3.数据仓库与线程生命周期管理

在Web项目中,网页数据需要载入本地缓存中储存,使用storage.ArticleRepository:

public class ArticleRepository {

    private final List<Article> articles = new ArrayList<>();

    public List<Article> findAll() {
        return new ArrayList<>(articles);
    }

    public void save(Article article) {
        articles.add(article);
    }
}

返回新的articles与储存

生命周期管理部分,在ServerBootstrap中:

       // 初始化文章存储
        ArticleRepository repo = new ArticleRepository();

        // 爬虫抓取网站
        SiteCrawler crawler = new SiteCrawler(repo);
        System.out.println("开始爬取网站...");
        crawler.crawl();
        System.out.println("爬取完成,文章总数:" + repo.findAll().size());

        //构建索引
        IndexBuilder builder = new IndexBuilder();
        InvertedIndex index = builder.build(repo.findAll());
        IndexServlet.INDEX = index;
        System.out.println("索引构建完成");

        // 初始化搜索服务
        SearchService searchService = new SearchService(index,repo);

新建一个存储对象repo,全程对repo进行操作

在之前的调试过程中,曾出现repo指向不明和多个潜在可能repo导致NullPointerException和搜索无结果,导致线程生命周期残废,这一部分将在课设报告中详细解释阐明。

4.基于Servlet+Jetty的小型Web服务

在之前的版本中,我使用的是SpringBoot进行网页设计,但是由于在实际设计中,发现使用SpingBoot的低程序员可见性会让调试更困难;没有办法针对URL进行显式映射,扩展可能性弱;还有一点就是SpringBoot管理复杂度与学习成本过高,对于小型Web来说有点过犹不及。所以本次设计最终选择Servlet+Jetty提供Web服务。

在bootstrap.ServerBootstrap中:

// 创建 Jetty 服务器
        int port = 9090; 
        Server server = new Server(port);

        //设置 ServletContext
        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
        context.setResourceBase(
                ServerBootstrap.class
                        .getClassLoader()
                        .getResource("web")
                        .toExternalForm()
        );

        server.setHandler(context);

        //注册 SearchServlet(传入 searchService)
        context.addServlet(new org.eclipse.jetty.servlet.ServletHolder(new SearchServlet(searchService)), "/search");

        context.setResourceBase("src/main/resources/web");
        context.addServlet(DefaultServlet.class, "/");

        server.start();
        System.out.println("服务器启动成功,访问 http://localhost:" + port);
        server.join();

通过类加载器获取Web资源并下载有关内容,使用Jetty服务器建立连接并注册Servlet手动路由调用HTML资源,完成爬虫和连接后使进程等待。

5.爬虫

本次设计的关键技术是网站内定向垂直爬虫,需要抽取信息后储存至数据仓库中,在设计中广泛使用Jsoup作为html解析工具,类crawler.SiteCrawler中:

private final ArticleRepository repository;
    private String rootUrl = "https://cec.jmu.edu.cn";
    private final Set<String> visited = new HashSet<>();

    public SiteCrawler(ArticleRepository repository) {
        this.repository = repository;

从根网址出发爬取,抓取信息后装入数据仓库。

private void crawlPage(String url) {
        if (visited.contains(url)) return;
        visited.add(url);

        try {
            Document doc = Jsoup.connect(url)
                    .timeout(10_000)
                    .userAgent("Mozilla/5.0")
                    .get();

            String text = doc.body().text();
            if (text == null || text.isBlank()) return;

            // 去掉导航页 / 目录页
            if (text.length() < 80) {
                return;
            }

            /* 构建 Article */
            String title = doc.title();
            String summary = text.length() > 30
                    ? text.substring(0, 30) + "..."
                    : text;

            Article article = new Article();
            article.setUrl(url);
            article.setTitle(title);
            article.setContent(text);
            article.setSummary(summary);

            repository.save(article);

            /* 递归抓取站内链接  */
            Elements links = doc.select("a[href]");
            for (Element link : links) {
                String absUrl = link.absUrl("href");

                if (isAttachment(absUrl)) {
                    saveAttachment(link, absUrl, doc);
                    continue;
                }

                // 普通页面递归
                if (absUrl.startsWith(rootUrl)) {
                    crawlPage(absUrl);
                }
            }

        } catch (IOException e) {
            System.out.println("跳过无法访问的页面: " + url);
        }
    }

页面内爬虫在排除内容过少的无效页后,把页面信息装入一个Article对象,然后调用递归抓取该地址的链接,组合成一个完整的网页Article,最后递归到下一页。


四、核心算法与其他功能类

1.搜索关键字匹配度算法

在最初的尝试中,我使用的是【score值制】算法,即给分词后的字符串与网页内容的匹配数设定一个阈值,当这个值大于等于某个阈值的时候就可以被纳入搜索结果。但这样的问题显而易见:阈值的精度把握难,即,当阈值设置过高,搜索结果变得非常少,尤其是当关键词不准确时 搜索结果差强人意;当阈值设置过低,搜索结果可能出现数十上百,比如搜索”计算机工程学院收到集美小学感谢信“,会搜索出所有包含”计算机工程学院“的结果,显然违背了搜索引擎的需要。

在改进版本中,我尝试使用【命中率】算法,即当命中字符数/总字符数>=某设定值时,纳入搜索结果。虽然这样的算法确实较阈值法提高了精确性,但是本质上还是阈值算法,治标不治本。

最终版本里,我使用【最低命中率阈值+相关性加权+结果score排序】算法,要求命中情况设定最低命中率50%;设定一个值score,对于命中标题的页面相关值+5,命中正文+1(会通过HashSet排除重复命中情况),score值<=0的自动略过;对于查找到的结果使用score大小作为根据降序排列。在现有结构上最大限度保证查找精确性。

    public List<SearchResult> search(String keyword) {
        Set<String> shownUrls = new HashSet<>();
        List<SearchResult> results = new ArrayList<>();

        if (keyword == null || keyword.isBlank() || index == null) return results;

        List<String> tokens = tokenizer.tokenize(keyword);
        if (tokens.isEmpty()) return results;

        // 命中词统计
        Map<String, Integer> hitCountMap = new HashMap<>();
        for (String token : tokens) {
            for (String id : index.search(token)) {
                hitCountMap.put(id, hitCountMap.getOrDefault(id, 0) + 1);
            }
        }

        // 至少命中一半
        int minHit = Math.max(1, tokens.size() / 2);

        for (Article article : repository.findAll()) {
            String id = article.getId();
            if (!hitCountMap.containsKey(id)) continue;
            if (hitCountMap.get(id) < minHit) continue;

            String content = article.getContent();
            if (content == null || content.isBlank()) continue;

            // 从正文中找 snippet
            String snippet = extractSnippet(content, keyword, 30);
            if (snippet == null) {
                snippet = article.getSummary(); // 兜底
            }

            if (snippet == null || snippet.isBlank()) continue;

            // 计算 score
            double score = 0;
            for (String token : tokens) {
                if (article.getTitle() != null && article.getTitle().contains(token)) {
                    score += 5;
                }
                if (snippet.contains(token)) {
                    score += 1;
                }
            }
            if (score <= 0) continue;

            String url = article.getUrl();
            if (shownUrls.contains(url)) continue;
            shownUrls.add(url);

            results.add(new SearchResult(
                    id,
                    article.getTitle(),
                    snippet,
                    url,
                    inferCategory(article),
                    score
            ));
        }

        results.sort((a, b) -> Double.compare(b.getScore(), a.getScore()));
        return results;
    }

2.model.Article/SearchResult

为搜索结果存储建立对象,含基本getter/setter方法

3.servlet.HomeServlet/IndexServlet

网页制作与入口,索引容器

4.在设计过程中被废止的类

在设计过程中,有一部分类在最初设计中存在,但在实际编写与调试过程中发现冗余,遂删去

1.SiteCrawlerService、PagePaser、Crawler类 原crawler包下专用于构建爬虫功能的多个分类,现整合合并入SiteCrawler类;

2.Ranker和SummaryGennerator 类 原search包下用于排序和摘要选取的类,现因SearchServicet增强被废止;

3.IndexServlet 类 原Servlet下首页类,后功能与ServerBootstrap和index.html重复,废止;

4.datastore类 原storage包下类,用于储存搜索后的数据,现发现没有这个必要,废止;

4.search.html crawler.html buildindex.html 原resource文件夹下web包html文件,后因其他类内嵌入html废止。


五、拓展的功能

在与老师的交流中,提出了一些改造提升可能

附件检索与下载

经资料查阅与学习,附件的检索最关键点在附件URL的识别,我发现文件的URL末尾都是文件名,那么也就会有文件的扩展名,这就可以供我捕捉一些常见的基础的文件

private boolean isAttachment(String url) {
        if (url == null) return false;
        String lower = url.toLowerCase();
        return lower.endsWith(".pdf")
                || lower.endsWith(".doc")
                || lower.endsWith(".docx")
                || lower.endsWith(".xls")
                || lower.endsWith(".xlsx")
                || lower.endsWith(".ppt")
                || lower.endsWith(".pptx");
    }

    private void saveAttachment(Element link, String url, Document doc) {
        String title = link.text();
        if (title == null || title.isBlank()) {
            title = url.substring(url.lastIndexOf("/") + 1);
        }

        Article article = new Article();
        article.setUrl(url);
        article.setTitle(title);
        
        article.setContent(
                title + " " + doc.title()
        );

        article.setSummary("附件:" + title);
        article.setAttachment(true);

        repository.save(article);
    }

基于RAG技术的大模型搜索拓展

经老师给的研讨方向,我发现目前很多搜索引擎都会扩展该项技术:

img

我搜索了一下该项技术。发现该项技术是我暑期参加的一个项目测试推广队并担任队长的技术来源(智能科学与技术系刘益玲老师实验室项目)下附当时的项目书截图

img

img

该项目使用Python语言,其套用语法与本项目虽大相径庭但也有可借鉴之处,经资料查询,我找到以下思路:

1.在现有搜索引擎前面加一个 “LLM 问答入口”;2. 在搜索结果后面加一个 “上下文构造 + 大模型推理层”

用户问题(自然语言)
        ↓
  中文分词 / 向量化
        ↓
  本地搜索引擎(现有的)
        ↓
  Top-K 相关文章(正文片段)
        ↓
  RAG Prompt 构造
        ↓
  多模态 / 文本大模型
        ↓
  结构化答案 + 引用来源

基于这样子的结构,我要求AI为我生成了一系列代码并尝试放入项目中,赎买了一个DeepSeekAPI并接入,

主要代码由于是AI生成这里略去,后续可查阅,经测试,成功调用并返回:

img

我通过AI的帮助简单潦草地实现了该功能,完成了对项目的初步扩展,这样的搜索引擎对用户对于关键词的把控度要求低,拉宽了使用搜索引擎的年龄跨度,精度更高,不需要再自行打开网页查阅被检索信息。


六、课程设计感想

在最初选择课程设计题目的时候,选择这个题目主要是针对自己是网络空间安全专业的学生,希望通过与网页有关的程序设计了解有关Web编程和网络技术的知识。从选题到目前为止的较长时间中,主要有以下感想: **

  1. 要做一名计算机从业者,掌握知识是最基本的要求。

    对于实际的企业工作或科研项目来说,掌握多少知识和实践都有巨大出入。最重要的是多敲代码,多思考程序,多和时代接轨,多和应用层打交道。这样才能真正做一名合格的计算机从业者,而不只是停留在课本上的知识;

  2. 计算机工作者学习的知识永远无法满足时代要求,计算机工作者必须保持终身学习的习惯。

    计算机是一门瞬息万变,时刻都有新技术被开发出来的工具。在我设计本项目时,其实在Github上看到了一些使用全向智能体实现的搜索工具,我们实际生活中也有看到这个的影子(集大爱问)。所以,对于计算机而言不可能学完全部知识,本科毕业也不是结束,只有不断保持新的学习,才有可能在行业中立足;

在课程设计结束之后,我希望能更深系统探索Java、C、Python等高级程序语言适合哪些场景,哪些业企事业单位更惯用哪一种,并选择一个语言进行深挖,探究其拓展可能。

posted @ 2026-01-13 22:00  KinthYu  阅读(21)  评论(0)    收藏  举报