asopi tech
asopi techIndie Developer

Asopitech With · ノートアプリ

Evernote Web Clipper with Alopex DB

Evernote Web Clipperは、ノートアプリのEvernoteに、Webページや記事をノートとして保存する機能です。これをAlopex DBを使って再現すると…?

更新: 2026/10/12

どんなサービスか

EvernoteのWeb Clipperは、Webページ、記事、PDFをノートとして保存します。保存したものにはタグを付け、ノートブックに分けて、キーワードで探せます。扱うデータは、ページの本文、URL、タグ、ノートブックです。

データの置き場所

  • SQLテーブル:クリップ、ノートブック、タグをテーブルに持ちます。
  • JSON:ページのタイトル、URL、取得した日時など、ページごとに形が違う情報をJSONの列に持ちます。
  • 全文検索:本文に全文検索の索引を張り、キーワードで探します。
  • ベクトル:本文の埋め込みをベクトル列に持ち、「去年保存した、旅行の宿の記事」のような問いで探します。

ページの取り込みと埋め込みの計算は、アプリが行います。

配置の形

Alopex DBを、端末とデスクトップのアプリに組み込みます。クリップは端末の中の1つのファイルに入ります。チームで共有するときは、同じSQLのまま、Alopex DBをサーバーとして動かします。

Alopex DBが合う理由

クリップは、あとで見つけられて初めて役に立ちます。Alopex DBは、決まった形のテーブルと、ページごとに違うJSONを同じSQLで扱い、キーワードと意味の両方で探せます。

コードで見る

クリップとタグをテーブルに持ち、キーワード(本文に語を含む)と意味(埋め込みの近さ)の両方で探します。探す部分だけを取り出したコードです。

import alopex
import numpy as np

# 端末のアプリに組み込む想定
db = alopex.Database.open("evernote.alopex")

# クリップ(本文+埋め込み)とタグを持つ。埋め込みはアプリが作る
db.execute_sql("""
    CREATE TABLE IF NOT EXISTS clips (
        id INTEGER PRIMARY KEY,
        title TEXT,
        url TEXT,
        body TEXT,
        embedding VECTOR(4)
    )
""")
db.execute_sql("""
    CREATE TABLE IF NOT EXISTS clip_tags (
        clip_id INTEGER,
        tag TEXT
    )
""")

# クリップを保存する(ベクトルは説明用の小さな値。実際はモデルの埋め込み)
clips = [
    (1, "京都の宿", "https://ex.com/kyoto", "旅行で泊まった宿の記事", [0.9, 0.1, 0.0, 0.0], "旅行"),
    (2, "確定申告のやり方", "https://ex.com/tax", "税金の手続きのまとめ", [0.0, 0.0, 0.9, 0.1], "お金"),
]
for cid, title, url, body, emb, tag in clips:
    db.execute_sql(
        "INSERT INTO clips (id, title, url, body, embedding) VALUES (?, ?, ?, ?, ?)",
        [cid, title, url, body, np.array(emb, dtype=np.float32)],
    )
    db.execute_sql("INSERT INTO clip_tags (clip_id, tag) VALUES (?, ?)", [cid, tag])

# キーワードで探す(本文に語を含む)
for row in db.execute_sql("SELECT title FROM clips WHERE body LIKE ?", ["%宿%"]):
    print("keyword:", row["title"])

# 意味で探す(「旅行」に近い埋め込みを上位に)
query = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float32)
rows = db.execute_sql(
    "SELECT title, vector_similarity(embedding, ?, 'cosine') AS score FROM clips ORDER BY score DESC LIMIT 1",
    [query],
)
for row in rows:
    print("semantic:", round(row["score"], 3), row["title"])

語での検索と、埋め込みの近さでの検索を、同じSQLで扱えます。言葉が思い出せなくても、意味の近さでたどり着けます。

決まった形のテーブルとページごとのJSONを同じSQLで扱うAlopex DBは、クリップのアプリによく合います。まずはクイックスタートをのぞいてみてください。

関連リンク