χρόνος ανάγνωσης : 14 minutes

Διαβάζετε ένα άρθρο για την ενσωμάτωση του pgvector με το LangChain4j. Στο κάτω μέρος της σελίδας, JBake συστήνει « Σχετικά άρθρα ». Κάνετε κλικ. Εinai ένα άρθρο για…​ τη διαμόρφωση από το Kitty Terminal. Το μοναδικό κοινó σημείο μεταξύ των δύο; Έχουν δημοσιευτεί λιγότερο από δέκα πέντε ημέρες διαφορά. Δεν είναι μια σύσταση. Ένα ημερολógιο που είναι μεταμφιεσμένο ως εξώφυλλο.

Η σκηνή : Τρίτη 12 Μαΐου, 17:30

Επαναβλέπω ένα από τα άρθρά μου — αυτό για το Knowledge Graph ως εργαλείο κατανόησης της codebase Το περιεχόμενο είναι πυκνό: κόμβοι, άκρες, κοινότητες, PlantUML, ενσωμάτωση. Ένα άρθρο τεχνική ανάγνωσης 15 λεπτών που ενεργοποιεί`graphify-gradle`, plantuml-gradle, και οι έννοιες της τοπολογίας γράφου.

Κάτω στη σελίδα, τα «Σχετικά άρθρα» μου προτείνουν :

  1. Ένα άρθρο για το Firebase Contact Form (0113)

  2. Ένα άρθρο για το μηχανισμό Eager/Lazy (0108)

  3. Ένα άρθρο για τη μεταφορά του Gradle σενάριο → plugin (0102)

  4. Ένα άρθρο για τις συσσωρευμένες συνδρομές Ollama Pro (0120)

Τρεις από αυτά τα τέσσερα άρθρα δεν έχουν aucun σχέση με το Knowledge Graph. Είναι εκεί επειδή είναι τα τελευταία που δημοσιεύτηκαν — γειτονία χρόνου, όχι σεμαντικό γείτονα.

Βλέπω το πρότυπο`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`είναι minha χρονολογική λίστα.`postStat.index lt 4`πάρνει τα Οι πρώτοι τέσσερις που δεν είναι η τρέχουσα ανάρτηση. Αυτό είναι. Καμία λογική. της ομοιότητας. Καμία έννοια του περιεχομένου. Μια απλή βρόχος`for`διαfraσμένη ως συστάση.

Αυτό δεν είναι bug του JBake. Είναι η προεπιλεγμένη συμπεριφορά του παντός γεννητής στατικού ιστοτόπου : η λίστα των αναρτήσεων είναι επίπεδη και ταξινομείται κατά ημερομηνία. Το πρότυπο κάνει ό, τι μπορεί με ό, τι έχει.

Αλλά αυτό δεν είναι λόγος για να το αποδεχτεί.

Η διάγνωση : Τρεις τύποι σχέσεων που κανένα πρότυπο δεν βλέπτη

Το σώμα άρθρων μου — 23 δημοσιεύσεις σε 4 μήνες — έχει μια πλούσια δομή που Το χρονολογικό πρότυπο καταστρέφει εντελώς :

  1. Επισήμαντες αναφορές : χρησιμοποιώ`xref:`πολύ στα άρθρά μου.

Το άρθρο 0122 κάνει αναφορά στο 0106 (knowledge graph) και 0116 (διαμερ επιστημονικό). Αυτοί οι σύνδεσμοι είναι επεξεργαστικοί hard linking — έχω Σκοπευτικά αποφάσισε να συνδέσει αυτές τις έννοιες.

  1. Shared tags : κάθε άρθρο έχει`:jbake-tags:`. Το άρθρο για

Graphify + PlantUML (0105) έχει`gradle, graphify, plantuml, knowledge-graph`. Το άρθρο για το Knowledge Graph (0106) έχει knowledge-graph, graphify, plantuml. Τρία tags κοινά από επτά — 43% καλύπτησης.

  1. συμπiptόντες ονομασμένες οντότητες: « pgvector », « RAG », « embedding

εμφανίζονται μαζί σε τέσσερα διαφορετικά άρθρα. « LangChain4j », Ollama », « plugin Gradle » σε έξι άλλα. Αυτά δεν είναι ετικέτες. Δηλώνεται — είναι εξελιγόμενα μοτίβα του σώματος που μόνο ένα NLP μπορεί να εντοπίζει

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center

title Trois couches de relation entre articles

package "Couche 1 — Références explicites" #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 "Couche 2 — Tags partagés" #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 "Couche 3 — Ontologie émergente" #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.

Η Λύση: Ένα Pipeline Gradle, όχι ένας ζεστός κλήσος LLM

Η πρώτη πρόκληση θα ήταν να καλέσεις ένα LLM κατά τη διάρκεια του bake: «Για αυτό το άρθρο, βρίσκει τα τρία πιο παρόμοια άρθρα στο σώμα κειμένου. Μην κάνετε αυτό.

Μια κλήση LLM σε κάθε`./gradlew bake`χρόνο, χρήματα, και Η απάντηση του LLM μπορεί να αλλάξετε μεταξύ δύο builds χωρίς να αλλάξει το περιεχόμενο. Η CI σας γίνεται μη επαναληπτο, τα τεστ σας γίνονται ασταθή.

Η λύση είναι determinist: μια Gradle pipeline που προ-υπολογίζει το γράφημα της ομοιότητας και το αποθηκεύει στο`graph.json`. Το πρόtotype διαβάζει το αποτέλεσμα — ποτέ δεν εκκινεί έναν υπολογισμό.

Η αρχιτεκτονική: Το Bakery εισάγει το Graphify, όχι το Engine

Αυτό είναι το σημαντικότερο σημείο αρχιτεκτονικής. Η πειρασμός θα ήταν να συνδέστε τη συνεργασία στο`engine/build.gradle.kts`engine εφαρμοσμένο graphify για το scan, bakery για το bake, και engine δημιουργεί τη γέφυρα μεταξύ και τα δύο.

Αυτό είναι ένα anti‑pattern. Το Engine είναι ένα terminal consommateur — αυτό εφαρμόζει των plugins, δεν υλοποιεί λογική επιχείρησης. Ο κανόνας είναι : η ευθύνη της απόδειξης είναι στο ιδιοκτησιακό πρόσθετο.

Σωστή αρχιτεκτονική — Bakery εισάγει Graphify
@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #FFF3CD

title Bakery importe Graphify — Engine ne sait rien

package "N3 — ENGINE" #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 { bakery }"
BAK -down-> GRF : "plugins { graphify } ← N2→N0 OK"
@enduml

Το συμβόλαιο DAG είναι σε ισχύ : η bakery (N2) εισάγει το graphify (N0), N2 > N0, κανένα παραβίασμα. Μηχανή (N3) εισάγει ψωμιοπωλείο (N2), N3 > N2, Εντάξει.

Engine δεν έχει καμία γραμμή κώδικα που αναφέρεται`graph.json`, relatedPosts, ή τίποτα άλλη έννοια ομοιότητας. Εφαρμόζει το bakery, ακριβώς. Η Η συνεργασία είναι εσωτερική στο bakery.

Το Pipeline: Σάρωση → Γράφος → Πρότυπο

Εδώ είναι το πλήρες pipeline σε τρία βήματα:

  1. Σάρωση (γραφικοποίηση) :`graphify-plugin`σαρώνει το workspace. Στην του

το τρέχον μορφότυπο, ήδη ανιχνεύει τα`xref:`μεταξύ`.adoc`και τα εμφανίζει σε`graph.json`ως ακμές τύπου`reference`.
Το εμπλουτίζουμε για το επεξεργαστικό περιεχόμενο: * Ανάλυσε τα μεταδεδομένα του JBake (:jbake-tags:, :jbake-description:) κάθε`.adoc`του blog * Υπολογίσε τις συμπιπτώσεις ετικετών → ακμές`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. Πρότυπο (post.thyme) : το μοντέλο JBake λαμβάνει τώρα έναν χάρτη

δομημένο αντί για ένα πλατύ χρονολογικό κατάλογο.

<!-- 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>

Το πρότυπο είναι το ίδιο — δεν γνωρίζει από πού έρχονται τα δεδομένα. Μόνο το συμβόλαιο μεταξύ bakery και JBake έχει αλλάξει :`published_posts`(λίστα χρονολογική) γίνεται`relatedPosts`(χάρτης βαρυνμένος από το γράφημα).

Το badge « λόγος » (π.χ:`xref 0122`, tag:gradle, cluster:pgvector-rag) Εξηγεί στον αναγνώστη γιατί αυτό το άρθρο είναι σχετικό. Δεν είναι μόνο της διαφάνειας — είναι παιδαγωγική για την τοπολογία του το δικό σας περιεχόμενο. ====

Εναλλακτική χρονολογική

Si `graph.json`είναι απουσιάζει (τοπική κατασκευή χωρίς προηγούμενο scan, πρώτος) διανομή, CI που δεν έχει εšte ενσωματώσει το σαρωτικό graphify), το προτύπο πρέπει να υποβιβάζεται ευχάριστα:

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)
}

Η προεπιλεγμένη συμπεριφορά είναι ίδια με την τρέχουσα συμπεριφορά. Ο είναι μία προοδευτική βελτίωση — όχι ένα διαρρηκτική αλλαγή.

Η ΕΠΙΒΡΥΣΣΟΜΕΝΗ ΟΝΤΟΛΟΓΙΑ: Όταν τα Άρθρα Συλλέγονται Χωρίς Να Ξέρουν Ένας τον Άλλον

Το πιο ενδιαφέρον στρώμα είναι το τρίτο: η αναδυόμενη οντολογία. Άρθρα που δεν αναφέρονται, που δεν έχουν τα ίδια ετικέτα, αλλά οι

Ας πάρουμε ένα συγκεκριμένο παράδειγμα. Τρία άρθρα από το σώμα μου:

  1. 0119 — Benchmark DGX Spark vs Cloud συνδρομή LLM

  2. 0121 — Plugin Gradle Διαχείριση δύο παρεγματοποιήσεων Ollama Pro

  3. 0122 — Σχέση Αποτελεσματικότητα 27x 45x Διοχή Ειδίκων ΤΝ

Αυτά τα τρία άρθρα δεν έχουν κανένα xref μεταξύ τους. Τα ετικέτες τους δεν Καλύπτονται μόνο 20% (« ollama », « llm » κοινά). Όμως, ένα ελαφρύ NLP σε τις περιγραφές αποκαλύπτει ένα προφανές σύμπλεγμα : « κόστος », « συνδρομή », νέφος », « GPU », « κλειδί API », « Ollama Pro », « αποδοτικότητα », « λόγος

Σχηματίζουν έναν οντλογικό σύμπυκνωμα: η οικονομία του self-hosted LLM vs cloud. Αυτό το κλάστερ δεν δηλείταιπουθενά. Εξέρχεται από τη συλλογή.

@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #E8F8F5

title Cluster Ontologique Émergent — "Économie LLM Self-Hosted"

package "Cluster détecté : é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 "coût", "GPU",
    "abonnement", "Ollama Pro".
end note
@enduml

Αυτό το οντολογικό σύμπλεγμα γίνεται ένα σύνθετο edge σε`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 + cosine similarity στα περιγραφές. Δεν χρειάζεται BERT, δεν χρειάζεται μοντέλο γλώσσας. Το σώμα κειμένων αποτελείται από 23 άρθρα, ο πίνακας ομοιότητας χωράει σε ένα αρχείο JSON 50 KB.

Η δύναμη του NLP δεν προέρχεται από τη πλοκότητα του αλγόριθμου, αλλά του μέγεθος του σώματος και της ποιότητας των περιγραφών. Μια `:jbake-description:`καλά γραμμένη από 150 χαρακτήρες περιέχει περισσότερο από Σήμα για το TF-IDF ότι ένα άρθρο από 3000 λέξεις.

Το σύμβατο DAG: Ποιος σημαίνει για ποιόν

Αυτό είναι το σημείο όπου η αρχιτεκτονική disipline πληρώνεται. Το DAG N0→N3 ορίζεται σε`engine/build.gradle.kts`δίνει έναν απλό κανόνα: Κανένας Έργος δεν αφορά Έργος υψηλότερου επιπέδου.

Καναλωτικός plugin Plugin εισαγμένο Επίπεδο καταναλωτή Εισαγόμενο επίπεδο Επικυρωμένο;

bakery-gradle

graphify-gradle

N2

N0

✅ N2 > N0 — OK

μηχανή

bakery-gradle

N3

N2

✅ N3 > N2 — OK

μηχανή

graphify-gradle

N3

N0

✅ Τεχνική OK, αλλά εννοιολογικά λάθος — η συνεργασία είναι εσωτερική στο bakery

Engine εφαρμόζει το φOURNO. Το φOURNO εφαρμόζει το graphify. Αυτό είναι tudo. Αν κάποια μέρα Θέλω να προσθέσω το vector store codebase (N1) για έρευνα διασυνάρτηση cross-corpus, το φουρνάκι το εισάγει επίσης. Ο μηχανός δεν αλλάζει.

Το pattern είναι : το plugin N2 είναι ο κόμβος των δικών του εξαρτήσεων Το Engine N3 δεν είναι ένα hub — είναι ένα τερματικό που εφαρμόζει hubs.

Ό,τι κερδίζουμε (και ό,τι δεν κερδίζουμε)

Ο κύριος κέρδος είναι επεξεργαστικό, όχι τεχνικό :

Πριν Μετά

Σχετικά άρθρα = 4 τελευταία δημοσιεύματα

Σχετικά άρθρα = τα 4 κορυφαία βάσει βάρους στο Knowledge Graph

Ο αναγνώστης διαβάζει RAG → σύσταση Kitty Terminal

Ο αναγνώστης διαβάζει το RAG → σύσταση pgvector, chunking, αποθήκη διανυσμάτων

Μηδενική διαφάνεια για το γιατί

μπαζ`xref 0122`, tag:gradle, `cluster:rag`ορατό

τυπικό JBake, μηδενικό κόπος, μηδενική αξία

Pipeline Gradle determinist, αναπαραγώσιμος, ελεγχόσιμος

καμία καμπή μάθησης για τον αναγνώστη

Ο αναγνώστης ανακαλύπτει τη topologie του περιεχομένου μου.

Τι δεν κερδίζουμε:

  • Δεν είναι ένας « αληθινός » μηχανισμός συνιστώσεων (δεν υπάρχει συνεργατική

filtering, δεν υπάρχει A/B testing, δεν υπάρχει feedback loop)

  • Η ποιότητα εξαρτάται από το πλουτότητα των μεταδεδομένων JBake — αν τα

Οι περιγραφές είναι κενές, το TF-IDF δεν βλέπει τίποτα

  • Το NLP είναι εκτός σύνδεσης — τα νέα άρθρα δεν είναι ομαδοποιημένα μόνο στο

το επόμενο σάρωμα (είναι καλό: η κατασκευή παραμένει determinist)

Προοπτικές: Το Blog ως Δημόσιο Γνωσιακό Γράφημα

Αυτή η λειτουργία ανοίγει μια ευρύτερη προοπτική : και αν`cheroliv.com` γίνονταν αυτός ένα knowledge graph navigable ?

Τα άρθρα είναι κόμβοι. Τα tags είναι κοινότητες. Τα xref είναι πλευρές. Ο αναγνώστης δεν διαβάζει πλέον ένα απομονωμένο άρθρο—περιηγείται σε ένα graphe de connaissance` οπου το τρέχον άρθρο είναι το σημείο εισόδου.

Φανταστείτε μια αρχική σελίδα που δεν εμφανίζει τη χρονολογική λίστα των τελευταίων αναρτήσεων, αλλά μία χάρτης του κόρπουσ : οντλογικά σύστατα, κύρια άρθρα (τα οποία έχουν το περισσότερο άκρα), διαδρομές ανάγνωσης προτεινόμενα (« Αν σας άρεσε το άρθρο για το Graphify, διαβάστε εξής αυτό στο Knowledge Graph ως εργαλείο κατανόησης » )

Δεν είναι πλέον ένα blog. Είναι ένα συμαντικός άτλας.

Και αυτό δεν είναι επιστημονική φαντασία. Ο`graph.json`υπάρχει ήδη. Μόνον το περιβάλλο της πλοήγυσης του λείπει.

Συμπέρασμα: Το σύστημα αρχείων ξέρει αυτό που το πρότυπο αγνοεί

Τι σχεδιάσα αυτό το Τρίτη βράδυ, είναι η αντικατάσταση μιας ευρετικής. αβαρής (η χρονολογική σειρά) από μια πιστή αναπαράσταση της η δομή του κόρπους μου (του Knowledge Graph).

Το template`post.thyme`δεν αλλάζει. Το που αλλάζει, είναι αυτό που αυτό του δίνει φαγητό. Πριν: μια λίστα Java ταξινομημένη από`date`. Μετά : ένας βαρυφορτισμένος γράφος που προκύπτει από τρεις στρώματα ανάλυσης — xrefs, co-occurrences ετικετών, και αναδυόμενη οντολογία με NLP

Ο βρόχος είναι κλειστός. graphify σαρώνει το workspace. Bakery διαβάζει το Γράφημα και τροφοδοτεί το πρότυπο. Ο αναγνώστης βλέπει προτάσεις. σχετικές. Και μηχανή — ο δι-ρι-γε-υ-τής — δεν ξέρει ακόμα και ότι Όλα αυτά υπάρχουν.

Αυτό είναι η σωστή αρχιτεκτονική. Κάθε πρόσθετο κάνει ένα πράγμα. Και ο τερματικός καταναλωτή δεν χρειάζεται να ξέρει πώς.

Σχετικά άρθρα