読了時間 : 14 minutes

あなたは pgvector と LangChain4j の統合に関する記事を読んでいます。ページの下部に、JBake 「関連記事」を提案されます。それをクリックします。それは…設定に関する記事です。 キティターミナルの。二つの間の唯一の共通点は?彼らは未満で公開されました 15日間隔。これは推奨ではありません。これは偽装されたカレンダーである 編集.

シーン : 火曜日 5月12日 17時30

私は自分の記事の一つを読み返しています — celui sur le Knowledge Graph comme outil de compréhension de codebase 内容は濃い:ノード、エッジ、コミュニティ、PlantUML、オンボーディング。記事 動員する15分間の読書テクニック`graphify-gradle`, plantuml-gradle, およびグラフトポロジーの概念。

ページ下部で、 「関連記事」 が提案しています:

  1. Firebase Contact Form (0113)の記事

  2. Eager/Lazy メカニズムに関する記事(0108)

  3. グラドルスクリプト→プラグインへの移行(0102)についての記事

  4. Ollama Pro(0120)のサブスクリプションの累積に関する記事

これらの4つの記事のうち、3つは Knowledge Graph と 全く 関係がない。 彼らはそこにいる理由は、彼らが最も新しく公開されたものだからだ — 時間的近接ではない 意味的近傍。

私はテンプレートを見ています`post.thyme`</think>

<!-- Articles connexes (liens internes SEO) -->
<th:block th:each="post,postStat : ${published_posts}">
    <th:block th:if="${!post.uri.equals(content.uri) and postStat.index lt 4}">
        ...
    </th:block>
</th:block>

`published_posts`時系列のリストです。`postStat.index lt 4`取る 現在の投稿ではない最初の4つです。それだけです。ロジックはありません。 類似性。内容の概念はない。ただのループ`for`として仮装されている 推奨。

JBakeのバグではありません。これはすべてのデフォルトの動作です。 静的サイトジェネレータ:投稿のリストはフラットで、日付順に並んでいる。 テンプレートは持っているものでできることをする。

しかし、それを受け入れる理由ではない。

診断:テンプレートが見逃す3つの関係のタイプ

私の記事コーパス — 4か月で23件の投稿 — は豊かな構造を持っている 時系列テンプレートは完全に上書きします:

  1. 明示的な参照 : 使っています`xref:`私の記事で大量に。

記事 0122 参照 0106 (ナレッジグラフ) および 0116 (区画) 認識論的)。 これらのリンクは編集による*hard linking*です — 私は 故意にこれらの概念を接続することを決めた。

  1. 共有タグ: 各記事には`:jbake-tags:`. 記事について

Graphify + PlantUML (0105) a`gradle, graphify, plantuml, knowledge-graph`. Knowledge Graph (0106)の記事は knowledge-graph, graphify, plantuml. 7つのうち3つのタグが共通 — 43%の重複率。

  1. 共起する固有名詞 : « pgvector », « RAG », « embedding

4つの異なる記事に共同で現れる。 « LangChain4j », Ollama », « plugin Gradle » は他の六つにあります。これらはタグではありません。 宣言された — これらは*patterns émergents*のコーパスで、ただNLPだけが 検出できる。

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center

title 記事間の関係の3層
package "層 1 — 明示的な参照" #CCFFCC {
    [Article A] as A1
    [Article B] as B1
    A1 -down-> B1 : xref:
    note right of A1
        Liens durs
        Déterministes
        Déjà dans graph.json
    end note
}

package "レイヤー 2 — 共有タグ" #FFFFCC {
    [Article A] as A2
    [Article C] as C2
    A2 ..> C2 : tag_cooccurrence
    note right of A2
        Poids = tags communs / tags totaux
        Déterministes
        À extraire via graphify
    end note
}

package "層3 — エマージェントオントロジー" #FFCCCC {
    [Corpus A] as A3
    [Corpus B] as B3
    A3 ..> B3 : entity_overlap
    note right of A3
        Clusters NLP
        Co-occurrences d'entités nommées
        TF-IDF / cosine similarity
    end note
}

@enduml

今日、私のサイトはこれらの三つの層の*いずれも*使っていない。それは使う ゼロ層: Javaリストにおける挿入順序

解決策:Gradle パイプライン、ホットな LLM 呼び出しではない

最初の誘惑は、bakeの瞬間にLLMを呼び出すことでしょう:「Pour この記事は、コーパスの中で最も類似している3つの記事を見つける。 それをしないでください。

各回のLLM呼び出し`./gradlew bake`時間とお金がかかり、 ビルドに非決定性を導入します。LLMの応答は 内容が変わらないまま、二つのビルド間を切り替える。あなたのCIが変化する。 再現できないため、あなたのテストは不安定になります。

解決策は決定論的です:事前にグラフを計算するGradleパイプライン 類似性があり、それを保存します`graph.json`. テンプレートは結果を読み取ります — 彼は決して計算を開始しない。

アーキテクチャ : BakeryはGraphifyをインポートし、エンジンはインポートしない

これは建築における最も重要な点です。 誘惑は…することになるだろう コラボレーションをケーブルする中で`engine/build.gradle.kts`:engine適用する スキャン用にgraphify、ベイク用にbakery、そしてエンジンがその間をつなぐ 両方。

これはアンチパターンです。Engine は 消費者端末 — それは適用する プラグインに関するビジネスロジックは実装していません。ルールは: �証明の責任はプロプライエタリなプラグインにあります。

正しいアーキテクチャ — Bakery は Graphify をインポートする
@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #FFF3CD

title Bakery が Graphify をインポートします — Engine は何も知りません
package "N3 — エンジン" #FFCCCC {
    [engine/build.gradle.kts] as ENG
    note right of ENG
        plugins { bakery }
        Zéro connaissance de graphify
        4 tâches, ~150 lignes
    end note
}

package "N2 — ベーカリー" #CCFFCC {
    [bakery-plugin] as BAK
    note right of BAK
        plugins { graphify } ← import interne
        Lit graph.json
        Résout articles connexes
        Expose le modèle à JBake
    end note
}

package "N0 — GRAPHIFY" #CCE5FF {
    [graphify-plugin] as GRF
    note right of GRF
        Scan workspace → graph.json
        Enrichi avec :
        - xref: edges
        - co-occurrences de tags
        - entités nommées (NLP)
    end note
}

ENG -down-> BAK : "プラグイン { ベーカリー }"
BAK -down-> GRF : "プラグイン { グラフィファイ } ← N2→N0 オーケー"
@enduml

DAG契約は守られています:bakery (N2) が graphify (N0) をインポートしています、N2 > N0, 違反はありません。エンジン (N3) はベーカリー (N2) をインポートします。N3 > N2、OK。

Engineには参照するコードの行が*ひとつも*ありません`graph.json`, relatedPosts, または他の類似性の概念も。 彼はbakeryを適用する、点。 その コラボレーションはベーカリー内部です。

パイプライン: スキャン → グラフ → テンプレート

以下は3段階の完全なパイプラインです:

  1. スキャン (グラフ化) :`graphify-plugin`ワークスペースをスキャンします。彼/彼女の

現在の形、すでにそれらを検出している`xref:`の間`.adoc`そして ~にさらす`graph.json`タイプのedgesのような`reference`.
私たちはそれを編集コンテンツのために豊かにします: * JBakeのメタデータを解析 (:jbake-tags:, :jbake-description:) それぞれの`.adoc`ブログの * タグの共起を計算 → edges`tag_cooccurrence`重さとともに * 名前付きエンティティをTF-IDFを使って説明から抽出 → エッジ`entity_overlap` セクションを注入する`blog_articles`の中`graph.json`存在する

// Extrait de l'enrichissement graphify pour le blog
fun enrichBlogSection(graphJson: File, blogDir: File): GraphJson {
    val articles = blogDir.listFiles { f -> f.extension == "adoc" }
        .map { parseJbakeMetadata(it) }

    val nodes = articles.map { ArticleNode(it.slug, it.title, it.tags) }
    val edges = mutableListOf<GraphEdge>()

    // Couche 1 : xref (déjà fait par scanWorkspace)

    // Couche 2 : co-occurrences de tags
    for (a in articles) {
        for (b in articles) {
            if (a.slug == b.slug) continue
            val common = a.tags.intersect(b.tags)
            if (common.isNotEmpty()) {
                edges.add(GraphEdge(
                    source = a.slug,
                    target = b.slug,
                    type = "tag_cooccurrence",
                    weight = common.size.toDouble() / (a.tags.size + b.tags.size)
                ))
            }
        }
    }

    return graphJson.copy(
        blogArticles = BlogSection(nodes, edges)
    )
}
  1. Bake (bakery) : その瞬間の`./gradlew bake`, `BakeryPlugin`ベッド

`graph.json`そして関連記事を各投稿ごとに解決します。

// BakeryPlugin — résolution des articles connexes
fun resolveRelatedPosts(
    currentSlug: String,
    graph: GraphJson,
    maxResults: Int = 4
): List<RelatedPost> {
    val edges = graph.blogArticles.edges
        .filter { it.source == currentSlug || it.target == currentSlug }

    return edges
        .sortedByDescending { it.weight }
        .take(maxResults)
        .map { edge ->
            val relatedSlug = if (edge.source == currentSlug) edge.target else edge.source
            graph.blogArticles.nodes.first { it.slug == relatedSlug }
        }
}
  1. Template (post.thyme) : JBakeモデルは現在、Mapを受け取ります

平坦な時系列リストではなく、構造化されたもの。

<!-- Articles connexes basés sur le Knowledge Graph -->
<th:block th:if="${relatedPosts != null and !relatedPosts.empty}">
    <section class="mt-5 pt-4 border-top">
        <h2 class="h4 mb-3">Articles connexes</h2>
        <th:block th:each="related : ${relatedPosts}">
            <div class="mb-2">
                <a th:href="${content.rootpath} + ${related.uri}"
                   th:text="${related.title}" class="fw-semibold"></a>
                <br/>
                <small class="text-muted">
                    <th:block th:each="reason,iterStat : ${related.reasons}">
                        <span class="badge bg-light text-dark"
                              th:text="${reason}"></span>
                    </th:block>
                </small>
            </div>
        </th:block>
    </section>
</th:block>

テンプレートは同じです — それはデータがどこから来たかを知りません。 ベーカリーとJBakeの間の契約だけが変更されました:published_posts(リスト 時系列) なる`relatedPosts`(グラフによる重み付けマップ)

「理由」バッジ(例:xref 0122, tag:gradle, cluster:pgvector-rag) 読者に説明します pourquoi この記事は関連しています。これは~ではない 透明性だけ — それはトポロジーの教育法だ あなた独自のコンテンツ。

クロノロジカルフォールバック

Si `graph.json`存在しない(ローカルビルド、事前スキャンなし、最初) デプロイ, CIはまだスキャン グラフファイ), テンプレート 優雅に劣化しなければならない:

fun resolveRelatedPosts(currentSlug: String, graph: GraphJson?): List<RelatedPost> {
    if (graph != null && graph.blogArticles != null) {
        return resolveFromGraph(currentSlug, graph)
    }
    // Fallback chronologique — même comportement qu'aujourd'hui
    logger.warn("[bakery] graph.json absent — fallback chronologique")
    return resolveFromChronology(currentSlug)
}

デフォルトの動作は現在の動作と同じです。その レコメンデーションエンジンは、段階的な改善 — ではない ブレイキング・チェンジ。

エマージェントオントロジー : 記事が互いを知らずに集まるとき

最も興味深い層は第三層:新興オントロジーです。 引用されない記事で、同じタグを持たないものだが 同じことを話している人は、それに気づいていない。

具体的な例を挙げましょう。 私のコーパスから3つの記事:

  1. 0119 — ベンチマーク DGX Spark 対 クラウド サブスクリプション LLM

  2. 0121 — Gradle プラグイン Ollama Pro 2 インスタンスを操作

  3. 0122 — 効率比 27x 45x IAエキスパートフリート

これらの3つの記事には、aucun xref がありません。彼らのタグは 彼らは20%しかカバーしていません(「ollama」、「llm」が共通です)。しかし軽量な NLP です。 説明について明らかなクラスターが示される:« コスト », « サブスクリプション », クラウド », « GPU », « APIキー », « Ollama Pro », « 効率 », « 比率

彼らはオントロジー的クラスターを形成します:LLMセルフホストドの経済 vs cloud このクラスターはどこにも宣言されていません。コーパスから現れます。

@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #E8F8F5

title Cluster Ontologique Émergent — "LLMセルフホステッド経済"

package "クラスター検出 : 経済-LLM" #D5F5E3 {
    [0119 — Benchmark DGX Spark\nvs Cloud Abonnement] as A
    [0121 — Plugin Gradle Piloter\nDeux Instances Ollama Pro] as B
    [0122 — Ratio Efficacité\n27x 45x Flotte Experts] as C
}

A .. B : entity_overlap (0.73)
B .. C : entity_overlap (0.81)
A .. C : entity_overlap (0.68)

note bottom of A
    Aucun xref entre ces articles.
    Tags communs : "オラマ", "llm".
    Cluster découvert par NLP :
    co-occurrences "コスト", "GPU",
    "サブスクリプション", "オラマ プロ".
end note
@enduml

このオントロジークラスターは複合エッジになります。graph.json:

{
  "source": "0119-benchmark-dgx-spark",
  "target": "cluster:economie-llm",
  "type": "entity_cluster",
  "weight": 0.73,
  "metadata": {
    "clusterLabel": "Économie LLM Self-Hosted vs Cloud",
    "commonEntities": ["coût", "GPU", "abonnement", "Ollama Pro", "ratio"],
    "articlesInCluster": [
      "0119-benchmark-dgx-spark",
      "0121-ollama-pro-deux-instances",
      "0122-ratio-efficacite-flotte-experts"
    ]
  }
}

NLPは意図的に軽量です — TF-IDF + コサイン類似度(sur les) 説明。BERTは必要ありません、言語モデルは必要ありません。 コーパスは23件の記事で構成されており、類似度行列は1つに収まります 50 KoのJSONファイル。

NLPの力は、アルゴリズムの高度さから来なくて しかし、*コーパスのサイズ*と*説明の品質*によるものです。 一つ `:jbake-description:`よく書かれた150文字はもっと含んでいる TF-IDFに、3000語の記事全体であることを示す信号。

DAG契約:誰が誰に重要か

これは建築の規律が報われる点です。DAG N0→N3 で定義されている`engine/build.gradle.kts`簡単なルールを与える: どのプロジェクトも上位のプロジェクトとは関係ない。

コンシューマープラグイン インポートされたプラグイン 消費者レベル インポートされたレベル 有効ですか?

bakery-gradle

graphify-gradle

N2

N0

✅ N2 > N0 — OK

エンジン

bakery-gradle

N3

N2

✅ N3 > N2 — OK

エンジン

graphify-gradle

N3

N0

�✅ 技術 OK, しかし 概念的に間違っている — コラボレーションはbakery内部にある

Engine 適用する bakery. Bakery 適用する graphify. それだけです。ある日 ベクトルストアコードベース (N1) を研究のために追加したいです。 クロス・コーパスの意味解析、ベーカリーもインポートします。Engineは変わりません。

パターンは:プラグイン N2 は自身の依存関係のハブです。 エンジンN3はハブではありません―それはハブを適用するターミナル。

得られることと得られないこと

主な利益は編集的で、技術的ではない:

前に 後

関連記事 = 最新4件の投稿

関連記事 = Knowledge Graph での重み上位 4件

読み手はRAGを読む → Kitty Terminalの推奨

リーダーが RAG を読む → pgvector、チャンク化、ベクターストアの推奨

なぜについての透明性はゼロ

バッジ`xref 0122`, tag:gradle, `cluster:rag`見える

JBake標準、ゼロ労力、ゼロ価値

決定的、再現可能、テスト可能なグレードルパイプライン

読者に学習曲線はありません

読者は私のコンテンツの*トポロジー*を発見する

得られないもの:

  • これは « 本当の » レコメンデーション エンジンではない (コラボラティブ

filtering,A/B testingなし,feedback loopなし)

  • 品質はJBakeのメタデータの豊富さに依存します — もしあなたの

説明は空です、TF-IDFは何も見ません

  • NLPはオフラインです — 新しい記事はただクラスター化されているだけです

次のスキャン (いいね:ビルドは依然として決定的です)

視点:パブリックナレッジグラフとしてのブログ

この機能はより広い視点を開く:もし~なら`cheroliv.com` 彼は自分自身が*ナビゲータブルな知識グラフ*になったのか?

記事はノードです。タグはコミュニティです。xrefは �辺。読者はもはや孤立した記事を読まず — 彼は~の中をナビゲートする 知識グラフ 現在の記事がエントリーポイントです。

時系列のリストを表示しないホームページを想像してください 最新の投稿だが、コーパスマップ:オントロジーのクラスター, ピボット記事(エッジが最も多いもの), 読み取りパス おすすめ「もしGraphifyの記事が気に入ったら、次に読んでください Knowledge Graphを理解するためのツールとしてのもの

これはもうブログではない。これは*セマンティックアトラス*です。

そしてこれはSFではありません。その`graph.json`既に存在しています。 彼に欠けているのはナビゲーションインターフェースだけだ。

結論:ファイルシステムはテンプレートが無視するものを知っている

私が火曜日の夜に考案したのは、ヒューリスティックの置き換えです。 怠惰な(年代順)による忠実な表現の 私のコーパスの構造 (Knowledge Graph)。

テンプレート`post.thyme`変わらない。変わるのは、私たちが 彼に食べ物を与える。 前 : 並び替えられた Java リスト`date`. 後: 3つの解析層から導出された重み付きグラフ — xrefs, 共起 タグ、およびNLPによる新興オントロジー

ループが閉じられました。 graphifyはワークスペースをスキャンします。 Bakeryは グラフとテンプレートを埋める。 読者はおすすめを見る そしてエンジン — 指揮者 — はそれについてさえ知らないと それらはすべて存在する。

これで正しいアーキテクチャです。各プラグインは一つのことだけを行います。 消費者端末はどのようにするかを知る必要はありません。

関連記事