Skip to content

Commit 3f5c121

Browse files
gmoonclaude
andcommitted
Add blog post: Your Knowledge Graph Needs a Search Engine
QMD + Lattice integration post covering semantic search over structured knowledge graphs. Adds image rendering to the blog markdown renderer and an architecture diagram SVG. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1dcc095 commit 3f5c121

4 files changed

Lines changed: 243 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Loading

src/data/blog-posts.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,145 @@ lattice init
166166
- [Live dashboard](${LATTICE_DASHBOARD_URL})
167167
- [Forkzero](https://forkzero.ai)`,
168168
},
169+
{
170+
id: 'post-002',
171+
slug: 'knowledge-graph-search-engine',
172+
title: 'Your Knowledge Graph Needs a Search Engine',
173+
date: '2026-03-07',
174+
author: {
175+
name: 'George Moon',
176+
bio: 'Building knowledge coordination tools for human-agent collaboration.',
177+
github: 'https://github.com/georgemoon',
178+
x: 'https://x.com/georgemoon',
179+
},
180+
excerpt:
181+
"Lattice gives you traceability. But traceability assumes you know what you're looking for. QMD adds the missing piece: semantic search over your knowledge graph, running entirely on-device.",
182+
discussionPrompt:
183+
'How do you search your knowledge bases today? Structured queries, semantic search, or something else?',
184+
sources: [
185+
{
186+
name: 'QMD',
187+
author: 'Tobi Lutke',
188+
description: 'An on-device search engine combining BM25, vector embeddings, and LLM re-ranking.',
189+
url: 'https://github.com/tobi/qmd',
190+
},
191+
{
192+
name: 'Lattice',
193+
author: 'Forkzero',
194+
description:
195+
'A knowledge coordination protocol connecting research, strategy, requirements, and implementation.',
196+
url: 'https://github.com/forkzero/lattice',
197+
},
198+
],
199+
content: `Lattice gives you traceability — a versioned graph from research to code. You can traverse it, check for drift, and give agents full context for any requirement.
200+
201+
But traceability assumes you know what you're looking for. You know the requirement ID. You know which thesis to check. You can formulate the exact query.
202+
203+
What about the question you *can't* quite formulate? "Something about token expiry that came up in that security review." "The research that made us rethink session handling." That's not a graph traversal — it's a search problem.
204+
205+
## Two kinds of search
206+
207+
Lattice search is structured. Query by ID, priority, category, or graph proximity. It's exact and fast:
208+
209+
\`\`\`bash
210+
lattice search --query "auth" --priority P0
211+
lattice search --related-to REQ-AUTH-001
212+
\`\`\`
213+
214+
But some questions are fuzzy. They're about concepts, associations, half-remembered context. Structured search can't help when you don't have the right keywords or don't know which node to start from.
215+
216+
That's where semantic search comes in.
217+
218+
## QMD: local hybrid search
219+
220+
[QMD](https://github.com/tobi/qmd) is a local search engine by Tobi Lutke. It combines three retrieval strategies — **BM25 full-text search**, **vector embeddings**, and **LLM re-ranking** — all running on-device. No API calls, no cloud dependency.
221+
222+
It ships with an MCP server, so agents can query it directly. 13k+ GitHub stars and actively maintained.
223+
224+
The key insight: QMD indexes files using glob masks. Point it at any directory and it indexes what's there. Including YAML.
225+
226+
## Zero-config integration
227+
228+
QMD can index your \`.lattice/\` directory directly — no export step, no format conversion:
229+
230+
\`\`\`bash
231+
# Add your lattice as a QMD collection
232+
qmd collection add .lattice/ --name lattice --mask "**/*.yaml"
233+
234+
# Annotate each layer for better context
235+
qmd context add qmd://lattice/sources "Research backing strategic theses"
236+
qmd context add qmd://lattice/theses "Strategic claims derived from research"
237+
qmd context add qmd://lattice/requirements "Testable specifications"
238+
qmd context add qmd://lattice/implementations "Code bindings satisfying requirements"
239+
240+
# Build the index
241+
qmd embed
242+
\`\`\`
243+
244+
That's it. Your knowledge graph is now semantically searchable.
245+
246+
## How it fits together
247+
248+
![Lattice + QMD architecture: structured and semantic search converging in an agent workflow](/blog/qmd-lattice-integration.svg)
249+
250+
Two paths from the same source of truth. Lattice provides structured traversal — by ID, metadata, and graph edges. QMD provides semantic discovery — fuzzy matching, conceptual similarity, cross-layer connections you didn't explicitly model.
251+
252+
## Complementary, not competing
253+
254+
These tools answer different questions from the same data:
255+
256+
\`\`\`bash
257+
# Structured: exact, filtered
258+
lattice search --query "auth" --priority P0
259+
260+
# Semantic: fuzzy, conceptual
261+
qmd query "how do we handle expired tokens"
262+
263+
# Graph traversal: follow edges
264+
lattice search --related-to REQ-AUTH-001
265+
266+
# Conceptual discovery: find related ideas
267+
qmd query "security implications of session management"
268+
\`\`\`
269+
270+
Lattice search is for when you know what you're looking for. QMD is for when you know what you're *thinking about*.
271+
272+
## Dual MCP for agents
273+
274+
Both tools expose MCP servers. An agent configured with both gets the best of each:
275+
276+
- **lattice_search** — structured graph queries, drift detection, version-bound edges
277+
- **qmd_search / qmd_query** — semantic discovery, fuzzy matching, cross-collection results
278+
279+
Configure both in your \`.mcp.json\` and agents can choose the right tool for each sub-task. Need the exact spec for a requirement? Lattice. Need to find everything related to a concept? QMD.
280+
281+
## What would make it even better
282+
283+
The integration works today with no changes to either tool. But two enhancements would make it smoother:
284+
285+
1. **\`lattice integrate qmd\`** — A convenience command that auto-configures the QMD collection, sets up context annotations per layer, and runs the initial embed. Replaces the six-line setup with one command.
286+
287+
2. **Title comments in YAML** — If lattice added a \`# Title: ...\` comment header to YAML files, QMD's title extractor could use it for better chunking and display. Currently it falls back to filename for non-markdown files.
288+
289+
Neither is required — but both would reduce friction.
290+
291+
## Get started
292+
293+
\`\`\`bash
294+
# Install Lattice
295+
${INSTALL_CMD}
296+
297+
# Install QMD
298+
npm install -g @tobilu/qmd
299+
300+
# Initialize and index
301+
lattice init
302+
qmd collection add .lattice/ --name lattice --mask "**/*.yaml"
303+
qmd embed
304+
\`\`\`
305+
306+
- [Lattice on GitHub](${GITHUB_REPO_URL})
307+
- [QMD on GitHub](https://github.com/tobi/qmd)
308+
- [Live dashboard](${LATTICE_DASHBOARD_URL})`,
309+
},
169310
]

src/pages/BlogPage.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ describe('renderInline', () => {
2626
const result = renderInline('visit [GitHub](https://github.com)')
2727
expect(result).toBeTruthy()
2828
})
29+
30+
it('renders images', () => {
31+
const result = renderInline('![diagram](/blog/test.svg)')
32+
expect(result).toBeTruthy()
33+
})
2934
})
3035

3136
describe('renderContent', () => {

src/pages/BlogPage.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,29 @@ export function renderInline(text: string): React.ReactNode {
290290
}
291291
}
292292

293+
// Image: ![alt](src)
294+
if (text[i] === '!' && text[i + 1] === '[') {
295+
const closeBracket = text.indexOf(']', i + 2)
296+
if (closeBracket !== -1 && text[closeBracket + 1] === '(') {
297+
const closeParen = text.indexOf(')', closeBracket + 2)
298+
if (closeParen !== -1) {
299+
flushPlain()
300+
const alt = text.slice(i + 2, closeBracket)
301+
const src = text.slice(closeBracket + 2, closeParen)
302+
parts.push(
303+
<img
304+
key={key++}
305+
src={src}
306+
alt={alt}
307+
style={{ maxWidth: '100%', borderRadius: radius, display: 'block', margin: '1.5rem 0' }}
308+
/>,
309+
)
310+
i = closeParen + 1
311+
continue
312+
}
313+
}
314+
}
315+
293316
// Link: [text](url)
294317
if (text[i] === '[') {
295318
const closeBracket = text.indexOf(']', i + 1)

0 commit comments

Comments
 (0)