sense-bases について
sense-basesは、Obsidian Basesのフィルター、数式、ビューをsenseのSQLクエリに置き換えるためのリファレンススキルです。Basesのデータ選択をsenseのクエリ機能へ移す際に、対応表現や注意点を確認できます。
主な機能
- 01BasesのプロパティをSQLに対応付け
- 02フィルターと述語の変換
- 03数式をSQLite式に変換
- 04並べ替えとウィンドウ関数でビューを表現
- 05変換時の注意点を説明
活用シーン
- 01Basesのビューをsenseクエリで再現
- 02タグ、リンク、リストのフィルターを変換
- 03数式とグループ集計を計算
- 04バインドしたパスで現在のノートを指定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ kmalakoff/ sensemaking --skill sense-basesインストール後、Claude Code で/sense-basesを入力して読み込まれたか確認します。
同梱ファイルEXAMPLES.md
詳細
sense-bases
.base ファイルは YAML です。ノートを選択するフィルター、値を計算する数式、並べ替えやグループ化を行うビューで構成されます。Obsidian は独自のメタデータキャッシュに対してこれを評価し、sense は同じデータを SQL テーブルに保持します。行を選択または計算する Bases の構成要素には、すべて SQL 相当のものがあります。相当するものがないのは表示形式(columnSize、カードレイアウト)です。これらは行ではなくピクセルを変えるため、翻訳時に無視しても情報は失われません。
1 つのビューを 1 つのクエリに変換します。ベースレベルの filters とビューの filters を AND で結合し、ビューの order/sort/limit を、それぞれ SELECT リスト、ORDER BY、LIMIT として指定します。ビューが 4 つあるベースは、4 つのクエリになります。これらを sense.config.json の queries ブロックにベース名で保存すると、sense <name> として実行できます。一般的なクエリ機能については sense スキルを参照してください。このドキュメントでは、Bases の構成要素をその機能に対応付ける方法のみを説明します。
プロパティ
| Bases | sense | 備考 |
|---|---|---|
note.field /フィールド名のみ | f.<field> | フロントマターの列。同じ名前です |
file.name | basename(f.path, '.md') | Name 列。ディレクトリと拡張子は含みません |
| 拡張子付きの名前 | basename(f.path) | サフィックス引数は Unix の basename と同じです。存在する場合は削除されます |
file.folder | f.path LIKE 'Folder/%' | inFolder("Folder") の場合 |
file.ext | 'md' | sense がインデックスするのは Markdown のみです |
file.size | f._size | バイト単位 |
file.mtime | datetime(f._mtime / 1000, 'unixepoch') | ミリ秒で保存されています |
file.ctime | datetime(f._ctime / 1000, 'unixepoch') | 下記の注意事項を参照してください |
file.tags | tags テーブル | フロントマターとインラインの #tags。重複は除去され、粒度は Obsidian と同じです |
file.links | links WHERE embed = 0 | 記述されたターゲットごとに 1 行 |
file.embeds | links WHERE embed = 1 | |
file.backlinks | links WHERE dst = f.path | |
this | バインドされた ? | 下記を参照してください |
file.ctime はファイルシステム上の作成日時です。git clone やコピーを行うとリセットされるため、チェックアウトとして取得したツリーでは、すべての ctime がチェックアウト日時になり、ファイルシステム上の日時としては正しくても、作成者についての経過時間数式は誤った結果になります。変更日時がこのような状態の場合、map は警告を表示します。作成日時も同様です。
述語
| Bases | 意味 |
|---|---|
== != > < >= <= | 同じ |
&& || ! | AND OR NOT |
and: / or: / not: フィルターブロック | 括弧付きの AND / OR / NOT |
field.isEmpty() | (f.field IS NULL OR f.field IN ('', '[]', '[null]')) |
file.hasTag("book") | EXISTS (SELECT 1 FROM tags WHERE tags.path = f.path AND (tag = 'book' OR tag LIKE 'book/%')) |
file.hasLink("Note") | EXISTS (SELECT 1 FROM links WHERE src = f.path AND dst = 'Note.md') |
list.contains(x) | EXISTS (SELECT 1 FROM json_each(f.list) WHERE value = x) |
string.contains(x) | instr(f.string, x) > 0 |
list.containsAny(...) | value IN (...) を使う json_each の EXISTS |
/regex/.matches(x) | SQLite に正規表現はありません。アンカーやワイルドカードの形式には LIKE / GLOB で対応できます |
value.isType("object") | json_each 自身の type 列:... FROM json_each(f.field) j WHERE j.type = 'object' |
ツリー内のどのノートでも宣言されていないフィールドには、列そのものが存在しません。そのため、そのフィールドを指定したフィルターは、すべての行を空として扱うのではなく、no such column エラーになります。Obsidian の評価処理では未知のプロパティは空として返されますが、SQL ではそうなりません。このエラーが示した内容と同じになるように句を削除してください。また、SELECT name FROM pragma_table_info('frontmatter') で存在する項目を一覧表示できます。
空は 3 通りの方法で保存されるため、isEmpty() が true になるケースは 4 つあります。NULL(キーがない)、''(空文字列の値)、'[]'([] と記述されたリスト)、'[null]'(値なしの - があるリストキー)です。Obsidian の isEmpty() はこれらすべてで true になりますが、IS NULL だけでは最初のケースしか見つかりません。IN リストが判定の全体です。json_array_length() では判定できません。'[null]' は長さ 1 として読み込まれ、通常の文字列ではエラーになるためです。リスト内でも同じ問題が起きます。json_each は文字列のメンバーを通常のテキストとして value に渡すため、json_type(value) は malformed JSON エラーになります。判別にはスキャン自身の type 列を使います。
contains(link("Movies")) はリンクの値と比較します。フロントマター内のリンクリストには記述されたテキストが格納されるため、json_each で比較する値はリテラルの [[Movies]] です。hasLink は解決済みパスと比較します。links.dst は解決済みのターゲット(リンク切れの場合は NULL)なので、比較する値は表示名ではなくターゲットのパスです。
sense 独自の has(field, x) は、どちらの contains よりも緩い判定です。JSON 配列では完全一致ですが、文字列では部分文字列で判定するため、has(f.status, 'active') は inactive にも一致します。フィールドがリストだとわかっている場合は短く書けますが、正確に置き換えるには json_each 形式を使います。
数式
数式は SELECT 式です。Bases の関数は SQLite の次の関数に対応します。
| Bases | 意味 |
|---|---|
if(c, a, b) | iif(c, a, b) または CASE WHEN |
now() / today() | datetime('now') / date('now') |
date(x) | datetime(x) |
(date1 - date2).days | julianday(date1) - julianday(date2) |
(now() - acquired).months | (julianday('now') - julianday(f.acquired)) / 30.44 |
x.round(n) / x.toFixed(n) | round(x, n) / printf('%.2f', x) |
d.format("YYYY-MM-DD") | strftime('%Y-%m-%d', d) |
x.toString() | CAST(x AS TEXT) |
list.length | JSON 列では json_array_length(f.list)、テーブルでは COUNT サブクエリ |
list(a).filter(list(b).containsAny(value)).unique() | EXISTS で結合したサブクエリ。動作例は EXAMPLES.md を参照してください |
Bases の期間は型付きですが、SQL の日付演算では julianday の日数の小数が使われます。期間フィールドを連結する数式(.days.round())は、julianday の差に対する算術演算に展開されます。
別の数式を参照する数式は、CTE の階層になります。SQL では同じ SELECT リスト内で SELECT のエイリアスを参照できないため、各依存レベルで数式を列として計算し、次のレベルでそれらを参照します(WITH t AS (SELECT ..., <level-1 formulas> FROM frontmatter) SELECT ..., <level-2 formulas> FROM t)。ただし、5 つの数式を連鎖させても、CTE が 5 つになるわけではありません。必要なのはその連鎖の「レベル」数です。基底列のみを参照する数式は、同じ階層にまとめられます。
ビュー
-
sort:(複数キーと、それぞれの並び順)→ORDER BY a DESC, b ASC。limit:→LIMIT。 -
groupByでは行は集約されません。Obsidianでは、各行がグループ見出しの下に分類され、グループごとの集計値が表示されます。同じ行と数値を生成するSQLは、GROUP BYではなく、並べ替えとウィンドウ関数を使います。SELECT f.path, f.status, f.days, AVG(f.days) OVER (PARTITION BY f.status) AS group_avg, COUNT(*) OVER (PARTITION BY f.status) AS group_n FROM frontmatter f WHERE f.days IS NOT NULL ORDER BY f.status, f.days DESCグループごとに1行だけ表示する折りたたまれたレポートには、通常の
GROUP BYを使います。これはBasesビューに表示される結果とは異なります。 -
ビューの
summaries(Sum、Average、並べ替えによるMedian、Unique、Filled、Checked)には、行を保持するために上記のようにウィンドウ関数を適用した対応する集計関数を使うか、別の集計クエリを使います。
this
thisは、ベースの評価対象となるノートです。埋め込み先のノート、またはObsidianのアクティブなペインを指します。senseにはペインがないため、呼び出し元がパスをバインドパラメーターとして渡します。sense sql "..." <path>、または保存済みクエリをsense <name> <path>として実行します。CTEを使うと、バインド値を1つにできます。
WITH me AS (SELECT ? AS p)
また、すべてのthis.file.*式でmeを結合します。EXAMPLES.mdにあるRelated.baseの完全な変換が実例です。リンクの重複、相互リンクのフィルター、GROUP_CONCATを使った共有ターゲットの一覧を、すべて1つのバインドパスから取得します。
変換できないものと、そのコストがかからない理由
columnSizeやカード/テーブル/リスト/マップの装飾:同じ行をピクセル単位でレイアウトするものです。- Obsidianがアクティブなペインから
thisを選択すること:ペインはありません。パスパラメーターによって解決します。 - ファイルの変更に伴うライブ再評価:CLIのデフォルトのクエリは、必要な機能を段階的に準備します。
--no-buildは最後に完了した世代を読み込み、ライブファイルをスキャンせずに、要求された準備状態を満たす必要があります。自動的に再レンダリングされることはありません。