Knowledge Graph sebagai mesin rekomendasi: bagaimana saya membunuh artikel terkait berurutan
Diterbitkan 12 May 2026
- Panggung: Selasa, 12 Mei, 17:30
- Diagnosa: Tiga Jenis Hubungan yang tidak terlihat oleh template
- Solusi : Sebuah Pipeline Gradle, Bukan Panggilan LLM Panas
- Ontologi yang Emergent: Ketika Artikel Berkelompok Tanpa Saling Mengenal
- Kontrak DAG: Siapa yang penting bagi siapa
- Apa yang Kita Dapatkan (Dan Apa yang Kita Tidak Dapatkan)
- Perspektif: Blog sebagai Knowledge Graph Publik
- Kesimpulan: sistem file tahu yang template abaikan
- referensi
Anda sedang membaca sebuah artikel tentang integrasi pgvector dengan LangChain4j. Di bagian bawah halaman, JBake Anda disarankan « Artikel terkait ». Anda mengklik. Ini adalah artikel tentang… konfigurasi. dari Kitty Terminal. Yang satu-satunya titik persamaan di antara keduanya? Mereka telah diterbitkan kurang dari Selisih sebelas hari. Ini bukan rekomendasi. Ini adalah kalender yang disamarkan sebagai editorial.
Panggung: Selasa, 12 Mei, 17:30
Saya membaca kembali salah satu artikel saya — itu tentang Knowledge Graph sebagai alat untuk memahami codebase Konten tersebut padat: node, edge, komunitas, PlantUML, onboarding. Sebuah artikel teknik membaca 15 menit yang menggerakkan`graphify-gradle`, plantuml-gradle, dan konsep topologi grafik.
Di bagian bawah halaman, artikel terkait menyarankan kepada saya:
-
Sebuah artikel tentang Firebase Contact Form (0113)
-
Sebuah artikel tentang mekanisme Eager/Lazy (0108)
-
Sebuah artikel tentang migrasi skrip Gradle ke plugin (0102)
-
Sebuah artikel tentang akumulasi langganan Ollama Pro (0120)
Tiga dari empat artikel ini tidak memiliki tidak ada hubungan dengan Knowledge Graph. Mereka ada karena mereka adalah yang terakhir dipublikasikan — kedekatan temporal, tidak lingkungan semantik
Saya melihat template`post.thyme`:
<!-- 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`adalah sebuah daftar kronologis.`postStat.index lt 4`mengambil empat pertama yang bukan post saat ini. Itu saja. Tidak ada logika de kemiripan. Tidak ada konsep konten. Hanya sebuah lingkaran`for`bersamaran sebagai rekomendasi.
|
Ini bukan bug dari JBake. Ini adalah perilaku default dari semua generator situs statis: daftar postingan datar dan diurutkan berdasarkan tanggal. Template melakukan apa yang dapat dilakukan dengan apa yang dimilikinya. |
Tapi itu bukan alasan untuk menerimanya.
Diagnosa: Tiga Jenis Hubungan yang tidak terlihat oleh template
Korpus artikel saya — 23 posting dalam 4 bulan — memiliki struktur kaya yang template kronologis sepenuhnya mengalahkan :
-
Referensi eksplisit : saya menggunakan`xref:`secara masif dalam artikel-artikel saya.
Artikel 0122 mereferensikan 0106 (knowledge graph) dan 0116 (kompartimentasi) epistemik). Ini adalah hard linking redaksi — saya sengaja memutuskan untuk menghubungkan konsep-konsep ini.
-
Tag berbagi : setiap artikel memiliki beberapa`:jbake-tags:`. Artikel tentang
Graphify + PlantUML (0105) memiliki`gradle, graphify, plantuml, knowledge-graph`. Artikel tentang Knowledge Graph (0106) memiliki knowledge-graph, graphify, plantuml. Tiga tag bersama dari tujuh — 43% kesamaan.
-
Co-occurring named entities : « pgvector », « RAG », « embedding
muncul bersama dalam empat artikel yang berbeda. « LangChain4j », Ollama », « plugin Gradle » dalam enam lainnya. Bukan tag yang dideklarasikan — ini adalah pola muncul dari korpus yang hanya sebuah NLP dapat mendeteksi.
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
title Tiga lapisan hubungan antara artikel
package "Lapisan 1 — Referensi eksplisit" #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 "Lapisan 2 — Tags dibagikan" #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 "Lapisan 3 — Ontologi emergens" #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
Hari ini, situs saya tidak menggunakan satu pun dari ketiga lapisan ini. Ia menggunakan lapisan nol: urutan pemasukan dalam daftar Java.
Solusi : Sebuah Pipeline Gradle, Bukan Panggilan LLM Panas
Accuan pertama akan memanggil LLM saat proses bake: « Untuk artikel ini, menemukan tiga artikel yang paling serupa dalam korpus. Jangan lakukan itu.
|
Satu panggilan LLM untuk setiap`./gradlew bake`Memakan waktu, uang, dan Memperkenalkan non-determinisme dalam build Anda. Respons dari LLM bisa beralih antara dua build tanpa mengubah konten. CI Anda menjadi tidak dapat direproduksi, tes Anda menjadi tidak stabil. |
Solusinya deterministik: sebuah pipeline Gradle yang pra-kalkulasi grafik. kesamaan dan menyimpannya di`graph.json`. template membaca hasil — dia tidak pernah memicu sebuah perhitungan.
Arsitektur : Bakery Mengimpor Graphify, Bukan Engine
Ini adalah titik arsitektur yang paling penting. Godaan akan menjadi mengkabel kolaborasi di`engine/build.gradle.kts`mesin mengaplikasikan graphify untuk pemindaian, bakery untuk memanggang, dan engine menjembatani antara kedua
Ini adalah anti-pola. Engine adalah terminal konsumen — menerapkan plugin, dia tidak mengimplementasikan logika bisnis. Aturan adalah : Beban bukti berada pada plugin bersifat proprietary.
@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #FFF3CD
title Bakery mengimpor Graphify — Engine tidak tahu apa-apa
package "N3 — MESIN" #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 — BAKERY" #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 — membuat grafik" #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 : "plugins { bakery }"
BAK -down-> GRF : "plugins { graphify } ← N2→N0 OK"
@enduml
Kontrak DAG dipatuhi: bakery (N2) mengimpor graphify (N0), N2 > N0, tidak ada pelanggaran. Mesin (N3) mengimpor bakery (N2), N3 > N2, OK.
Engine tidak memiliki satu pun baris kode yang mereferensikan`graph.json`, relatedPosts, atau apa pun konsep kesamaan. Dia menerapkan bakery, titik. La kolaborasi adalah internal bakery
Pipeline: Scan → Grafik → Template
Ini adalah pipeline lengkap dalam tiga langkah:
-
Pemindaian (menggrafikan) :`graphify-plugin`memindai workspace. Di dalamnya
bentuk saat ini, ia sudah mendeteksi`xref:`di antara`.adoc`dan menunjukkan di`graph.json`seperti edges jenis`reference`.
Kita melengkapinya untuk konten redaksi : Uraikan metadata JBake (:jbake-tags:, :jbake-description:) setiap`.adoc`blog * Menghitung ko-occurence tag → edges`tag_cooccurrence`dengan berat * Mengekstrak entitas bernama dari deskripsi melalui TF-IDF → tepi`entity_overlap` Menyuntikkan sebuah bagian`blog_articles`dalam`graph.json`yang ada
// 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)
)
}
-
Bake (bakery) : pada saat`./gradlew bake`, `BakeryPlugin`tempat tidur
`graph.json`dan menyelesaikan artikel terkait untuk setiap post
// 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 }
}
}
-
Template (
post.thyme) : model JBake sekarang menerima sebuah peta
terstruktur alih-alih sebuah daftar kronologis yang datar
<!-- 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>
Template-nya sama — dia tidak tahu dari mana data berasal. Hanya kontrak antara bakery dan JBake yang berubah:`published_posts`(daftar kronologis) menjadi`relatedPosts`(peta yang berbobot berdasarkan graf).
|
Lencana « alasan » (contoh:`xref 0122`, |
Fallback Kronologis
Si `graph.json`tidak hadir (build lokal tanpa pemindaian sebelumnya, pertama penerapan, CI yang belum diintegrasikan pemindaian graphify), templat harus menurunkan dengan anggun:
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)
}
Perilaku default sama dengan perilaku saat ini. Le mesin rekomendasi adalah sebuah perbaikan progresif — bukan sebuah perubahan yang merusak.
Ontologi yang Emergent: Ketika Artikel Berkelompok Tanpa Saling Mengenal
Lapisan yang paling menarik adalah yang ketiga: ontologi emergente. Artikel yang tidak dikutip, yang tidak memiliki tag yang sama, tetapi yang berbicara tentang hal yang sama tanpa mengetahuinya.
Mari kita ambil contoh konkret. Tiga artikel dari korpus saya:
-
0119 — Perbandingan DGX Spark vs Cloud Langganan LLM
-
0121 — Plugin Gradle Mengendalikan Dua Instansi Ollama Pro
-
0122 — Rasio Efisiensi 27x 45x Armada Ahli AI
Ketiga artikel ini tidak memiliki aucun xref di antara mereka. Tag mereka tidak mereka hanya tumpang tindih hingga 20% (« ollama », « llm ») umum. Namun NLP ringan pada deskripsi menunjukkan klaster yang jelas: « biaya », « langganan », awaan », « GPU », « kunci API », « Ollama Pro », « efektivitas », « rasio
Mereka membentuk kluster ontologis : ekonomi LLM self-hosted vs cloud. Klaster ini tidak dideklarasikan di mana pun. Ia muncul dari korpus.
@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #E8F8F5
title Cluster Ontologique Émergent — "Ekonomi LLM Self-Hosted"
package "Cluster terdeteksi : ekonomi-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 : "ollama", "llm".
Cluster découvert par NLP :
co-occurrences "biaya", "GPU",
"langganan", "Ollama Pro".
end note
@enduml
Ini kluster ontologi menjadi sebuah edge komposit dalam`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 sengaja ringan — TF-IDF + cosine similarity pada deskripsi. Tidak perlu BERT, tidak perlu model bahasa. Corpus terdiri dari 23 artikel, matriks kemiripan muat dalam satu file JSON berukuran 50 KB.
|
Kekuatan NLP tidak berasal dari sofistikasi algoritme tapi dari ukuran korpus dan dari kualitas deskripsi. Se `:jbake-description:`ditulis dengan baik dari 150 karakter mengandung lebih dari sinyal untuk TF-IDF bahwa sebuah artikel penuh berjumlah 3000 kata. |
Kontrak DAG: Siapa yang penting bagi siapa
Ini adalah titik di mana disiplin arsitektur membayar. DAG N0→N3 ditentukan dalam`engine/build.gradle.kts`memberi sebuah aturan sederhana: Tidak ada proyek yang merupakan proyek tingkat lebih tinggi.
| Plugin Pengguna | Plugin yang diimpor | Tingkat konsumen | Tingkat yang diimpor | Sah? |
|---|---|---|---|---|
bakery-gradle |
graphify-gradle |
N2 |
N0 |
✅ N2 > N0 — OK |
mesin |
bakery-gradle |
N3 |
N2 |
✅ N3 > N2 — OK |
mesin |
graphify-gradle |
N3 |
N0 |
✅ Teknik OK, tetapi konseptual salah — kolaborasi internal pada bakery |
Engine menerapkan bakery. Bakery menerapkan graphify. Itu saja. Jika suatu hari saya ingin menambahkan vector store codebase (N1) untuk penelitian semantik cross-corpus, bakery juga mengimpornya. Engine tidak berubah.
|
Polanya adalah : plugin N2 adalah hub dari dependensi pribadinya Engine N3 bukan sebuah hub — ini adalah sebuah terminal yang menerapkan hub. |
Apa yang Kita Dapatkan (Dan Apa yang Kita Tidak Dapatkan)
Keuntungan utama adalah editorial, bukan teknis:
| sebelum | Setelah |
|---|---|
Artikel terkait = 4 posting terakhir |
Artikel terkait = 4 teratas berdasarkan bobot dalam Knowledge Graph |
Membaca dari RAG → rekomendasi Kitty Terminal |
Pembaca membaca RAG → rekomendasi pgvector, chunking, vector store |
Tidak ada transparansi tentang mengapa |
Badge`xref 0122`, |
JBake standar, nol usaha, nol nilai |
Pipeline Gradle deterministik, dapat direproduksi, dapat diuji |
Tidak ada kurva belajar untuk pembaca |
Pembaca menemukan topologi dari konten saya |
Apa yang tidak kita dapatkan:
-
Ini bukan mesin rekomendasi « sebenarnya » (tidak ada kolaboratif
pemfilteran, tidak ada pengujian A/B, tidak ada loop umpan balik)
-
Kualitas tergantung pada kekayaan metadata JBake — jika Anda
deskripsi kosong, TF-IDF tidak melihat apa-apa
-
NLP offline — artikel baru hanya kluster di
scan berikutnya (ini bagus: build tetap deterministik)
Perspektif: Blog sebagai Knowledge Graph Publik
Fitur ini membuka perspektif yang lebih luas: dan jika`cheroliv.com` menjadi dirinya sendiri sebuah knowledge graph navigable ?
Artikel adalah node. Tag adalah komunitas. xref adalah tepi. Pembaca tidak lagi membaca artikel yang terisolasi — ia menavigasi di dalam graf pengetahuan yang artikel saat ini adalah titik masuk.
Bayangkan sebuah halaman utama yang menampilkan bukan daftar kronologis dari postingan terbaru, tetapi sebuah peta corpus : kluster ontologis, artikel pivot (yang memiliki banyak sisi), jalur bacaan disarankan (« Jika Anda menyukai artikel tentang Graphify, baca selanjutnya itu tentang Knowledge Graph sebagai alat pemahaman
Ini bukan lagi sebuah blog. Ini sebuah atlas semantik.
Dan ini bukan science fiction. Yang`graph.json`sudah ada. Dia hanya memerlukan antarmuka navigasi.
Kesimpulan: sistem file tahu yang template abaikan
Apa yang saya rancang pada malam Selasa ini adalah penggantian sebuah heuristik malas (urutan kronologis) oleh representasi akurat dari struktur korpus saya (Knowledge Graph).
template`post.thyme`tidak berubah. Yang berubah adalah apa yang kita Memberinya makan. Sebelumnya: sebuah daftar Java yang diurutkan berdasarkan`date`.Setelah : graf berbobot yang berasal dari tiga lapisan analisis — xrefs, co-occurrences tag, dan ontologi yang muncul melalui NLP.
Lingkaran sudah selesai. graphify memindai workspace. Bakery membaca le Graf memberi makan template. Pembaca melihat rekomendasi. Dan engine — maestro — bahkan tidak tahu bahwa semua ini ada.
Ini adalah arsitektur yang baik. Setiap plugin melakukan satu hal. Dan terminal konsumen tidak perlu mengetahui caranya.
referensi
-
Article 0105 — Mengintegrasikan Graphify dalam alur kerja Gradle
-
Artikel 0117 — Granularisasi OSS/CSS
-
Artikel 0121 — Plugin Gradle Mengendalikan Dua Instansi Ollama Pro
-
Article 0122 — 27× hingga 45× lebih efektif : armada ahli IA