asopi tech
asopi techIndie Developer

Asopitech With · ノートアプリ

Notion with Alopex DB

Notionは、ページとブロックでメモや資料をまとめるノートアプリです。Alopex DBで同じものを作るとしたら…?

更新: 2026/10/12

どんなサービスか

Notionのようなノートアプリでは、ページの中身がすべてブロックです。テキスト、画像、表をブロックとして積み重ね、ブロックは別の種類に変えたり、ドラッグで並べ替えたりできます。扱うデータは、ワークスペース、ページの階層、ブロックの並びと中身、そしてページを横断する検索です。

データの置き場所

  • SQLテーブル:ワークスペース、ページ、ブロックをテーブルに持ちます。ブロックは親のブロックを指す列で木の形になり、再帰CTEで、ページの中身を上から順にたどります。ブロックの種類ごとに違う属性は、JSONの列に入れます。
  • ベクトル:ブロックごとの埋め込みを、同じテーブルのベクトル列に持ちます。「先月の議事録で、価格の話をしていたところ」のような問いを、HNSW索引で探します。
  • 全文検索:見出しや固有名詞は、全文検索の索引で探します。意味の検索と言葉の検索を、同じSQLで組み合わせます。

埋め込みはアプリが作ります。ブロックを書き換えたら、アプリが新しい埋め込みを作り、本文とベクトルを同じトランザクションでコミットします。検索の結果が、古い本文を指すことはありません。

配置の形

ワークスペースごとに、ひとつの .alopex ファイルを持ちます。デスクトップのアプリにAlopex DBを組み込み、ネットワークがなくても、読み書きと検索が手元で完結します。ワークスペースの複製や受け渡しは、ファイルのコピーで終わります。チームで共有するワークスペースは、同じファイルをAlopex DBのサーバーで開き、HTTPかgRPCでつなぎます。

Alopex DBが合う理由

ノートアプリの中心は、木の形の文書と、それを横断する検索です。Alopex DBは、テーブル、JSON、全文検索、ベクトルを、ひとつのファイルとひとつのトランザクションで扱います。ノートの中身と検索の索引が、いつも同じ時点を指します。

コードで見る

ページのブロックを parent_id で木にして持ち、再帰CTEで上から順にたどります。文書を組み立てる中心だけを取り出したコードです。

import alopex

# ワークスペースごとに1つの .alopex ファイル
db = alopex.Database.open("notion.alopex")

# ページとブロックを持つ。ブロックは parent_id で木になる
db.execute_sql("""
    CREATE TABLE IF NOT EXISTS blocks (
        id INTEGER PRIMARY KEY,
        page_id INTEGER,
        parent_id INTEGER,
        pos INTEGER,
        kind TEXT,
        body TEXT
    )
""")

# あるページのブロックを書き込む(見出しの下に段落、その下にネスト)
blocks = [
    (1, 1, None, 0, "heading", "議事録"),
    (2, 1, 1, 0, "text", "価格の方針を決めた"),
    (3, 1, 1, 1, "text", "次回までに見積もり"),
    (4, 1, 2, 0, "text", "基準は原価の1.5倍"),
]
for b in blocks:
    db.execute_sql("INSERT INTO blocks (id, page_id, parent_id, pos, kind, body) VALUES (?, ?, ?, ?, ?, ?)", list(b))

# 再帰CTEで、ページの中身を上から順に、入れ子の深さつきでたどる
page = db.execute_sql("""
    WITH RECURSIVE tree AS (
        SELECT id, parent_id, pos, body, 0 AS depth
        FROM blocks WHERE page_id = ? AND parent_id IS NULL
        UNION ALL
        SELECT b.id, b.parent_id, b.pos, b.body, t.depth + 1
        FROM blocks AS b JOIN tree AS t ON b.parent_id = t.id
    )
    SELECT depth, body FROM tree ORDER BY depth, pos
""", [1])

for row in page:
    print("  " * row["depth"] + row["body"])

再帰CTEで、入れ子のブロックを深さつきでたどれます。木の形の文書が、ひとつのSQLで並びます。

ノートアプリには、ブロックと意味検索をひとつの .alopex ファイルに収めるAlopex DBをおすすめします。クイックスタートから試せます。

関連リンク