Home
Softono

Njupt Search

Open source Python
17
Stars
0
Forks
0
Issues
0
Watchers
2 months
Last Commit

 About Njupt Search

Serverless Edge Search Engine powered by Rust WASM, Block-Max WAND, and Declarative Site Graphs.

Platforms

Web Self-hosted

Languages

Python

Need Help Installing Njupt Search?

We provide expert installation service for this software. Our team will install, configure, and secure Njupt Search on your server. plans start at just $30.

Njupt Search

View on GitHub
njupt-search logo

njupt-search

南京邮电大学无服务端聚合检索与信息服务系统

License TypeScript Rust Python PWA

在线访问设计初衷核心特性核心架构剖析代码架构本地开发


📖 项目定位与初衷 (The Vision)

痛点:严重的信息孤岛与获取门槛
南京邮电大学的全域知识(如规章制度、学术讲座、教务通知、考试排期、竞赛奖助等),长期分散在数百个独立运作的学院官网与管理部门站点中。更加棘手的是,大量关键信息以非结构化文档附件(PDF、DOCX、XLSX)的形式存在。师生在需要跨域检索特定政策或查询业务流程时,面临着极高的“信息差”壁垒。

破局方案:njupt-search
本项目旨在彻底打破这种信息屏障,构建一个完全去服务端化、静态边缘分发的南邮全域聚合知识引擎。我们不仅包含内置的考试日历服务,更在外部生态(njupt-site-graph)的加持下,通过自动化全网爬虫建立庞大的校园知识图谱。在用户侧浏览器内(依赖 Web Worker + WebAssembly)闭环完成毫秒级的极致全文检索。没有中心化数据库,没有高昂的服务器成本,以纯端侧计算重构信息检索体验。


✨ 核心特性矩阵 (Core Features)

⚡️ 无服务端架构 (Serverless Edge) 🦀 Next-Gen 检索引擎 (WASM) 📅 幂等自动化教务管线 (ETL) 📱 极致离线体验 (PWA)
摒弃传统关系型数据库与 Elasticsearch。全量索引静态分发,依托 CDN 边缘节点实现无限并发与毫秒级 TTFB 响应,永不宕机。 基于 Rust 编译的 WebAssembly 算分模块,配合 Block-Max WAND 动态剪枝算法,在浏览器内实现海量倒排表的 Sub-ms 级竞争性打分。 基于 Python pydantic 与高阶正则的自动化爬虫与数据清洗工厂。智能容错教务处表格的“脏数据”,自动结构化生成 ICS 考试日历。 核心检索能力深度集成 Service Worker 缓存策略,离线状态下依然可进行全文检索与日历查阅。并支持 Android 端 TWA 原生打包。

🛠️ 核心架构剖析 (Architecture)

本项目绝非简单的聚合页面,其背后蕴含着高度定制化的数据结构与编译级优化策略。

1. SGIXB002 极致压缩二进制倒排协议

传统的 JSON 倒排索引在网络传输时极其臃肿。本项目在离线阶段 (tools/collection-indexer) 彻底舍弃了文本结构,独创了 SGIXB002 紧凑二进制格式:

  • Delta-Encoding (差值编码):将倒排表中的绝对文档 ID ($D_i$) 替换为相邻文档 of 差值 ($D_i - D_{i-1}$),从而将大整数降维成极小的正整数差值。
  • VarInt (7-bit 变长整数压缩):针对降维后的差值,采用可变长度编码(字节最高位第 8 位作为延续标志位,其余 7 位存储数值)。对于小型差值仅需 1 个字节即可存储。
  • 按需路由与渐进加载 (Query-Planned Progressive Hydration):首屏启动仅拉取源注册表(source_registry)和全局路由表(global_query_directory),体积仅数百 KB。倒排索引的分片和主体部分 (light_index_packedbody_index_packed) 仅在用户输入特定查询词并完成匹配路径规划后,才会按需加载和实例化。
  • 成果:二进制倒排体积较原始 JSON 数据缩减达 82% 以上,显著降低了初次网络传输与解包的资源开销。

2. Rust WASM 的 O(1) 块级剪枝算法 (Block-Max WAND)

在数以万计的文档倒排表合并时,普通的前端 JS 循环会导致主线程严重掉帧。我们在 tools/wasm/packed-impact-decoder 中,使用 Rust 实现了一种激进的提前跳跃算法:

  • SGIXB002 格式的 O(1) 词项跳过:在解析二进制索引时,SGIXB002 头部维护了词项 payload 长度目录。对于未被查询命中的词项,WASM 引擎会直接移动读取指针(Offset Jump)在 $O(1)$ 时间内跳过其整块数据,在底层实现了物理级别的零解压(Zero-Decompression)
  • Block-Max WAND 动态剪枝:块(Block,默认 32 个文档 ID)的文档打分上限按 Impact 降序排列。如果当前评估块的最大可能分数(当前块的 Term Impact 加上后续未评估 Term 的最大 Impact 之和)低于已收集的 Top-K 候选结果的最低门槛(Competitive Threshold),则引擎在算分循环中直接整块剪枝跳过
// 计算当前词项块及后续未评估词项块的最大可能算分上限
let max_possible_for_unseen_doc = block.impact + suffix.get(index + 1).copied().unwrap_or(0.0);

// 如果该块所有文档理论最大分数依然低于已评估的 Top-K 候选门槛 (competitive_threshold)
// 且该块内的文档不属于已知的候选者,则引擎将直接整块剪枝跳过,免去对该块文档的算分评估与 Top-K 插入!
if !has_known_candidate && scores.len() >= target && max_possible_for_unseen_doc <= threshold {
    impact_blocks_pruned += 1;
    continue; 
}

3. Web Worker 编排与多阶段证明检索

为保证 React UI 绝对流畅(60fps),所有的网络拉取、分片解包、正则比对全部隔离在独立的 Web Worker 中 (collectionSearch.worker.ts):

  • 热路径前置 (Hot Query Bypass):针对高频短词,引擎直接通过 hot_query_proof_directory 获取预先编译的 HotQueryProofCertificate,在 $O(1)$ 时间内绕过所有的倒排索引扫描与算分循环,直接下发结构化结果。
  • IndexedDB 强缓存与边缘容灾 (Network Resilience):网络层内置了 njupt-search-artifact-cache IndexedDB 持久化缓存,将 ArrayBuffer 级的分片数据强缓存在用户本地硬盘。如果遇到 CDN 节点 502/504 等错误,引擎会自带 __njupt_retry 时间戳发起 Cache-Busting 重试,实现极端的弱网容错。
  • 多阶段渐进式注水 (Multi-Stage Progressive Hydration):根据 @njupt-search/search-core 的路由规划,检索过程分为以下多阶段,按需流式推进:
  1. first_trusted_results:快速拉取体积极小的轻量倒发索引 (light_index_packed),提取核心文档元数据,提供即时的第一屏结果。
  2. top_results_hydrated:拉取完整倒排主体 (body_index_packed),调用 Rust WASM 算分引擎,深化候选文档打分与排序。
  3. global_exhaustive_complete / scoped_exhaustive_complete:进行全量分片扫描与完备性验证。
  • 基于 Bloom 过滤器的分片排除证明 (Shard Filter Verification):每个数据源的 proof_catalog 维护了对每个 Full Shard 生成的 bloom-fnv1a32-utf8 签名。在进入全量分片扫描阶段前,Worker 会在本地提取搜索分词,通过 FNV-1a 算法计算多重哈希并在 Shard 签名中检索(bloomMayContain)。如果过滤器证明该分片必定不含关键词,则直接在网络层阻断拉取请求,实现精准的 0 带宽浪费。

4. 数据管道:跨库 Webhook 触发与确定性构建

本引擎的数据并非凭空产生,而是依赖于极为完善的上游数据管线和确定性的构建逻辑:

  • 全域知识图谱生态 (njupt-site-graph):本仓库作为检索引擎的“下游分发器”,通过 Github Actions 的 repository_dispatch Webhook,实时接收来自独立的兄弟项目 hicancan/njupt-site-graph 爬取并推送的全网静态文档数据包。
  • 确定性日历生成 (tools/exam-pipeline):Python pandas 与正则化处理了复杂的混合时间字符串,前端在生成 .ics 订阅链接时,抛弃随机 UUID,采用 FNV-1a 算法根据课程元数据计算确定性的唯一 UID。当教务处临时调整考场时,日历会自动覆盖更新,杜绝“幽灵叠加”。
  • 跨语言安全沙箱 (packages/contracts):TypeScript 侧采用 Zod 定义严格的 Schema 契约。将上游 Python 爬虫引擎产出的静态文件视为“不可信输入”,在客户端侧强制进行运行时反序列化校验,形成坚不可摧的接口安全防护。

🗺️ 系统架构图 (System Architecture)

graph TD
    subgraph 外部生态 ["外部数据管线生态 (Upstream)"]
        A["南邮全域信息源<br>(官网/教务处/讲座等)"] -->|结构化抓取| B("hicancan/njupt-site-graph<br>静态网站图谱仓库")
    end

    subgraph 离线管线 ["njupt-search 离线引擎 (Data Builder)"]
        B -.->|Webhook: repository_dispatch| C("Github Actions 构建集群")
        C -->|图谱数据| D("tools/collection-indexer<br>Python 倒排构建")
        C -->|考试表格| E("tools/exam-pipeline<br>Python 日历清洗")
        
        E -->|生成| F[("ICS 日历源")]
        D -->|Delta+VarInt 压缩| G[("SGIXB002 二进制索引包")]
    end

    subgraph 边缘分发 ["CDN Edge"]
        F -.静态缓存.-> H["CDN 边缘节点"]
        G -.分片缓存.-> H
    end

    subgraph 客户端 ["客户端运行时 (PWA)"]
        H ==>|按需 Hydration| I["Web Worker 编排中心<br>packages/search-core"]
        I <-->|内存读写| J(("Rust WASM 算分引擎<br>Block-Max WAND"))
        I -->|异步返回结果| K["React 主线程 UI"]
    end

    classDef python fill:#4B8BBE,stroke:#306998,stroke-width:2px,color:white;
    classDef rust fill:#DEA584,stroke:#A57A5A,stroke-width:2px,color:black;
    classDef ts fill:#3178C6,stroke:#235A97,stroke-width:2px,color:white;
    classDef storage fill:#f9f9f9,stroke:#333,stroke-width:2px;
    classDef external fill:#eee,stroke:#999,stroke-width:2px,stroke-dasharray: 5 5;
    
    class D,E python;
    class J rust;
    class I,K ts;
    class F,G storage;
    class B external;

🏗️ Monorepo 代码架构 (Project Structure)

项目采用 NPM Workspaces 与 Python uv 混合构建,实现了严密的职责隔离。

njupt-search/
├── apps/
│   └── web/                # React 19 + Tailwind v4 + Vite PWA 核心单页应用
├── packages/
│   ├── contracts/          # Zod 强类型契约层 (被前后端共同引用)
│   ├── exam-core/          # 考试解析、正则匹配与 ICS 标准日历生成引擎
│   └── search-core/        # 搜索引擎调度核心:Web Worker 状态机、算分器、分词器
├── tools/
│   ├── wasm/
│   │   └── packed-impact-decoder/ # Rust 编写的底层 Block-Max WAND WASM 算分器
│   ├── collection-indexer/ # Python 离线倒排构建核心 (生成 SGIXB002 二进制包)
│   ├── exam-pipeline/      # Python 自动化教务爬虫与 Pydantic 数据清洗
│   ├── quality-gates/      # 监控产物体积防劣化门禁脚本
│   └── search-eval/        # 搜索召回率与算分一致性基准测试
└── android/                # 基于 Bubblewrap 的 TWA 壳工程 (数字资产链接验证)

🚀 本地开发与部署 (Quick Start)

初始环境准备

  1. 安装 Node.js 依赖
    npm ci
    
  2. 构建 Python 虚拟环境并同步工具链依赖(项目默认使用 uv,无需手动激活环境):
    uv sync
    

本地启动

# 启动 Vite 本地热重载服务器 (端口 5173)
npm run dev

数据离线编译与测试

有关详细的源数据索引构建、教务日历爬虫更新以及回归测试跑交流程,您可以直接查阅对应模块的 Python 脚本。所有构建管线均内置了完备的 --help 参数供调试:

# 例如,更新并抓取最新的考试安排:
uv run python tools/ci/prepare_public_assets.py update-exam-lock
uv run python tools/ci/prepare_public_assets.py build-exam-public-data
uv run python tools/ci/prepare_public_assets.py verify-exam-public-data

# 执行静态包体积监控与回归验证:
uv run python tools/quality-gates/scripts/check_public_artifact_sizes.py

📄 许可证 (License)

本项目开源协议基于 AGPL-3.0 License。南京邮电大学基础数据源及部分涉校名称归属原著作权方。本项目仅作技术交流与非盈利性校园聚合服务。