Architecture: Personalized PageRank (PPR) graph engine — v1.23.0 direction (consolidates #117, #157, #175) #235
Replies: 23 comments
|
I like PPR as the primitive for a wiki where links are intentional, but I would not lean too hard on the embedding-equivalence argument. PPR over the link graph captures relation-by-link behavior; embeddings can still add signal for unlinked, newly added, cross-lingual, or vocabulary-shift pages. So I’d frame PPR as the default graph-native ranker, not as a full semantic replacement. For cold start, I’d keep a hybrid fallback: lexical/title match seeds first, PPR expands from those seeds only when the graph has enough edges. A simple guard like min_edges/min_outdegree before using PPR could avoid the “mostly zeros” case. For evaluation, a small fixture of query -> expected pages would be useful before implementation. Compare lex-only, PPR-only, and lex-seeded-PPR on recall@k and noise in the final context. That would make the primitive choice easier to defend than the theoretical equivalence alone. |
|
Picking up on cold start (#3), the #1 equivalence question, and performance/caching (#4/#5). The hub lifecycle has two ends; PPR detection only covers the middle. Scaffolding goes up before construction and comes down after:
On #3 — I'd gently push back on a purely algorithmic cold start. The missing input on an empty vault is which domains exist, and only the user has it. A guided "pick 5 seeds" step is cheaper, more correct, and gives a real UX answer (re: @Linate): Query Wiki works on day one because you planted 5 seeds. On #1 — +1 to @GioiaZheng: the unlinked / newly-added / cross-lingual page is exactly where PPR is blind (a fresh page scores ~0 however central it is semantically). Worth adding that the seed step doubles as the pragmatic mitigation for the new-vault slice of that gap — you don't wait for the topology to know a page is central, you plant it. On #4/#5 — one option worth tabling: Monte Carlo PPR. Your sketch is power-iteration: compute the walk's stationary distribution deterministically, ~50 sweeps over the whole graph (the 500k-ops figure). The alternative is to sample the walk rather than solve it (Fogaras et al. 2005, Towards scaling fully personalized PageRank): simulate K short random walks from the seed, count visits, take top-k.
The cost is that it's approximate (sampling noise), but that's a non-issue under your own framing: PPR is a ranker/prefilter, not a judge — top-k is robust to the noise, exact scores were never needed. The retirement signal stays orthogonal either way: clustering is a seed-independent global node property, computed once and invalidated on local ingests, however you run the query-time walk. On the build itself — I won't pre-empt the discussion, but the clustering-retirement signal is a small, self-contained piece: a pure graph property, seed-independent, so it doesn't block on the PPR core, and I already have a reference implementation. Happy to own that as the separate commit you mentioned once the direction settles — and to keep reviewing the framing in the meantime. |
|
Thanks @DocTpoint, this distinction makes sense to me. I agree that the fully empty-vault case is different from the sparse-but-not-empty case. I’d frame them as two separate cold-start paths:
In that sense, guided seeds feel less like a retrieval fallback and more like graph initialization / temporary scaffolding. Lexical seeding is still useful, but only after there is already something in the vault to match against. That also gives a clean evaluation matrix across wiki lifecycle stages:
Comparing those separately would avoid judging one strategy against the wrong stage of the wiki lifecycle. Monte Carlo PPR also seems like a good fit if the goal is top-k context selection rather than exact global scores. The approximation noise feels acceptable as long as the LLM remains the verifier and the eval checks recall@k / context noise instead of score stability. |
Summary of where we agree (and what I'm hearing)Thank you both — this is exactly the kind of discussion I was hoping for. Concise summary of the convergence so far so we're working from the same map. Consensus
Maintainer decisionsThree decisions I'm taking into v1.23.0 scope. Calling them out so we can keep this thread focused on the open questions.
ROI for the proposed v1.23.0 scopeRoughly:
Skip-link-distinctiveness for v1.23.0 if scope is tight — it's a refinement, not a closure. Open questions I'd still like to nail down
Friendly noteI don't have a hard timeline on this — the v1.22.0 release closed yesterday and the next sprint (v1.22.1 PATCH) is collecting in-the-wild P0 fixes (#187 fix in PR #200 just landed from @DocTpoint, #197 dead-link fabrication going into the same PATCH, #196 Query Wiki side panel from @YounianC under review). v1.23.0 is the sprint after. So the v1.23.0 implementation can start whenever the eval fixture questions above are answered, and the implementation itself is ~5 days. If anyone wants to drive any of these modules, the door is open — same pattern @DocTpoint set with #200 (one self-contained PR per module, with a reference impl if you have one). The PR #200 pattern (PR description naming the two named test cases that catch the regression) is exactly what I want for the rest of v1.23.0. Thanks again — this is the cleanest version of the hub-detection / PPR / query-retrieval discussion I could have hoped for. |
|
On Q2 (Modal vs Notice) — non-blocking, but I'd reframe the axis. Agreed on non-blocking: a forced modal on install fights Obsidian's deferral style, and you lose nothing by letting the user arrive at the step on their own terms. But Modal-vs-Notice is really an information-capacity axis, and for the actual goal — a first run that's self-explanatory, no docs required — both ends are weak:
The form that actually satisfies "understand it without a manual" is a first-run welcome note — a real markdown note the plugin writes to the vault and opens in a tab on first launch. It's non-blocking (just a tab you can close), it's self-explanatory (the note is the instructions, by example), and for a wiki plugin it can pull double duty: pre-fill it as the founding note —
The user edits one note — the most native action in Obsidian — and that content becomes the domain declaration. Those domains then seed the controlled tag vocabulary, which is what gives later-ingested notes distinct basins for PPR to separate. The note seeds the vocabulary; real ingested content seeds the graph. Two things worth flagging:
So my vote on Q2: non-blocking, delivered as an editable welcome/founding note rather than a toast or a settings panel — one intuitive action (fill in a note) that does onboarding, domain declaration, and vocabulary seeding at once. |
|
On Q4 (eval fixture) — shared synthetic fixture is the right call; I'm a no on contributing a real vault. For the public repo, a small CC0 synthetic fixture (your My vault is not a candidate for that, and I'd rather be clear-eyed about why than leave it ambiguous: it's a working clinical knowledge base — German-language medical content authored from a physician's notes. Public-domain release isn't on the table in any form, anonymization included; medical material in lay hands carries real-world risk that a fixture's value doesn't justify. So please don't size the plan around it being shareable. What is shareable, if it ever helps, is pure topology — page/edge counts, degree distribution, clustering coefficients, link density at ~900 pages — numbers that carry zero content and reconstruct nothing. If a real mature-vault density check would help sanity-check the |
|
You're right to call that out, @DocTpoint — apology owed. I should have caught it before sending. Privacy is one of this project's core commitments (data stays in your vault, period), and extending that posture to how we build the test suite — not just how the runtime handles data — should have been my first instinct. The "shared fixture" framing slipped through because I didn't actually re-read the AI draft before posting. That's on me. Sorry. Going forward, the public repo gets a synthetic or CC-licensed fixture only — no real vault content, ever. I'll spec the build script as part of the v1.23.0 work. If you're ever up for sharing a one-shot aggregate topology report (page count, edge count, degree buckets — zero content), that would sharpen the Thanks for the steady work on this thread. The hub-lifecycle framing and the Monte Carlo argument are the spine of the v1.23.0 plan, and they came from digging past the first easy answer. That's the kind of contribution that makes a project better — and it's worth saying out loud. |
|
On Q1/Q3: Q1: embeddings as opt-in enrich. I would keep them out of the v1.23.0 core. The provider picture is better than "only OpenAI" now, but still not a clean common denominator: OpenAI, Gemini, Ollama, and OpenRouter expose embedding endpoints; Anthropic's Claude API docs still list Messages / Batches / Token Counting / Models / Files / etc., not embeddings. More importantly, even where embeddings exist, the contract differs: task formatting, dimensionality controls, batch shape, local model availability, cost, and persistence semantics. So I would make the future shape:
That keeps provider support from becoming a v1.23 dependency. Q3: threshold/fallback. I would not use
So my answer on "pure lex vs lex-seeded PPR": lex-seeded PPR when there is a real seed with a neighborhood; pure lex only when the graph is too sparse or the seed is isolated. The fallback should be a small cascade, not one global mode. For defaults before eval, I would start low and conservative:
Then tune those with the synthetic fixture. If @DocTpoint can later report only topology aggregates from the 900-page vault, that is enough to sanity-check whether the defaults are absurd without needing private content. Provider docs I checked quickly: OpenAI embeddings, Gemini embeddings, Ollama |
|
@green-dalii — no apology needed, honestly. It was an AI-draft slip on a detail, not a posture; the moment it surfaced you extended the privacy commitment from runtime to test suite without hesitation, which is the part that matters. Appreciated all the same. Here's the one-shot topology aggregate — pure structure, zero content, no page titles. Live wiki graph of the vault. Method (so the numbers are interpretable): nodes = wiki page files (entities + concepts + sources); edges = deduped On the Q3 thresholds (@GioiaZheng): the cascade shape is right, and a mature vault sits comfortably above every global guard — edges/node 6.44 >> 1.0, largest weak component 99.9% >> 50%, well past The more interesting signal is the tail: even at 910 pages and mean degree ~13, there's a real low-connectivity fringe — 69 nodes at total-degree 1–2, 46 with in-degree 0. That's the empirical case for keeping the per-seed One note for the hub-retirement side (v1.24.0, no rush): avg local clustering is 0.537 vault-wide, which gives the clustering-coefficient retirement signal a realistic baseline — hubs sit well below that (their children don't interlink), so there's clear separation to work with at this density. Snapshot only; happy to regenerate against a future build if the graph definition shifts — e.g., once PPR settles the canonical node identity for the source/entity duality. |
v1.23.0 implementation kickoff — 2026-06-27Picking up this thread for real. Thank you all for the design work — the consensus map is clean and I'm working from it. What's decided (recap from thread)
Implementation priorityFull table in
Total: ~7-8 days (5 for P1 + 2-3 for P0/P2 overhead that the consensus raised vs the original ~5-day estimate). WorkflowPer the project's TDD standard, every module goes failing-test → minimal implementation → refactor → Gate 1 verification. After each phase completes, I'll cut a Open questions (in order of how much they block the first phase)
Privacy boundary (per @DocTpoint's call-out)The public repo gets a synthetic or CC-licensed fixture only — no real vault content, ever. The build script for the synthetic fixture will be part of P0-1. The 910-page topology aggregate @DocTpoint shared stays as the only "real-vault signal" in the design thread. I'll post phase-by-phase updates here as each module lands, with the recall@k vs lex-only numbers from P0-2 once the fixture is in place. |
I would say it seem reasonable to prioritize implementing the PPR if it is robust enough to render index management obsolete. Frankly, I cannot really comprehend the conversation so far. That being said, I am in the opinion that the proposal in #184 can still serve some purposes as "Human-readable UI layer." The PPR engine solves the problem of automated machine retrieval, while Obsidian Bases index can additionally improve human navigation and content management. When both are implemented, they are addressing different issues from my point of view. Because both systems rely on identical YAML frontmatter, the Bases implementation should not be major technical debt. |
|
@green-dalii — yes on Q2, happy to own it. One seam adjustment and one addition. Scope I'd take: the whole mechanism, EN-only. Welcome-note markdown + the field→controlled-vocabulary seeding contract + first-launch wiring (the The load-bearing guardrail, in tests not just prose: the note seeds the tag prior and never materializes domain pages. That's the exact line #197 just closed, so the regression cases fail if a filled-in field ever fabricates a page. An addition — a second cold-start entry point the existing The selection rule is the load-bearing one from the 5-seed onboarding upthread — not all tags, but ≥5 chosen for maximum spread (floor, not cap: 8 distinct domains → 8 anchors). Max-spread self-filters the qualifier tags — a second-position taxonomy qualifier sits near its domain, so it's never picked — which removes the only knob that would otherwise need a manual "is this a domain?" decision. On how spread is measured at cold start (no graph yet, embeddings deferred): it's a small graceful-degradation cascade, not one metric — vocabulary structure proposes → an LLM completion call ranks spread for a flat custom vocab → optionally a light ingest-folder analysis grounds it in real notes — user confirms at every rung. Worth stressing: a completion call is not an embedding call. Completion is already a hard requirement (ingest/lint/query), so this adds zero new provider dependency — it doesn't reopen the embedding-contract question, and as a one-time user-confirmed seeding step it doesn't touch order-invariance. I'd ship one rung for v1 and leave the rest as named design space. I'd keep this anchor path as a fast-follow PR to the welcome note, not folded in — same pipeline (vocabulary → spread anchors → crystallizing hubs), second entry point — so the first PR stays focused. Scheduling: I'm back at a real machine now, so last week's async caveat is lifted — not a blocker either way. |
|
A substantive progress report on the v1.23.0 branch. Completed (Phase 5.1.5 — UX Onboarding + Multi-File Ingest)
On the Welcome note vs the design discussion The Welcome note we shipped is a pure reading guide — install verification, how-to-use, wiki structure explanation, Quick start. No editable domain-declaration fields, no vocabulary-seeding flow, no injected tag vocabulary from Welcome content. This was a deliberate decision. Two reasons this divergence is the right call:
On Embeddings Evaluated from first principles + actual code paths, and I've decided to close the embeddings path entirely, not defer it.
Decision: No embedding layer. Not for v1.23.0, not for v1.25.0. P1 (Graph Engine) — next on the branch The three core modules are already in the repo. Remaining work:
Branch visibility The Look-ahead @DocTpoint — the clustering-retirement reference implementation is still welcome. A self-contained PR covering @GioiaZheng — your threshold cascade ( This thread remains open — if anyone has a concrete case where PPR + hybrid cascade is insufficient, or a better approach to the cold-start question, I'd like to hear it. The project aims to get this right, not just ship it. |
|
No quibble that the cascade runs at cold start — but worth being precise about which arm does the work. With a handful of edges, MC-PPR's per-node estimate is just Binomial(K, p)/K — i.e. the MLE — and for the small-p tail its relative error is ~1/√(Kp). That's the lex arm answering; PPR mostly adds variance until the graph is dense. Mild irony worth a line: Fogaras MC earns its keep on large graphs — the sparse cold-start regime is exactly where exact power-iteration PPR is both cheaper and variance-free. "The graph is sparse" reads as an argument for exact PPR there, not against helping it separate faster. Not blocking anything — just noting which knob is load-bearing. |
|
@DocTpoint — really appreciate both comments. Each one added clarity. On the vocabulary→spread anchors idea: The mechanism is well thought through, and the seam you've drawn (you ship EN + mechanism, we backfill i18n) is exactly the right split for this project. I'd like to pick this up as a v1.24.0 discussion rather than a fast-follow to the current sprint — the remaining P1 work (PPR→query integration, lint hub-link strip) needs to land before the graph engine is usable end-to-end, and the cold-start gap you're addressing is already covered by the #130 queue + PPR cascade (lex arm answers until the graph is dense enough). Your proposal adds headroom but the current path doesn't regress without it — so I want to give it the attention it deserves rather than rushing it into an already-packed sprint. I agree that On MC-PPR variance at cold start: This is a nuance I should have included in my own write-up. You're absolutely right: Fogaras MC earns its keep on large graphs, and the sparse regime is exactly where exact power-iteration PPR is cheaper and variance-free. The cascade already routes sparse graphs to the lex arm, so the practical impact is zero for current users — but I appreciate you taking the time to spell it out anyway. It's a genuinely sharp observation, and it confirms the cascade architecture is on solid ground. I've noted the MC-vs-exact-PPR question for the synthetic fixture eval — if the results show variance issues at moderate densities, switching to exact PPR in that mid-range band is a one-line routing change in The thread stays open. If anything else comes to mind, I'd love to hear it. |
|
Thanks @green-dalii — v1.24.0 for the anchor path works for me; no reason to crowd the current sprint when the cascade doesn't regress without it. On the diversity→time-to-maturity data: my current vault is well past cold start, so I can't reconstruct that curve from it retrospectively. But I'm setting up an instrumented fresh run precisely to get it — a separate vault seeded with ≥5 max-spread anchors, ingesting a controlled subset and snapshotting topology (modularity over the domain-tag partition, core size, clustering) after each batch. That measures the seeding→separation relationship directly, and it composes with the MC-vs-exact eval you flagged. I'll share aggregates once I have a clean series. Near-term I'll get the hub-retirement signal to you as the self-contained |
|
Thanks for the implementation update. The current On the numbers: I would treat I would split the guard into two layers:
The 910-page aggregate is useful evidence that the mature-vault arm is sane, but I would not use it to justify cold-start thresholds by itself. It says the global graph has enough structure; it does not say every query seed has enough local support. That is exactly why the per-seed guard matters. On MC vs exact PPR, I agree with @DocTpoint's nuance. MC is attractive once the graph is large enough that local sampling wins, but sparse/mid-density graphs may be better served by exact PPR or by staying lex-seeded until variance stops moving top-k. I would let the fixture compare three routes: lex-only, lex-seeded exact PPR, and lex-seeded/graph-first MC-PPR. If exact wins in the middle band, routing through it is just an implementation detail, not an architecture reversal. On embeddings: no objection to keeping them out of v1.23.0. The only line I would preserve in the design notes is a future opt-in enrichment escape hatch if real user fixtures show a relation class that link extraction consistently misses. That keeps current scope clean without overclaiming that links cover every semantic case. |
|
Re: observation data on anchor/vocabulary diversity ↔ cold-start maturity — I ran a small controlled A/B and the result lands squarely on your first-principles side, so it seems worth sharing. Setup. One 50-note corpus spanning 6 domains (microbiome, immunology, mitochondria, metabolism, nutrition, psychology), identical model and settings (local Gemma, temp 0.15), the only variable being ingest order:
I measured link-intrinsic topology (Louvain modularity over the wiki-link graph, k-core / 3-core fraction, average clustering, connected components) — deliberately not the tag partition, since tags don't drive retrieval. Result (end state, same corpus):
Statistically indistinguishable on every macro-structural metric. During the seeding phase the domains do nucleate as separate components and then merge along genuine shared-concept bridges (e.g. cytokines pulling the molecular domains together; psychology stays isolated until the body notes bridge it via stress/HPA concepts) — so you can watch the cold-start dynamics — but the mature topology converges to the same place regardless of order. Takeaway. For this corpus, deliberate spread-seeding buys a transiently cleaner early structure but no lasting topological advantage: the link graph the cascade needs emerges from the content, not the ingest sequence. That matches your read that anchors are an optimization rather than a necessity. The one consistent (but n=1) difference was redundancy, not structure — the batch arm ended with slightly more duplication (521 vs 496 nodes; 34 vs 27 high-confidence dedup candidates), i.e. seeding gave later notes canonical merge targets. Marginal, and below the noise floor of single runs. Caveats: n=1 per arm (LLM extraction is stochastic — hence the 496 vs 521 node gap; the macro-metrics converging despite different node sets is itself the robustness signal), single corpus, single local model. Happy to share the snapshot script (pure-stdlib Louvain + k-core, deterministic) and the per-step CSV if either is useful for the fixture-eval you mentioned for the PPR routing. |
|
Follow-up observation that bears on the "structure emerges from content" framing — this time about the quality of the link graph the cascade consumes, not just its order-invariance. I'd been assuming our vault's dense core (≈76% 3-core) was partly an LLM artifact — specific pages lazily linking to prominent hubs, inflating interconnection. I measured it three ways and the assumption was wrong on every axis: 1. Is the core hub-scaffolded? (structural, link-graph only) 2. Are the hub-links themselves lazy? (bge-m3 embeddings, distinctness = cos(P,hub) − centrality(hub)) 3. Do peer↔peer edges discriminate semantically? (AUROC of edge-cos vs random-pair-cos) Takeaway: the dense core is genuine content density, not a linking artifact. For a graph-based retrieval engine this is the encouraging read — the link substrate the PPR cascade walks is semantically sound (edges track relatedness, AUROC 0.90), so PPR is propagating over real signal rather than noise. It also means no structural intervention (active hub-suppression or anchor-engineering during ingest) is warranted — there's no artifact to correct, which lines up with your first-principles call that the structure should be left to emerge. Caveats: single corpus (medical/biology, genuinely densely connected), single local embedding model (bge-m3), distinctness/centrality are relative measures that drift as the corpus grows. Scripts (pure-stdlib graph + the embedding AUROC harness) available if useful for the fixture-eval. |
|
@DocTpoint Our end: leave-one-out eval on a 2142-page real vault (3473 vertices, 12158 edges), 6 param combos tested:
Winner: Where it lines up with your data:
Also landed: PR #215 (@DocTpoint's hub-retirement module). Judges hub-tag lifecycle, not hub-suppression. Zero callers in current release — headed for future lint integration. Two questions your posts raise:
The full eval table is up at |
|
@green-dalii — ran the nearest-neighbor baseline you asked for (Q2), on your own Setup. Same 53 pages, same 10 ground-truth queries, same R@k. Three arms:
Read. A pure semantic retriever with zero graph lands within ~1–3 pts of the cascade (R@10 36.4 vs 37.8); both ≈2× the keyword baseline. So most of the cascade's lift is semantic-over-keyword, not graph-over-semantic — the lex baseline understates what a non-graph retriever gets. That's exactly the separation Q2 was after: "graph is well-structured" and "PPR is specifically good at it" turn out to be largely the same fact (the structure tracks semantics — consistent with the AUROC 0.90), so PPR ≈ embedding-kNN as retrievers. Where they diverge — the interesting part, they're complementary by query type:
That divergence is the precise relation-class for @GioiaZheng's opt-in enrichment escape-hatch: embeddings catch conceptual synonyms link-extraction misses; the graph catches structural/proper-noun cohesion embeddings miss. So the honest case for the graph engine isn't retrieval quality — it's cost. It delivers embedding-grade R@k with no embedding model, offline, over links that exist anyway. Strong argument, just a different one than "2.3× over lex" implies. If you keep that table around, I'd report Complementary regime (second eval). On our real vault (902 entity/concept pages, 2669 edges) I ran the inverse task — leave-one-out link prediction, query = the full source page, recover one held-out neighbor:
Here knn ≈ lex ≫ single-source PPR. When the query is rich (a whole page of context), content similarity already captures relatedness and graph propagation from one node trails it. Combined with the fixture: the graph earns its lift specifically in the impoverished-query regime — a short keyword that names a hub but not the cluster. That's the cold-start case, and a clean characterization of when the cascade helps. Re your two follow-ups: (1) lower-density/general-interest vault — agree it shrinks, but the governing variable is edge-density × edge-AUROC (the PPR retrieval ceiling), independent of the MC-vs-exact estimator question. (2) Yes, add the NN baseline — it's the one control that separates "well-structured" from "PPR-specifically-good," and on this fixture they're the same thing. Caveats: fixture n=10 queries, single corpus each (both medical/biology, English), single embedding model (bge-m3); knn is query→page while the cascade is the production path. Scripts (kNN fixture arm + LOO harness, pure-stdlib + LM-Studio embeddings) available if useful for the fixture-eval. |
|
@DocTpoint — thanks for turning the Q2 baseline around so fast, and especially for grounding it in the same fixture so the comparison is apples-to-apples. Cascade vs knn vs lex on identical queries is the control I was missing; the per-query divergence table is exactly the granularity I needed. Where this lands for v1.23.0:
For your kind offer to run additional evals on your private vault — yes, please, whenever you have time. Not urgent; v1.24.0+ planning, not the v1.23.0 release on 2026-07-02. Three things that would be useful (each independent, take whatever fits your bandwidth):
Whenever you have spare cycles — and only if it's fun. The scripts and harness you've already shared (kNN fixture arm + LOO link-prediction harness, the AUROC distinctness harness) are extremely valuable; if you can publish them or share a link to a branch, I'll fold them into the fixture-eval repo alongside the cascade harness for future contributors. Special thanks for the quality of this thread. Three rounds of evidence — the link-graph quality analysis (AUROC 0.897, 3-core >70% peer-to-peer), the seeded-vs-batch cold-start experiment, and now the knn baseline — each one independent, each one grounded in the same fixture methodology. That's exactly the kind of contribution that lets us ship v1.23.0 with confidence rather than hand-waving. Genuinely appreciated. |
|
Closing this thread now that v1.23.0 shipped — the PPR cascade architecture you proposed is live, and the data points from this discussion directly shaped the implementation. Thank you all for the depth; this is the kind of design work that makes a project better. What shipped in v1.23.0 (closed by this release):
Empirical evidence that informed the implementation:
Decisions that diverged from specific proposals:
What's next (v1.24.0+):
Special thanks:
If any of you want to run your own evals on the v1.23.0 cascade or its MC-vs-exact behavior on private vaults, the eval script is at |
Uh oh!
There was an error while loading. Please reload this page.
The shape of the problem
We have a wiki that is, structurally, a graph. Every
[[wiki-link]]between pages is an explicit assertion by the LLM at ingest time that these pages are related. We are sitting on a graph that already encodes exactly the semantic relationships we'd otherwise need an embedding model to compute — and currently we are not using it for any of the three things it should power.This is a tracking issue for the v1.23.0 direction. It supersedes the embedding-layer proposal in #175 and consolidates the hub-detection (#117), link-distinctiveness (#157), and Query-Wiki-relevance work into a single graph-native engine. It is the discussion thread for the architecture; PRs will follow once we agree on the primitive.
The proposal in one sentence
A single Personalized PageRank engine (Haveliwala, "Topic-Sensitive PageRank", WWW 2002) over the existing
[[wiki-link]]graph, computing all three of the signals we need from one iteration.If that sounds abstract — picture your wiki as a small town. Every wiki page is a house. Every
[[wiki-link]]is a well-worn footpath between two houses. The houses that the town's residents visit most often are the important ones. Now imagine you walk into town and ask "where's the bakery?" — you start at the bakery, and every minute you have an 85% chance of following a random footpath to a neighbor, and a 15% chance of teleporting back to the bakery. After walking for an hour, the houses you've visited the most are the ones most relevant to the bakery. That's PageRank. "Personalized" just means we start the walk from the bakery, not from a random house — so the answer is "what's near the bakery?", not "what's important in town?"That's exactly what we need for a wiki question: the user asks "what is Kardiologie?" — we seed the walk at Kardiologie, run for 50 iterations, and the pages that accumulate the most visits are the ones we should put into the LLM's context. No embedding model, no provider lock-in, no persistent cache, no API cost.
Why not embeddings (reprising the decision in #175)
Embedding-based relevance was the original proposal in #175. After discussion we rejected it on three grounds:
/v1/embeddings. DeepSeek, GLM, MiniMax, Ollama, OpenRouter — the providers most of our users actually run — don't. A shared infrastructure built on a single-provider endpoint locks out the majority of the user base.The LLM stays the verifier. Cosine is noisy in dense mechanistic clusters, and there's no natural threshold for "is this related enough" — we use PPR as a ranker / prefilter, not a judge, exactly as #175 proposed. The LLM's job is unchanged; we just give it better candidates.
The single primitive, four consumers
One PPR engine (
src/core/ppr.ts, ~80 LOC, O(V+E·iter), pure JS, no deps) provides:inDegree + PageRank. Add @DocTpoint's local-clustering-coefficient retirement signal from the 2026-06-23 reply — hubs retire when their neighbors have interlinked into chains rather than radiating out from a center.distinctiveness(P→T) ≈ shared(P,T) / PageRank(T). Replaces @DocTpoint's embedding-cosine formula with a link-overlap ratio that has the same ranking behavior on the existing graph.index.md.Tier B redesign (zero LLM)
The old ROADMAP Tier B called for a per-query LLM call to generate a "summary" for each page. We are cancelling that.
Every wiki page already has a
## Descriptionsection (entity pages) or a## Definitionsection (concept pages). It was generated by the LLM at ingest time. We will extract it on demand, at query time, with a ~30-line pure function that:##or end of document)[[wikilink]]syntax to its display text (so the LLM gets clean prose, not raw links)No new frontmatter field, no LLM call, no schema migration, no compatibility issue with old wikis. The summary the LLM sees in the query context is the description the LLM itself wrote at ingest — the most reliable source of "what is this page" that exists.
What this does NOT include (deferred)
aliases:to the wiki pages they describe #185, Enhancement: Obsidian Bases for wiki index management #184, Feature: in-place batch ingest queue for the Ingest button — avoid moving source notes into a staging folder #130, Enhancement: adopt Obsidian Keychain (SecretStorage API) #182, Nested tags for entity/concept pages based on source note tag hierarchy #91, Feature: Add explicit event marker/type and enhanced local indices for improved LLM-Wiki structuring #112, 'auto' granularity setting - maps granularity to file size based on an equation #168, Is it possible to create multiple wikis on various topics? #142: independent features; not in this thread.What we are asking
This is the discussion thread before we cut code. Specifically we want feedback on:
Anyone who has thoughts, especially from #117, #157, #175, #164, and the Lint / Query code paths, please weigh in. We will not start implementation until this thread has had at least a few days of discussion.
cc / ping
fixDeadLinkreintroduces the same hallucination class. The dead-link hub check (consumer Generated wiki is in Chinese, even when choosing English #4) is the seam where the two meet.References
All reactions