Knowledge Graph kao motor preporuke: kako sam ubio « povezani članci » hronološki
Објављено 12 May 2026
- Сцена: уторак 12. мај, 17:30
- Дијагностика : Три типа односа которым шаблон не види
- Решење: Gradle pipeline, nije poziv LLM na vruću
- Nastajuća ontologija: Kad članci se skupe bez da se poznaju
- Ugovor DAG : Ko znači za koga
- Шта добијамо (ишта не добијамо)
- Perspektive: Blog kao javni knowledge graph
- Zaključak: Sistem fajlova zna ono što template ne zna
- Референце
Читаете članak о интеграцији pgvector с LangChain4j. На дну странице, JBake Vam preporučuje « Srodni članci ». Kliknete. To je članak o… konfiguraciji. od Kitty Terminala. Jedina zajednička tačka između njih? Objavljeni su manje od petnaest dana razmaka. Ovo nije preporuka. Ovo je kalendar pretvaran u editorial
Сцена: уторак 12. мај, 17:30
Пречитам један од моjih чланaka — taj o Knowledge Graphu kao alat za razumevanje codebase Sadržaj je gust: čvorovi, grane, zajednice, PlantUML, uvođenje. Jedan članak tehnika čitanja od 15 minuta koja mobilizuje`graphify-gradle`, plantuml-gradle, i koncepti grafovne topologije
На дну странице, « Сродни чланкови » ми предлажу:
-
Један чланак о Firebase Contact Form (0113)
-
Članak o mehanizmu Eager/Lazy (0108)
-
Članak o migraciji Gradle skript → plugin (0102)
-
Članak o kumulanju pretplata Ollama Pro (0120)
Tri od ovih četiri članka nemaju ništa veze sa Knowledge Graph-om. Они су ту jer су последњи објављени — временска близост, не semantičko susjedstvo
Gledam šablon`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`је хронолошка листа.`postStat.index lt 4`uzima četiri prvi koji nisu trenutni post. To je sve. Nema logike. sličnosti. Nema pojma o sadržaju. Samo jedna petlja`for`maskirana kao препорака.
|
Није то грешка у JBake. Ово је подразумевано понашање свих generator statičkog sajta : lista postova je ravna i uređena po datumu. Шаблон ради шта може са тим што има. |
Ali to nije razlog da ga prihvati.
Дијагностика : Три типа односа которым шаблон не види
Moj korpus članaka — 23 posta u 4 meseca — ima bogatu strukturu koja vremenski šablon potpuno prevlada :
-
Eksplicitne reference : ja koristim`xref:`масовно у мојим чланцима.
Član 0122 referencuje 0106 (knowledge graph) i 0116 (kompartmenatizacija) epistemološki). Ove veze su uredničko hard linking — ja sam Svesno je odlučio da poveže ove koncepte.
-
Deljeni tagovi : svaki članak ima`:jbake-tags:`. Članak o
Graphify + PlantUML (0105) ima`gradle, graphify, plantuml, knowledge-graph`. Članak o Knowledge Graph-u (0106) ima knowledge-graph, graphify, plantuml. Tri taga iz sedam su zajednička — 43% preklapanja.
-
Sukupne imenovane entitete : « pgvector », « RAG », « embedding
pojavljuju se zajedno u četiri različitog članka. « LangChain4j », Ollama », « plugin Gradle » u šest ostalih. Nisu to tagovi. objavljeni — to su patterns émergents korpusa koje samo NLP може да детектује.
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
title Три слојеве везе између чланака
package "Sloj 1 — Eksplicitne reference" #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 "Sloj 2 — deljeni tagovi" #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
Danas, moj sajt ne koristi nijedna od ova tri sloja. On koristi nulta sloj : redoslaganje ubacivanja u Javini listi.
Решење: Gradle pipeline, nije poziv LLM na vruću
Прво искушење би било да се позове LLM у време bake : « За Ovaj članak, pronađi tri najsličajnija članka u korpusu. Ne radite to.
|
Један poziv LLM svakom`./gradlew bake`košta vreme, novac i Uvođenje nedeterminizma u vašu gradnju. Odgovor LLM-a može preći između dva build-a bez da se sadržaj promenio. Vaša CI postaje neponovljiv, vaši testovi postaju nestabilni. |
Rešenje je determinističko: Gradle pipeline koji pre-računava graf sličnosti i čuva ga u`graph.json`. Шаблон чита результат — Nikada ne pokreće izračun.
Arhitektura: Bakery uvozi Graphify, ne Engine
Ово је најважнија архитектурна тачка. Искушaње би било да uvezati saradnju u`engine/build.gradle.kts`: primenjen motor graphify za skeniranje, bakery za pečenje, i engine čini most između обојих.
Ovo je anti-patern. Engine je potrošački terminal — on primenjuje dodaci, ne implementira poslovnu logiku. Pravilo je : Teret dokaza je na vlasničkom pluginu.
@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #FFF3CD
title Pekara uvozi Graphify — Motor ne zna ništa.
package "N3 — MOTOR" #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 — 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 : "plugins { pekara }"
BAK -down-> GRF : "plugins { graphify } ← N2→N0 OK"
@enduml
Ugovor DAG je poštovan: bakery (N2) uvozi graphify (N0), N2 > N0, Није нарушење. Engine (N3) привози pekara (N2), N3 > N2, OK.
Engine ne ima ni jedne linije koda koja referencira`graph.json`, relatedPosts, или било którą другу pojam sličnosti. On primenjuje bakery, tačka. La saradnja je interna u bakery.
Pipeline: Scan → Graf → Template
Ово је потпуни pipeline у три корака :
-
Скан (графишење) :`graphify-plugin`Сканира workspace. У њему
trenutna forma, već detektuje`xref:`među`.adoc`i izlaga u`graph.json`kao ivice tipa`reference`.
Ono se obogaćuje za uredni sadržaj: * Прочитај metapodatke JBake (:jbake-tags:, :jbake-description:) svake`.adoc`блога * Računa ko-pojavnosti tagova → ivice`tag_cooccurrence`sa teznik * Izvlačuje imenovane entitete iz opisnika kroz TF-IDF → edges`entity_overlap` * Уметни секцију`blog_articles`u`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)
)
}
-
Bake (pekara) : u trenutku`./gradlew bake`, `BakeryPlugin`чита
`graph.json`i rešava povezane članke za svaku objavu.
// 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 }
}
}
-
Шаблон (
post.thyme) : model JBake sada prima mapu
strukturalna umesto ravne hronološke liste
<!-- 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>
Шаблон је ист — он не зна одакле подаци долажу. Samo ugovor između bakery i JBake je promenio :`published_posts`(lista хронолошки) stanie`relatedPosts`(mape ponderisane grafom).
|
Баџ « разлог » (пример:`xref 0122`, |
Chronološki fallback
Si `graph.json`nedostupan (lokalan build bez prethodnog skeniranja, prv разврштање, CI која још није интегровала скан graphify), шаблон treba da se degraduje elegantno :
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)
}
Podrazumevano ponašanje je identično trenutnom ponašanju. On preporučni motor je postapono unapređenje — nije rušna promena
Nastajuća ontologija: Kad članci se skupe bez da se poznaju
Najzanimljiviji sloj je treći: emergentna ontologija Статије које се не цитирају, које немају исте тагове, али koji govore o istoj stvari bez da to znaju
Uzmimo konkretan primer. Tri članka iz mojeg korpusa:
-
0119 — Benchmark DGX Spark vs Cloud абонемент LLM
-
0121 — Gradle plugin za pokretanje dve instanci Ollama Pro
-
0122 — Stopa efikasnosti 27x 45x Flot AI eksperata
Ova tri članka nemaju nijedan xref medusobno. Njihove oznake ne se Preklapaju se samo do 20% (« ollama », « llm » zajednički). Ali jedan lagan NLP На описima открива се јасак кластер: « трошак », « претплата», oblak », « GPU », « ključ API », « Ollama Pro », « efikasnost », « odnosi
Oni čine ontološki klaster: ekonomija samodržanog LLM vs cloud. Ovaj klaster nije deklarisan nigde. Pojavljuje se iz korpusa.
@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #E8F8F5
title Cluster Ontologique Émergent — "Економија LLM Self-Hosted"
package "Detektovan klaster: économie-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 "trošak", "GPU",
"pretplata", "Ollama Pro".
end note
@enduml
Ovaj ontološki klaster postaje kompozitni edge u`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 je namerno lako — TF-IDF + cosine similarity na Opis. Nije potrebno BERT, nije potreban model jezika. Corpus ima 23 članka, matrica sličnosti se nalazi u JSON fajl od 50 KB.
|
Moć NLP-a ne dolazi iz sofisticiranosti algoritma, ali veličina korpusa i kvalitet opisivanja. Jedna `:jbake-description:`dobro napisana od 150 znakova sadrži više сигнал за TF-IDF да је цео чланак од 3000 речи. |
Ugovor DAG : Ko znači za koga
Ovo je tačka gde arhitektonska disciplina plaća. DAG N0→N3 definisano u`engine/build.gradle.kts`daje jednostavno pravilo : Nijedan projekat ne uvozi projekat višeg nivoa.
| Потрошачки плагин | Uvezen plugin | Nivo potrošača | Uvezen nivo | Валидно? |
|---|---|---|---|---|
пекара-gradle |
graphify-gradle |
N2 |
N0 |
✅ N2 > N0 — OK |
motor |
bakery-gradle |
N3 |
N2 |
✅ N3 > N2 — OK |
мотор |
graphify-gradle |
N3 |
N0 |
✅ Tehnika OK, konceptualno netačno — suradnja je unutar bakery |
Motor nametni pekara. Pekara nametni graphify. To je sve. Ako jednom dan Želim da dodam vector store codebase (N1) za pretragu kros-korpusna semantika, pekara takođe uvozi. Motor se ne menja.
|
plugin N2 je hub svojih vlastitih zavisnosti Engine N3不是 хуб — то је терминал који примијује хубове. |
Шта добијамо (ишта не добијамо)
Главна користи је уредничка, а не техничка :
| Pre | Posle |
|---|---|
Srodni članci = 4 poslednjih postova |
Сродни чланци = топ 4 по тежини у графу знања |
Čitač čita iz RAG → preporuka Kitty Terminal |
Čitač čita iz RAG → preporuka pgvector, chunking, vektor skladište |
Nula transparencija zašto |
Ознака`xref 0122`, |
JBake standard, bez truda, bez vrednosti |
Pipeline Gradle deterministički, reproducibilni, testabilni |
Nema krivulje učenja za čitaoca |
Čitač otkriva topologiju mojeg sadržaja |
Шта не освајаш :
-
Ово није «вистински» механизам за препорuke (неколаборативна
filtering, nema A/B testa, nema feedback petlje)
-
Kvalitet zavisi od bogatstva JBake metapodataka — ako vaš
opisи су празни, TF-IDF не види ништа
-
NLP je offline — novi članci nisu klasterovani samo do
sledeći sken (to je dobro : build ostaje deterministički)
Perspektive: Blog kao javni knowledge graph
Ова функција отвара широку перспективу: и ако`cheroliv.com` postajao sam knowledge graph navigabilan?
Članci su čvorovi. Tagovi su zajednice. xref su rubovi. Čitač više ne čita isolovan članak — on se kreće unutar graf znalja čiji je trenutni članak ulazna tačka.
Претпоставите домашњу страну која не приказује хронолошку листу. последњих постова, али једна карта корпуса : онтолошки кластерe, pivotni članci (oni sa najviše grana), putevi čitanja препоручено (« ако вам је допадло чланак о Graphify, прочитајте наставик onaj o Knowledge Graphu kao alat za razumevanje )
Ово више није блог. Ово је семантички атрас.
I to nije naučna fantastika.`graph.json`већ постоји. Mu nedostaje samo navigacioni interfejs.
Zaključak: Sistem fajlova zna ono što template ne zna
Шта сам дизајнио у уторак вечеру, то је заменa heuristike lenja (chronološki red) putem verna predstnost Struktura mojeg korpusa (Knowledge Graph).
Šablon`post.thyme`Не се мења. Шта се мења, то је што ми Mu hrani. Pre: Java lista uređena po`date`. Posle : ponderisan graf nastao iz tri sloja analize — xrefs, ko-pojavljivanja tagova, i emergentna ontologija putem NLP.
Петља је затворена. graphify скенира радњи простор. Pekara чита graf i hranjuje šablon. Čitač vidi preporuke. релевантне. И engine — диригент — чак и не зна да Sve to postoji.
To je to, dobra arhitektura. Svaki plugin radi jedno. I potrošački terminal ne mora da zna kako.
Референце
-
Članak 0105 — Integracija Graphify u Gradle radni tok
-
Article 0117 — Granularizacija OSS/CSS
-
Članak 0121 — Gradle dodatak za upravljanje dve instanze Ollama Pro