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 外部函数。

下一步