읽기 시간 : 14 minutes

pgvector와 LangChain4j 통합에 관한 기사를 읽고 계십니다. 페이지 하단에 JBake 연관 문서를 제안합니다. 클릭하면…​ 구성에 관한 문서입니다. Kitty Terminal의. 두 가지 사이의 유일한 공통점은? 그들은 미만으로 출판되었습니다. 15일 간격. 이것은 권고가 아닙니다. 이는 위장한 캘린더입니다. 사설.

장면 : 화요일 5월 12일, 오후 5시 30분

나는 내 기사 중 하나를 다시 읽고 있다 — Knowledge Graph를 코드베이스 이해 도구로 하는 것에 관하여 내용이 밀집되어 있습니다: 노드, 엣지, 커뮤니티, PlantUML, 온보딩. 한 기사 15분 읽기 기술은 참여를 유도한다`graphify-gradle`, plantuml-gradle, 그리고 그래프 토폴로지의 개념들

페이지 하단에, ‘관련 기사’가 나에게 제시한다:

  1. Firebase Contact Form (0113)에 대한 기사

  2. Eager/Lazy 메커니즘에 관한 기사 (0108)

  3. Gradle 스크립트 → 플러그인 마이그레이션에 대한 기사 (0102)

  4. Ollama Pro 구독 누적에 관한 기사 (0120)

이 네 개의 기사 중 세 개는 아무 관계도 지식 그래프와 관련이 없습니다. 그들이 거기에 있는 이유는 그들이 가장 최근에 게시되었기 때문입니다 — 시간적 근접성, 아니 의미론적 이웃

저는 템플릿을 보고 있어요.post.thyme[empty]

<!-- 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`잡는다 현재 게시물이 아닌 처음 네 개. 그게 다야. 논리가 없습니다. 유사성. 내용에 대한 개념이 없다. 단지 루프일 뿐이다.`for`로 가장한 추천.

JBake의 버그가 아닙니다. 모든 것의 기본 동작입니다. 정적 사이트 생성기: 게시물 목록은 플랫하고 날짜순으로 정렬되어 있습니다. 템플릿은 가진 것을 가지고 가능한 한 최선을 다합니다.

하지만 그렇다고 해서 그것을 받아들일 이유는 아니다.

진단 : 어떤 템플릿도 보지 못하는 세 가지 유형의 관계

내 기사 코퍼스 — 4개월 동안 23개의 게시물 — 풍부한 구조를 가진 시간순 템플릿이 완전히 압도한다 :

  1. 명시적 참조 : 저는 사용합니다`xref:`내 기사에 대량으로

문서 0122는 0106(knowledge graph)과 0116(compartimentage)를 참조합니다. (에피스테믹적). 이러한 링크들은 편집상의 hard linking 이다 — 나 의도적으로 이 개념들을 연결하기로 결정했습니다.

  1. 공유된 태그 : 각 기사는 몇 가지의`:jbake-tags:`. 기사에 대한

Graphify + PlantUML (0105) 있다`gradle, graphify, plantuml, knowledge-graph`. Knowledge Graph (0106) 문서는 knowledge-graph, graphify, plantuml. 일곱 중 세 개의 태그가 일치합니다 — 43% 일치율.

  1. 공동 발생하는 명명된 엔티티 : « pgvector », « RAG », « embedding

네 개의 다른 기사에 함께 나타납니다. « LangChain4j », Ollama", "플러그인 Gradle"는 여섯 다른 곳에 있습니다. 이것들은 태그가 아닙니다. 선언된 — 그것들은 코퍼스의 *patterns émergents*이며, 오직 한 NLP만 감지할 수 있다.

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center

title 기사들 사이의 관계의 세 계층
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 이 문서에서 corpus에서 가장 유사한 세 개의 문서를 찾습니다. 그것을 하지 마세요.

각각에 대한 LLM 호출`./gradlew bake`시간과 돈이 들고, 그리고 비결정성을 빌드에 도입합니다. LLM의 응답은 두 빌드 사이에서 내용이 변경되지 않은 채 전환할 수 있습니다. 귀하의 CI가 됩니다. 재현할 수 없습니다, 여러분의 테스트는 불안정해집니다.

해결책은 결정적입니다: 그래프를 사전 계산하는 Gradle 파이프라인 유사성과 그것을 안에 저장합니다`graph.json`. 템플릿이 결과를 읽는다 그는 절대로 계산을 시작하지 않는다.

아키텍처 : 베이커리가 그래피파이를 가져오고, 엔진이 아님

이것은 건축 설계에서 가장 중요한 점입니다. 유혹이 될 것이다 협업을 연결하여 안에`engine/build.gradle.kts`: 엔진이 적용됩니다 graphify는 스캔을 위해, bakery는 베이크를 위해, engine은 사이에 다리를 놓는다 둘 다

이것은 안티패턴입니다. Engine은 *소비자 터미널*입니다 — 적용합니다 플러그인이지만 비즈니스 로직을 구현하지 않습니다. 규칙은 : 증명 책임은 독점 플러그인에 있다.

올바른 아키텍처 — Bakery가 Graphify를 가져옵니다
@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #FFF3CD

title 베이커리가 Graphify를 가져옵니다 — 엔진은 아무것도 모릅니다.
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 — 그래파이" #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 : "플러그인 { graphify } ← N2→N0 확인"
@enduml

DAG 계약이 준수됩니다: bakery (N2)는 graphify (N0)를 임포트합니다, N2 > N0, 위반 없음. Engine (N3)는 bakery (N2)를 가져옵니다. N3 > N2, 정상.

엔진에는 참조하는 코드 줄이 *aucune*개 없습니다.graph.json, relatedPosts, 또는 다른 모든 유사성 개념. 그는 베이커리를 적용한다, 마침표. 라 협력은 베이커리 내부적이다

파이프라인 : 스캔 → 그래프 → 템플릿

다음은 세 단계로 구성된 전체 파이프라인입니다 :

  1. 스캔 (그래프화) :`graphify-plugin`워크스페이스를 스캔합니다. 그의

현재 형태로는 이미 그것들을 감지합니다`xref:`사이`.adoc`그리고 에 노출하다`graph.json`유형 테두리처럼`reference`.
편집 콘텐츠를 위해 이를 풍부하게 합니다: * JBake 메타데이터를 파싱 (:jbake-tags:, :jbake-description:) 각의`.adoc`블로그의 * 태그 공출현 계산 → 간선`tag_cooccurrence`무게와 함께 * TF-IDF를 통해 설명에서 명명된 엔티티 추출 → edges`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 (베이커리) : 순간의`./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 모델은 이제 맵을 받습니다

플랫한 연대순 목록 대신 구조화된 형태

<!-- 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) 설명해 줍니다: 독자에게 왜 이 기사가 관련되는지. 이것은 아니다 투명성만으로는 — 토폴로지에 대한 교육이야 당신의 내용. (Empty)

연대순의 폴백

Si `graph.json`없습니다 (로컬 빌드, 사전 스캔 없이, 첫 배포, 아직 graphify 스캔을 통합하지 않은 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)
}

기본 동작은 현재 동작과 동일합니다. Le 추천 엔진은 점진적인 개선 — 아닙니다 파괴적인 변경.

신흥 온톨로지: 기사들이 서로를 알지 못한 채 모일 때

가장 흥미로운 층은 세 번째 층: 신흥 온톨로지. 인용되지 않은 글, 태그가 같은 것이 아닌 글, 하지만 서로 같은 것에 대해 말하는 사람들도 모르게.

구체적인 예를 들어 봅시다. 내 코퍼스에 있는 세 개의 기사:

  1. 0119 — 벤치마크 DGX Spark vs Cloud LLM 구독

  2. 0121 — Gradle 플러그인 Ollama Pro 두 인스턴스 제어

  3. 0122 — 효율 비율 27x 45x 함대 AI 전문가

이 세 개의 기사 사이에는 xref가 아무것도 없습니다. 그들의 태그는 하지 않다 20% (« ollama », « llm » 일반적)으로만 일치합니다. 그러나 가벼운 NLP 설명에 대해 명백한 클러스터가 드러납니다 : « 비용 », « 구독 », 클라우드 », « GPU », « API 키 », « Ollama Pro », « 효율성 », « 비율

그들은 온톨로지 클러스터를 형성합니다 : LLM 자체 호스팅의 경제 vs cloud. 이 클러스터는 어디에서도 선언되지 않았습니다. 이것은 코퍼스에서 발생합니다.

@startuml
skinparam backgroundColor #FEFEFE
skinparam packageBackgroundColor #E8F8F5

title Cluster Ontologique Émergent — "경제적 LLM Self-Hosted"

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 : "ollama", "대규모 언어 모델".
    Cluster découvert par NLP :
    co-occurrences "비용", "GPU",
    "구독", "Ollama Pro".
end note
@enduml

이 온톨로지 클러스터는 복합 엣지가 된다`graph.json`[No output - empty string]

{
  "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 + 코사인 유사성 (대상에 대한) 설명. BERT가 필요하지 않습니다, 언어 모델이 필요하지 않습니다. 코퍼스는 23개의 문서로 구성되어 있으며, 유사도 행렬은 한 50 Ko 크기의 JSON 파일.

NLP의 힘은 알고리즘의 정교함에서 비롯되지 않습니다, 하지만 *코퍼스 크기*와 *설명 품질>. 한 `:jbake-description:`bien 쓰여진 150자에는 더 많은 TF-IDF에 대한 신호: 3000단어 전체 기사

DAG 계약: 누가 누구에게 중요한가

이것이 건축 학문이 보상을 받는 지점이다. 이 DAG N0→N3 정의된`engine/build.gradle.kts`간단한 규칙을 제시한다 : 어떤 프로젝트도 상위 수준의 프로젝트를 임포트하지 않습니다.

소비자 플러그인 가져온 플러그인 소비자 수준 가져온 레벨 유효합니까 ?

베이커리-그레이들

graphify-gradle

N2

N0

✅ N2 > N0 — OK

엔진

베이커리-그레이들

N3

N2

✅ N3 > N2 — OK

엔진

graphify-gradle

N3

N0

✅ 기술 OK, 그러나 개념적으로 틀림 — 협력은 빵집 내부에서 이루어진다.

Engine 적용하다 bakery. Bakery 적용하다 graphify. 이건데 다. manifests 언젠가 저는 연구를 위해 벡터 스토어 코드베이스 (N1)를 추가하고 싶어요. 의미 교차 코퍼스, 베이커리도これを인포트합니다. 엔진은 변하지 않습니다.

패턴은 : N2 플러그인은 자체 의존성의 허브입니다 Engine N3은 허브가 아니라 허브를 적용하는 터미널이다.

우리가 얻는 것 (그리고 우리가 얻지 못하는 것)

주요 이점은 편집적인 것이지 기술적인 것이 아닙니다

이전 이후

관련 기사 = 4 최신 글

관련 글 = Knowledge Graph에서 가중치 상위 4개

리더가 RAG를 읽음 → Kitty Terminal 추천

독자가 RAG를 읽는다 → 추천 pgvector, chunking, vector store

왜에 대한 투명함이 전혀 없습니다.

뱃지`xref 0122`, tag:gradle, `cluster:rag`보이는

JBake 표준, 노력 제로, 가치 제로

결정적, 재현 가능, 테스트 가능한 Gradle 파이프라인

읽는 사람에게 학습 곡선이 없습니다

독자는 내 내용의 *topologie*를 발견한다.

우리가 얻지 못하는 것 :

  • 이것은 "참된" 추천 엔진이 아닙니다 (협업 기반이 아닌

필터링, A/B 테스트 없음, 피드백 루프 없음)

  • 품질은 JBake 메타데이터의 풍부함에 달려 있다 — 만약 당신의

설명이 비어 있습니다, TF-IDF는 아무것도 보지 못합니다

  • NLP는 오프라인 상태이며 — 새 기사들은 여기서만 클러스터화됩니다

다음 스캔 (좋아요 : 빌드는 여전히 결정적입니다)

전망: 블로그가 공개된 지식 그래프이다

이 기능은 더 넓은 관점을 열어줍니다: 그리고 만약`cheroliv.com` 그 자신 *knowledge graph navigable*가 되고 있었는가?

기사는 노드입니다. 태그는 커뮤니티입니다. xref는 간선들. 독자는 더 이상 고립된 기사를 읽지 않는다 — 그는 에서 탐색한다 *지식 그래프*에서 현재 문서는 진입점입니다.

시간순 목록이 아닌 홈페이지를 상상해 보세요. 최근 게시물, 하지만 corpus 지도 : 온톨로지 클러스터, 피벗 항목 (가장 가장자리가 많은), 읽기 경로 추천 (« Graphify에 대한 글을 좋아하셨다면, 다음을 읽어보세요 Knowledge Graph를 이해 도구로 삼는 것

이제 블로그가 아닙니다. 이것은 *시맨틱 아틀라스*입니다.

그리고 이것은 과학 소설이 아닙니다.`graph.json`이미 존재한다 그에게는 탐색 인터페이스만 부족합니다.

결론: 파일 시스템은 템플릿이 무시하는 것을 알고 있다.

내가 화요일 저녁에 설계한 건 한 가지 휴리스틱의 대체다. 게으른 (연대순) 에 의한 정확한 표현의 내 코퍼스(지식 그래프)의 구조.

템플릿`post.thyme`변하지 않는다. 바뀌는 것은 우리가 하는 것이다. 그에게 먹이를 준다. 전 : 정렬된 Java 목록`date`. 후 : 세 층 분석에서 유도된 가중 그래프 — xrefs, 동시 발생 태그와, 그리고 NLP에 의한 신흥 온톨로지.

루프가 닫혔습니다. graphify가 워크스페이스를 스캔합니다. Bakery가 읽습니다 그래프가 템플릿을 먹입니다. 독자는 추천을 봅니다 관련된. 그리고 엔진 — 지휘자 — 는조차도 모르는 그 모든 것이 있다.

이게 바로 좋은 아키텍처입니다. 각 플러그인은 한 가지 일을 합니다. 그리고 소비자 단말은 어떻게 해야 하는지 알 필요가 없습니다.

참고문헌

관련 기사