static_search 教程
本教程介绍如何使用 MoonBit 的 static_search 包以与静态站点搜索相同的方式规范化文本,如何构建并提供带搜索索引的静态站点,以及如何为它编写查询。static_search 设计详细解释了索引和排序。
快速开始
把模块添加到你的项目:
moon add Luna-Flow/mooncake-impact-factor@0.1.2
该包只为 JavaScript 目标构建,所以导入它的包也必须如此:
import {
"Luna-Flow/mooncake-impact-factor/static_search",
}
supported_targets = "js"
不区分大小写的匹配就是在小写文本上做子串测试:
test "quick start" {
let needle = @static_search.normalize_text("JSON")
let text = @static_search.normalize_text("moonbit-community/json5 JSON5 parser")
println(text.contains(needle))
}
true
用 moon test --target js 运行它。
日常任务
规范化搜索框中的值
搜索 worker 会去除查询值的首尾空白并转为小写。normalize_text 只转小写,所以先去除空白:
test "normalise a needle" {
let raw = " Http Client "
let needle = @static_search.normalize_text(raw.trim().to_owned())
println("[\{needle}]")
}
[http client]
构建并提供静态站点
在仓库根目录构建数据库、导出静态数据并构建站点:
npm install
npm run build:static-data # builds cli and static_search, the database and public/data
npm run build:static # compiles static_search again, then runs next build into out/
npm run serve:static # serves out/ on http://localhost:4173
build:static-data 需要 ~/.moon/registry/index/user 下的本地注册表索引(先运行 moon update),并且除非已有缓存,否则会从 mooncakes.io 获取下载量。搜索索引是 public/data/search/search-index.json;浏览器会把它一次性加载到 Web Worker 中。
编写查询
搜索框接受原生表达式语言。各项用显式的 AND、OR 和 NOT 连接,并用括号分组:
| 查询 | 查找结果 |
|---|---|
json | 名称、所有者、描述或关键词中包含 json 的包。 |
"http client" | 精确的子串 http client,包括其中的空格。 |
owner:moonbitlang AND keyword:json | 该所有者的、关键词包含 json 的包。 |
json AND score>=180 | 提到 json 且分数至少为 180 的包。 |
(yaml OR toml) AND NOT rank=D | 等级不是 D 的 YAML 或 TOML 包。 |
momentum=Rising AND recent_dependents>=5 | 至少有五个近期依赖方的快速增长的包。 |
字段包括 text、owner、package、keyword、description、license、repository、rank、momentum、score、dependents、recent_dependents、downloads、year、has_repository 和 has_license。运算符为 :(包含)、=、>= 和 <=。没有 > 或 <。
理解结果的顺序
没有显式指定排序时,静态站点先按包匹配查询中多少项排序,再按分数,最后按名称。下面的 MoonBit 程序针对单词的 OR 重现了这一点:
priv struct Record {
name : String
text : String
score : Double
}
fn record(name : String, description : String, score : Double) -> Record {
{ name, text: @static_search.normalize_text("\{name} \{description}"), score }
}
fn coverage(r : Record, words : Array[String]) -> Int {
let mut n = 0
for w in words {
if r.text.contains(@static_search.normalize_text(w)) {
n += 1
}
}
n
}
test "coordination ranking" {
let records = [
record("alice/json", "Fast JSON parser", 120.0),
record("bob/toml", "TOML parser", 300.0),
record("carol/yaml", "YAML and JSON emitter", 80.0),
]
let words = ["json", "parser"]
let hits = records.filter(r => coverage(r, words) > 0)
hits.sort_by((a, b) => {
let by_rel = coverage(b, words).compare(coverage(a, words))
if by_rel != 0 {
return by_rel
}
let by_score = b.score.compare(a.score)
if by_score != 0 { by_score } else { a.name.compare(b.name) }
})
for r in hits {
println("\{r.name} \{coverage(r, words)}")
}
}
alice/json 2
bob/toml 1
carol/yaml 1
查询 json OR parser 在静态站点上给出同样的顺序:alice/json 匹配两个词,另外两个各匹配一个而并列,按分数排序。对于 json AND parser,每个结果都匹配两个词,因此顺序就是普通的分数顺序。
进阶
在 JavaScript 中使用编译后的模块。 moon build src/static_search --target js 会写出一个包含 runtime_version 和 normalize_text 的 ES 模块;导入路径见 static_search API。当你的 JavaScript 代码依赖于某个特定版本的行为时,请检查 runtime_version()。
与动态站点比较。 本地服务器(npm run dev)在 SQLite 上运行同样的查询语言。那里的文本项通过 FTS5 匹配词的前缀而不是子串,未显式指定排序时结果的顺序也不同。设计页面列出了所有差异。
发布到 GitHub Pages。 deploy-static 工作流按计划运行同样的命令。站点部署在子路径下时请设置 NEXT_PUBLIC_BASE_PATH,使 worker 从 /<base>/data/... 获取数据。
常见陷阱
- 隐式 AND。 在表达式语言中,不带
AND的json parser是语法错误。在简单搜索字段(q)中,同样的文本是一个子串json parser。 - 去除空白。
normalize_text保留空格。带尾随空格的查询词比去除空白后的匹配得更少。 - 字段运算符。
score:180表示score = 180,而不是至少 180。请使用score>=180。 - 标签需完全匹配。 在静态站点上
rank=s什么也匹配不到;请写rank=S和momentum=Rising。 - 目标错误。 从为
wasm-gc或native构建的包中导入static_search会失败,因为该包使用了 JavaScript 外部函数。
下一步
- static_search 设计:索引布局、相关度计数和查询的复杂度。
- static_search API:MoonBit 和 JavaScript 函数。
- score 教程:搜索所筛选和排序的那些数字。
- 架构指南:完整的数据管道。