Время читања : 14 minutes

Читаете č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

На дну странице, « Сродни чланкови » ми предлажу:

  1. Један чланак о Firebase Contact Form (0113)

  2. Članak o mehanizmu Eager/Lazy (0108)

  3. Članak o migraciji Gradle skript → plugin (0102)

  4. Č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 :

  1. 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.

  1. 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.

  1. 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.

Ispravna arhitektura — Bakery uvozi Graphify
@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 у три корака :

  1. Скан (графишење) :`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)
    )
}
  1. 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 }
        }
}
  1. Шаблон (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`, tag:gradle, cluster:pgvector-rag) objašnjava čitaču zašto ovaj članak je povezan. Nije samo transparencija — to je pedagogija na topologiji vaš sopstrani sadržaj. (empty)

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:

  1. 0119 — Benchmark DGX Spark vs Cloud абонемент LLM

  2. 0121 — Gradle plugin za pokretanje dve instanci Ollama Pro

  3. 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`, tag:gradle, `cluster:rag`видимло

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.

Повезани чланци