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 で実行します。

よくある作業

検索ボックスの値を正規化する

検索ワーカーはクエリの値の前後の空白を除いて小文字にします。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>=180json に言及し、スコアが 180 以上のパッケージ。
(yaml OR toml) AND NOT rank=Dランク D 以外の YAML または TOML のパッケージ。
momentum=Rising AND recent_dependents>=5最近の被依存パッケージが 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 は両方の単語に一致し、残りの 2 つは 1 つずつ一致して同順位となり、スコアで並びます。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 ワークフローが同じコマンドを定期的に実行します。サイトをサブパスで配信する場合は、ワーカーが /<base>/data/... から取得するよう NEXT_PUBLIC_BASE_PATH を設定してください。

よくある落とし穴

  • 暗黙の AND。 式言語では AND のない json parser は構文エラーです。簡易検索フィールド(q)では、同じテキストが 1 つの部分文字列 json parser になります。
  • 空白の除去。 normalize_text は空白を残します。末尾に空白のある検索語は、空白を除いたものより一致する範囲が狭くなります。
  • フィールドの演算子。 score:180 は 180 以上ではなく score = 180 を意味します。score>=180 を使ってください。
  • ラベルは完全一致。 静的サイトでは rank=s は何にも一致しません。rank=S や momentum=Rising と書いてください。
  • ターゲットの誤り。 wasm-gc や native 向けにビルドするパッケージから static_search をインポートすると失敗します。このパッケージは JavaScript の外部関数を使っているためです。

次のステップ