추천 엔진으로서의 Knowledge Graph: 시간 순서의 « 관련 기사 »를 죽인 방법
게시: 12 May 2026
pgvector와 LangChain4j 통합에 관한 기사를 읽고 계십니다. 페이지 하단에 JBake 연관 문서를 제안합니다. 클릭하면… 구성에 관한 문서입니다. Kitty Terminal의. 두 가지 사이의 유일한 공통점은? 그들은 미만으로 출판되었습니다. 15일 간격. 이것은 권고가 아닙니다. 이는 위장한 캘린더입니다. 사설.
장면 : 화요일 5월 12일, 오후 5시 30분
나는 내 기사 중 하나를 다시 읽고 있다 — Knowledge Graph를 코드베이스 이해 도구로 하는 것에 관하여 내용이 밀집되어 있습니다: 노드, 엣지, 커뮤니티, PlantUML, 온보딩. 한 기사 15분 읽기 기술은 참여를 유도한다`graphify-gradle`, plantuml-gradle, 그리고 그래프 토폴로지의 개념들
페이지 하단에, ‘관련 기사’가 나에게 제시한다:
-
Firebase Contact Form (0113)에 대한 기사
-
Eager/Lazy 메커니즘에 관한 기사 (0108)
-
Gradle 스크립트 → 플러그인 마이그레이션에 대한 기사 (0102)
-
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개의 게시물 — 풍부한 구조를 가진 시간순 템플릿이 완전히 압도한다 :
-
명시적 참조 : 저는 사용합니다`xref:`내 기사에 대량으로
문서 0122는 0106(knowledge graph)과 0116(compartimentage)를 참조합니다. (에피스테믹적). 이러한 링크들은 편집상의 hard linking 이다 — 나 의도적으로 이 개념들을 연결하기로 결정했습니다.
-
공유된 태그 : 각 기사는 몇 가지의`:jbake-tags:`. 기사에 대한
Graphify + PlantUML (0105) 있다`gradle, graphify, plantuml, knowledge-graph`. Knowledge Graph (0106) 문서는 knowledge-graph, graphify, plantuml. 일곱 중 세 개의 태그가 일치합니다 — 43% 일치율.
-
공동 발생하는 명명된 엔티티 : « 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은 *소비자 터미널*입니다 — 적용합니다 플러그인이지만 비즈니스 로직을 구현하지 않습니다. 규칙은 : 증명 책임은 독점 플러그인에 있다.
@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, 또는 다른 모든 유사성 개념. 그는 베이커리를 적용한다, 마침표. 라 협력은 베이커리 내부적이다
파이프라인 : 스캔 → 그래프 → 템플릿
다음은 세 단계로 구성된 전체 파이프라인입니다 :
-
스캔 (그래프화) :`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)
)
}
-
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 }
}
}
-
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`, |
연대순의 폴백
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 추천 엔진은 점진적인 개선 — 아닙니다 파괴적인 변경.
신흥 온톨로지: 기사들이 서로를 알지 못한 채 모일 때
가장 흥미로운 층은 세 번째 층: 신흥 온톨로지. 인용되지 않은 글, 태그가 같은 것이 아닌 글, 하지만 서로 같은 것에 대해 말하는 사람들도 모르게.
구체적인 예를 들어 봅시다. 내 코퍼스에 있는 세 개의 기사:
-
0119 — 벤치마크 DGX Spark vs Cloud LLM 구독
-
0121 — Gradle 플러그인 Ollama Pro 두 인스턴스 제어
-
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`, |
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가 읽습니다 그래프가 템플릿을 먹입니다. 독자는 추천을 봅니다 관련된. 그리고 엔진 — 지휘자 — 는조차도 모르는 그 모든 것이 있다.
이게 바로 좋은 아키텍처입니다. 각 플러그인은 한 가지 일을 합니다. 그리고 소비자 단말은 어떻게 해야 하는지 알 필요가 없습니다.
참고문헌
-
문서 0105 — Graphify를 Gradle 워크플로에 통합
-
문서 0122 — 27배에서 45배 더 효율적 : AI 전문가 함대
관련 기사
31 May 2026
14 May 2026