Updated sample-sites.

This commit is contained in:
Kristian Benestad 2026-05-18 14:30:49 +07:00
parent b3c46ec4bc
commit 59efc20dde
214 changed files with 36195 additions and 848 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

12
sample-sites/README.md Normal file
View file

@ -0,0 +1,12 @@
## Add folder info using this document
* The contents of a **Readme.md** will show up embedded on the top of the folder it is in (in the web interface and the mobile apps)
* Formatting is supported with the bar on top (using Markdown)
* It uses Nextcloud Text so you can collaborate on it 🎉
* You can use and remix the templates as you like, they are in the public domain via the [CC0 license](https://creativecommons.org/publicdomain/zero/1.0/)
## Action items
* [ ] Try out the new templates
* [ ] Add your own templates in this folder
* [ ] …

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -0,0 +1,6 @@
# mdcms v0.3 | DO NOT REMOVE THIS COMMENT
sitename: The Kitchen Table
sitedescription: Recipes, techniques, and stories from Amelia Fontaine
navigation: topbar
search: true
footer: "© 2026 Amelia Fontaine · The Kitchen Table"

View file

@ -1,5 +1,6 @@
<!-- Minimum supported version: mdcms v0.3.8 | DO NOT REMOVE THIS COMMENT -->
<!--
MD-CMS v0.2 — Renderer
MD-CMS v0.3.8 — Renderer
Copyright 2026 Kristian Benestad | kbenestad.codeberg.page
@ -31,8 +32,6 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css" id="hljs-light">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css" id="hljs-dark" disabled>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined">
<style>
/* ═══════════════════════════════════════════
@ -116,6 +115,11 @@
--font-body-weight: 400;
--main-width: 80em;
--nav-width: 20em;
--line-height-body: 1.7;
--colour-info: #2563EB;
--colour-warning: #D97706;
--colour-success: #16A34A;
--colour-error: #DC2626;
}
html { font-size: 16px; scroll-behavior: smooth; }
@ -125,7 +129,7 @@ body {
font-weight: var(--font-body-weight);
color: var(--font-colour);
background: var(--bg-main);
line-height: 1.7;
line-height: var(--line-height-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color 0.2s ease, color 0.2s ease;
@ -276,13 +280,15 @@ body {
}
.nav-section-heading.toggleable:hover { color: var(--font-colour); }
.nav-section-heading .toggle-icon {
font-family: var(--font-code);
font-weight: 700;
font-size: 0.9rem;
width: 0.9em;
display: inline-block;
line-height: 1;
display: inline-flex;
align-items: center;
width: 1em;
height: 1em;
flex-shrink: 0;
opacity: 0.6;
}
.mdcms-icon { display: inline-flex; align-items: center; line-height: 1; }
.mdcms-icon svg { width: 1em; height: 1em; fill: currentColor; display: block; }
.nav-item.depth-1 { padding-left: 2.5rem; }
.nav-item.depth-2 { padding-left: 3.5rem; }
.nav-item.depth-3 { padding-left: 4.5rem; }
@ -370,6 +376,33 @@ body {
}
.topbar-nav .nav-item.active { border-left: none; background: var(--nav-active-bg); }
/* ─── Topbar grouped navigation (dropdowns) ─── */
.topbar-nav .nav-group { position: relative; }
.topbar-nav .nav-trigger {
display: flex; align-items: center; gap: 0.25rem;
padding: 0.35rem 0.75rem; border-radius: 5px;
background: none; border: none; cursor: pointer;
color: var(--font-colour); font-size: 0.85rem;
font-family: inherit; white-space: nowrap; text-decoration: none; line-height: inherit;
}
.topbar-nav .nav-trigger:hover,
.topbar-nav .nav-group.open > .nav-trigger { background: var(--nav-hover-bg); }
.topbar-nav .nav-group.has-active > .nav-trigger { background: var(--nav-active-bg); }
.topbar-nav .nav-caret { font-size: 0.6rem; color: var(--font-colour-muted); opacity: 0.55; line-height: 1; }
.topbar-nav .nav-dropdown {
display: none; position: absolute; top: calc(100% + 4px); left: 0;
background: var(--bg-nav); border: 1px solid var(--divider);
border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.1);
min-width: 160px; z-index: 200; padding: 0.25rem 0;
}
.topbar-nav .nav-group.open > .nav-dropdown { display: block; }
.topbar-nav .nav-dropdown .nav-item {
display: block; padding: 0.45rem 1rem;
border-left: none; border-radius: 0; white-space: nowrap;
}
.topbar-nav .nav-dropdown .nav-item:hover { background: var(--nav-hover-bg); }
.topbar-nav .nav-dropdown .nav-item.active { background: var(--nav-active-bg); font-weight: 600; }
.topbar-actions { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.topbar-search { position: relative; }
@ -511,9 +544,9 @@ body {
.category-bar[dir="rtl"] { justify-content: flex-start; }
.category-icon {
font-family: 'Material Symbols Outlined', 'Material Icons', sans-serif;
display: inline-flex;
align-items: center;
font-size: 1.1rem;
line-height: 1;
color: var(--font-colour-muted);
}
@ -731,6 +764,19 @@ body {
border-left: none;
}
.layout-topbar .mobile-nav-panel .nav-item.active { border-left: none; font-weight: 600; }
.layout-topbar .mobile-nav-panel .nav-group-row { display: flex; align-items: stretch; }
.layout-topbar .mobile-nav-panel .nav-group-row .nav-item { flex: 1; }
.layout-topbar .mobile-nav-panel .nav-section-label {
flex: 1; display: flex; align-items: center;
padding: 0.6rem 1.25rem; font-size: 1rem; color: var(--font-colour); font-weight: 500;
}
.layout-topbar .mobile-nav-panel .nav-expand-btn {
background: none; border: none; cursor: pointer;
color: var(--font-colour-muted); padding: 0 1.25rem; font-size: 1.2rem; line-height: 1; flex-shrink: 0;
}
.layout-topbar .mobile-nav-panel .nav-group-children { display: none; }
.layout-topbar .mobile-nav-panel .nav-group-children.open { display: block; }
.layout-topbar .mobile-nav-panel .nav-group-children .nav-item { padding-left: 2.5rem; }
}
@media (max-width: 480px) {
@ -739,6 +785,40 @@ body {
.main-content { padding: 1rem 1rem 3rem; }
}
/* ═══════════════════════════════════════════
TAG SYSTEM: CALLOUTS
═══════════════════════════════════════════ */
.mdcms-callout {
border-left: 4px solid var(--callout-primary, var(--accent));
background: var(--callout-bg, transparent);
border-radius: 0 6px 6px 0;
padding: 0.85rem 1rem 0.85rem 1rem;
margin: 1.25rem 0;
}
.mdcms-callout-title {
display: flex;
align-items: center;
gap: 0.4rem;
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.45rem;
}
.mdcms-callout-title .mdcms-icon { font-size: 1.1em; }
.mdcms-callout-body { font-size: 0.95rem; }
.mdcms-callout-body > *:first-child { margin-top: 0; }
.mdcms-callout-body > *:last-child { margin-bottom: 0; }
/* ═══════════════════════════════════════════
TAG SYSTEM: TABLE OF CONTENTS
═══════════════════════════════════════════ */
.mdcms-toc { margin: 1rem 0; }
.mdcms-toc-section { font-size: 1rem; font-weight: 600; margin: 1.5rem 0 0.4rem; color: var(--font-colour-muted); border-bottom: 1px solid var(--divider); padding-bottom: 0.25rem; }
.mdcms-toc-list { list-style: none; padding: 0; margin: 0 0 0.5rem; }
.mdcms-toc-list li { padding: 0.2rem 0; border-bottom: 1px solid var(--divider); }
.mdcms-toc-list li:last-child { border-bottom: none; }
.mdcms-toc-list a { color: var(--accent); text-decoration: none; font-size: 0.95rem; }
.mdcms-toc-list a:hover { text-decoration: underline; }
/* ═══════════════════════════════════════════
TAG SYSTEM: POST LISTINGS
═══════════════════════════════════════════ */
@ -847,6 +927,7 @@ body {
let searchIndex = [];
let fuseInstance = null;
let currentPage = null;
let themeConfig = {};
// Category state (phase 3)
let categoriesUse = false;
@ -858,13 +939,35 @@ body {
let loadedFonts = new Set(); // track which font files have been loaded
// ─── Icons ────────────────────────────────────────────────
const ICONS = {
sun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>',
moon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>',
search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
menu: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>',
close: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'
};
const STANDARD_ICONS = ['dark_mode','light_mode','menu','search','arrow_right','arrow_drop_down','mobile_arrow_down','language','info','warning','error','success','exclamation','dangerous','report','history','text_compare'];
const iconCache = {};
function normaliseIconName(name) {
return String(name).trim().replace(/\.svg$/i, '').toLowerCase().replace(/[\s-]+/g, '_') + '.svg';
}
async function loadIcon(name) {
const filename = normaliseIconName(name);
if (filename in iconCache) return iconCache[filename];
try {
const resp = await fetch('assets/icons/' + filename);
iconCache[filename] = resp.ok ? await resp.text() : null;
} catch (e) { iconCache[filename] = null; }
return iconCache[filename];
}
function getIcon(name) {
return iconCache[normaliseIconName(name)] || null;
}
function iconEl(name, className) {
const svg = getIcon(name);
const span = document.createElement('span');
span.className = 'mdcms-icon' + (className ? ' ' + className : '');
const filename = normaliseIconName(name);
span.innerHTML = svg || '<img src="assets/icons/' + filename + '" alt="[missing: ' + filename + ']" style="width:1em;height:1em;display:inline-block;">';
return span;
}
// ─── Helpers ──────────────────────────────────────────────
function el(tag, attrs, children) {
@ -1091,8 +1194,9 @@ body {
const btn = document.querySelector('.theme-toggle');
if (btn) {
const isDark = theme === 'dark';
btn.innerHTML = (isDark ? ICONS.sun : ICONS.moon) +
'<span>' + (isDark ? 'Light mode' : 'Dark mode') + '</span>';
btn.innerHTML = '';
btn.appendChild(iconEl(isDark ? 'light_mode' : 'dark_mode'));
btn.appendChild(el('span', { textContent: isDark ? 'Light mode' : 'Dark mode' }));
}
}
@ -1111,6 +1215,53 @@ body {
applyTheme(current === 'dark' ? 'light' : 'dark');
}
function applyThemeYml(tc) {
if (!tc) return;
const root = document.documentElement;
const getOrCreateStyle = id => {
let s = document.getElementById(id);
if (!s) { s = document.createElement('style'); s.id = id; document.head.appendChild(s); }
return s;
};
let modeCss = '';
['light', 'dark'].forEach(mode => {
const m = tc[mode];
if (!m) return;
const vars = [];
if (m.accent) {
const rgb = hexToRgb(m.accent);
vars.push(`--accent: ${m.accent}`);
vars.push(`--accent-rgb: ${rgb}`);
vars.push(`--nav-active-bg: rgba(${rgb}, 0.10)`);
vars.push(`--nav-hover-bg: rgba(${rgb}, 0.05)`);
vars.push(`--table-header-bg: rgba(${rgb}, 0.08)`);
vars.push(`--link-colour: ${m.accent}`);
}
if (m.background) { vars.push(`--bg-main: ${m.background}`); vars.push(`--search-bg: ${m.background}`); }
if (m['nav-background']) vars.push(`--bg-nav: ${m['nav-background']}`);
if (m.text) { vars.push(`--font-colour: ${m.text}`); vars.push(`--code-font: ${m.text}`); }
if (m['text-muted']) vars.push(`--font-colour-muted: ${m['text-muted']}`);
if (vars.length) modeCss += `:root[data-theme="${mode}"] { ${vars.join('; ')}; }\n`;
});
if (modeCss) getOrCreateStyle('theme-overrides').textContent = modeCss;
if (tc['colours-semantic']) {
const sem = tc['colours-semantic'];
const semVars = [];
if (sem.info) semVars.push(`--colour-info: ${sem.info}`);
if (sem.warning) semVars.push(`--colour-warning: ${sem.warning}`);
if (sem.success) semVars.push(`--colour-success: ${sem.success}`);
if (sem.error) semVars.push(`--colour-error: ${sem.error}`);
if (semVars.length) getOrCreateStyle('theme-semantic').textContent = `:root { ${semVars.join('; ')}; }`;
}
if (tc['main-width']) root.style.setProperty('--main-width', tc['main-width']);
if (tc['nav-width']) root.style.setProperty('--nav-width', tc['nav-width']);
if (tc['line-height']) root.style.setProperty('--line-height-body', String(tc['line-height']));
if (tc['font-size']) document.documentElement.style.fontSize = `${tc['font-size'] * 16}px`;
}
function applyConfigTheme() {
const root = document.documentElement;
['light', 'dark'].forEach(mode => {
@ -1170,35 +1321,49 @@ body {
}
// ─── Fonts ────────────────────────────────────────────────
function loadFonts() {
const fonts = {};
['font-title', 'font-body', 'font-code'].forEach(key => {
const val = config[key];
if (!val) return;
const [name, weight] = val.split(':');
fonts[key] = { name: name.trim(), weight: weight ? weight.trim() : '400' };
});
const googleFonts = [];
Object.entries(fonts).forEach(([, { name, weight }]) => {
googleFonts.push(`${name.replace(/ /g, '+')}:wght@${weight}`);
});
function loadFonts(tc) {
if (document.querySelector('link[data-mdcms-fonts]')) return;
function parseFont(spec) {
if (!spec) return null;
const parts = spec.split(':');
if (parts.length >= 3) return { provider: parts[0].trim(), name: parts.slice(1, -1).join(':').trim(), weight: parts[parts.length - 1].trim() };
if (parts.length === 2) return { provider: 'google', name: parts[0].trim(), weight: parts[1].trim() };
return { provider: 'google', name: parts[0].trim(), weight: '400' };
}
const src = tc || {};
const bodyFont = parseFont(src['font-body'] || config['font-body']);
const headingFont = parseFont(src['font-heading'] || src['font-title'] || config['font-title']);
const codeFont = parseFont(src['font-code'] || config['font-code']);
const allFonts = [bodyFont, headingFont, codeFont].filter(Boolean);
const bunnyFonts = allFonts.filter(f => f.provider === 'bunny');
const googleFonts = allFonts.filter(f => f.provider === 'google');
if (bunnyFonts.length) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `https://fonts.bunny.net/css?family=${bunnyFonts.map(f => `${f.name.replace(/ /g, '+')}:${f.weight}`).join('&family=')}`;
document.head.appendChild(link);
}
if (googleFonts.length) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = `https://fonts.googleapis.com/css2?${googleFonts.map(f => `family=${f}`).join('&')}&display=swap`;
link.href = `https://fonts.googleapis.com/css2?${googleFonts.map(f => `family=${f.name.replace(/ /g, '+')}:wght@${f.weight}`).join('&')}&display=swap`;
document.head.appendChild(link);
}
const root = document.documentElement;
if (fonts['font-title']) {
root.style.setProperty('--font-title', `"${fonts['font-title'].name}", system-ui, sans-serif`);
root.style.setProperty('--font-title-weight', fonts['font-title'].weight);
if (headingFont) {
root.style.setProperty('--font-title', `"${headingFont.name}", system-ui, sans-serif`);
root.style.setProperty('--font-title-weight', headingFont.weight);
}
if (fonts['font-body']) {
root.style.setProperty('--font-body', `"${fonts['font-body'].name}", system-ui, sans-serif`);
root.style.setProperty('--font-body-weight', fonts['font-body'].weight);
if (bodyFont) {
root.style.setProperty('--font-body', `"${bodyFont.name}", system-ui, sans-serif`);
root.style.setProperty('--font-body-weight', bodyFont.weight);
}
if (fonts['font-code']) {
root.style.setProperty('--font-code', `"${fonts['font-code'].name}", monospace`);
if (codeFont) {
root.style.setProperty('--font-code', `"${codeFont.name}", monospace`);
}
}
@ -1239,8 +1404,11 @@ body {
} else {
codeText = code; codeLang = lang;
}
if (codeLang === 'mdcms') {
const tag = parseMdcmsTag(codeText);
// Match both ```mdcms (type in content) and ```mdcms callout-info (type in fence)
if (codeLang && (codeLang === 'mdcms' || codeLang.startsWith('mdcms '))) {
const fenceType = codeLang === 'mdcms' ? '' : codeLang.slice('mdcms '.length).trim();
const fullText = fenceType ? (fenceType + '\n' + (codeText || '')) : (codeText || '');
const tag = parseMdcmsTag(fullText);
const encoded = JSON.stringify(tag).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
return '<div class="mdcms-tag" data-config="' + encoded + '"></div>';
}
@ -1343,13 +1511,14 @@ body {
}
function fmtDate(dateStr) {
var parts = dateStr.split('-');
var s = dateStr instanceof Date ? dateStr.toISOString().slice(0, 10) : String(dateStr);
var parts = s.split('-');
var y = parseInt(parts[0], 10), m = parseInt(parts[1], 10), d = parseInt(parts[2], 10);
return applyDatePattern(getDatePattern(), y, m, d);
}
function fmtDatetime(dtStr) {
var sp = dtStr.split(' ');
var s = dtStr instanceof Date ? dtStr.toISOString().slice(0, 16).replace('T', ' ') : String(dtStr);
var sp = s.split(' ');
var datePart = sp[0], timePart = sp[1] || '00:00';
return fmtDate(datePart) + ' at ' + formatTime(timePart);
}
@ -1358,23 +1527,30 @@ body {
var lines = text.trim().split('\n');
var tagName = lines[0].trim();
var options = {};
var bodyStart = lines.length;
for (var i = 1; i < lines.length; i++) {
var m = lines[i].match(/^\s*([a-z\-]+)\s*:\s*(.+)$/i);
if (m) options[m[1].toLowerCase()] = m[2].trim();
var m = lines[i].match(/^\s*([a-z\-]+)\s*:\s*(.*)$/i);
if (m) {
options[m[1].toLowerCase()] = m[2].trim();
} else {
bodyStart = i;
break;
}
return { tagName: tagName, options: options };
}
var body = lines.slice(bodyStart).join('\n').trim();
return { tagName: tagName, options: options, body: body };
}
function parsePostTagName(name) {
var m = name.match(
/^posts-(date|datetime)-(chronological|reversechronological)(?:-(byyear|byyearmonth|lastyear|lastmonth))?$/
/^posts-created-(chronological|reversechronological)(?:-(byyear|byyearmonth|lastyear|lastmonth))?$/
);
if (!m) return null;
return { field: m[1], order: m[2], modifier: m[3] || null };
return { order: m[1], modifier: m[2] || null };
}
function getPostEntries(parsed, options) {
const { field, order, modifier } = parsed;
const { order, modifier } = parsed;
// Start with posts from search index
let posts = (searchIndex || []).filter(function(e) {
@ -1387,11 +1563,7 @@ body {
}
// Field filter
if (field === 'datetime') {
posts = posts.filter(function(e) { return !!e.datetime; });
} else {
posts = posts.filter(function(e) { return !!e.date; });
}
posts = posts.filter(function(e) { return !!e.created; });
// Time-window filter
if (modifier === 'lastyear' || modifier === 'lastmonth') {
@ -1400,15 +1572,13 @@ body {
if (modifier === 'lastyear') cutoff.setDate(cutoff.getDate() - 365);
else cutoff.setDate(cutoff.getDate() - 30);
posts = posts.filter(function(e) {
var raw = field === 'datetime' ? e.datetime.replace(' ', 'T') : e.date;
return new Date(raw) >= cutoff;
return new Date(e.created.replace(' ', 'T')) >= cutoff;
});
}
// Sort
var sortKey = field === 'datetime' ? 'datetime' : 'date';
posts.sort(function(a, b) {
var da = a[sortKey] || '', db = b[sortKey] || '';
var da = a.created || '', db = b.created || '';
return order === 'chronological' ? da.localeCompare(db) : db.localeCompare(da);
});
@ -1521,7 +1691,6 @@ body {
var opts = tag.options;
var posts = getPostEntries(parsed, opts);
var field = parsed.field;
var modifier = parsed.modifier;
var paginate = opts.paginate || 'no';
var limitVal = opts.limit || 'all';
@ -1530,7 +1699,7 @@ body {
// Format each entry
var formatted = posts.map(function(p) {
return {
display: field === 'datetime' ? fmtDatetime(p.datetime) : fmtDate(p.date),
display: fmtDatetime(p.created),
title: p.title,
file: p.file
};
@ -1563,12 +1732,10 @@ body {
// Grouped by year (or year+month)
function getYear(p) {
var d = field === 'datetime' ? p.datetime : p.date;
return d ? d.substring(0, 4) : 'Unknown';
return p.created ? p.created.substring(0, 4) : 'Unknown';
}
function getYearMonth(p) {
var d = field === 'datetime' ? p.datetime : p.date;
return d ? d.substring(0, 7) : 'Unknown';
return p.created ? p.created.substring(0, 7) : 'Unknown';
}
function monthLabel(ym) {
var m = parseInt(ym.substring(5, 7), 10);
@ -1649,11 +1816,155 @@ body {
renderYear();
}
// Callout type defaults (fallback when theme.yml has no callouts block)
const CALLOUT_DEFAULTS = {
info: { icon: 'info', colour: '#2563EB' },
warning: { icon: 'warning', colour: '#D97706' },
success: { icon: 'success', colour: '#16A34A' },
error: { icon: 'error', colour: '#DC2626' },
};
function renderCalloutTag(container, tag) {
var typeMatch = tag.tagName.match(/^callout-(info|warning|success|error)$/);
var calloutType = typeMatch ? typeMatch[1] : 'info';
var opts = tag.options;
var msgKey = opts.message || null;
var title = opts.title || null;
var iconName = opts.icon || null;
var bodyMd = tag.body || '';
// Resolve message: key — config.yml callouts block
if (msgKey) {
var msgDefs = config.callouts || {};
var msgDef = msgDefs[msgKey];
if (msgDef) {
// Override callout type from message definition
if (msgDef.type) calloutType = msgDef.type;
// Language resolution: activeCategory → defaultCategoryCode → first key
var lang = activeCategory || defaultCategoryCode;
var langEntry = (lang && msgDef[lang]) || msgDef[defaultCategoryCode];
if (!langEntry) {
var keys = Object.keys(msgDef).filter(function(k) { return k !== 'type'; });
langEntry = msgDef[keys[0]];
}
if (langEntry) {
title = langEntry.title || null;
bodyMd = langEntry.text || '';
}
if (opts.title || tag.body) {
console.warn('[mdcms] callout: message: key takes precedence; inline title/body ignored.');
}
}
}
// Get callout colours/icon from theme.yml callouts block, then fallback
var themeCallouts = (themeConfig.callouts || {})[calloutType] || {};
var fallback = CALLOUT_DEFAULTS[calloutType] || CALLOUT_DEFAULTS.info;
var primaryColour = themeCallouts['primary-colour'] || fallback.colour;
var bgColour = themeCallouts['background-colour'] || fallback.colour;
if (!iconName) iconName = themeCallouts.icon || fallback.icon;
// Build element
container.className = 'mdcms-callout mdcms-callout-' + calloutType;
container.style.setProperty('--callout-primary', primaryColour);
container.style.setProperty('--callout-bg', hexToRgba(bgColour, 0.08));
if (title) {
var titleRow = document.createElement('div');
titleRow.className = 'mdcms-callout-title';
titleRow.style.color = primaryColour;
titleRow.appendChild(iconEl(iconName));
var titleText = document.createElement('span');
titleText.textContent = title;
titleRow.appendChild(titleText);
container.appendChild(titleRow);
}
if (bodyMd) {
var bodyEl = document.createElement('div');
bodyEl.className = 'mdcms-callout-body';
bodyEl.innerHTML = marked.parse(bodyMd);
container.appendChild(bodyEl);
}
}
function hexToRgba(hex, alpha) {
var h = hex.replace('#', '');
if (h.length === 3) h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2];
var r = parseInt(h.substring(0,2), 16);
var g = parseInt(h.substring(2,4), 16);
var b = parseInt(h.substring(4,6), 16);
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
}
function renderTocTag(container) {
const byCode = {};
navSections.forEach(s => { byCode[s.code] = s; });
const sortedSections = navSections
.filter(s => !isDraftSection(s.code, byCode))
.sort((a, b) => ((a.sort ?? 999) - (b.sort ?? 999)) || (a.code || '').localeCompare(b.code || ''));
const visiblePages = navData.filter(p => {
if (p.file === currentPage) return false;
if (!pageShouldDisplay(p)) return false;
const sid = p['section-id'];
if (sid && isDraftSection(sid, byCode)) return false;
return true;
});
const bySection = {};
const unsectioned = [];
visiblePages.forEach(p => {
const sid = p['section-id'] || null;
if (sid) { (bySection[sid] = bySection[sid] || []).push(p); }
else unsectioned.push(p);
});
function sortPages(pages) {
return [...pages].sort((a, b) =>
((a.sort ?? 999) - (b.sort ?? 999)) || a.file.localeCompare(b.file));
}
function makeList(pages) {
const ul = document.createElement('ul');
ul.className = 'mdcms-toc-list';
pages.forEach(p => {
const a = el('a', { href: '#' + p.file, textContent: pageDisplayTitle(p) });
a.addEventListener('click', e => { e.preventDefault(); navigateTo(p.file); });
ul.appendChild(el('li', {}, a));
});
return ul;
}
const div = el('div', { className: 'mdcms-toc' });
if (unsectioned.length) div.appendChild(makeList(sortPages(unsectioned)));
sortedSections.forEach(section => {
const pages = bySection[section.code];
if (!pages || !pages.length) return;
div.appendChild(el('h3', { className: 'mdcms-toc-section', textContent: sectionDisplayName(section) }));
div.appendChild(makeList(sortPages(pages)));
});
if (!div.children.length) div.textContent = 'No pages found.';
container.replaceWith(div);
}
function hydrateMdcmsTags() {
document.querySelectorAll('.mdcms-tag').forEach(function(tagEl) {
try {
var cfg = JSON.parse(tagEl.getAttribute('data-config'));
if (/^callout-(info|warning|success|error)$/.test(cfg.tagName)) {
renderCalloutTag(tagEl, cfg);
} else if (cfg.tagName === 'toc') {
renderTocTag(tagEl);
} else {
renderPostTag(tagEl, cfg);
}
} catch (e) {
tagEl.textContent = 'Error rendering tag.';
}
@ -1668,7 +1979,8 @@ body {
const mobileHeader = el('div', { className: 'mobile-header' });
mobileHeader.style.display = 'none';
const hamburger = el('button', { className: 'hamburger', 'aria-label': 'Open menu', innerHTML: ICONS.menu });
const hamburger = el('button', { className: 'hamburger', 'aria-label': 'Open menu' });
hamburger.appendChild(iconEl('menu'));
const mobileName = el('span', { className: 'sidebar-sitename', textContent: config.sitename || 'MD-CMS' });
mobileHeader.appendChild(hamburger);
if (config.logo) {
@ -1733,12 +2045,12 @@ body {
hamburger.addEventListener('click', () => {
sidebar.classList.toggle('open');
overlay.classList.toggle('active');
hamburger.innerHTML = sidebar.classList.contains('open') ? ICONS.close : ICONS.menu;
hamburger.innerHTML = ''; hamburger.appendChild(iconEl('menu'));
});
function closeMobileMenu() {
sidebar.classList.remove('open');
overlay.classList.remove('active');
hamburger.innerHTML = ICONS.menu;
hamburger.innerHTML = ''; hamburger.appendChild(iconEl('menu'));
}
window._closeMobileMenu = closeMobileMenu;
}
@ -1759,7 +2071,8 @@ body {
brand.appendChild(el('span', { className: 'topbar-sitename', textContent: config.sitename || 'MD-CMS' }));
topbar.appendChild(brand);
const hamburger = el('button', { className: 'hamburger', 'aria-label': 'Open menu', innerHTML: ICONS.menu });
const hamburger = el('button', { className: 'hamburger', 'aria-label': 'Open menu' });
hamburger.appendChild(iconEl('menu'));
const navLinksEl = el('div', { className: 'topbar-nav', id: 'navLinks' });
topbar.appendChild(navLinksEl);
@ -1800,19 +2113,23 @@ body {
hamburger.addEventListener('click', () => {
const panel = document.getElementById('mobileNavPanel');
panel.classList.toggle('open');
hamburger.innerHTML = panel.classList.contains('open') ? ICONS.close : ICONS.menu;
hamburger.innerHTML = ''; hamburger.appendChild(iconEl('menu'));
});
window._closeMobileMenu = function() {
const panel = document.getElementById('mobileNavPanel');
if (panel) panel.classList.remove('open');
hamburger.innerHTML = ICONS.menu;
hamburger.innerHTML = ''; hamburger.appendChild(iconEl('menu'));
};
document.addEventListener('click', () => {
document.querySelectorAll('.topbar-nav .nav-group.open').forEach(g => g.classList.remove('open'));
});
}
function buildSearchWidget() {
const container = el('div', { className: 'search-container' });
const wrapper = el('div', { className: 'search-wrapper' });
const icon = el('span', { className: 'search-icon', innerHTML: ICONS.search });
const icon = iconEl('search', 'search-icon');
const input = el('input', { className: 'search-box', type: 'text', placeholder: 'Search...' });
const results = el('div', { className: 'search-results' });
wrapper.appendChild(icon);
@ -1843,7 +2160,7 @@ body {
const bar = el('div', { className: 'category-bar' });
if (config['categories-selecticon']) {
bar.appendChild(el('span', { className: 'category-icon material-icons', textContent: config['categories-selecticon'] }));
bar.appendChild(iconEl(config['categories-selecticon'], 'category-icon'));
}
if (config['categories-selecttext']) {
bar.appendChild(el('span', { className: 'category-label', textContent: config['categories-selecttext'] }));
@ -1853,7 +2170,7 @@ body {
const trigger = el('button', { className: 'category-trigger', type: 'button' });
const triggerLabel = el('span', { id: 'categoryTriggerLabel' });
trigger.appendChild(triggerLabel);
trigger.appendChild(el('span', { className: 'caret', textContent: '▾' }));
trigger.appendChild(iconEl('arrow_drop_down', 'caret'));
dropdown.appendChild(trigger);
const panel = el('div', { className: 'category-panel' });
@ -2070,8 +2387,9 @@ body {
if (isHidden) {
const expanded = sectionExpanded(section.code);
heading.innerHTML = `<span class="toggle-icon">${expanded ? '' : '+'}</span><span></span>`;
heading.querySelector('span:last-child').textContent = name;
heading.innerHTML = '';
heading.appendChild(iconEl(expanded ? 'arrow_drop_down' : 'arrow_right', 'toggle-icon'));
heading.appendChild(el('span', { textContent: name }));
heading.addEventListener('click', () => {
toggleSection(section.code);
renderNav();
@ -2107,18 +2425,141 @@ body {
tree.forEach(root => renderTreeSection(container, root, 0, groups));
}
function renderFlat(container) {
// Topbar inline: pages only, sorted by global sort, draft-section pages excluded.
function buildTopbarNavItems() {
const byCode = {};
navSections.forEach(s => { if (s.code) byCode[s.code] = s; });
const visible = navData.filter(p => {
const sid = p['section-id'];
return !sid || !isDraftSection(sid, byCode);
const items = [];
// Sections (non-draft), each becomes a dropdown trigger
navSections.forEach(s => {
if (!s.code || isDraftSection(s.code, byCode)) return;
const pages = navData.filter(p => p['section-id'] === s.code && pageShouldDisplay(p));
pages.sort((a, b) => ((a.sort ?? 999) - (b.sort ?? 999)) || a.file.localeCompare(b.file));
if (!pages.length) return;
items.push({ type: 'section', sort: s.sort ?? 999, section: s, pages });
});
visible.sort((a, b) => ((a.sort ?? 999) - (b.sort ?? 999)) || a.file.localeCompare(b.file));
visible.forEach(p => {
const item = makeNavItem(p, 0);
if (item) container.appendChild(item);
// Unsectioned pages (or pages whose section isn't in nav), grouped by sort century
const unsectioned = navData.filter(p => {
if (!pageShouldDisplay(p)) return false;
const sid = p['section-id'];
return !sid || !byCode[sid];
});
unsectioned.sort((a, b) => ((a.sort ?? 999) - (b.sort ?? 999)) || a.file.localeCompare(b.file));
const centuryMap = new Map();
unsectioned.forEach(p => {
const c = Math.floor((p.sort ?? 999) / 100);
if (!centuryMap.has(c)) centuryMap.set(c, []);
centuryMap.get(c).push(p);
});
for (const [, pgs] of centuryMap) {
items.push({ type: 'group', sort: pgs[0].sort ?? 999, primary: pgs[0], children: pgs.slice(1) });
}
items.sort((a, b) => a.sort - b.sort);
return items;
}
function makeTopbarPageGroup({ primary, children }, isMobile) {
const group = el('div', { className: 'nav-group' });
const hasChildren = children.length > 0;
if (isMobile) {
const row = el('div', { className: 'nav-group-row' });
const link = makeNavItem(primary, 0);
if (link) row.appendChild(link);
if (hasChildren) {
const childrenEl = el('div', { className: 'nav-group-children' });
children.forEach(p => { const it = makeNavItem(p, 1); if (it) childrenEl.appendChild(it); });
const btn = el('button', { className: 'nav-expand-btn', 'aria-label': 'Expand', textContent: '+' });
btn.addEventListener('click', () => {
const open = childrenEl.classList.toggle('open');
btn.textContent = open ? '' : '+';
});
row.appendChild(btn);
group.appendChild(row);
group.appendChild(childrenEl);
} else {
group.appendChild(row);
}
} else {
const title = pageDisplayTitle(primary);
const trigger = el('a', { className: 'nav-trigger', href: '#' + primary.file, 'data-file': primary.file });
trigger.appendChild(el('span', { textContent: title }));
if (hasChildren) trigger.appendChild(iconEl('arrow_drop_down', 'nav-caret'));
group.appendChild(trigger);
if (hasChildren) {
const dropdown = el('div', { className: 'nav-dropdown' });
children.forEach(p => { const it = makeNavItem(p, 0); if (it) dropdown.appendChild(it); });
group.appendChild(dropdown);
group.addEventListener('mouseenter', () => group.classList.add('open'));
group.addEventListener('mouseleave', () => group.classList.remove('open'));
trigger.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
group.classList.toggle('open');
navigateTo(primary.file);
if (window._closeMobileMenu) window._closeMobileMenu();
});
} else {
trigger.addEventListener('click', e => {
e.preventDefault();
navigateTo(primary.file);
if (window._closeMobileMenu) window._closeMobileMenu();
});
}
}
return group;
}
function makeTopbarSection({ section, pages }, isMobile) {
const group = el('div', { className: 'nav-group' });
const isHidden = section.pagesvisibility === 'hidden';
const name = sectionDisplayName(section);
if (isMobile) {
const row = el('div', { className: 'nav-group-row' });
row.appendChild(el('span', { className: 'nav-section-label', textContent: name }));
const childrenEl = el('div', { className: 'nav-group-children' + (isHidden ? '' : ' open') });
pages.forEach(p => { const it = makeNavItem(p, 1); if (it) childrenEl.appendChild(it); });
const btn = el('button', { className: 'nav-expand-btn', 'aria-label': 'Expand', textContent: isHidden ? '+' : '' });
btn.addEventListener('click', () => {
const open = childrenEl.classList.toggle('open');
btn.textContent = open ? '' : '+';
});
row.appendChild(btn);
group.appendChild(row);
group.appendChild(childrenEl);
} else {
const trigger = el('button', { className: 'nav-trigger', type: 'button' });
trigger.appendChild(el('span', { textContent: name }));
trigger.appendChild(iconEl('arrow_drop_down', 'nav-caret'));
trigger.addEventListener('click', e => { e.stopPropagation(); group.classList.toggle('open'); });
group.appendChild(trigger);
const dropdown = el('div', { className: 'nav-dropdown' });
pages.forEach(p => { const it = makeNavItem(p, 0); if (it) dropdown.appendChild(it); });
group.appendChild(dropdown);
if (!isHidden) {
group.addEventListener('mouseenter', () => group.classList.add('open'));
group.addEventListener('mouseleave', () => group.classList.remove('open'));
}
}
return group;
}
function renderTopbarGrouped(container, isMobile) {
const items = buildTopbarNavItems();
items.forEach(item => {
container.appendChild(
item.type === 'group'
? makeTopbarPageGroup(item, isMobile)
: makeTopbarSection(item, isMobile)
);
});
}
@ -2128,19 +2569,23 @@ body {
const mobile = document.getElementById('mobileNavLinks');
if (main) {
main.innerHTML = '';
if (topbar) renderFlat(main);
if (topbar) renderTopbarGrouped(main, false);
else renderTree(main);
}
if (mobile) {
mobile.innerHTML = '';
renderTree(mobile);
if (topbar) renderTopbarGrouped(mobile, true);
else renderTree(mobile);
}
}
function highlightNav(file) {
document.querySelectorAll('.nav-item').forEach(item => {
document.querySelectorAll('.nav-item, .nav-trigger[data-file]').forEach(item => {
item.classList.toggle('active', item.getAttribute('data-file') === file);
});
document.querySelectorAll('.topbar-nav .nav-group').forEach(group => {
group.classList.toggle('has-active', !!group.querySelector('[data-file].active'));
});
}
// ─── Page loading ─────────────────────────────────────────
@ -2187,12 +2632,12 @@ body {
hydrateMdcmsTags();
const firstH = contentEl.querySelector('.md-content h1, .md-content h2');
if (firstH && (meta.author || meta.created || meta.date || meta.datetime)) {
if (firstH && (meta.author || meta.created)) {
const metaEl = document.createElement('div');
metaEl.className = 'page-meta';
let metaText = '';
if (meta.author) metaText += meta.author;
const displayDate = meta.datetime || meta.date || meta.created;
const displayDate = meta.created;
if (displayDate) {
if (metaText) metaText += ' | ';
metaText += 'Published ' + formatDate(displayDate);
@ -2254,14 +2699,26 @@ body {
if (link) link.href = `assets/images/${config.logo}`;
}
loadFonts();
if (config.theme) {
try {
const themeResp = await fetch(config.theme);
if (themeResp.ok) themeConfig = jsyaml.load(await themeResp.text()) || {};
} catch (e) { /* fall back to hardcoded CSS defaults */ }
}
loadFonts(themeConfig);
initCategories();
const iconsToPreload = [...STANDARD_ICONS];
if (config['categories-selecticon']) iconsToPreload.push(config['categories-selecticon']);
await Promise.all(iconsToPreload.map(name => loadIcon(name)));
const navMode = config.navigation || 'sidebar';
if (navMode === 'topbar') buildTopbar();
else buildSidebar();
applyConfigTheme();
if (config.theme) applyThemeYml(themeConfig);
else applyConfigTheme();
applyTheme(getInitialTheme());
// nav.yml — phase 2 expects `sections:` + `pages:` blocks; phase 1 flat

View file

@ -0,0 +1,50 @@
# nav.yml — generated by mdcms.py
sections:
- code: site
defaultname: The Blog
sort: 100
pagesvisibility: visible
pages:
- file: pages/home.md
title: Welcome
section-id: site
sort: 100
variants: [en]
titles:
en: Welcome
- file: pages/about.md
title: About Amelia
section-id: site
sort: 110
variants: [en]
titles:
en: About Amelia
- file: pages/recipe-index.md
title: Recipe Index
section-id: site
sort: 120
variants: [en]
titles:
en: Recipe Index
- file: pages/techniques.md
title: Techniques
section-id: site
sort: 130
variants: [en]
titles:
en: Techniques
- file: pages/pantry.md
title: The Pantry
section-id: site
sort: 140
variants: [en]
titles:
en: The Pantry
- file: pages/kitchen-notes.md
title: Kitchen Notes
section-id: site
sort: 150
variants: [en]
titles:
en: Kitchen Notes

View file

@ -0,0 +1,56 @@
---
title: About Amelia
sort: 110
section-id: site
keywords: Amelia Fontaine, about, Lyon, Turin, cooking, Italian grandmother, French chef
description: Amelia Fontaine's story — growing up between Lyon and Turin, learning to cook from her grandmother and father, and why she started writing about food.
language: en
---
![Amelia's market in Lyon](assets/images/market.jpg)
# About Amelia
I grew up between two kitchens.
My mother's family is Italian, from Turin — the kind of Turin that is proud of its *gianduiotto* and its *bagna càuda* and the way Sunday lunch extends, inevitably, into Sunday afternoon. My grandmother Lucia kept a kitchen that operated more or less continuously: something was always soaking, something was always reducing, something was cooling on a rack. She baked her own bread until she was 82. She made her own pasta until she was 85. I do not know a person who cooked with more authority and less fuss.
My father is French. He trained as a chef in Lyon — the city that produced Paul Bocuse, Fernand Point, and the *mères lyonnaises*, the women who defined what French bourgeois cooking could be. He worked in professional kitchens for twelve years before deciding that he wanted a different life. He left the restaurant world, married my mother, and for as long as I can remember he cooked dinner every night as if he were still making something worth caring about.
Between the two of them, I received an education in food that took me years to understand the value of.
## Growing Up in Two Cuisines
In Italy, we cooked by season and by tradition. Lucia had dishes she made in autumn and dishes she made in spring, and the idea of making a pumpkin gnocchi in June would have struck her as slightly eccentric. Food was not meant to transcend its season; it was meant to celebrate it. Tomatoes in August were a different ingredient from tomatoes in January, and she treated them accordingly.
In France — or at least in my father's kitchen — technique was everything. Not in a cold, academic way; he was a home cook by the time I knew him, and the restaurant rigidity had softened. But there was always a *why*. Why do you sweat the onion before adding the liquid? Why do you deglaze the pan? Why do you not stir the risotto too fast? The questions were as much a part of cooking as the stirring and the chopping.
I spent my teenage summers in Lucia's kitchen and my school years in Lyon. I studied literature at the Université Lumière Lyon 2, which is where I discovered that I was more interested in food than in anything I was actually studying. I would cook for friends, obsessively, and I would stay up too late reading cookbooks and then wake up early to go to the market on the Quai Saint-Antoine.
## Cooking School and After
After my degree, I enrolled in a professional cooking programme in Paris. I did not want to be a chef — I had seen what the restaurant kitchen life took out of my father, and I knew I did not want it. But I wanted to understand technique at a level that home cooking had not given me. The programme was eight months of fundamentals: stocks, sauces, pastry, butchery, the French brigade system. I emerged with knife skills I had not had before, a better understanding of heat control, and a confirmed sense that my real interest was in home cooking rather than restaurant cooking.
After Paris, I spent time in Italy again — first with Lucia, then working in a trattoria in Bologna for a season, and then travelling through the south, which taught me that Italian food is a category containing enormous variety. The food of Puglia is not the food of Piedmont is not the food of Sicily. Each region has its own logic, its own pantry, its own idea of what a meal should be.
## Why I Started Writing
I began writing about food because I kept noticing that the recipes I was reading online were, very often, missing the interesting part. They gave you the ingredients and the steps, and if you were lucky they gave you some headnotes, but they rarely told you *why*. Why this method and not another? What should it look like at this stage? What does it mean when the sauce breaks, and how do you fix it?
I wanted to write the recipes I wished I had been given as a young cook — recipes that explained the reasoning, that described what you were looking for rather than just listing steps, that treated the reader as someone capable of understanding and not just following.
## What I Cook
My food is not particularly exotic. I cook Italian and French food, the food I grew up with, and other cuisines I have learned from books and travel and cooking with friends. I have a strong interest in the food of North Africa and the Levant, which shows up in some of my braised dishes. I bake bread twice a week. I keep a sourdough starter that is older than this blog.
I cook seasonally, not because I am precious about it but because seasonal produce tastes better and costs less. I use whole animals and whole fish when I can. I make stock on Sundays.
## About the Blog
I started writing here in 2024, posting once or twice a week. The posts fall into three rough categories: full recipes with technique explanations, shorter technique-focused pieces, and occasional essays about food and cooking. I test every recipe at least twice before I publish it.
I do not do sponsored content or paid partnerships. If I mention a product or a producer, it is because I use it and think it is worth telling you about.
The name comes from Lucia's kitchen in Turin. She had a kitchen table with a marble top, and everything happened at that table — pasta making, pastry, homework, wine in the evening. It was the centre of the house. I wanted to name the blog after that.
Come cook with me.

View file

@ -0,0 +1,44 @@
---
title: Welcome
sort: 100
section-id: site
keywords: cooking blog, home cooking, recipes, techniques, Amelia Fontaine, kitchen, food
description: The Kitchen Table — a home cooking blog by Amelia Fontaine. Recipes, techniques, and stories from a lifelong cook.
language: en
---
![The Kitchen Table](assets/images/hero.jpg)
# Welcome to The Kitchen Table
Pull up a chair. There is always something on the stove.
This is a blog about cooking — real cooking, in a real kitchen, with the kind of attention and care that makes meals memorable. I am Amelia Fontaine, and I have been cooking since I was tall enough to stand on a step stool beside my grandmother's range in Turin. Everything I know about food comes from people who cooked before me: my grandmother Lucia, who rolled pasta every Sunday morning without looking at a recipe; my father, who trained as a chef in Lyon and taught me that French technique is mostly about paying attention; and the long tradition of cooks who figured out, through taste and repetition and curiosity, how things work.
I started writing this blog because I wanted a place to share what I have learned — not just the recipes, but the reasoning behind them. Understanding *why* you roast bones before making stock, why you rest meat before slicing it, why you add pasta water to the sauce — that understanding changes how you cook. It makes you more confident, more adaptable, and more able to fix things when they go wrong.
## What You Will Find Here
**Recipes** — complete, tested recipes with actual measurements, actual steps, and actual explanations of what to look for at each stage. I do not round up to the nearest "a handful" and I do not omit the part where things can go wrong.
**Technique** — posts focused on a specific technique rather than a specific dish. How to roast, how to braise, how to make stock, how to achieve an emulsion. These are the foundation skills that make every recipe easier.
**Stories** — food without context is just fuel. I write about my grandmother's kitchen, about markets in Lyon in early spring, about the first time I got a hollandaise right. The stories are as much a part of cooking as the recipes.
**Kitchen Science** — I am fascinated by why things work. The Maillard reaction, the role of fat in emulsification, what happens to proteins when you cook them. Where the science helps explain the technique, I include it.
## Recent Posts
```mdcms
posts-datetime-reversechronological
limit: 10
paginate: yes
```
## A Note on Ingredients
I use European-style butter, good olive oil, and fresh ingredients as much as possible. I give metric measurements first with approximate imperial equivalents. Most things in cooking are forgiving; I will tell you when they are not.
Welcome to the table. I hope you find something here that you want to cook tonight.
— Amelia

View file

@ -0,0 +1,83 @@
---
title: Kitchen Notes
sort: 150
section-id: site
keywords: kitchen tips, equipment, seasonal produce, substitutions, cooking notes
description: Tips, equipment recommendations, notes on seasonal produce, and a substitutions guide for The Kitchen Table recipes.
language: en
---
# Kitchen Notes
Accumulated notes on equipment, seasonal produce, and practical matters that come up across the recipes on this blog. Updated regularly.
## Equipment I Actually Use
**Knives:** Three knives cover everything. A 20cm chef's knife is the most important; I use mine for probably 90% of all cutting tasks. A small paring knife (8cm) for fine work and peeling. A serrated bread knife. These three cover everything. I sharpen my chef's knife on a whetstone every two weeks and hone it before every use. See my knife skills post for the full guide.
I prefer German-style knives (heavier, more robust) for most tasks. Japanese knives are sharper and more precise but require more careful maintenance and are not forgiving with harder vegetables.
**Pans:**
- A 28cm stainless steel frying pan: for searing, making omelettes, pan sauces. Do not use non-stick for tasks that require high heat or where fond (browned bits) is needed.
- A 24cm non-stick frying pan: for eggs. That is mostly what a non-stick pan is for.
- A 30cm cast iron frying pan: for searing large pieces of meat, for cooking that goes from stovetop to oven.
- A 5-litre saucepan: for stocks, pasta water, soups.
- A 2-litre saucepan: for sauces.
- A 28cm or 30cm Dutch oven / cocotte: the most useful single piece of equipment in my kitchen. For braises, sourdough bread, soups, anything that goes in the oven.
**Other equipment I reach for constantly:**
- Kitchen scales: weight measurements are more accurate than volume for baking and are how professional recipes are written.
- An instant-read thermometer: for checking the internal temperature of roasts and bread. Removes the guesswork.
- A spider/skimmer: for pulling pasta, blanched vegetables, and fried food from boiling water or oil without draining away everything.
- A bench scraper: for transferring chopped food, handling pastry, and cleaning work surfaces.
- A mortar and pestle: for spices, garlic paste, and pestos. Better than a food processor for small quantities and for maintaining texture.
**What I do not have:** A stand mixer, a food processor, a sous vide machine, a pressure cooker. These are all useful tools; I choose not to have them because I prefer to cook with fewer, simpler tools. The absence of a stand mixer means I knead bread by hand; this takes longer and I find the process satisfying.
## Seasonal Produce Notes (Northern Europe)
The seasons I cook by are for Northern Europe (UK, France, Germany, Benelux). Adjust for your location.
**Spring (March-May):** Asparagus (the main event of spring; eat as much as you can afford for the 6-week season), purple sprouting broccoli, watercress, wild garlic, spring onions, radishes, new season morels. Lamb (the seasonal spring meat).
**Summer (June-August):** Tomatoes (peak in July-August; buy from farms, not supermarkets), courgettes (in abundance — the recipes are for using them before they become marrows), cucumber, broad beans, French beans, sweetcorn, basil. Stone fruits (cherries, peaches, apricots, plums).
**Autumn (September-November):** Squash and pumpkins, mushrooms (wild mushroom season; also when cultivated mushrooms are at their best), apples and pears, quince, root vegetables beginning, walnuts and hazelnuts fresh from the shell, game season begins.
**Winter (December-February):** Root vegetables (parsnips, swede, celeriac, carrots — all improve after a frost), brassicas (cavolo nero, Brussels sprouts, red cabbage), forced chicory, blood oranges from January. Citrus fruit generally.
**Year-round:** Good onions, garlic, potatoes, leeks, spinach (but prefer to use in season), celery.
## Substitutions Guide
A selection of substitutions that work well when you cannot find the original ingredient:
**Guanciale → Pancetta (not streaky bacon):** Guanciale (cured pork cheek) is used in authentic carbonara and amatriciana. Pancetta is an acceptable substitute; it has a similar fat ratio and cures cleanly. Streaky bacon, smoked or unsmoked, is not a good substitute — the smoking and different fat structure produce different results.
**San Marzano tomatoes → Good quality tinned plum tomatoes:** Any tinned plum tomato from a reputable producer will work. Avoid cheap tinned tomatoes in recipes where tomato quality is paramount.
**00 flour → Plain flour for fresh pasta:** In a pinch, plain flour works for fresh pasta. The texture will be less silky but perfectly acceptable. Not recommended for pizza dough, where 00 flour's specific protein content matters more.
**Parmesan → Grana Padano:** Very similar in flavour profile. Grana Padano has slightly less intensity and is generally cheaper. For finishing pasta or using as a condiment, Parmesan; for cooking into sauces where it will melt, Grana Padano works equally well.
**Fresh herbs → Dried (factor):** Not all herbs substitute equally. For robust herbs like oregano, rosemary, and thyme: use one-third the amount of dried compared to fresh. For delicate herbs like basil and parsley: no substitute. Dried basil bears no relation to fresh basil and should not be used in the same way.
**White wine (in cooking) → Dry white vermouth:** Vermouth's higher concentration of flavour compounds means you use slightly less, and it keeps in the cupboard indefinitely. I use it for any recipe that calls for white wine in a sauce.
**Buttermilk → Milk with lemon juice:** Add 1 tablespoon of lemon juice or white wine vinegar to 240ml of regular milk, stir, and let stand 5 minutes. The milk will curdle slightly. This works well for baking recipes.
## Notes on Heat
The most common mistake home cooks make is not getting pans hot enough before adding food. When you add food to an insufficiently hot pan, the food steams in its own moisture rather than searing. Chicken skin does not crisp; meat does not brown; vegetables go soft rather than caramelise.
Test heat with a drop of water: it should dance and evaporate within a second. Or use an infrared thermometer if you have one.
The corollary: do not leave food unattended over high heat. High heat gives excellent results quickly; it also burns food quickly.
## Notes on Salt
Professional kitchens season throughout cooking, not just at the end. Each layer of cooking is an opportunity to build flavour.
I use flaky sea salt (Maldon, or fleur de sel for finishing) and fine sea salt for cooking. I do not use table salt; the iodine imparts an off-flavour.
Salt pasta water generously — it should taste pleasantly salty, not like sea water. This is the only opportunity to season the pasta itself.

View file

@ -0,0 +1,102 @@
---
title: The Pantry
sort: 140
section-id: site
keywords: pantry guide, olive oil, vinegar, tinned fish, pasta, spices, flour, pantry essentials
description: Amelia Fontaine's essential pantry guide — what to keep, why it matters, and how to choose well.
language: en
---
# The Pantry
A well-stocked pantry is the difference between cooking feeling like a chore and cooking feeling like an opportunity. When you open the cupboard and find good olive oil, the right vinegars, and the pasta shapes you want, the question "what's for dinner?" becomes easier to answer well.
This is not a list of everything you could have. It is a list of the things I consider non-negotiable — the things I always have and that I think are worth spending money on when budget allows.
## Olive Oils
I keep two olive oils. One for cooking; one for finishing.
**Cooking olive oil:** A good, mid-range extra virgin olive oil from any reputable source. It will lose its delicate flavour compounds when heated, but it will still taste like olive oil and will not introduce off-flavours. I buy this in 3-litre tins for economy. The Sicilian and Calabrian oils are good value; look for a harvest date on the tin, not just a best-before date, and buy oil pressed within the past 18 months.
**Finishing olive oil:** This is worth spending money on. A single-estate extra virgin from Liguria, Tuscany, or Crete, pungent and fresh, used as a condiment rather than a cooking fat. Drizzled on beans, soup, burrata, grilled fish, roasted vegetables at the moment of serving. You use less of it, so the cost per use is not as high as it appears. Taste before you buy if possible.
**A note on "extra virgin":** Extra virgin means the oil is cold-pressed and has low acidity. It says nothing about flavour quality or freshness. There is a significant industry of inferior oils labelled EVOO; tasting is the only way to tell. Good finishing olive oil should taste grassy, peppery, and fresh; it should catch in your throat slightly. If it tastes flat or rancid, it is old or was never good.
## Vinegars
**Red wine vinegar:** The workhorse. For dressings, deglazing, marinades, quick pickles. I want a proper aged vinegar, not a cheap acidic substitute. The difference is enormous.
**White wine vinegar:** Similar uses to red, but milder and less assertive. Better for delicate dressings and where you do not want red tones.
**Aged balsamic from Modena:** Not the cheap stuff, which is caramel-coloured grape must. Traditional balsamic is aged for a minimum of 12 years, sweet-sour, thick, and extraordinary on strawberries, Parmigiano, or vanilla ice cream. You use it by the drop. A small bottle lasts for years.
**Sherry vinegar:** The most underused vinegar in my opinion. It has a nuttiness and complexity that suits braised dishes, bean soups, and Spanish-influenced food. I use it to finish lentil soup and it transforms the dish.
**Apple cider vinegar:** Good for pickling, dressings, and as an acid balance in certain meat dishes.
## Tinned Fish
My pantry considers tinned fish a staple rather than an emergency protein. Good tinned fish is not a compromise.
**Anchovies in olive oil:** One of the most useful ingredients in the kitchen. They dissolve into almost any dish they are added to, leaving flavour rather than fishiness. I add them to tomato sauces, to braised meat dishes, to dressings. A tin of Ortiz anchovies is worth the premium.
**Tinned sardines:** Portuguese sardines are exceptional — meaty, flavourful, and sustainable. I eat them on toast with good butter and lemon, or in pasta with breadcrumbs and raisins (pasta con le sarde, the Sicilian classic).
**Tinned tuna in olive oil:** Not in water. The oil-packed version has a completely different texture and flavour. Good for tonnato sauce, for pasta, for salads. The Ortiz brand is excellent; Spanish albacore is my standard.
**Tinned clams:** For quick pasta alle vongole when fresh clams are unavailable.
## Dried Pasta
Pasta shapes matter because the sauce adhesion, cooking time, and mouthfeel vary by shape. I keep:
**Rigatoni or penne rigate:** For hearty sauces, baked pasta, and dishes where the sauce needs to go inside as well as outside.
**Spaghetti:** For carbonara, aglio e olio, and the classics.
**Linguine:** Slightly flatter than spaghetti; better with seafood sauces.
**Pappardelle:** Wide, flat; made for mushroom and game ragù.
**Casarecce or trofie:** Short, twisted shapes that hold pesto and chunky sauces.
I buy pasta made with bronze-die extrusion, which gives a rougher texture that holds sauce better. De Cecco and Rummo are widely available and reliable. Setaro is exceptional if you can find it.
## Tinned and Jarred Tomatoes
**Whole San Marzano tomatoes:** For tomato sauces that need to cook down. The San Marzano variety has thick flesh, few seeds, and low acidity. The DOP (Denominazione di Origine Protetta) certification is meaningful here; it designates tomatoes actually grown in the Agro Sarnese-Nocerino area of Campania.
**Passata:** Sieved tomato purée, for quick sauces and soups. I make my own in late summer; otherwise I buy the Mutti brand.
**Tomato paste:** Concentrated tomato flavour, to be used in small quantities as a base layer in ragù, braises, and other long-cooked dishes.
## Spices
I keep fewer spices than most kitchens and replace them more often. Spices go stale. The most important ones to keep fresh:
- Whole black pepper (always grind fresh)
- Whole nutmeg (for béchamel and pasta)
- Cumin seeds (toast and grind as needed)
- Coriander seeds
- Smoked paprika (Spanish pimentón, ideally)
- Dried chilli flakes
- Bay leaves (dried; fresh are better but dried are reliable)
- Cinnamon stick (for braises, not powder)
- Saffron threads
## Flours
**00 flour:** The finely milled, low-gluten flour for fresh pasta and pizza doughs. The protein content and fine milling give the silky, supple dough that pasta requires.
**Plain (all-purpose) flour:** For general baking, thickening sauces, and most everyday uses.
**Strong bread flour:** High-gluten flour for bread. The higher protein content creates the gluten network that gives bread its structure.
**Fine semolina:** For dusting work surfaces when rolling pasta, for certain breads, and as a dusting agent to prevent sticking.
## Pantry Organisation
I keep oils, vinegars, and dried goods in a cool, dark cupboard away from the stove. Heat and light degrade oils and spices quickly. Tinned goods on a dedicated shelf, oldest at the front. Spices in tightly sealed jars, checked annually — if a spice smells of nothing when you open the jar, replace it.
The pantry does not need to be large. It needs to be thoughtful.

View file

@ -0,0 +1,58 @@
---
title: Recipe Index
sort: 120
section-id: site
keywords: recipe index, pasta, risotto, soups, roasts, baking, salads, sauces
description: An organised index of recipes on The Kitchen Table, organised by category with descriptions of each.
language: en
---
# Recipe Index
All recipes published on The Kitchen Table, organised by category. Every recipe has been tested multiple times. Measurements are given in metric first, with approximate imperial equivalents. Difficulty notes are honest.
## Pasta and Risotto
The backbone of my Italian side. I grew up eating pasta several times a week, and I still do. The recipes here range from the very simple (cacio e pepe, which is three ingredients and takes twenty minutes but can go wrong in a dozen ways) to the more involved (fresh pasta in all its shapes). Risotto has its own section because risotto is its own world.
Key recipes: carbonara (the real way, with guanciale and egg yolk emulsion), wild mushroom pappardelle, spring pea and mint risotto, cacio e pepe with proper technique.
## Soups and Stews
Soup is the most forgiving thing in cooking and also the category that rewards the most attention. A good stock makes a great soup. A mediocre stock makes a mediocre soup. These recipes include the stock foundation and build from there. The slow-braised lamb shoulder belongs in this section as much as the stews section — the line between a braise and a stew is the liquid ratio.
Key recipes: ribollita (slow Tuscan bean soup), roasted butternut squash soup with brown butter, French onion soup with proper technique.
## Roasts and Braises
Low-and-slow cooking produces the most deeply flavoured food. These are the recipes I return to when I want to impress without stress — the magic of a proper braise is that it gets better the longer you leave it. Roasting is faster but equally rewarding when done right.
Key recipes: the perfect roast chicken (with dry brining and pan sauce), slow-braised lamb shoulder with preserved lemon and olives, cassoulet (the two-day version, worth every minute).
## Baking
I bake bread twice a week: usually a sourdough loaf and a focaccia. Pastry appears occasionally. The bread recipes here require time and patience but no special equipment beyond a Dutch oven. The focaccia is the most forgiving thing I bake; the sourdough requires the most sustained attention.
Key recipes: sourdough starter from scratch (7-day guide), Ligurian focaccia with rosemary, Grandmother Lucia's Christmas cookies (cuccidati and brutti ma buoni).
## Salads and Vegetables
I eat a lot of vegetables, but I rarely make salads the centrepiece. The recipes in this section are more about preparations that showcase vegetables — roasted, confit, dressed with interesting things — than about assembled salads. The tomato preparations are in this section and are some of the things I am most proud of on this blog.
Key recipes: six preparations for peak-season tomatoes, seasonal eating guide by month.
## Sauces and Condiments
Hollandaise, pan sauces, stocks, and the preserved and fermented things I keep in my fridge. These are the foundations that other recipes build on. The hollandaise post includes a detailed discussion of emulsion science; the stocks post is the one I recommend to new cooks first.
Key recipes: hollandaise (with the science and the fixes), chicken and veal stocks, lacto-fermented kimchi and sauerkraut.
---
## Latest Recipes
```mdcms
posts-datetime-reversechronological
limit: 10
paginate: yes
```

View file

@ -0,0 +1,82 @@
---
title: Techniques
sort: 130
section-id: site
keywords: cooking techniques, mise en place, deglazing, rendering fat, emulsification, caramelisation, blanching
description: A guide to the fundamental cooking techniques referenced throughout The Kitchen Table blog.
language: en
---
# Techniques
This page is a reference for the fundamental techniques that appear repeatedly throughout the blog. Understanding these techniques means you can adapt any recipe rather than just follow it. I will keep adding to this page as new techniques come up in posts.
## Mise en Place
*Everything in its place.*
Mise en place is a French professional kitchen concept that translates simply as preparing everything before you start cooking. Chopped vegetables, measured spices, stocks ready, equipment assembled. Before the pan goes on the heat, everything should be within reach.
Why it matters: Heat does not wait. When a pan is at the right temperature, a sauce is reducing, or an omelette is setting, you cannot stop to find the lid or chop the garlic. The moment you turn away, something burns. Mise en place is the habit that gives you control.
At home, this does not require professional kitchen organisation. It means: read the recipe all the way through before you start. Then prepare everything the recipe will need before you light the first burner. Chop, measure, bring things to room temperature, get your tools out. Then cook.
The five minutes of preparation pays back ten minutes of calm.
## Deglazing
Deglazing is the technique of adding liquid to a hot pan after searing or roasting, then using that liquid to dissolve the caramelised bits (the *fond*) stuck to the bottom.
The fond — the browned proteins and sugars that have cooked onto the pan surface — contains enormous flavour. It is not burnt food to be discarded; it is concentrated, caramelised flavour to be incorporated. Deglazing releases it.
**How to deglaze:**
1. Remove the meat or vegetables from the pan, leaving the fond.
2. If there is excess fat, pour most of it off (leave a tablespoon or so).
3. With the pan still hot, add your deglazing liquid: wine, stock, water, cider, brandy.
4. The liquid will steam violently. This is correct.
5. Scrape the bottom of the pan with a wooden spoon or spatula as the liquid comes up to temperature. The fond will dissolve into the liquid.
6. Reduce the liquid to a sauce consistency, or use it as the base for a longer braise.
Wine (red or white) and stock are the most common deglazing liquids. The choice shapes the flavour of the resulting sauce.
## Rendering Fat
Rendering is the process of melting fat from meat (bacon, pancetta, guanciale, duck) over low heat so that it can be used as a cooking medium. The remaining solids — called lardons, when the meat is pork — become crispy and flavourful.
**Why render rather than add oil:** The rendered fat carries the flavour of the meat and will flavour everything cooked in it. Pancetta-rendered fat is the starting point for many Italian dishes. Duck fat is the basis for confit. The flavour integration is a feature, not a byproduct.
**How to render:** Start in a cold pan. Cut the fat into small pieces and put them in a cold, dry pan over low-medium heat. Resist the urge to turn up the heat. Low heat melts the fat without burning the surrounding meat. As the fat melts out, the temperature of the pan stabilises. Stir occasionally. After 8-15 minutes (depending on the fat), the pieces will be golden and crispy. Remove them with a slotted spoon and proceed with the recipe using the fat in the pan.
## Emulsification
An emulsion is a stable mixture of two liquids that would not naturally mix — most often, fat and water. Vinaigrette, hollandaise, mayonnaise, and the sauce on a properly-finished pasta are all emulsions.
Emulsions are stabilised by emulsifiers — molecules that have both fat-soluble and water-soluble ends, allowing them to bind to both phases simultaneously. Egg yolk lecithin is the most common culinary emulsifier; it is why hollandaise, mayonnaise, and carbonara work. Mustard contains emulsifying compounds, which is why a vinaigrette made with mustard stays together longer than one without.
**Temporary emulsions** (like vinaigrette whisked quickly) separate when left to stand — the fat globules coalesce and the water phase settles out. **Permanent emulsions** (like mayonnaise) remain stable because the egg lecithin has formed a physical barrier around each fat droplet, preventing them from merging.
**When emulsions break:** A hollandaise that breaks — where the sauce separates into greasy pools and watery liquid — has lost its emulsification. The fat and water phases have separated. The causes: too much heat, too much fat added too quickly, or not enough lecithin to stabilise the amount of fat. The fix is in my hollandaise post.
## Caramelisation and the Maillard Reaction
These are two distinct chemical reactions that both produce browning and flavour, and they are frequently confused.
**Caramelisation** is what happens when sugar is heated: it breaks down into hundreds of flavour compounds, producing the characteristic nutty, complex sweetness of caramel. Caramelisation requires temperatures above 160°C/320°F. It is what happens when you make caramel sauce, when you caramelise onions over low-medium heat for 45 minutes until they are sweet and deeply brown, or when the sugars in a crème brûlée crust.
**The Maillard reaction** is a chemical reaction between amino acids and reducing sugars that produces browning, complex flavour, and hundreds of flavour compounds. It requires temperatures above approximately 140°C/285°F. It is what produces the crust on bread, the sear on a steak, the colour on roasted vegetables, the golden skin of a roast chicken. It is not caramelisation — it involves proteins, not just sugars — and the flavour compounds it produces are different and more complex.
For practical cooking: both reactions require high heat and low moisture. Wet surfaces steam rather than brown. This is why you pat meat dry before searing, why you roast vegetables at high heat with space between them, and why bread crust forms in the dry heat of the oven rather than the moist heat of a steamer.
## Blanching
Blanching is the technique of briefly cooking a vegetable in vigorously boiling, generously salted water, then immediately transferring it to ice water to stop the cooking.
**Why blanch:** The brief cooking sets colour (the vibrant green of blanched green beans comes from heat driving air out of the cells and stabilising the chlorophyll). It also softens vegetables enough to make them pleasant to eat while maintaining their texture. The ice bath stops the cooking instantly at exactly the moment you choose.
Blanching is the technique behind *mise en place* vegetable prep in professional kitchens: blanch the vegetables in advance, ice-bath them, then finish them in butter or olive oil at service. The hard work is done; the final cooking takes two minutes.
**Ratios and timing:** Use a large pot of water — the more water, the faster it returns to the boil after you add the vegetables. Salt it generously (the water should taste like pleasant seawater). Timing varies by vegetable: green beans 2-3 minutes, asparagus 1-2 minutes, broccoli 2 minutes, potatoes longer. The goal is "just cooked but still with texture."
---
*More techniques are added regularly as they come up in posts. Check the blog or use the search function to find technique discussions in specific recipe posts.*

View file

@ -0,0 +1,68 @@
---
title: "The Only Carbonara Recipe You Need (And Why Most Are Wrong)"
created: 2024-02-14 10:00
author: Amelia Fontaine
keywords: carbonara, pasta, eggs, guanciale, Italian, technique
description: Authentic spaghetti alla carbonara — no cream, no shortcuts — with a deep dive into why the technique matters and how to nail the emulsification every time.
---
![Pasta carbonara](assets/images/pasta.jpg)
# The Only Carbonara Recipe You Need (And Why Most Are Wrong)
I learned to make carbonara from a Roman butcher named Giorgio who sold guanciale out of a refrigerated cabinet the size of a wardrobe. It was 2011. I was twenty-three, living in Trastevere for the summer on a fellowship that paid almost nothing, and I ate pasta four nights a week because it was what I could afford. Giorgio noticed I kept buying pancetta instead of guanciale and, with the patience of a man who had seen tourists make terrible decisions for thirty years, spent fifteen minutes explaining why this was wrong.
That conversation changed how I cook.
## Why Cream is Not Just "a Variation"
Let me be clear before we begin: carbonara does not contain cream. This is not culinary snobbery or Italian chauvinism. It is a matter of understanding what the dish is. Carbonara is a demonstration of emulsification — the technique by which fat, egg proteins, and starchy pasta water combine into a glossy, clingy sauce. Cream short-circuits this process. It works, yes. You get something vaguely carbonara-like, pale and rich. But you have bypassed the thing the dish is teaching you, which is how to make a sauce from almost nothing using heat and motion.
Learning carbonara without cream is like learning to drive on an automatic: functional, but you miss something important about how the machine works.
## The Ingredients
For two people:
- **200g spaghetti** (or rigatoni, if you prefer something to grip the sauce)
- **150g guanciale**, cut into lardons roughly 1cm × 0.5cm
- **3 egg yolks** plus 1 whole egg
- **60g Pecorino Romano**, finely grated (or a 50/50 blend with Parmigiano)
- **Freshly ground black pepper** — and lots of it
- **Salt** for the pasta water only
Guanciale is cured pig cheek. It is fattier and more flavourful than pancetta, with a particular sweetness that pancetta lacks. In Rome, there is no substitute. In the UK or US, good-quality pancetta works as a reasonable second. Bacon does not work — the smoke flavour fights the egg.
The pepper is not optional. "Carbonara" takes its name from *carbone* (charcoal). The dish was allegedly made by charcoal workers, and the pepper represents the charcoal dust. Use it generously.
## The Method
**1. Get the pasta water boiling.** Heavily salted — it should taste like mild seawater. This starch-rich water is your sauce's best friend.
**2. Render the guanciale slowly.** In a large pan (you will need the surface area later), cook the guanciale over medium-low heat until the fat is mostly rendered and the edges are crispy but the interior is still yielding, about 810 minutes. Do not go too high — you want rendered fat, not burnt crisps. Turn off the heat.
**3. Make the egg mixture.** In a bowl, whisk together the yolks, whole egg, Pecorino, and a very generous amount of pepper. The mixture should be thick and pale yellow. Set aside.
**4. Cook the pasta until 90% done.** It will finish cooking in the pan, so pull it out a minute before al dente. Reserve at least 200ml of pasta water before draining.
**5. The critical moment.** Transfer the pasta directly into the guanciale pan (heat off). Add 34 tablespoons of pasta water and toss vigorously for 30 seconds until the pasta is well-coated and the temperature has dropped slightly — you want it hot but not searing.
**6. Add the egg mixture off the heat.** Pour the egg and cheese mixture over the pasta and toss constantly and rapidly. The residual heat from the pasta and the pan cooks the eggs gently. Add pasta water a tablespoon at a time to adjust consistency — you want the sauce creamy and flowing, not dry and clumped. The whole process takes about 6090 seconds.
**7. Serve immediately.** Carbonara waits for no one. The sauce continues to thicken as it cools. Finish with more Pecorino and more pepper at the table.
## Why It Scrambles (And How to Stop It)
Egg proteins begin to set at around 63°C and are fully cooked at 73°C. The goal is to stay below 73°C while getting the proteins warm enough to thicken the sauce — you want the texture of custard, not scrambled eggs.
The safeguards:
- **Turn the heat off** before adding the egg mixture. Always.
- **The pasta water** lowers the temperature of the pan and adds starch, which buffers the egg proteins and prevents rapid coagulation.
- **Constant motion** distributes heat evenly and coats every strand.
- **Working quickly** matters more than anything. Have everything ready before you cook the pasta.
If it scrambles anyway: the pan was too hot or the water was too starchy. Cool the pan in cold water for 10 seconds before adding the egg. Add more pasta water. Breathe.
## The Version Giorgio Made
Giorgio's carbonara was almost indistinguishable from mine except for two things. He used only Pecorino, never Parmigiano. And he always added one extra yolk "per il colore" (for the colour) — which turned the sauce a deeper, more vivid gold. I now do the same. It makes no rational sense that I have never been able to verify, but the carbonara tastes better for it, and that is probably all the reason I need.

View file

@ -0,0 +1,89 @@
---
title: Starting a Sourdough Starter from Scratch
created: 2024-03-22 09:00
author: Amelia Fontaine
keywords: sourdough, starter, fermentation, bread, wild yeast
description: A complete seven-day guide to creating a sourdough starter from nothing but flour, water, and patience — with troubleshooting and the science of wild fermentation.
---
![Freshly baked bread](assets/images/bread.jpg)
# Starting a Sourdough Starter from Scratch
A sourdough starter is, at its most fundamental, a controlled environment for wild yeast and lactic acid bacteria. You are not adding anything to the flour and water except conditions — warmth, time, regular feeding. The microorganisms are already present on the grain, in the air, on your hands. Your job is to select for the ones you want.
This sounds mystical. It is actually chemistry. Lactic acid bacteria produce acids that lower the pH of the mixture, creating conditions that favour *Saccharomyces cerevisiae* (the yeast responsible for rise) and *Lactobacillus* species (responsible for flavour). By day seven, if conditions are right, you will have a stable, predictable culture you can use for the rest of your life.
## What You Need
- **Flour**: Wholegrain rye or wholemeal wheat is ideal for starting — higher in wild yeast and nutrients than white flour. Once established, you can switch to white.
- **Water**: Filtered or left to stand overnight if chlorinated. Chlorine inhibits fermentation.
- **A jar**: At least 500ml capacity. Glass is ideal so you can observe activity.
- **A scale**: Precision matters here. Volume measurements are unreliable.
- **Temperature**: 2426°C is ideal. A kitchen counter in summer works. In winter, try near (not on) a warm appliance, or inside the oven with just the light on.
## The Seven-Day Guide
### Day 1 — Creating the Base
Combine 50g wholegrain rye flour with 50g room-temperature water in your jar. Mix thoroughly until no dry flour remains. Scrape down the sides, cover loosely (a cloth held with a rubber band, or a jar lid placed on top without sealing), and leave at room temperature.
Do nothing else today.
### Day 2 — First Signs
You may see small bubbles. You may see nothing. Both are normal. The mixture might smell slightly unpleasant — musty or even nail-polish-like. This is also normal; undesirable bacteria are colonising first before the yeast creates conditions that suppress them.
Discard all but 50g of the mixture. Add 50g rye flour and 50g water. Mix, cover, leave.
### Day 3 — Activity Increases
By now you should see more consistent bubbling. The smell may be getting more sour and less unpleasant. This is the lactic acid bacteria beginning to dominate.
Repeat the discard and feed: keep 50g, add 50g flour, 50g water.
### Day 4 — The Starter Wakes Up
You should now be seeing a predictable rise — the mixture expanding within a few hours of feeding before dropping back. Mark the level on the jar with a rubber band after feeding to track the rise. Aim for 50100% increase at peak.
Switch to twice-daily feeding if your kitchen is above 24°C. Once daily is fine below that. Continue: 50g starter, 50g flour, 50g water.
### Day 5 — Transition to White Flour
If using rye to establish the culture, you can now transition: use 25g rye and 25g white bread flour for a couple of days, then move to all white bread flour if you prefer a milder flavour and lighter bread.
The starter should now smell pleasantly sour and yeasty, like a good craft beer. If it smells of cheese or acetone at this stage, it's too warm or not being fed frequently enough.
### Day 6 — The Float Test
A healthy, active starter will float when a small amount is dropped into a glass of water. Try this 46 hours after feeding, when the starter is at or near peak activity. If it floats, you're ready to bake.
If it sinks, continue feeding twice daily for another day or two. Patience.
### Day 7 — Ready
A starter that passes the float test and reliably doubles within 46 hours of feeding is ready to use. You have created a living culture that, with basic maintenance, can last indefinitely. Some bakeries use starters that are decades old.
## Ongoing Maintenance
**If baking daily**: Keep at room temperature, feed once or twice a day (same discard-and-feed routine).
**If baking occasionally**: Store in the refrigerator. Feed once a week. Remove from the fridge 1224 hours before baking to bring it to room temperature and let it peak.
**The discard**: You discard starter each time you feed to prevent the jar from overflowing and to maintain a consistent ratio of culture to fresh flour. The discarded starter is excellent in pancakes, flatbreads, crackers, and waffles.
## Troubleshooting
**No activity after four days**: Make sure the water isn't chlorinated. Try a slightly warmer location. Use wholegrain flour.
**Pink or orange streaks**: These indicate contamination. Discard everything, sterilise the jar, start again.
**Liquid layer on top ("hooch")**: Grey-black liquid means the starter is hungry and hungry for longer than it should be. Pour it off and feed immediately; consider switching to twice-daily feeding.
**The smell**: Acetone/nail polish = too warm or too acidic; cheese = too cold; pleasantly sour and yeasty = correct.
## Why This Works
Wild yeast produces carbon dioxide (creating rise) and ethanol (which evaporates in baking). Lactic acid bacteria produce lactic and acetic acids, which contribute flavour and preservation. The ratio of these two acids depends on temperature and hydration: warmer and wetter conditions favour lactic acid (milder, yoghurt-like); cooler and stiffer conditions favour acetic acid (sharper, vinegar-like). This is why bakeries in different climates produce sourdough with distinct flavour profiles even from similar flour.
Your starter is genuinely local. The wild yeasts on your flour, in your kitchen air, on your hands — they are specific to your environment. A starter begun in Manchester will differ from one begun in Marseille. This is one of the things I find quietly extraordinary about bread.

View file

@ -0,0 +1,66 @@
---
title: Spring Pea and Mint Risotto
created: 2024-04-10 11:00
author: Amelia Fontaine
keywords: risotto, peas, mint, spring, Italian, technique
description: The first spring peas at the market, a technique deep-dive on why risotto works, and a recipe using pea purée and crispy prosciutto.
---
# Spring Pea and Mint Risotto
Every year the first fresh peas at the market feel like a small event. They arrive sometime in April, in pods that are bright and firm and squeak when you press them. The ratio of pod to pea is almost never in your favour — you need a lot — but the flavour of a just-shelled pea is one of those things that makes you understand why people have grown food for ten thousand years.
This risotto uses peas two ways: most of them puréed into a deeply green, sweet sauce that coats the rice, and a handful kept whole for texture. Crispy prosciutto on top because salt and fat and crunch are exactly what the sweetness needs.
## On Risotto Technique
There is a persistent myth that risotto requires constant stirring. It does not. What it requires is *frequent* stirring and attention — you should not walk away — but continuous stirring actually over-develops the starch and produces gluey results. Every two minutes or so works well.
The mechanism: Arborio (or Carnaroli, which I prefer) rice contains a starchy exterior that dissolves into the cooking liquid, producing the creaminess. The interior of the grain stays somewhat firm. Stirring mechanically releases this surface starch; too much stirring releases all of it at once and produces paste.
The wine is not optional. Its acidity balances the sweetness of the rice and the richness of the stock. White wine that you wouldn't drink is fine here — but not cooking wine, which is salted.
## Ingredients (serves 4)
- 350g Carnaroli or Arborio rice
- 1.5 litres hot vegetable or light chicken stock, kept warm
- 600g fresh peas in pods, shelled (about 200g shelled weight)
- 1 medium white onion, finely diced
- 2 cloves garlic, minced
- 120ml dry white wine
- 60g unsalted butter, cold and diced
- 50g Parmigiano Reggiano, finely grated
- A handful of fresh mint leaves, roughly torn
- 4 slices prosciutto crudo
- 3 tbsp olive oil
- Salt and white pepper
## Method
### The Pea Purée
Blanch two-thirds of the peas in boiling salted water for 90 seconds, then plunge immediately into ice water. Drain and blend with 45 tbsp of warm stock, a pinch of salt, and half the mint until very smooth. Pass through a sieve for a silkier result, or leave it slightly textured — your preference. Set aside. Keep the remaining peas raw.
### The Crispy Prosciutto
Lay the prosciutto slices flat in a dry frying pan over medium-high heat. Press down with a spatula. They will take 6090 seconds per side to become dark, crisp, and fragrant. Remove onto kitchen paper. They will crisp further as they cool.
### The Risotto
Heat the olive oil with half the butter in a wide, heavy-bottomed pan over medium heat. Soften the onion with a pinch of salt for 8 minutes until translucent and very soft — do not let it colour. Add the garlic, cook for 1 minute more.
Add the rice and toast, stirring, for 2 minutes until the grains are slightly translucent at the edges. Add the wine; it will steam dramatically. Stir until completely absorbed.
Now begin adding the stock: one ladleful at a time, stirring every couple of minutes and adding the next ladleful only once the previous one is absorbed. The heat should be brisk but not violent — you want a gentle, active simmer. This process takes about 18 minutes total. The rice is done when it is tender with a slight bite at the very centre.
### Bringing It Together
Remove from the heat. Add the pea purée and stir to combine — the mixture will turn a striking green. Add the cold butter and Parmigiano. Beat vigorously with a wooden spoon for 60 seconds (this is the *mantecatura* — the final emulsification of fat into the rice). The risotto should flow slowly when you shake the pan: the Italians call this *all'onda*, wave-like.
Fold in the raw peas and the remaining mint. Taste and adjust salt and white pepper.
Spoon into warm, wide bowls. Top with the shards of crispy prosciutto, a drizzle of good olive oil, and a few more mint leaves. Serve immediately.
## Notes
**Make it vegetarian**: Omit the prosciutto. The dish is more than sufficient without it. A handful of toasted hazelnuts adds the needed crunch and a pleasant nutty contrast.
**On the stock**: Good risotto requires good stock. Cube stock will produce cube-flavoured risotto. A simple vegetable stock takes 30 minutes and costs almost nothing.
**Leftovers**: Risotto does not reheat well (it sets solid as it cools). The traditional solution is *risotto al salto* — fry cold risotto patties in butter until crispy on both sides. Excellent for lunch the next day.

View file

@ -0,0 +1,61 @@
---
title: "The Perfect Roast Chicken: Everything I Know"
created: 2024-05-05 10:00
author: Amelia Fontaine
keywords: roast chicken, dry brine, pan sauce, technique, Sunday roast
description: Dry brining, the trussing debate, temperature science, resting, and a glossy pan sauce — everything you need to roast the best chicken you've ever made.
---
# The Perfect Roast Chicken: Everything I Know
Roast chicken is the thing I cook most often when I want to impress without appearing to try. It arrives at the table deeply bronzed and crackling, the kitchen filled with the smell of caramelised skin and rendered fat, and there is very little effort involved once you understand a few things about what is actually happening in the oven.
The technique I use now is the result of about eight years of incremental adjustment. I will share everything.
## The Single Most Important Step: Dry Brining
Twenty-four to forty-eight hours before roasting, season the chicken generously all over — including inside the cavity — with fine sea salt. Use about 1 teaspoon per kilogram of bird. Pat dry with paper towel, then refrigerate uncovered.
What happens: the salt draws moisture to the surface through osmosis. The moisture then dissolves the salt, creating a concentrated brine. This brine is then reabsorbed into the meat via osmosis. Simultaneously, the exposed skin dries out in the fridge, which is exactly what you want.
The result is twofold: the meat is seasoned all the way through (not just on the surface), and the skin becomes dry enough to crisp dramatically in the oven.
Do not skip this step. More than any other single technique, this is what separates a good roast chicken from a great one.
## The Trussing Debate
Trussing — tying the legs together and tucking the wings — is traditional but, I now believe, counterproductive for even cooking. The legs take longer to cook than the breast. If you truss the bird tightly, you bring all parts into proximity and the breast overcooks while waiting for the dark meat to finish.
I leave the bird untrussed, legs loosely apart. The breast still finishes first, but I compensate by starting the chicken breast-side down for the first third of the cooking time, then flipping to finish breast-side up. The direct pan heat starts rendering the back fat; the eventual breast-up position crisps the skin.
## Temperature and Timing
I roast chickens at a single consistent temperature: 220°C (fan)/240°C (conventional), no lower. High heat renders fat quickly and drives the Maillard reaction on the skin. A lower temperature produces pale, soft skin even if the interior is correctly cooked.
**Timing**: approximately 20 minutes per 500g, plus 20 minutes resting. A 1.5kg bird takes about 80 minutes in the oven. But timing is a guide, not a rule.
**The test that matters**: an instant-read thermometer in the thickest part of the thigh (not touching bone) should read 74°C. The juices, when the thigh is pierced, should run clear. If you see any pink, return it to the oven for 10 more minutes.
## Resting
Resting is not optional. After the chicken comes out of the oven, rest it uncovered (not tented with foil, which creates steam and softens the skin) for at least 20 minutes. During this time the internal temperature continues to rise slightly and the muscle fibres, contracted from heat, relax and allow the juices to redistribute. A chicken carved immediately after roasting loses significantly more juice than one that has rested.
While the chicken rests, make the pan sauce.
## Pan Sauce
Pour off most of the fat from the roasting tin, leaving the browned fond and about 2 tablespoons of fat. Place the tin directly over medium heat. Add a glass of white wine or vermouth and scrape vigorously — every browned bit is flavour. Add 200ml chicken stock. Reduce by half, stirring occasionally. Taste: it should be intensely savoury and slightly glossy. Swirl in a small knob of cold butter off the heat. Strain through a fine sieve, pushing gently on any solids.
This sauce takes 8 minutes and rewards the roast enormously.
## The Aromatics
Inside the cavity: half a lemon, a few garlic cloves (unpeeled, slightly crushed), some thyme. These aromatics perfume the inside of the bird gently as it cooks. They are not a recipe element — they are flavour infrastructure. On the roasting tin: roughly chopped onion, carrot, celery, which will contribute to the pan sauce and the flavour of the drippings.
## The Variations
**With tarragon butter**: Mix 80g softened butter with 2 tbsp tarragon, a clove of garlic, lemon zest, salt. Carefully loosen the breast skin with your fingers and push the butter underneath. The butter bastes the breast from the inside as it melts.
**Spatchcocked**: Remove the backbone with kitchen shears and flatten the bird. It cooks in about 45 minutes and the skin coverage is even and extraordinary. Excellent for weeknights.
**The next day**: Strip the carcass. Make stock. Use the stock to make the risotto from last month's post. This is not a meal plan — this is a system.

View file

@ -0,0 +1,57 @@
---
title: "What to Do with Peak-Season Tomatoes (Besides Salad)"
created: 2024-06-18 09:30
author: Amelia Fontaine
keywords: tomatoes, summer, slow roast, confit, gazpacho, umami
description: Six preparations for peak summer tomatoes — from slow roasting to a raw sauce and tomato jam — plus the science of why cooked tomatoes taste different.
---
# What to Do with Peak-Season Tomatoes (Besides Salad)
There is a window each summer, roughly six to eight weeks, when tomatoes are worth cooking with. Outside this window — and for most of the year in Northern Europe this is outside this window — tomatoes are a disappointment, flavourless and watery, and you are better off using good tinned San Marzano. But in July and August, when the tomatoes at the market have been grown outside in actual sunshine and handled briefly before they reach you, they are extraordinary.
The problem is that most people only know how to do one thing with a great tomato: put it in a salad. Here are six more.
## Why Cooked Tomatoes Taste Different
Tomatoes contain glutamates — the amino acids responsible for umami — as well as volatile aromatic compounds. When raw, the aromatics are what you notice: fresh, bright, slightly acidic. When cooked, two things happen. First, heat drives off the volatile compounds, reducing the fresh brightness. Second, the glutamates become more concentrated as water evaporates, intensifying the savoury quality. This is why tomato paste has much more umami impact than raw tomato, and why slow-cooked tomato sauces taste fundamentally different from quick ones.
Different preparations exploit these properties in different ways.
## 1. Slow Roasted
Cut tomatoes in half, place cut-side up in a single layer on a baking sheet. Season with salt, pepper, sugar (a pinch), olive oil. Roast at 150°C for 2.53 hours until collapsed, concentrated, and slightly caramelised.
These are transformative. The flavour intensity is extraordinary — ten times the tomato per square centimetre of a raw slice. Use them on bruschetta, in pasta (they are a sauce already), on pizza, alongside burrata, in sandwiches. They keep refrigerated in olive oil for a week.
## 2. Confit
Confit is slower and lower than slow roasting: 120°C for 34 hours, generously covered with olive oil, with garlic cloves and fresh thyme in the pan. The tomatoes do not colour; they soften into silky, oil-poached jewels. The oil they are cooked in becomes extraordinary — use it on everything.
## 3. Raw Sauce (Pasta al Pomodoro Crudo)
No cooking required. Dice a pound of ripe tomatoes roughly. Add a clove of crushed garlic, a lot of torn basil, 4 tablespoons of your best olive oil, salt, a pinch of sugar if needed. Let sit at room temperature for 30 minutes — the salt draws juice from the tomatoes, which mixes with the oil and basil into a sauce.
Cook pasta until slightly undercooked, drain with a little water still clinging to it, and toss with the raw sauce. The heat of the pasta gently warms the tomatoes without cooking them. Serve in warm bowls. This is the best pasta of the entire year when the tomatoes are right.
## 4. Gazpacho
Blend 1kg roughly chopped tomatoes, half a cucumber, half a red pepper, 2 garlic cloves, a thick slice of stale white bread (soaked in water, squeezed dry), 100ml olive oil, 3 tbsp sherry vinegar, salt. Blend until completely smooth. Season. Refrigerate at least 2 hours, ideally overnight.
Serve in cold glasses with a drizzle of olive oil and very finely diced cucumber, pepper, and tomato on top. It should be thick but pourable. The bread emulsifies the olive oil; without it the gazpacho separates.
## 5. Tomato Tart
Make a rough puff pastry or buy good butter puff. Spread with a thin layer of Dijon mustard. Scatter over Gruyère or Comté. Arrange sliced tomatoes in slightly overlapping rows. Season with salt, pepper, thyme. Bake at 200°C for 2530 minutes until the pastry is deeply golden and the tomatoes have collapsed slightly.
This is a Provençal dish by temperament — it requires very good tomatoes, very good cheese, and the confidence to do almost nothing to them.
## 6. Tomato Jam
This is the unexpected one, and people are always suspicious until they eat it. Combine 1kg chopped tomatoes with 200g sugar, a tablespoon of lemon juice, a teaspoon of grated fresh ginger, half a teaspoon of cumin, a pinch of chilli. Cook over medium heat, stirring frequently, for 4560 minutes until thick and jammy.
It is extraordinary with aged cheese, cold cuts, and pork. It will keep refrigerated for a month. The sweetness and acidity of the jam makes sense once you taste it — it is actually just another way of intensifying the tomato.
## The One Rule
Whatever you make, use the best tomatoes you can find and trust them. Good tomatoes need very little help. Poor tomatoes cannot be rescued.

View file

@ -0,0 +1,53 @@
---
title: "The French Omelette: A Meditation on Technique"
created: 2024-07-30 10:00
author: Amelia Fontaine
keywords: omelette, French, technique, eggs, Jacques Pépin
description: Twenty failures, Escoffier's technique, pan choice, heat control, and the roll vs. fold debate — a complete guide to the most technically demanding dish in basic cooking.
---
# The French Omelette: A Meditation on Technique
I failed at the French omelette approximately twenty times before I got it right. I say approximately because I stopped counting around failure fourteen. It is, I am convinced, the most technically demanding simple dish in cooking — more difficult than hollandaise, which at least gives you warning signs, more difficult than soufflé, which has more margin than its reputation suggests. The French omelette happens in ninety seconds and forgives nothing.
## What Makes It French
The French omelette (omelette française) is pale, barely coloured, folded into a torpedo shape, with a creamy, slightly underdone interior. It is not the British omelette (folded in half, lightly browned). It is not the American diner omelette (rolled with filling, browned all over). The French method is distinguished by high heat, constant motion, and a very short cooking time that leaves the interior *baveux* — a word French cooks use to mean just barely set, almost wet, definitely still trembling when the plate arrives.
This is the egg at its most civilised. The flavour is pure and clean, just egg and butter. It is not the thing you make when eggs are a vehicle for other things. It is the thing you make when the egg is the point.
## What Escoffier Said
Auguste Escoffier, the architect of classical French cuisine, said that the omelette is nothing more than "scrambled eggs enclosed in a coating of coagulated egg." He is correct and his description helps: you are making very soft scrambled eggs and then convincing the exterior to set around them into a smooth, closed shape.
The exterior and interior cook differently. The exterior is in direct contact with the pan and sets quickly. The interior is cooked by radiated heat from the exterior and remains fluid longer. The motion — constant, vigorous stirring — disperses the heat so that the transition from fluid to set happens gradually rather than all at once.
## The Equipment
**Pan**: A 20cm non-stick pan, reserved for eggs only. This is not a counsel of perfection. It is a practical necessity. Carbon-steel pans work once perfectly seasoned but require maintenance. Stainless steel requires precision that even experienced cooks find difficult. Non-stick is honest: it tells you exactly what is happening rather than hiding problems.
**Heat**: Medium-high to high. The professional instruction is "very hot," and I know this is terrifying, but slow heat produces rubbery, overdone eggs. The butter should foam actively the moment it hits the pan and the foam should subside after 2030 seconds. If it doesn't foam at all, the pan is not hot enough.
## The Method
Beat 3 eggs with a fork until thoroughly combined — no streaks of white remaining. Season with fine salt only (add pepper on the plate; pepper in a hot omelette turns bitter).
Heat the pan over medium-high heat for 12 minutes. Add 15g unsalted butter. As the foam subsides (but before the butter browns), add the eggs all at once.
Immediately begin shaking the pan forward and backward while simultaneously stirring the eggs in the pan with a heatproof spatula or fork in small circular motions. The combination of motion creates tiny curds throughout the egg mass while the shaking prevents the bottom from setting. Keep this up for about 45 seconds.
When the eggs are barely set — still trembling, still slightly liquid on the surface — stop stirring. Let the omelette sit for 5 seconds to allow the bottom to set into a smooth surface.
Tilt the pan at 45 degrees and, using the spatula, gently fold the near side of the omelette towards the centre. Then tip the pan further, letting the far edge fold over as you slide the omelette onto the plate, rolling it off the pan so it arrives seam-side down in a neat torpedo shape.
## The Roll vs. Fold Debate
There is an alternative approach, used by many French home cooks, which involves folding the omelette in thirds rather than rolling: fold the near third over the centre, fold the far third over the near, slide onto the plate. This produces a more rectangular shape and is significantly easier. Jacques Pépin does it. I have seen it described as "the real" French omelette as opposed to the rolled version.
Both are correct. The rolled version is more impressive and produces a slightly creamier interior because the folding is faster. The folded version is more forgiving and produces consistently good results even for beginners. Learn the rolled version; use the folded version when you're tired.
## Why It's Worth Learning
The French omelette teaches you more about heat control, timing, and attention than almost any other dish. Once you can make one reliably — pale, soft, creamy, rolled in ninety seconds — you understand something about cooking that is difficult to learn any other way. The difficulty is precisely the lesson.
Cook it for breakfast on a Sunday when nothing else is demanding your attention. Make three in a row. The second will be better than the first; the third will be better than the second.

View file

@ -0,0 +1,71 @@
---
title: "Slow-Braised Lamb Shoulder with Preserved Lemon and Olives"
created: 2024-09-12 11:00
author: Amelia Fontaine
keywords: lamb, braise, preserved lemon, olives, North African, slow cooking
description: A North African-inspired lamb shoulder braise with the science of why slow cooking transforms tough cuts, served with couscous. Recipe for six.
---
# Slow-Braised Lamb Shoulder with Preserved Lemon and Olives
Lamb shoulder is one of those cuts that rewards patience so generously you wonder why anyone would rush. It is inexpensive, flavourful, and absolutely requires the low-and-slow treatment — braising over three to four hours — to surrender its considerable potential. At high heat it is tough and unpleasant. Given time and liquid, the collagen in its connective tissue converts to gelatin, and the result is meat that pulls apart easily, silky and rich, in a sauce of deep complexity.
This preparation is North African in spirit, though not strictly authentic to any one cuisine. The combination of preserved lemon, olives, coriander, and saffron is Moroccan in temperament; the method is classical French. The two coexist happily.
## Ingredients (serves 6)
**For the lamb:**
- 2kg bone-in lamb shoulder
- 2 tbsp olive oil
- 2 large onions, roughly chopped
- 4 cloves garlic, sliced
- 2 tsp ground cumin
- 2 tsp ground coriander
- 1 tsp smoked paprika
- 1 tsp ground ginger
- ½ tsp cinnamon
- 1 pinch saffron, steeped in 2 tbsp warm water
- 400g tin chopped tomatoes
- 300ml lamb or chicken stock
- 1 preserved lemon, pulp discarded, rind finely sliced
- 150g pitted green olives (Castelvetrano work well)
- Salt and black pepper
**To serve:**
- 400g couscous
- 30g unsalted butter
- A large bunch of fresh coriander
- Pomegranate seeds (optional but excellent)
- Plain yoghurt
## Method
**Day before (if possible):** Season the lamb shoulder generously all over with salt. Refrigerate uncovered overnight. This dry brine improves the crust and seasons the meat throughout.
**Searing.** The single most important step for flavour. Pat the lamb completely dry. Heat the olive oil in a large casserole or Dutch oven over high heat until smoking. Sear the lamb on all sides — do not rush this — until deeply browned, almost mahogany, on all surfaces. This takes 1012 minutes total. Remove and set aside.
The Maillard reaction is happening here: amino acids and sugars on the surface of the meat are combining under heat to create hundreds of new flavour compounds. You cannot get this depth of flavour from poaching or slow-cooking from raw. The sear is not about sealing in juices (that is a myth); it is about creating new ones.
**Building the braise.** Reduce heat to medium. In the same pan, cook the onions in the residual fat with a pinch of salt for 810 minutes until soft and golden. Add the garlic and all the spices; cook for 2 minutes until fragrant. Add the saffron with its soaking water. Add the tomatoes and stock, scraping up all the browned bits from the pan.
Nestle the lamb back in, fat-side up. The liquid should come about halfway up the meat — add more stock or water if needed. Bring to a gentle simmer.
**Braising.** Cover tightly and transfer to an oven set to 160°C. Braise for 33.5 hours, turning the lamb once at the halfway point. The meat is done when it yields completely to gentle pressure and a probe thermometer reads above 90°C in the centre.
**Adding the preserved lemon and olives.** In the last 30 minutes of braising, add the preserved lemon rind and the olives. These are added late to preserve their bright, assertive flavours — too long in the braise and they lose their character.
**Finishing.** Remove the lamb to a warm dish. Skim the fat from the surface of the braising liquid. If the sauce is thin, reduce it over high heat for 510 minutes on the stovetop. Taste for seasoning.
## The Couscous
Pour 400g couscous into a large bowl. Add 1 tsp salt and 1 tsp olive oil. Pour over 420ml boiling water. Cover tightly with cling film for 5 minutes. Uncover, add the butter, and fluff with a fork. Stir through most of the coriander.
## Serving
Bring the whole casserole to the table if possible. Pull the lamb apart with two forks — it should offer no resistance. Serve on a bed of couscous, ladled with sauce, scattered with remaining coriander, pomegranate seeds if using, and a spoonful of yoghurt alongside.
## Notes
This improves overnight. The flavours deepen and the fat congeals, making it easy to remove completely from the surface before reheating gently. Leftovers make an extraordinary filling for flatbreads with harissa and more yoghurt.
The preserved lemon can be made at home (and should be — it takes 10 minutes to prepare and four weeks to mature) or found in any Middle Eastern grocery. Do not skip it: no other ingredient produces exactly that combination of brininess and preserved citrus intensity.

View file

@ -0,0 +1,73 @@
---
title: "Wild Mushroom Pasta: An Autumn Recipe and a Foraging Story"
created: 2024-10-25 09:00
author: Amelia Fontaine
keywords: mushrooms, pasta, pappardelle, foraging, autumn, dried mushrooms
description: A day foraging in the countryside, what different mushrooms taste like, how to dry and rehydrate them, and a pappardelle recipe with wild mushrooms.
---
# Wild Mushroom Pasta: An Autumn Recipe and a Foraging Story
It was an October Saturday, dense fog in the valleys and the light coming through the beech trees at a particular angle that I associate entirely with this season. A friend who has been foraging since she was a child had invited me along, with the clear instruction that I was not to pick anything she hadn't identified and that I should wear waterproof boots regardless of what the forecast said.
She was right about the boots.
We found chanterelles first — unmistakable, egg-yolk orange, smelling faintly of apricots, firm as you'd want. Then a large cluster of hen-of-the-woods (*Grifola frondosa*) at the base of an oak, grey-brown and fanning outward. Some bay boletes, beautiful with their reddish-brown caps and pale undersides. A single, enormous porcino — boletus edulis — which my friend regarded with the reverence usually reserved for something religious.
I took home about 600g of mixed mushrooms and some deeply practical lessons about what I didn't know.
## On Flavour
Different mushrooms taste genuinely different in ways that matter for cooking.
**Chanterelles**: Fruity, slightly peppery, delicate. Best treated simply — butter, garlic, thyme. They do not need much company.
**Porcini (ceps)**: The most intensely savoury wild mushroom. Glutamate-rich. Their flavour is deeper and meatier than anything farmed. Dried porcini are one of the most powerful flavour concentrators in any kitchen.
**Hen-of-the-woods (maitake)**: More substantial in texture than most, with a woodsy, slightly spicy quality. Excellent for high-heat searing because their fronds crisp beautifully.
**Bay boletes**: Milder than porcini but with the same family character. Good for drying.
**Farmed alternatives**: Oyster mushrooms have a gentle, shellfish quality and a beautiful texture. Shiitake are reliable and rich. Cremini and chestnut are neutral workhorses. None have the character of wild mushrooms, but dried porcini added to any combination will provide the umami backbone that wild mushrooms supply naturally.
## Drying Mushrooms
Drying concentrates flavour dramatically and extends shelf life indefinitely. Slice mushrooms thinly (57mm). Spread on a rack in a low oven (5060°C) with the door slightly ajar, or use a dehydrator, for 46 hours until completely dry and brittle. Store in an airtight jar.
**Rehydrating**: Soak in warm water for 2030 minutes. The soaking liquid is extremely flavourful — use it as stock. Pour carefully to leave the grit at the bottom of the bowl.
## Wild Mushroom Pappardelle (serves 4)
**Ingredients:**
- 400g pappardelle (or tagliatelle)
- 400g mixed fresh mushrooms (whatever you have — chanterelle, chestnut, oyster, maitake)
- 20g dried porcini, soaked in 150ml warm water
- 3 cloves garlic, thinly sliced
- 3 tbsp olive oil
- 40g unsalted butter
- 100ml dry white wine
- 100ml double cream
- A small bunch of fresh thyme
- Fresh flat-leaf parsley, roughly chopped
- Parmigiano Reggiano to serve
- Salt and black pepper
**Method:**
Drain the porcini, reserving the soaking liquid. Chop the porcini roughly.
Tear or cut the fresh mushrooms into pieces — irregular shapes are fine and create more texture than uniform slices.
Heat olive oil and half the butter in a large, wide pan over high heat until shimmering. Add the fresh mushrooms in a single layer (work in batches if needed — overcrowding causes steaming instead of browning). Leave them undisturbed for 2 minutes, then toss. You want golden-brown patches on the mushrooms, not grey-steamed ones. Season with salt.
Add the garlic and thyme, cook for 1 minute. Add the chopped porcini. Add the wine and let it reduce by half. Add the porcini soaking liquid, pouring carefully to leave any grit behind. Reduce by half again. Add the cream. Simmer gently for 34 minutes until slightly thickened. Remove the thyme stems.
Cook the pappardelle in heavily salted water until al dente. Reserve a mug of pasta water. Drain and add to the mushroom sauce with the remaining butter. Toss vigorously, adding pasta water if needed, until the sauce coats the pasta.
Finish with parsley and serve immediately with Parmigiano grated at the table.
## The Lesson About Foraging
My friend told me something on that October walk that I have thought about often since. She said: "When you forage, you stop looking at the forest as scenery and start reading it. You notice moisture patterns, which trees grow where, how the light affects the soil temperature. You stop being a visitor." This is true of cooking too. When you understand what you're doing and why, you stop following recipes and start reading the food.
I came home with the mushrooms, with mud on my boots, and with a much clearer sense of what I had been doing wrong in my kitchen for years.

View file

@ -0,0 +1,76 @@
---
title: "Why I Make Stock Every Sunday (And You Should Too)"
created: 2024-11-28 10:00
author: Amelia Fontaine
keywords: stock, broth, chicken stock, veal stock, vegetable stock, technique
description: Chicken, veal, vegetable, and fish stock recipes — the difference between stock and broth, how to freeze it, and what stock makes possible in your cooking.
---
# Why I Make Stock Every Sunday (And You Should Too)
Stock is not glamorous. It is also not optional if you want to cook seriously. Every great sauce, every braised meat, every risotto, every soup — behind all of them is a question: what liquid are you using? If the answer is water or a stock cube, there is a ceiling on what is possible. Good stock removes that ceiling.
I make stock every Sunday, usually while doing other things. The bones go in the pot, the water goes on, and three hours later I have something that will unlock the whole week's cooking.
## Stock vs. Broth: The Actual Difference
These terms are used interchangeably in casual conversation, but they refer to different things.
**Stock** is made primarily from bones, with or without some meat. The long cooking time (26 hours depending on type) extracts gelatin from the collagen in the bones. When chilled, good stock sets to a jelly. This gelatin is what gives sauces their body, their gloss, their ability to coat a spoon.
**Broth** is made primarily from meat, cooked for a shorter time. It has more flavour from the meat but less body from gelatin. Broths are better for drinking or for light soups; stocks are better for reducing into sauces.
The distinction matters most when reducing. If you reduce a stock to intensify it, the gelatin concentrates and the result is viscous, glossy, and extraordinary. If you reduce a broth to the same degree, you often get something oversalted and thin.
## Chicken Stock
This is the most versatile and the one to start with.
**Ingredients:**
- Roast chicken carcass (or 11.5kg raw chicken bones/wings/feet)
- 1 large onion, halved
- 2 carrots, roughly chopped
- 2 celery stalks
- 1 head of garlic, halved crosswise
- 1 bay leaf
- A few peppercorns
- Cold water to cover (about 22.5 litres)
**Method:** If using raw bones, roast them at 220°C for 30 minutes until golden (this adds depth and colour). Place everything in a large pot and cover with cold water. Bring very slowly to a simmer — this is important; a rapid boil makes the stock cloudy. Skim the grey foam from the surface in the first 15 minutes. Simmer very gently, partially covered, for 34 hours. Strain through a fine sieve. Cool completely, then refrigerate; skim the solidified fat from the surface.
## Veal (Brown) Stock
The king of stocks. More laborious, more complex, the foundation of the classical French kitchen's grand sauces.
Roast 2kg veal bones (knuckles and necks are best) at 220°C until deeply browned, 45 minutes. Brown 2 onions, 3 carrots, and 3 celery stalks in a large pot until dark. Add the bones and cover with cold water. Simmer 68 hours. Strain, reduce by a third.
This stock, reduced by three-quarters, becomes *demi-glace* — a dark, intensely gelatinous, barely pourable concentrate that transforms any sauce it touches.
## Vegetable Stock
Made in 45 minutes. No long cooking required — vegetables release their flavour quickly and can turn bitter if cooked too long.
Onion, carrot, celery, fennel, leek tops, garlic, mushroom stems, parsley stems, peppercorns, bay leaf. No cruciferous vegetables (cabbage, broccoli) which add bitterness. Cover with cold water, bring to a simmer, cook 40 minutes. Strain.
Flavour it from the beginning; it will not develop complexity over time like bones do.
## Fish Stock
The quickest of all: 2025 minutes only, or it turns bitter.
Fish frames (the bones and head, with gills removed), leek, onion, fennel, white wine, peppercorns, bay leaf. Add all to cold water, bring to a simmer, skim, cook 20 minutes. Strain.
## Freezing
Stock freezes perfectly. For storage efficiency, reduce it by half before freezing — you can always add water when you use it. Freeze in ice cube trays for small amounts (good for pan sauces), in 300ml containers for risotto and soups, and in 1 litre bags for braises. Label with date and type. Use within six months.
## What Stock Makes Possible
Once you have good stock in your freezer:
- **Pan sauces** become 10-minute wonders rather than 45-minute projects
- **Risotto** tastes completely different (see the spring pea risotto post)
- **Braised meats** have a depth of flavour impossible to achieve with water
- **Soups** need almost no other flavouring — the stock does the work
- **Rice dishes** gain complexity that is impossible to fake
Stock is, at bottom, a patience tax: you invest Sunday morning so that the rest of the week's cooking is easier and better. It is the most productive kitchen habit I know.

View file

@ -0,0 +1,83 @@
---
title: "Grandmother Lucia's Christmas Cookies: Cuccidati and Brutti ma Buoni"
created: 2024-12-20 09:00
author: Amelia Fontaine
keywords: Christmas cookies, cuccidati, brutti ma buoni, Italian baking, Sicilian
description: Two Italian Christmas cookies from my grandmother's kitchen — fig-filled Sicilian cuccidati and craggy hazelnut brutti ma buoni — with the family story behind them.
---
# Grandmother Lucia's Christmas Cookies: Cuccidati and Brutti ma Buoni
My grandmother Lucia baked these cookies every December without a written recipe. She had made them so many times since her mother taught her in Catania in the 1950s that the quantities lived in her hands rather than in her head. The first time I watched carefully enough to write things down, she was eighty-one, and she regarded my notebook with amused scepticism. "You're going to measure everything?" she said in Italian, as if this were a charming eccentricity.
I was. These are the results.
## Cuccidati (Sicilian Fig Cookies)
*Cuccidati* — pronounced ku-chi-DAH-tee — are the Christmas cookie of Sicily: a buttery pastry encasing a dark, fragrant filling of dried figs, nuts, candied fruit, and spices. They are sometimes called *buccellati* in western Sicily, and the variations are endless. Every family has their version. This is Lucia's.
### The Filling (make first — it needs to rest)
- 400g dried figs, stems removed
- 100g raisins
- 80g blanched almonds, roughly chopped and toasted
- 50g walnuts, roughly chopped
- 50g candied orange peel, chopped
- 4 tbsp honey
- 50ml Marsala (or brandy or orange juice)
- Zest of 1 orange
- 1 tsp ground cinnamon
- ½ tsp ground cloves
- ¼ tsp black pepper
Place the figs and raisins in a food processor and pulse until finely chopped but not puréed — you want texture. Combine with all other ingredients in a bowl and mix well. Cover and refrigerate for at least one hour, ideally overnight, so the flavours meld. The filling can be made three days ahead.
### The Pastry
- 400g plain flour
- 1 tsp baking powder
- Pinch of salt
- 80g caster sugar
- 150g cold unsalted butter, cubed
- 1 large egg
- 60ml cold water (approximately)
- Zest of 1 lemon
Combine flour, baking powder, salt, and sugar. Rub in the butter until it resembles breadcrumbs. Beat the egg with the lemon zest and add to the bowl with most of the water. Bring together into a soft, non-sticky dough, adding more water if needed. Wrap and refrigerate for 30 minutes.
### Assembly
Preheat oven to 180°C. Roll the pastry to about 3mm thickness. Cut into rectangles approximately 8cm × 12cm. Place a sausage of filling (about 1.5cm diameter, 8cm long) along the long edge. Roll up and pinch the seam closed. Bend into a horseshoe shape, or cut into 3cm pieces for straight cookies.
Place on baking paper-lined trays. Make three diagonal slashes across the top of each. Bake for 1822 minutes until golden. Cool completely before glazing.
**Glaze**: Mix 150g icing sugar with enough lemon juice to make a thick glaze. Drizzle over cooled cookies. Top with coloured sugar strands or finely chopped pistachios if you like.
---
## Brutti ma Buoni (Ugly but Good)
The name is exactly right. These hazelnut and meringue cookies look craggy, irregular, and entirely resistant to elegance. They are extraordinary: intensely nutty, chewy at the centre, crisp at the edge, flavoured with vanilla and a touch of spice.
- 300g blanched hazelnuts
- 200g caster sugar
- 3 egg whites
- 1 tsp vanilla extract
- ½ tsp cinnamon
- Pinch of salt
Toast the hazelnuts at 180°C for 8 minutes until golden. Cool slightly, then pulse in a food processor until roughly chopped — you want some chunks, some powder.
Whisk the egg whites and salt to stiff peaks. Gradually add the sugar, whisking between each addition, until you have a stiff, glossy meringue. Fold in the hazelnuts, vanilla, and cinnamon.
Transfer the mixture to a saucepan. Cook over medium heat, stirring constantly, for 58 minutes until the mixture dries slightly and pulls away from the sides. It will become quite stiff and less glossy. Remove from heat.
Drop spoonfuls (about the size of a tablespoon) onto baking paper-lined trays. They will be irregular — this is their character. Bake at 160°C for 2530 minutes until firm and lightly golden. They firm further as they cool.
---
## How to Gift-Pack Them
Line a tin with tissue paper. Place cuccidati in a single layer, then a layer of greaseproof paper, then brutti ma buoni on top. The cookies keep for 1014 days in a cool place. They improve after two or three days as the flavours settle.
Lucia always sent them wrapped in newspaper (the most insulating material available in 1970s Catania) secured with kitchen string. I use better packaging now but the principle is the same: make more than you think you need, give most of them away, and eat the imperfect ones yourself.

View file

@ -0,0 +1,63 @@
---
title: "Knife Skills: The One Investment Worth Making in Your Cooking"
created: 2025-01-15 10:00
author: Amelia Fontaine
keywords: knife skills, chopping, chef's knife, sharpening, technique
description: Which knife to buy, how to sharpen it, and five essential cuts explained with technique — including why a sharp knife is not just convenient but fundamental.
---
# Knife Skills: The One Investment Worth Making in Your Cooking
If someone asked me where to spend their first serious kitchen investment, I would not say a stand mixer, a cast-iron pan, or a sous vide machine. I would say: one good knife and the time to learn to use it.
A skilled cook with a sharp knife and a wooden board can do almost anything. The average home cook with a drawer full of mediocre, dull knives is constantly fighting their ingredients, which is why cooking sometimes feels exhausting.
## Which Knife
You need one knife primarily: an 8-inch (20cm) chef's knife. Everything else is supplementary. A paring knife for small work, a serrated bread knife for bread — but the chef's knife is the tool you will use for 90% of kitchen tasks.
**What to look for:**
- **Full tang**: The metal should extend all the way through the handle. Partial-tang knives are less balanced and more likely to fail.
- **Weight**: A matter of preference. Heavier knives (German-style, like Wüsthof or Henckels) power through root vegetables. Lighter knives (Japanese-style, like Global or MAC) are more agile for precise work. Try them in your hand before buying if possible.
- **Steel hardness**: Measured in Rockwell hardness (HRC). Japanese knives are typically HRC 6065 (harder, holds an edge longer, more brittle). German knives are typically HRC 5658 (softer, easier to resharpen, less likely to chip). Both work excellently.
You do not need to spend a fortune. A reliable £6080 chef's knife from Victorinox or similar will outperform a poorly maintained £300 knife. What matters more than the knife is the sharpening.
## Sharpening: The Non-Negotiable
A dull knife is dangerous. It requires force, which causes slipping and loss of control. A sharp knife does the work; your job is merely to guide it.
**Whetstone** (recommended): The best method. Use a double-sided stone, 1000 grit (medium) on one side for regular maintenance, 30006000 grit (fine) for polishing. Hold the blade at 1520 degrees to the stone (German knives: 20 degrees; Japanese: 15 degrees). Move the blade across the stone as if slicing a thin layer off the top, heel to tip. Ten strokes per side, then switch. Finish on the fine grit.
This takes practice. The first few times are imperfect. Within a month of weekly sessions, you will feel the difference.
**Honing steel**: Not sharpening — honing realigns the edge between sharpenings. Use before every significant cooking session. The edge of a knife bends microscopically with use; honing straightens it.
**Electric sharpener**: Convenient but aggressive. Removes more metal than a whetstone. Use occasionally as a backup, not as a primary method.
Test sharpness: a sharp knife should glide through a sheet of paper cleanly, or shave arm hair without dragging.
## The Five Essential Cuts
**1. The Chop (rough cut)**
For onions, root vegetables, and anything that doesn't require precision. Curl your fingers so the knuckles act as a guide for the flat of the blade. The knife never leaves contact with the board — you rock from the tip forward. This technique protects your fingertips.
**2. The Slice**
For meat, fish, bread, soft vegetables. A single, fluid motion from heel to tip. Never press or saw; draw the knife through the material. Pressure causes crushing; motion causes cutting.
**3. The Julienne**
Cut the vegetable into planks, then stack and cut into matchsticks. Requires a very sharp knife to avoid crushing soft vegetables. Essential for stir-fries and salads.
**4. The Brunoise (fine dice)**
Julienne first, then cut across the matchsticks to produce tiny cubes (23mm). The gold standard for mirepoix, garnishes, and anything where you want the vegetable to disappear into a sauce.
**5. The Chiffonade**
Stack leafy herbs or greens, roll tightly, and slice crosswise into thin ribbons. Minimises bruising compared to chopping.
## The Onion, Properly
The onion is the test of knife skill. Make two horizontal cuts parallel to the board, stopping before the root end. Make multiple vertical cuts from top to root end, again stopping before the root. Then slice crosswise to produce a fine dice that holds together until the last cut because the root end acts as the anchor. The whole thing should take 20 seconds with practice.
Achieving this requires a sharp knife and the technique above. With a dull knife, onions do not cut — they crush, releasing their sulphurous compounds and causing significantly more eye irritation. Another reason sharpening is not optional.
The investment in knife skill repays itself immediately and permanently. It is the one thing in cooking that, once learned, never stops saving you time.

View file

@ -0,0 +1,63 @@
---
title: "On Slow Food: Why I Stopped Making Quick Dinners"
created: 2025-02-08 11:00
author: Amelia Fontaine
keywords: ribollita, Tuscan soup, slow cooking, beans, bread soup, philosophy
description: A personal essay on unhurried cooking, what it teaches, and a recipe for ribollita — the classic Tuscan bean and bread soup that rewards patience.
---
# On Slow Food: Why I Stopped Making Quick Dinners
There is a cooking genre that has dominated food media for years: the thirty-minute meal. I understand its appeal. Most evenings, after work, a long recipe feels like a burden rather than a pleasure. And yet I have become increasingly resistant to optimising everything in my kitchen for speed, because the things I cook quickly are consistently the things I care about least.
This is not an argument against efficiency. It is an argument for noticing what the efficient mode costs you.
When I make ribollita — the Tuscan bean and bread soup that requires at minimum a day of preparation and ideally two — I am in contact with something different from when I assemble a weeknight pasta. The process demands attention at intervals: checking the beans as they soak, tasting the soup as it reduces, deciding whether it needs more kale, more bread, more time. The cooking is a form of thinking, and the thinking changes how I relate to what I'm eating.
Carlo Petrini, who founded the Slow Food movement in 1989, was arguing partly about sourcing — buying from small producers, preserving food cultures — but the underlying idea is also about tempo: that the pace at which we engage with food shapes what the food means to us.
I am not evangelical about this. Quick meals have their place. But I notice that the meals I remember, the ones that feel genuinely nourishing rather than merely functional, are almost always the ones that took time.
## Ribollita
The name means "reboiled." This is a soup that is made one day and eaten the next, when the bread has fully absorbed the broth and the whole pot needs to be reheated — reboiled — before serving. It is cheap, warming, and in its complexity of flavour, as satisfying as any elaborate preparation.
**Ingredients (serves 6):**
- 400g dried cannellini beans, soaked overnight
- 1 head cavolo nero (black kale), stalks removed, roughly chopped
- ½ savoy cabbage, roughly shredded
- 1 large onion, roughly chopped
- 3 stalks celery, chopped
- 3 carrots, chopped
- 4 cloves garlic, sliced
- 400g tin whole plum tomatoes
- 4 tbsp olive oil, plus more for serving
- 1 sprig rosemary
- 2 bay leaves
- 200g day-old Tuscan or sourdough bread, roughly torn
- Salt and black pepper
- Parmigiano Reggiano rind (if you have it — adds considerable depth)
**Day One:**
Drain the soaked beans and cover with fresh cold water in a large pot. Add the Parmigiano rind if using. Bring to a simmer and cook for 11.5 hours until completely tender. Do not add salt until the last 10 minutes — it toughens the skins. Drain, reserving the cooking liquid. Mash or blend about a third of the beans until smooth; leave the rest whole.
In the same pot, warm the olive oil over medium heat. Cook the onion, celery, and carrot for 12 minutes until soft. Add the garlic, rosemary, and bay. Cook for 2 minutes. Add the tomatoes, crushing them with your hand as you add them. Cook for 10 minutes.
Add the whole and puréed beans, the cabbage, and the cavolo nero. Pour in the reserved bean cooking liquid and enough water to make a thick, substantial soup. Bring to a simmer and cook for 30 minutes.
Add the torn bread and stir it in. The bread will absorb the liquid and disintegrate partially, thickening the soup into something between a soup and a stew. Season generously. Remove the rosemary sprig and bay leaves.
Cool, cover, and refrigerate overnight.
**Day Two:**
Reheat gently, adding a little water if needed — it will have thickened further. Bring back to a simmer and cook for 1520 minutes. Taste and adjust seasoning.
Serve in deep bowls with a generous pour of your best olive oil over the top, a grinding of black pepper, and Parmigiano if you like. In Florence they sometimes add a drizzle of new-season olive oil and nothing else.
## What Slow Cooking Teaches
It teaches you that the most important ingredient in cooking is often time, which money cannot substitute for. It teaches patience, because you cannot make the beans cook faster without a pressure cooker, and even then the texture changes in ways that are less interesting. It teaches attention, because slow-cooked food needs checking and tasting as it develops.
And it teaches proportion — that a Sunday afternoon in the kitchen is not time lost but time spent. The ribollita that arrives on Monday evening required no effort that day at all. It is simply there, waiting, improved by its rest, ready to give you something back.

View file

@ -0,0 +1,66 @@
---
title: "Eggs Benedict and the Science of Hollandaise"
created: 2025-03-28 09:30
author: Amelia Fontaine
keywords: eggs benedict, hollandaise, poaching eggs, brunch, sauce
description: The emulsion science behind hollandaise, why it breaks and how to fix it, perfect poached eggs, and classic eggs Benedict assembly.
---
# Eggs Benedict and the Science of Hollandaise
Hollandaise has a fearsome reputation, and the fear is understandable: it is an emulsified butter sauce that breaks easily, cannot be made far in advance, and requires you to pay attention during service — the moment when you least want another technical demand. The reputation is somewhat deserved.
But understanding *why* hollandaise behaves as it does makes it dramatically more manageable. It is, at its core, chemistry. The chemistry is not complicated once you know it.
## The Emulsion
An emulsion is a stable mixture of two liquids that would normally separate: in hollandaise, fat (butter) and water (lemon juice, the water in egg yolks). These two phases resist mixing because fat molecules are nonpolar and water molecules are polar. Left alone, they separate.
The stabiliser is lecithin, found in egg yolks at high concentrations. Lecithin molecules have a nonpolar end (attracted to fat) and a polar end (attracted to water). They position themselves at the boundary between fat and water droplets, surrounding the fat droplets and preventing them from coalescing.
This works only within a temperature range: warm enough to keep the butter fluid and to partially cook the egg proteins (which helps stabilise the emulsion), but not so hot that the proteins cook fully and coagulate, which causes the sauce to "break" — separate into greasy curds.
The ideal temperature for hollandaise is 6070°C. Below this range it is too thin; above it breaks.
## The Method
**Ingredients (serves 4):**
- 4 egg yolks
- 250g unsalted butter, clarified (or just very good quality, melted and warm)
- 2 tbsp water
- 1 tbsp white wine vinegar
- Juice of half a lemon
- Salt and white pepper
**Clarifying butter** removes the milk solids and water, leaving pure butterfat. This gives a more stable emulsion and a cleaner flavour, though whole butter (used at room temperature) also works and is easier.
**Method:**
1. In a small saucepan, reduce the white wine vinegar with the water by half. Set aside to cool slightly.
2. In a heatproof bowl, whisk the egg yolks with the reduced liquid until pale and thickened — they should leave a trail (a "ribbon") when the whisk is lifted.
3. Set the bowl over a pan of barely simmering water (the bowl should not touch the water). Continue whisking while the mixture warms, 34 minutes, until it thickens further and holds a ribbon. This is the *sabayon* — the base.
4. Remove from the heat. Begin adding the warm clarified butter in a very thin stream while whisking constantly. The first few tablespoons are critical — add them very slowly, building the emulsion. Once the emulsion is established, you can add the butter more quickly.
5. When all the butter is incorporated, adjust with lemon juice, salt, and white pepper. The sauce should coat the back of a spoon heavily.
**If it breaks:** If you see greasy, curdled separation, it is usually because the temperature was too high or the butter was added too quickly. To rescue: start with a clean bowl and a fresh egg yolk whisked with a tablespoon of warm water. Very slowly whisk the broken sauce into this new base, treating it as the butter in the original recipe.
Keep hollandaise warm by setting the bowl over warm (not hot) water, whisking occasionally. Use within 3045 minutes.
## Poaching Eggs
Use the freshest eggs possible — older eggs spread more because the white becomes more liquid. Room temperature is preferable to cold from the fridge.
Bring a wide pan of water to a bare simmer. Add a splash of white wine vinegar (it helps the white cohere, though this is debated). Create a gentle swirl in the water with a spoon. Crack the egg into a small cup, lower the cup to the water surface, and slide the egg in gently. The swirl wraps the white around the yolk. Poach for 3 minutes for a runny yolk, 4 minutes for a more set result.
Lift with a slotted spoon, drain on kitchen paper. Trim any ragged white edges with scissors for a clean presentation.
## Assembly
**Eggs Benedict:** Split, toast, and butter English muffins. Top each half with a slice of back bacon (or Canadian bacon) that has been briefly warmed. Set a poached egg on top. Pour hollandaise generously over. Finish with a small amount of cayenne or smoked paprika.
**Eggs Royale:** As above but with smoked salmon instead of bacon.
**Eggs Florentine:** As above but with wilted spinach, squeezed very dry, instead of bacon.
The whole assembly takes about 15 minutes once you have the hollandaise made. The eggs can be poached ahead of time and kept in cold water, then reheated by placing in warm water for 60 seconds.
Eggs Benedict is weekend cooking at its best: technically interesting, visually impressive, and deeply satisfying to eat.

View file

@ -0,0 +1,55 @@
---
title: "A Year of Eating Seasonally: What I Learned"
created: 2025-05-10 10:00
author: Amelia Fontaine
keywords: seasonal eating, produce, UK seasons, local food, vegetables
description: A month-by-month guide to seasonal produce in Northern Europe, what eating with the seasons changed about cooking, and the real cost comparison.
---
# A Year of Eating Seasonally: What I Learned
Three years ago I made an experiment: for one year, I would cook primarily with whatever was in season in the UK, buying from farmers' markets when possible and from supermarkets when necessary but choosing seasonal produce. No tomatoes in January, no asparagus in October. I kept a food diary.
What I expected: virtuous inconvenience, occasional genuine pleasure, and a sense of moral superiority.
What I found: far better food than I had been eating, a dramatically changed relationship with the kitchen calendar, and — this surprised me most — lower grocery bills.
## Month-by-Month: Northern European Seasonal Guide
**January / February**
Root vegetables at their peak after frost (parsnips, celeriac, beetroot, swede). Leeks, Brussels sprouts, kale, Savoy cabbage. Forced rhubarb arrives in late January — pink and tender. Blood oranges from Sicily and Spain. Bergamot lemons briefly. Game birds if you eat them.
**March / April**
The hungry gap — the hardest time. Purple sprouting broccoli bridges it magnificently. Spring greens, wild garlic (from hedgerows and woods), radishes, early spinach. Jersey Royal new potatoes appear in late April — small, earthy, best boiled and eaten with good butter. Nothing else required.
**May / June**
The garden wakes up. Asparagus season (MayJune, approximately six weeks — eat it every day). Peas and broad beans, best eaten young and raw or barely cooked. Strawberries from mid-June. Early courgettes and their flowers. Elderflower for cordial and fritters. Wet garlic — young, soft-skinned, sweet, milder than cured.
**July / August**
The abundance. Tomatoes, courgettes, cucumbers, French beans. Sweetcorn. Raspberries, blueberries, gooseberries. Plums and early apples. Fennel. Aubergines in a good summer.
**September / October**
The transition into autumn is the most dramatic flavour shift of the year. Wild mushrooms. Autumn raspberries continue. Quince — underused and extraordinary in paste and as an accompaniment to cheese. Cobnuts. Main crop apples and pears at their best. Butternut squash and pumpkins. Jerusalem artichokes begin.
**November / December**
Roots again, and brassicas at their best after frost (a frost improves both Brussels sprouts and parsnips by converting starch to sugar). Chestnuts. Seville oranges arrive in December for marmalade. Celery.
## What Changed
**Cooking became easier.** When you stop trying to make out-of-season ingredients taste like themselves, you stop fighting your food. A parsnip in January is magnificent. A parsnip in July is pointless.
**The same vegetables, prepared differently, stopped boring me.** By February I had been eating celeriac for three months and had remoulade, dauphinoise, roasted, puréed, raw, in soup, and with preserved lemon. The constraint produced creativity I would not have found otherwise.
**I discovered vegetables I had ignored.** Swede. Salsify. Jerusalem artichokes (marvellous despite their intestinal reputation, which is somewhat exaggerated). Purple sprouting broccoli, which I now grow in my own small garden because the window of freshness before it reaches shops is part of its quality.
## The Cost Comparison
Seasonal, locally produced vegetables at farmers' markets are sometimes more expensive per unit than supermarket imports. But the yield is different. A genuinely ripe July tomato is twice the tomato of a January hothouse import — you use fewer, you eat more slowly, it satisfies better.
Across the year, my food costs were slightly lower, primarily because I stopped throwing away vegetables that had been disappointing enough to not eat.
## The One Compromise
I kept buying citrus fruit year-round. The lemons that are fundamental to most of my cooking have no British equivalent, and I was not prepared to become a purist at the expense of most of my sauces. I also kept tinned tomatoes for winter — at their best they are superior to fresh winter tomatoes and I feel no guilt about this at all.
Within those limits, the experiment became permanent. I cook this way now not because I decided to continue the experiment but because it simply became how I cook.

View file

@ -0,0 +1,65 @@
---
title: "The Focaccia That Changed How I Think About Bread"
created: 2025-06-25 09:00
author: Amelia Fontaine
keywords: focaccia, Ligurian, bread, olive oil, high hydration, overnight
description: Ligurian focaccia with rosemary — the high-hydration dough science, dimple technique, olive oil pools, and an overnight cold proof that transforms the texture.
---
# The Focaccia That Changed How I Think About Bread
I had made focaccia a dozen times before I went to Liguria, and each time it had been perfectly acceptable: flat, dimpled, herbed, slightly oily. Fine. The focaccia I ate at a bakery in Recco on a Monday morning in October was something else entirely — thin, blistered, puffy at the edges and nearly hollow in the centre, utterly saturated with local olive oil and sea salt, the texture something between bread and a cloud.
I stood outside eating it from a paper bag and immediately began trying to understand what had happened to it.
## What Makes Ligurian Focaccia Different
Standard focaccia is about 7075% hydration (water weight as a percentage of flour weight). Ligurian focaccia — *focaccia al formaggio* and the simpler *focaccia classica* — runs at 8085% or higher. More water means a more open crumb structure, lighter texture, and larger air pockets. It also means the dough is harder to handle: it spreads and sticks and refuses to behave like normal bread dough.
The solution is not more flour. The solution is understanding that this dough is not meant to be shaped the way a boule or a baguette is shaped. It is poured into the pan. Handled with wet hands. Stretched gently by gravity. This is a fundamentally different relationship with the dough.
The olive oil is not a finishing touch. It is a structural element. An absurd quantity of olive oil goes into the bottom of the pan before the dough, and a generous pour goes on top after dimpling. As the focaccia bakes, this oil fries the bottom of the bread while the steam from the high-water dough creates the airy interior. The result is a bread that is crisp underneath, soft and pillowy within, and absolutely soaked with oil throughout.
## The Recipe
**Ingredients:**
- 500g strong bread flour (or 00 flour)
- 430ml warm water (86% hydration)
- 10g fine salt
- 7g instant dried yeast (or 14g fresh)
- 1 tsp honey
- 8 tbsp (120ml) good quality olive oil, divided
- Flaky sea salt
- Fresh rosemary sprigs
**Equipment:** A 30×40cm baking tray (rimmed), or two smaller trays.
**Method:**
Dissolve the honey in the warm water. Add the yeast and let stand for 5 minutes. Combine flour and salt in a large bowl. Add the liquid and 4 tablespoons of the olive oil. Mix until combined — the dough will be sticky and shaggy. Do not add more flour.
Cover the bowl and leave at room temperature for 30 minutes. Then perform three sets of stretch-and-folds at 30-minute intervals: reach underneath the dough, stretch upward, fold over the top, rotate the bowl a quarter turn, repeat four times per set.
After the final fold, cover tightly and refrigerate overnight (816 hours). Cold fermentation develops flavour dramatically and makes the dough much easier to handle.
**The next day:**
Remove from the fridge and allow to warm for 1 hour. Pour 3 tablespoons of olive oil into the baking tray, coating the base entirely. Tip the dough onto the tray and gently stretch it toward the corners — do not force it; let it rest 5 minutes and stretch again. With a very wet or oiled hand, prod the dough to dimple it all over: press firmly, all the way to the base of the pan, every centimetre.
Mix the remaining olive oil with 4 tablespoons of water and pour this emulsion over the surface. The dimples will fill with oil-water pools — this is correct and desirable. Scatter generously with flaky salt and press rosemary sprigs into the dimples.
Leave to prove at room temperature for 4560 minutes until noticeably puffed.
Bake at 230°C (fan 210°C) for 2025 minutes until deep golden and blistered. The top should have some very dark patches — this is part of the character, not burning.
Cool for at least 10 minutes before eating, though it is extraordinarily good still warm.
## The Oil-Water Emulsion
The poured emulsion on top — oil and water together — is the technique I learned from reading about the Ligurian focaccerie. By the time it goes into the oven, the oil and water have separated into their constituent phases. The water steams in the oven, helping the surface bubble and blister, while the olive oil prevents the surface from drying and enables the characteristic golden-speckled finish. This is the technique that produces the texture I was eating in Recco.
## What It Taught Me
Focaccia taught me that hydration is not just a technical variable but a choice about what kind of bread you want to make. More water means more open texture means more delicacy. The trade-off is handling difficulty. Once I stopped trying to handle high-hydration dough like regular dough — stopped treating it as a problem to be solved — it became significantly more interesting to work with.
Bread, I think, rewards an attitude of curiosity more than one of mastery. It changes with the flour, the season, the humidity, the particular wild microorganisms in your starter or your water. The focaccia you make in January is not quite the focaccia you make in July. This is not a flaw. It is the thing that makes it interesting to keep making.

View file

@ -0,0 +1,58 @@
---
title: "Everything I Know About Olive Oil (It Took 10 Years to Learn)"
created: 2025-08-14 11:00
author: Amelia Fontaine
keywords: olive oil, extra virgin, cooking, polyphenols, sourcing guide
description: Pressing methods, polyphenols, smoke points, fraudulent EVOO, which to cook with versus finish with, and a practical sourcing guide.
---
# Everything I Know About Olive Oil (It Took 10 Years to Learn)
I have been thinking about olive oil for ten years and I am still not sure I understand it fully. It is, in the world of cooking ingredients, unusually complex — variable by region, variety, vintage, harvest date, pressing method, and storage — and the fraud rate in the industry is high enough that even careful shoppers are routinely misled.
What follows is what I have learned, mostly through buying, tasting, cooking, and occasionally ruining things.
## What "Extra Virgin" Actually Means
Extra virgin olive oil (EVOO) is produced by mechanically pressing fresh olives — no heat, no chemicals — and meeting specific quality standards: free acidity below 0.8%, and organoleptic standards requiring the oil to have positive attributes (fruitiness, bitterness, pungency) and no defects (rancidity, mustiness, winey notes).
The category below this is "virgin" (acidity up to 2%, some defects permitted), and below that is "olive oil" — a blend of refined oil (chemically treated to remove defects) and virgin oil. These are quite different products.
The problem is that "extra virgin" on a label guarantees very little in practice. The fraud issue is substantial: studies repeatedly find that 5080% of olive oil sold as Italian EVOO in international markets either fails quality standards or has been diluted with other oils. The EU has certification systems, but enforcement is inconsistent.
## How to Buy Better
The indicators of genuine quality:
- **Harvest date**: Look for it on the label. EVOO is perishable — it should ideally be used within 18 months of harvest and definitely within 2 years. "Best before" is a poor proxy; harvest date is what matters.
- **Single origin, single estate**: Oil from a single producer in a specific region is more verifiable than blends.
- **PDO/PGI designation**: Protected Designations of Origin (Tuscan EVOO, Kalamata PDO) have more rigorous controls.
- **Tin rather than dark glass**: Light degrades oil. Opaque tins are the ideal container. Clear glass is the worst.
- **Peppery burn**: Good EVOO — especially Tuscan and Sicilian varieties — should cause a noticeable burn at the back of the throat when tasted neat. This is the polyphenols, and it is a quality marker, not a flaw.
## Polyphenols
Polyphenols are the antioxidant compounds in olive oil, associated with most of its health benefits and responsible for the distinctive bitterness and pungency of high-quality oils. Early harvest oils (October-November, before full ripeness) contain more polyphenols but are more aggressive in flavour. Late harvest oils are milder and rounder.
High-polyphenol olive oils — which are increasingly available from specialist producers — have measurements above 250mg/kg on the label. These are robust, almost savoury, and can taste almost bitter neat. They are extraordinary for dressing strong-flavoured foods.
## Smoke Point and Cooking
The great misunderstanding: "olive oil has a low smoke point and shouldn't be used for high-heat cooking." This is largely wrong. **Extra virgin olive oil's actual smoke point is 190210°C**, depending on quality and age. This is more than sufficient for sautéing, shallow frying, and roasting. The smoke point of cheap refined oils is often higher, but polyphenols in EVOO make it more oxidatively stable at high temperatures.
The practical advice: do not deep-fry in EVOO (the economics are prohibitive and the smoke point, while adequate, gives less margin). For everything else — sautéing, roasting, making dressings — extra virgin is fine and often produces better flavour than neutral oils.
## Finishing vs. Cooking
There is a meaningful distinction between oils used to cook with and oils used to finish dishes. Cooking oil gets hot; much of its aroma evaporates and its flavour integrates into the dish. Finishing oil goes on cold or room-temperature food and its full character is experienced directly.
For cooking: a mid-range EVOO (£812 for 500ml) is excellent and economical. The heat will integrate rather than present its flavour.
For finishing: this is where a genuinely exceptional oil earns its price. A Sicilian *Nocellara del Belice* (full, fruity, grassy) or a Tuscan *Moraiolo* (pungent, bitter, peppery) drizzled over bruschetta, soup, fish, or legumes transforms the dish in a way that a cooking oil cannot.
## Storage
Away from heat, away from light, used within 6 months of opening. Not next to the stove; not on a windowsill; not in a clear bottle on a kitchen counter. In a cool cupboard, in a tin or dark bottle, used regularly. Rancid oil — which smells of crayons or old butter — is the most common quality problem and entirely preventable.
## The Practical Upshot
Buy from a specialist importer or directly from a producer if you can. Look for a harvest date. Spend more on the finishing oil you taste directly; spend less on the cooking oil the heat will transform. Keep it in the dark and use it promptly. Taste it out of the bottle sometimes — you will learn more about it that way than any other. It should make you want to eat something.

View file

@ -0,0 +1,70 @@
---
title: "Roasted Butternut Squash Soup with Crispy Sage and Brown Butter"
created: 2025-09-30 10:00
author: Amelia Fontaine
keywords: butternut squash, soup, brown butter, sage, autumn, roasting
description: Roasting vs steaming for depth, brown butter science, sage frying technique, and a full recipe with variations for a perfect autumn soup.
---
# Roasted Butternut Squash Soup with Crispy Sage and Brown Butter
The question with any squash soup is whether to roast the squash first or to steam or boil it. The answer, if you want the best-tasting soup, is always to roast. Steaming produces a pale, sweet, slightly watery result. Roasting produces caramelisation and depth that dramatically change what the soup can be.
The science is the Maillard reaction again: sugars in the squash combine with amino acids at high heat to produce hundreds of new flavour compounds. Squash is particularly susceptible to this because of its high sugar content. Roasted at 200°C until the cut surfaces are deeply golden and sticky, a butternut squash is a fundamentally different ingredient from a boiled one.
The brown butter amplifies this in a direction that seems designed for this vegetable specifically.
## Brown Butter
*Beurre noisette* — noisette meaning hazelnut, for the colour and aroma — is butter that has been cooked until the milk solids brown. The transformation happens in three stages:
1. Butter melts and the water begins to evaporate (you hear it fizzing).
2. The milk solids separate and begin to colour. The butter goes from opaque and milky to clear and golden.
3. The milk solids turn brown and the butter smells of toasted hazelnuts and toffee. This is the goal.
The danger: stage 3 transitions to stage 4 (burning) in about 30 seconds. Watch it constantly and remove from the heat the moment it smells right — it will continue to colour from the residual heat of the pan.
Brown butter adds a toasted, nutty complexity to anything that contains cream or dairy. On soup, drizzled over at serving, it is remarkable.
## Sage Crisps
Sage fried in butter or oil becomes a completely different ingredient: the volatile aromatic compounds that can make fresh sage taste slightly medicinal transform into something woody and resinous and addictive. Fried sage crisps are one of the best things you can put on soup, pasta, risotto, or gnocchi.
Heat a shallow layer of olive oil or clarified butter in a small pan until a sage leaf sizzles immediately on contact. Fry the leaves in batches for 3045 seconds until darkened and crisp — they continue to crisp as they drain. Remove to kitchen paper immediately. They keep for several hours at room temperature.
## The Recipe (serves 4)
**Ingredients:**
- 1 large butternut squash (about 1.2kg), halved, seeds removed
- 1 large onion, roughly chopped
- 4 cloves garlic, unpeeled
- 750ml vegetable or chicken stock, warm
- 100ml double cream
- 3 tbsp olive oil
- Salt, pepper, nutmeg
**For the brown butter:**
- 80g unsalted butter
- A handful of fresh sage leaves
**Method:**
Brush the squash halves with 2 tablespoons of olive oil. Season generously with salt and pepper. Place cut-side down on a baking tray with the unpeeled garlic cloves and roast at 200°C for 4555 minutes until the cut surface is deeply golden and the flesh is completely tender. Cool slightly.
Meanwhile, soften the onion in 1 tablespoon of olive oil in a large pot over medium heat until translucent and sweet, about 12 minutes.
Scoop the squash flesh from the skin. Squeeze the roasted garlic from its skins. Add both to the pot with the onion. Add the stock and bring to a simmer for 5 minutes.
Blend until completely smooth — a high-powered blender produces the best result; use a hand blender if that's all you have. Add the cream, a generous grating of nutmeg, and more salt and pepper. Taste carefully.
**To serve:** Reheat the soup gently if needed. Ladle into warm bowls. Make the brown butter in a small pan, watching carefully, and pull off the heat at hazelnut colour. Add the sage leaves to crisp in the same pan (the butter will sizzle vigorously). Drizzle brown butter over each bowl and top with crispy sage.
## Variations
**Spiced version**: Add 1 tsp ground cumin, ½ tsp smoked paprika, and a pinch of chilli to the onion as it softens. Finish with a swirl of yoghurt instead of cream, and toasted pumpkin seeds instead of sage.
**Thai-inspired**: Replace the cream with coconut milk. Add a stalk of lemongrass and a slice of galangal (or ginger) while blending, then strain. Finish with lime juice and fresh coriander.
**The bread bowl**: Hollow out a small round sourdough loaf and serve the soup inside it. Theatrical and more satisfying than it has any right to be.
The plain version, with brown butter and sage, is my preferred autumn lunch: made on a Sunday, reheated during the week, always excellent. The depth from the roasting means it doesn't need elaborate garnishes — the soup itself is doing most of the work.

View file

@ -0,0 +1,85 @@
---
title: "Cassoulet: A Two-Day Recipe Worth Every Minute"
created: 2025-11-05 09:00
author: Amelia Fontaine
keywords: cassoulet, confit duck, Toulouse sausage, beans, French, slow cooking
description: History of cassoulet, the Toulouse vs Castelnaudary debate, confit duck, Toulouse sausages, haricot tarbais beans — the full two-day recipe.
---
# Cassoulet: A Two-Day Recipe Worth Every Minute
Cassoulet is the greatest argument for unhurried cooking that I know. It is a Languedoc bean casserole made with confit duck, Toulouse sausages, and slow-cooked pork — a dish that takes two days and rewards the effort with something that no quick version can approximate.
There is a formal, quasi-legal dispute about its origins that I find charming in its intensity. The towns of Carcassonne, Toulouse, and Castelnaudary all claim cassoulet as their own, and the specific composition of the "authentic" version differs by town. Carcassonne includes lamb; Toulouse includes lamb and preserved goose; Castelnaudary uses pork, pork rind, and duck confit only. The Academy of Cassoulet in Toulouse publishes official rules.
My version is closer to Toulouse. I do not have official standing.
## The Components
**Haricot Tarbais beans** are the traditional choice — grown in the Bigorre region of the Pyrénées since the 17th century, with a thin skin and mealy, creamy interior. They are available online from specialist suppliers and are genuinely worth seeking out. Dried haricot blanc or cannellini are acceptable alternatives.
**Confit duck legs** can be made at home (the method follows), or bought from a good butcher or online supplier. The home-made version is superior.
**Toulouse sausages** are coarse-ground pork with salt, pepper, nutmeg, and wine — nothing more. They are available from French specialist butchers. Do not substitute ordinary sausages; the flavour and texture are fundamentally different.
## Day One: Confit Duck and Bean Preparation
### Confit Duck Legs
- 4 duck legs
- 30g coarse salt
- 4 bay leaves
- 10 peppercorns
- 4 sprigs fresh thyme
- 4 cloves garlic, crushed
- About 600g duck fat (or a combination of duck fat and lard)
Combine the salt, bay, peppercorns, thyme, and garlic. Coat the duck legs and refrigerate for 1224 hours. Rinse, pat dry.
Melt the duck fat in a deep casserole. Add the duck legs — they should be submerged. Cook at 90°C (just barely simmering) for 2.53 hours until the meat is tender when pierced. Remove and reserve. Store in the fat if making ahead — this is the principle of confit, and the duck keeps for weeks this way.
### Beans
Soak 500g dried haricot tarbais overnight in plenty of cold water. Drain, cover with fresh cold water by 5cm. Add a carrot, an onion, and a bouquet garni. Simmer for 45 minutes until tender but not quite done — they will cook more in the cassoulet. Reserve with their cooking liquid. Season with salt only in the last 10 minutes.
## Day Two: Assembly
**The base:**
- 200g pork belly, cut into thick pieces
- 200g pork rind, blanched for 5 minutes and cut into strips
- 2 onions, chopped
- 4 cloves garlic, sliced
- 400g tinned whole plum tomatoes
- 200ml white wine
- Reserved bean cooking liquid
- Salt, pepper
Brown the pork belly in a large pot until deeply coloured. Remove. Soften the onions in the rendered fat. Add the garlic, then the white wine; reduce by half. Add the tomatoes, crushing them. Add the bean cooking liquid (about 500ml) and the pork rind. Simmer for 20 minutes.
**Building the cassoulet:**
You need a large, wide casserole — traditional earthenware cassoles are ideal; a large Dutch oven works.
Layer one: one-third of the beans, some of the pork belly, pork rind, and tomato base.
Layer two: the duck legs and Toulouse sausages (46 sausages, depending on size, slightly browned in a pan first).
Layer three: the remaining beans, the remaining pork, more base sauce. The liquid should just reach the top of the beans — add more bean cooking liquid or stock if needed.
**Breadcrumbs**: Strew a generous layer of fine dried breadcrumbs over the top. Drizzle with duck fat or olive oil.
Bake at 150°C for at least 2 hours. At intervals, break the crust that forms on the surface with a spoon and push it into the cassoulet. Add liquid if it looks dry. The traditional instruction is to break the crust seven times; the practical instruction is to break it whenever you walk past.
The cassoulet is done when the top is deep golden-brown, the interior is thick and bubbling, and the whole kitchen smells of Gascony.
## Serving
Bring the cassoulet to the table in its dish. Serve with nothing more than good bread and a simple green salad dressed only with vinaigrette. The cassoulet is the meal; it needs nothing.
Leftovers — there will be leftovers — are better still the next day. This is one of the dishes that improves with every reheating.
## On the Investment
Two days is a lot. The answer to this objection is that very little of those two days involves active cooking: the confit sits in the oven unattended, the beans soak overnight, the cassoulet bakes slowly. The active time is perhaps two hours spread across both days.
What you get for this investment is a dish that feeds six to eight people generously, that improves overnight, that contains more depth and complexity than almost anything you could make in an hour, and that marks the meal as an occasion — which is sometimes exactly what cooking should do.

View file

@ -0,0 +1,92 @@
---
title: "My Year of Fermentation: Kimchi, Kraut, and Lacto-Pickles"
created: 2026-01-08 10:00
author: Amelia Fontaine
keywords: fermentation, kimchi, sauerkraut, lacto-fermentation, pickles
description: Lacto-fermentation science, basic kimchi recipe, simple sauerkraut, quick lacto-pickles, equipment needed, and safety notes.
---
# My Year of Fermentation: Kimchi, Kraut, and Lacto-Pickles
Fermentation feels like the opposite of modern cooking: it is slow, unpredictable, invisible, and yields results that are difficult to specify in advance. You cannot ferment something in thirty minutes. You cannot reliably repeat results across batches. What you can do, with basic understanding and some patience, is create a range of preserved, probiotic-rich, complex-flavoured foods from very simple ingredients.
I spent much of last year making things in jars. This is what I learned.
## The Science of Lacto-Fermentation
Lacto-fermentation is not about dairy. It refers to *Lactobacillus* bacteria, which are present naturally on the surface of most vegetables. When vegetables are salted and submerged in brine, these bacteria produce lactic acid as they metabolise the sugars in the vegetables. The lactic acid lowers the pH, creating an environment inhospitable to pathogens but hospitable to more Lactobacillus bacteria.
This is why lacto-fermentation is safe without refrigeration or sterilisation: the lactic acid is the preservative. The pH drops quickly enough in the first 2448 hours to prevent pathogenic bacteria from establishing themselves. As long as the vegetables remain submerged below the brine — where anaerobic conditions prevail — the process is reliable and safe.
Salt concentration matters: 22.5% salt by weight of the vegetables (plus added water if making a brine) is the standard range for most lacto-fermented vegetables. Too little salt and unwanted bacteria can establish themselves before the pH drops; too much and the Lactobacillus bacteria are inhibited.
## Equipment
You need:
- **Glass jars** (wide-mouth mason jars or Kilner jars) — 1 litre or larger
- **A clean weight** to keep vegetables submerged — a small jar filled with water, a zip-lock bag filled with water and brine, or commercial fermentation weights
- **A kitchen scale** — weight measurements are essential for correct salt concentration
- **Patience** — most ferments take 37 days at room temperature before they are ready
You do not need: special airlocks (useful but optional), canning equipment, expensive fermentation crocks (though they are excellent), or a vacuum sealer.
## Sauerkraut
The simplest lacto-ferment. One ingredient, plus salt.
- 1kg white or green cabbage, finely shredded (about 2mm)
- 20g fine sea salt (2% by weight)
Combine in a large bowl and massage vigorously for 510 minutes until the cabbage has released significant liquid — it will reduce in volume by half and the brine should be sufficient to submerge the cabbage when pressed.
Pack tightly into a 1-litre jar, pressing down hard after each handful. The brine should rise to cover the cabbage. Place your weight on top to keep the cabbage submerged. Cover with a cloth and secure with a rubber band.
Leave at room temperature (1822°C is ideal) for 57 days. "Burp" the jar daily by pressing the cabbage down if it has floated. Taste after day 3 — it should be slightly sour. Continue until it reaches your preferred sourness, then seal and refrigerate.
Sauerkraut keeps refrigerated for months.
## Simple Kimchi
Kimchi is more complex than sauerkraut but follows the same principles.
**For the cabbage:**
- 1 medium napa (Chinese) cabbage (about 1kg)
- 60g coarse salt
Quarter the cabbage lengthwise. Dissolve the salt in enough water to submerge the cabbage. Soak for 12 hours until pliable. Rinse thoroughly and squeeze as dry as possible.
**The paste (yangnyeom):**
- 4 tbsp gochugaru (Korean red pepper flakes — not chilli flakes)
- 1 tbsp fish sauce (or soy sauce for vegan version)
- 4 cloves garlic, grated
- 1 tsp fresh ginger, grated
- 1 tsp sugar
- 3 spring onions, cut into 5cm pieces
Mix the paste ingredients. Cut the cabbage quarters crosswise into 5cm pieces. Combine with the paste and spring onions, wearing gloves (the gochugaru stains). Mix thoroughly until all the cabbage is coated.
Pack into jars. There should be little to no brine visible initially — it will develop within 24 hours. Leave at room temperature for 2448 hours, then refrigerate. The kimchi is ready to eat but improves over two to four weeks as it continues to ferment slowly in the fridge.
## Quick Lacto-Pickles
These use a salt brine rather than the vegetable's own liquid, which makes them work for firmer vegetables and more varied ingredients.
**Brine**: Dissolve 20g salt in 1 litre of water (2% brine).
**Suitable vegetables**: Cucumber (cut into spears or coins), carrot, radish, green beans, cauliflower florets, garlic.
**Additions**: Fresh dill, bay leaf, peppercorns, coriander seeds, garlic, chilli.
Pack the vegetables and aromatics tightly into a jar. Pour brine over to cover. Weight and cover as with sauerkraut. At room temperature for 35 days, then refrigerate. These are lighter and more delicate in flavour than sauerkraut — a bridge between vinegar pickles and full fermentation.
## Safety
Lacto-fermentation has an excellent safety record when done correctly. The key rules:
- Keep vegetables fully submerged throughout fermentation
- Use the correct salt concentration
- Ferment at room temperature, not in the warmth of the oven or near a heat source
- If you see pink or black mould (not the white kahm yeast, which is harmless), discard and start again
The strong smell of fermentation can be alarming — sauerkraut at day two smells aggressively sour. This is normal. Trust the science.
Fermentation changed how I think about food preservation: not as the avoidance of microbial activity, but as the selective cultivation of it. The bacteria doing the work are on the vegetables already; you are just creating conditions in which they thrive and others don't. There is something deeply satisfying about that collaboration.

View file

@ -0,0 +1,64 @@
---
title: "A Love Letter to Pasta: Making Every Shape by Hand"
created: 2026-04-12 09:00
author: Amelia Fontaine
keywords: pasta, handmade, tagliatelle, pappardelle, pici, orecchiette, technique
description: The dough formula, rolling technique, and how to make tagliatelle, pappardelle, pici, and orecchiette by hand — a complete guide to fresh pasta.
---
# A Love Letter to Pasta: Making Every Shape by Hand
I grew up with a mother who bought fresh pasta from the supermarket and a father who considered this acceptable on weeknights. My grandmother, who had come from Lyon and regarded Italian cooking with the respectful curiosity of someone married into another culture, learned to make pasta by hand from Lucia. When I was old enough, I learned from her.
Making pasta by hand is one of the most meditative tasks in cooking. It requires no special equipment beyond a rolling pin and a clean surface. It takes about 45 minutes from flour to cut pasta. The result is something that dried pasta cannot be: light, silky, absorbent, and briefly alive — it cooks in 6090 seconds and grips sauce in a way that even very good dried pasta does not.
## The Dough
There are two foundational pasta doughs:
**Egg pasta (pasta all'uovo)**: 100g 00 flour + 1 large egg. This is the standard for Northern Italian pasta — tagliatelle, pappardelle, maltagliati. The eggs add richness and colour.
**Eggless pasta (pasta di semola)**: 100g semolina flour + approximately 50ml warm water. This is the standard for Southern Italian pasta — orecchiette, pici, cavatelli. The semolina provides more structure and a pleasantly chewy bite.
**Method for egg pasta:**
Mound the flour on a clean surface. Make a well in the centre. Crack the eggs into the well. Beat gently with a fork, gradually incorporating flour from the inner edge of the well. When the dough is shaggy, use your hands to bring it together. Knead for 810 minutes until smooth, elastic, and slightly tacky — it should spring back when poked. Wrap and rest at room temperature for 30 minutes. This rest is important: the gluten relaxes and the dough becomes easier to roll.
**Method for semolina pasta:** Combine flour and water until it comes together. Knead for 10 minutes — semolina dough is stiffer and more resistant than egg pasta. Wrap and rest for 30 minutes.
## Tagliatelle
The canonical fresh pasta of Emilia-Romagna. Traditionally the width of a tagliatelle ribbon should equal one-twelfth the height of the Amalfi tower (8mm), according to a 1972 decree by the Italian Academy of Cuisine. This is the kind of precision I respect in all the wrong areas of life.
Roll the egg dough to about 12mm thickness. Dust well with flour. Fold loosely into a cylinder (fold once, then again). Cut into strips 68mm wide. Immediately unfurl and dust with more flour to prevent sticking. Cook within the hour or refrigerate.
**Thickness guide:** Thicker (2mm) for Bolognese and ragù — the pasta holds up to a heavy sauce. Thinner (1mm) for butter and sage, browned butter, or delicate cream sauces.
## Pappardelle
As tagliatelle but wider — 2cm or more. Traditionally paired with rich braises: wild boar ragù, the braised hare ragù of Tuscan restaurants, or the mushroom pappardelle from earlier in this blog. The width means more sauce per bite; the egg richness stands up to strong flavours.
Roll to 2mm, fold and cut to 22.5cm widths.
## Pici
Pici is the hand-rolled pasta of Tuscany — the pasta every Sienese child learns before they learn anything else. It requires semolina dough, no special equipment, and works through pure repetition.
Roll the semolina dough into a rough rectangle about 1cm thick. Cut into strips 1cm wide. Take each strip and, working from the centre outward, roll against the clean surface with both palms to create a long, uneven, thick spaghetti — ideally 3040cm long and about 34mm in diameter. Irregularity is correct and desirable.
Pici is paired classically with a simple sugo of garlic and tomato (*all'aglione*) or with a ragù. Their thickness means they cook in 56 minutes in well-salted boiling water.
## Orecchiette
"Little ears" — the pasta of Puglia, shaped by dragging a small piece of semolina dough across a rough wooden board with a blunt butter knife. This is the pasta I find most satisfying to make because the technique is so counterintuitive until it clicks.
Work with small pieces of semolina dough (about 10g each). With the tip of a butter knife, press down and drag the dough towards you across a lightly floured rough wooden surface. The dough will curl around the tip of the knife. With your thumb, invert the curl over your thumb to create the ear shape. It takes 1015 attempts before the motion becomes automatic.
Orecchiette is traditionally served with cime di rapa (turnip tops) in a sauce of garlic, anchovy, chilli, and olive oil. The ear shape collects the sauce and the pieces of wilted green in their hollows.
## On Making Pasta
There is a moment, usually on the third or fourth time you make tagliatelle, when the dough feels right under your hands without thinking about it. The texture, the resistance, the way it responds to the rolling pin — it becomes familiar. This is the beginning of what cooking by feel means: not improvisation or instinct but accumulated sensory memory.
Pasta is one of the fastest routes to this kind of knowledge because the feedback is immediate — you see the result within an hour, eat it within two — and because the variables are limited: flour, egg, water, time. There is nowhere to hide in a good plate of tagliatelle al burro. You can taste the pasta clearly, assess the flour, assess the dough, and begin to understand the decisions that produced what you're eating.
This is why I keep making it by hand even when I have perfectly good dried pasta in the cupboard. The point is not efficiency. The point is understanding.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,46 @@
# mdcms theme — The Kitchen Table
light:
accent: "#B7410E"
background: "#FFFDF7"
nav-background: "#FFF8EE"
text: "#2D2016"
text-muted: "#8B6914"
dark:
accent: "#F59E0B"
background: "#1C1208"
nav-background: "#241910"
text: "#FEF3C7"
text-muted: "#D97706"
colours-semantic:
info: "#2563EB"
warning: "#D97706"
success: "#16A34A"
error: "#DC2626"
callouts:
info:
icon: info
primary-colour: "#2563EB"
background-colour: "#2563EB"
warning:
icon: warning
primary-colour: "#D97706"
background-colour: "#D97706"
success:
icon: success
primary-colour: "#16A34A"
background-colour: "#16A34A"
error:
icon: error
primary-colour: "#DC2626"
background-colour: "#DC2626"
font-body: "bunny:Merriweather:400"
font-heading: "bunny:Playfair Display:700"
font-size: 1.0
line-height: 1.8
main-width: 72em
nav-width: 18em

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,7 @@
# mdcms v0.3 | DO NOT REMOVE THIS COMMENT
sitename: Foundations of Modern Philosophy
sitedescription: A Systematic Introduction by Prof. James Okafor
navigation: sidebar
nav-position: left
search: true
footer: "© 2026 James Okafor. Published under Creative Commons CC BY-NC 4.0."

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,204 @@
# nav.yml — generated by mdcms.py
sections:
- code: front-matter
defaultname: Front Matter
sort: 50
pagesvisibility: visible
- code: epistemology
defaultname: Part I — Epistemology
sort: 100
pagesvisibility: visible
- code: metaphysics
defaultname: Part II — Metaphysics
sort: 200
pagesvisibility: visible
- code: ethics
defaultname: Part III — Ethics
sort: 300
pagesvisibility: visible
- code: conclusion
defaultname: Conclusion
sort: 400
pagesvisibility: visible
pages:
- file: pages/preface.md
title: Preface
section-id: front-matter
sort: 100
variants: [en]
titles:
en: Preface
- file: pages/how-to-use.md
title: How to Use This Book
section-id: front-matter
sort: 110
variants: [en]
titles:
en: How to Use This Book
- file: pages/ep-01-knowledge.md
title: What is Knowledge?
section-id: epistemology
sort: 100
variants: [en]
titles:
en: What is Knowledge?
- file: pages/ep-02-perception.md
title: Perception and Reality
section-id: epistemology
sort: 110
variants: [en]
titles:
en: Perception and Reality
- file: pages/ep-03-reason.md
title: Reason and Rationalism
section-id: epistemology
sort: 120
variants: [en]
titles:
en: Reason and Rationalism
- file: pages/ep-04-empiricism.md
title: Empiricism
section-id: epistemology
sort: 130
variants: [en]
titles:
en: Empiricism
- file: pages/ep-05-scepticism.md
title: Scepticism and Its Responses
section-id: epistemology
sort: 140
variants: [en]
titles:
en: Scepticism and Its Responses
- file: pages/ep-06-truth.md
title: Theories of Truth
section-id: epistemology
sort: 150
variants: [en]
titles:
en: Theories of Truth
- file: pages/meta-01-existence.md
title: Existence and Being
section-id: metaphysics
sort: 100
variants: [en]
titles:
en: Existence and Being
- file: pages/meta-02-identity.md
title: Identity and Persistence
section-id: metaphysics
sort: 110
variants: [en]
titles:
en: Identity and Persistence
- file: pages/meta-03-causation.md
title: Causation
section-id: metaphysics
sort: 120
variants: [en]
titles:
en: Causation
- file: pages/meta-04-freewill.md
title: Free Will and Determinism
section-id: metaphysics
sort: 130
variants: [en]
titles:
en: Free Will and Determinism
- file: pages/meta-05-mind.md
title: Philosophy of Mind
section-id: metaphysics
sort: 140
variants: [en]
titles:
en: Philosophy of Mind
- file: pages/meta-06-time.md
title: The Nature of Time
section-id: metaphysics
sort: 150
variants: [en]
titles:
en: The Nature of Time
- file: pages/eth-01-foundations.md
title: Foundations of Ethics
section-id: ethics
sort: 100
variants: [en]
titles:
en: Foundations of Ethics
- file: pages/eth-02-consequentialism.md
title: Consequentialism
section-id: ethics
sort: 110
variants: [en]
titles:
en: Consequentialism
- file: pages/eth-03-deontology.md
title: Deontological Ethics
section-id: ethics
sort: 120
variants: [en]
titles:
en: Deontological Ethics
- file: pages/eth-04-virtue.md
title: Virtue Ethics
section-id: ethics
sort: 130
variants: [en]
titles:
en: Virtue Ethics
- file: pages/eth-05-applied.md
title: Applied Ethics
section-id: ethics
sort: 140
variants: [en]
titles:
en: Applied Ethics
- file: pages/eth-06-political.md
title: Political Philosophy
section-id: ethics
sort: 150
variants: [en]
titles:
en: Political Philosophy
- file: pages/synthesis.md
title: Synthesis and Open Questions
section-id: conclusion
sort: 100
variants: [en]
titles:
en: Synthesis and Open Questions
- file: pages/further-reading.md
title: Further Reading
section-id: conclusion
sort: 110
variants: [en]
titles:
en: Further Reading

View file

@ -0,0 +1,55 @@
---
title: "What is Knowledge?"
sort: 100
section-id: epistemology
description: The JTB analysis of knowledge, the Gettier problem, and the major responses to Gettier.
language: en
---
# What is Knowledge?
Epistemology — from the Greek *episteme* (knowledge) and *logos* (account or study) — is the branch of philosophy concerned with the nature, sources, and limits of knowledge. Its central question is deceptively simple: what is it to know something?
## The Traditional Analysis: Justified True Belief
The dominant account in Western philosophy from Plato through most of the twentieth century held that knowledge is *justified true belief* (hereafter JTB). To know that *p* is to (1) believe that *p*, (2) have *p* be true, and (3) be justified in believing that *p*.
Each condition plays a role. The truth condition rules out lucky coincidences: if I believe the train departs at 10am and it in fact departs at 10am, I do not know this if I formed the belief by guessing. The belief condition rules out propositions I accept without endorsing: I may act as if London is south of Edinburgh (it is not) without believing this, in which case I cannot be said to know it. The justification condition — the most philosophically contested of the three — distinguishes knowledge from mere true belief: if I believe, on no grounds whatsoever, that there is a spider behind the bookcase, and there happens to be a spider there, I do not thereby *know* there is a spider. Knowledge requires that one's belief be appropriately supported.
The JTB analysis has Platonic roots: in the *Meno*, Socrates distinguishes knowledge (*episteme*) from right opinion (*ortho doxa*) by the presence of an "account" that tethers the belief to its object. In the *Theaetetus*, Plato examines and ultimately rejects several definitions of knowledge, leaving the question famously open.
## The Gettier Problem
In a short, devastating paper published in 1963, Edmund Gettier showed that the JTB analysis is insufficient ^[Gettier, E., "Is Justified True Belief Knowledge?", *Analysis* 23, 1963, pp.121-123]. He produced two counterexamples — cases where an agent has a justified true belief but, intuitively, does not know.
The original cases are somewhat technical, but their structure can be illustrated as follows. Smith has good evidence that Jones will get the job, and that Jones has ten coins in his pocket. He infers: "The man who will get the job has ten coins in his pocket." In fact, Smith himself gets the job — and unbeknownst to him, he also has ten coins in his pocket. Smith's belief is true and justified. But he does not know it, because his justification for the belief is the evidence about Jones, not about himself. The truth of his belief is, in the relevant sense, a matter of luck.
Gettier cases share a common structure: the belief is true, and it is justified, but the justification and the truth are *accidentally* connected in a way that undermines knowledge. The epistemic luck that disqualifies knowledge is sometimes called *veritic luck* — the belief could easily have been false, even given the justification.
## Responses to Gettier
The philosophical literature on Gettier is vast ^[For surveys, see Shope, R., *The Analysis of Knowing*, Princeton UP, 1983; Ichikawa, J. and Steup, M., "The Analysis of Knowledge", *Stanford Encyclopedia of Philosophy*, 2018]. Several broad strategies have been pursued.
**The No-False-Lemmas Approach.** Some responses add a fourth condition: knowledge requires that the justification not pass through any false beliefs. In the Smith-Jones case, Smith's inference passes through the false belief that Jones will get the job. This approach handles many Gettier cases but fails against variants that generate knowledge through no false belief.
**Defeasibility Theories.** Lehrer and Paxson proposed that knowledge requires that one's justification not be *defeatable* by true information ^[Lehrer, K. and Paxson, T., "Knowledge: Undefeated Justified True Belief", *Journal of Philosophy* 66, 1969, pp.225-237]. If there is some truth that, were the agent to learn it, would undermine her justification, she does not know. This captures the intuition that Gettier cases involve misleading justification, but the correct formulation of the defeasibility condition has proven elusive.
**Reliabilism.** Alvin Goldman proposed replacing the traditional internalist justification condition with an externalist one: knowledge requires that the belief be produced by a *reliable belief-forming process* ^[Goldman, A., "What is Justified Belief?", in *Justification and Knowledge*, ed. Pappas, Reidel, 1979]. A reliable process is one that tends to produce true beliefs in the actual world. Perception, memory, and valid inference are typically reliable; guessing and wishful thinking are not. Reliabilism handles Gettier cases naturally: if a belief is produced by a reliable process, there is no epistemic luck.
**Safety and Sensitivity Conditions.** Sosa and Nozick proposed modal conditions on knowledge. Nozick's *tracking theory* required that the belief "tracks" the truth: if *p* were false, the agent would not believe *p* (the sensitivity condition); and if *p* were true, the agent would believe *p* (the adherence condition) ^[Nozick, R., *Philosophical Explanations*, Harvard UP, 1981, ch.3]. Sosa's *safety* condition required that the agent could not easily have been wrong ^[Sosa, E., "How to Defeat Opposition to Moore", *Philosophical Perspectives* 13, 1999].
**Knowledge First.** Timothy Williamson has argued that the traditional project of analysing knowledge in terms of more basic conditions is fundamentally misguided ^[Williamson, T., *Knowledge and Its Limits*, Oxford UP, 2000]. Knowledge, he argues, is a primitive mental state — not reducible to belief plus conditions. Rather than asking what conditions must supplement belief to yield knowledge, we should take knowledge as the starting point and explain belief and justification in terms of it. The slogan is "knowledge first."
## Internalism and Externalism
The Gettier debate brought into focus a broader dispute about the nature of epistemic justification. *Internalists* hold that the factors that determine whether a belief is justified must be *accessible* to the agent — available through reflection alone ^[Chisholm, R., *Theory of Knowledge*, Prentice-Hall, 1966]. On this view, two agents who are internally identical (same beliefs, same phenomenal states) must be equally justified, even if their environments differ dramatically.
*Externalists* deny this. Goldman's reliabilism is paradigmatically externalist: whether a belief is produced by a reliable process is a fact about the external world, not something the agent can determine by reflection. An agent might have a perfectly reliable belief-forming process that she has no way of knowing is reliable.
The internalism/externalism debate intersects with questions about scepticism (discussed in Chapter 5). Externalism offers a natural reply to sceptical scenarios — brain-in-a-vat believers may have reliably formed beliefs even in their abnormal environment — but faces the challenge of explaining the felt force of sceptical intuitions, which seem to appeal precisely to considerations accessible by reflection.
## Knowledge and Understanding
A growing body of work distinguishes *knowledge that* (propositional knowledge) from *knowledge how* (ability knowledge) and from *understanding*. Ryle's distinction between knowing-that and knowing-how influentially challenged the assumption that all knowledge is propositional ^[Ryle, G., *The Concept of Mind*, Hutchinson, 1949]. Understanding — grasping why something is the case, how the pieces fit together — seems to go beyond a collection of propositional beliefs and is increasingly seen as a distinct epistemic achievement worthy of investigation in its own right.
The question "What is knowledge?" turns out, as Plato suspected, to be genuinely difficult. The Gettier problem demonstrated that the most natural answer — justified true belief — is insufficient, and the subsequent fifty years of philosophy have not produced a consensus on what must replace it. But the failure to find a reductive analysis does not mean we have learned nothing. We have learned precisely why the question is hard, and that is progress.

View file

@ -0,0 +1,61 @@
---
title: Perception and Reality
sort: 110
section-id: epistemology
description: Direct realism, indirect realism, idealism, and phenomenalism — the major theories of perception and its relation to reality.
language: en
---
# Perception and Reality
Perception is our most immediate route to knowledge of the external world, and yet it is philosophically treacherous. We trust our senses — and then we discover that sticks look bent in water, towers look small from a distance, and the table that appears brown under incandescent light appears subtly different under daylight. These illusions and variations prompt an epistemological crisis: if our senses can mislead us, how can we trust them? And if we cannot fully trust them, what can we know about the world?
## The Argument from Illusion
The *argument from illusion* is a traditional challenge to naive perceptual realism. It proceeds roughly as follows ^[Ayer, A.J., *The Foundations of Empirical Knowledge*, Macmillan, 1940]:
1. In cases of illusion, what we are directly aware of (the bent stick, the shrunken tower) is not identical to the physical object.
2. Perceptual experience in veridical (non-illusory) cases is intrinsically similar to experience in illusory cases.
3. Therefore, what we are directly aware of even in veridical cases is not the physical object itself, but some intermediate object — a *sense datum*, a subjective representation, a mental image.
If this argument is correct, we never perceive the external world directly. We perceive representations of it, and must infer the world from those representations.
## Direct Realism
*Direct realism* (also called naïve realism or common-sense realism) holds that in ordinary perception, we are directly aware of the physical world. There are no intermediary mental objects standing between us and the things we perceive.
Contemporary direct realists reject the argument from illusion by contesting its first premise. When I see the bent stick, I am not aware of some private sense datum; I am aware of the stick itself, and my experience has the representational content that the stick is bent — which is a false content, but this does not require a separate object ^[Martin, M.G.F., "The Transparency of Experience", *Mind and Language* 17, 2002, pp.376-425].
**Disjunctivism** is a sophisticated variant of direct realism that draws a fundamental distinction between veridical experience and illusion/hallucination ^[McDowell, J., "Criteria, Defeasibility, and Knowledge", *Proceedings of the British Academy* 68, 1982]. On this view, there is no common factor between seeing a tree and hallucinating a tree. Veridical perception genuinely consists in being acquainted with the object; hallucination is a numerically distinct kind of event that merely mimics it. This dissolves the argument from illusion by denying that veridical and illusory experiences must have the same fundamental nature.
## Indirect Realism
*Indirect realism* (or representationalism) accepts that we never perceive the external world directly. Our direct objects of experience are mental representations — sense data, *qualia*, or "ideas" in the empiricist terminology. These representations are caused by, and typically resemble, the physical objects that produce them.
Locke is the canonical indirect realist in the early modern period ^[Locke, J., *An Essay Concerning Human Understanding*, 1689, Book II]. He distinguished *primary qualities* (extension, shape, motion, number) — features of objects that genuinely resemble our ideas of them — from *secondary qualities* (colour, taste, smell, temperature) — features that our ideas do not resemble; they are simply the powers of objects to produce certain experiences in us.
Indirect realism faces a significant epistemological challenge: if we only ever directly perceive our representations, how can we know that those representations accurately track the external world? Locke acknowledged this; Berkeley exploited it to devastating effect.
## Berkeley's Idealism
George Berkeley argued that indirect realism collapses into idealism ^[Berkeley, G., *A Treatise Concerning the Principles of Human Knowledge*, 1710]. If we only directly perceive ideas, and ideas are inherently mental, then matter — that supposed cause of ideas existing independently of all minds — is a philosopher's fiction. *Esse est percipi*: to be is to be perceived.
Berkeley was not denying the existence of the ordinary objects of experience — tables, trees, other people. He was claiming that their existence consists in their being perceived, either by finite minds or, when unobserved by us, by the mind of God. This is idealism, but of a commonsensical variety: Berkeley insisted his view was closer to common sense than Locke's.
The main objection to Berkeley is the arbitrariness of experience. If physical objects are collections of ideas, why do we not simply experience whatever we imagine? Berkeley's answer — the regularity of experience is guaranteed by God — is metaphysically expensive and not universally persuasive.
## Phenomenalism
*Phenomenalism* is a non-theistic descendant of Berkeley, associated with Hume, Mill, and twentieth-century logical empiricists like A.J. Ayer. Rather than reducing physical objects to ideas in God's mind, phenomenalism analyses statements about physical objects as equivalent to conditionals about what experiences would occur under certain conditions ^[Mill, J.S., *An Examination of Sir William Hamilton's Philosophy*, 1865; Ayer, A.J., *Language, Truth and Logic*, Gollancz, 1936].
"There is a table in the next room" is analysed as something like: "If anyone were to look in the next room under normal conditions, they would have table-experiences." The table is, in Mill's phrase, a "permanent possibility of sensation."
Phenomenalism faces serious difficulties with conditionals involving unfulfillable antecedents and with the enormous complexity required to capture ordinary physical-object claims in purely phenomenal terms. It has largely been abandoned as a research programme.
## Contemporary Debates
Current philosophy of perception engages with cognitive science and debates about the *format* of perceptual representation (is it propositional? imagistic? iconic?), the *reach* of perception (does it extend to abstract objects, high-level properties, or is it limited to low-level sensory features?), and the relationship between perception and belief ^[Siegel, S., *The Richness of the Senses*, Oxford UP, 2010].
The *enactivist* tradition, drawing on Merleau-Ponty's phenomenology, challenges representationalism from a different direction: perception, on this view, is not a matter of constructing internal representations but of active engagement with the environment ^[Noë, A., *Action in Perception*, MIT Press, 2004].
The debate between direct and indirect realism remains active and unresolved. What is clear is that perception — however it ultimately works — does not give us a transparent window onto the world; it gives us something whose relationship to the world requires careful philosophical examination.

View file

@ -0,0 +1,59 @@
---
title: Reason and Rationalism
sort: 120
section-id: epistemology
description: Descartes, Leibniz, and Kant — the rationalist tradition, a priori knowledge, and the role of reason in epistemology.
language: en
---
# Reason and Rationalism
*Rationalism* is the view that reason — independent of or prior to sensory experience — is a significant source of knowledge. The paradigmatic rationalist claim is that some truths can be known *a priori*: known on the basis of reason alone, without appeal to experience. Mathematics and logic are the clearest cases. That 7 + 5 = 12, or that if all humans are mortal and Socrates is human then Socrates is mortal — these seem knowable by pure thought, without conducting experiments or making observations.
## The A Priori / A Posteriori Distinction
The distinction between *a priori* and *a posteriori* knowledge — between knowledge independent of experience and knowledge dependent on it — was systematised by Kant, though it has roots in earlier philosophy ^[Kant, I., *Critique of Pure Reason*, 1781/1787, B1-B6].
*A priori* knowledge is justified independently of experience. It includes logical truths, mathematical truths, and perhaps certain conceptual truths (a bachelor is unmarried). Crucially, a priori knowledge is typically characterised by *necessity* and *universality*: a priori propositions are true in all possible worlds and admit of no exceptions.
*A posteriori* (or *empirical*) knowledge is justified by experience. Contingent facts about the world — there are seven continents, water is H₂O, the temperature today is 22°C — are known a posteriori. Such propositions could have been otherwise, and we know them by observing the world.
Kant also introduced the analytic/synthetic distinction. An *analytic* judgment is one where the predicate is contained in the concept of the subject ("All bachelors are unmarried"). A *synthetic* judgment adds something beyond the subject concept ("The cat is on the mat"). Rationalists typically claim there is a priori synthetic knowledge — knowledge that is both independent of experience and genuinely informative about the world. Kant thought mathematics and the principles of pure science were synthetic a priori.
## Descartes and the Method of Doubt
René Descartes is the foundational figure of early modern rationalism. His *Meditations on First Philosophy* (1641) begins with systematic doubt: he resolves to suspend belief in anything he can doubt, to find, if anything survives, a foundation for knowledge that is absolutely certain ^[Descartes, R., *Meditations on First Philosophy*, AT VII:17-18].
The senses can deceive. Dreams can be indistinguishable from waking life. And most radically: could there be an evil demon, infinitely powerful and infinitely cunning, whose sole purpose is to deceive him? Under this hypothesis, even the truths of mathematics might be false.
From this radical doubt, Descartes extracts one certain truth: *cogito ergo sum* — "I think, therefore I am." ^[Descartes, R., *Discourse on the Method*, AT VI:32]. Even if a demon deceives me, the deceiving requires that I exist as a thinking thing. The *cogito* survives the most radical doubt.
From this single certainty, Descartes attempts to rebuild knowledge. He argues for the existence of a benevolent God who would not systematically deceive him, thereby reinstating trust in clear and distinct perception. The circularity of this reconstruction — using clear and distinct perception to prove God's existence, then using God's existence to validate clear and distinct perception — has been widely noted and is known as the *Cartesian circle* ^[Arnauld, A., *Fourth Objections*, in Descartes, *Meditations*, AT VII:214].
Despite these difficulties, Descartes' contribution is foundational: he established the *epistemological turn* — the idea that a systematic theory of knowledge is the prerequisite for metaphysics and science.
## Leibniz: Necessary Truths and Monads
Gottfried Wilhelm Leibniz distinguished *truths of reason* (necessary truths, knowable a priori, the opposite of which is impossible) from *truths of fact* (contingent truths, known a posteriori, the opposite of which is conceivable) ^[Leibniz, G.W., *Monadology*, §33-34, 1714].
For Leibniz, the basic furniture of reality consists of *monads* — immaterial, indivisible, mind-like substances. Each monad perceives (in a broad sense) every other monad, though with varying degrees of clarity. The apparent causal interaction between things is, in reality, a *pre-established harmony* installed by God: things do not genuinely cause each other but are programmed to correspond.
Leibniz's principle of *sufficient reason* — there must be a sufficient reason for everything being as it is rather than otherwise — is a cornerstone of his system and has remained influential in metaphysics and the philosophy of science.
## Kant's Copernican Revolution
Immanuel Kant transformed the rationalism/empiricism debate with his *Critique of Pure Reason* (1781). He accepted from the rationalists that there is genuine a priori knowledge and from the empiricists that all knowledge *begins* with experience. His synthesis: experience is possible only because the mind structures it using a priori *forms* (space and time) and *categories* (substance, causation, necessity, and others).
Kant called this the *Copernican revolution* in philosophy ^[Kant, I., *Critique of Pure Reason*, Bxvi]: just as Copernicus moved the sun to the centre, Kant moved the knowing subject. We do not passively receive an already-structured world; we actively structure the world we experience, using the forms of intuition and the categories of the understanding.
This generates *transcendental idealism*: objects as we know them (*phenomena*) are partly constituted by our cognitive apparatus. Things as they are in themselves (*noumena*) — beyond the conditions of our experience — are unknowable.
The great achievement of Kant's epistemology is explaining how synthetic a priori knowledge is possible: mathematical and scientific principles are synthetic a priori because they describe the structure that the mind imposes on experience, not features of mind-independent reality. The cost is that our knowledge is bounded by the limits of possible experience.
## The Analytic Critique
The logical empiricists of the early twentieth century (Carnap, Schlick, Ayer) challenged the very possibility of synthetic a priori knowledge ^[Ayer, A.J., *Language, Truth and Logic*, ch.4]. On their view, apparent a priori knowledge either reduces to analytic truths (true by definition) or is meaningless. Mathematics is analytic — true by virtue of the meanings of mathematical terms. There are no synthetic a priori truths.
Quine's "Two Dogmas of Empiricism" (1951) challenged even the analytic/synthetic distinction itself, arguing that no proposition is immune from revision in light of experience ^[Quine, W.V.O., "Two Dogmas of Empiricism", *Philosophical Review* 60, 1951]. This radical empiricism has been broadly influential but is not without critics ^[Grice, P. and Strawson, P., "In Defense of a Dogma", *Philosophical Review* 65, 1956].
The status of a priori knowledge remains one of epistemology's central contested questions.

View file

@ -0,0 +1,59 @@
---
title: Empiricism
sort: 130
section-id: epistemology
description: Locke, Berkeley, and Hume — the empiricist tradition and the limits of sensory knowledge.
language: en
---
# Empiricism
*Empiricism* is the epistemological view that knowledge derives from, and must be grounded in, sensory experience. Where rationalism privileges reason, empiricism insists that the mind at birth is a *tabula rasa* — a blank slate — and that all our concepts and knowledge are acquired through experience. The great British empiricists of the seventeenth and eighteenth centuries — John Locke, George Berkeley, and David Hume — explored this view with increasing rigour and found, perhaps to their own surprise, that it leads to deeply uncomfortable places.
## Locke's Empiricism
John Locke set the agenda for British empiricism in his *Essay Concerning Human Understanding* (1689). His starting point is a polemic against innate ideas: there are no ideas or principles inscribed in the mind from birth, contrary to what Descartes and Leibniz held ^[Locke, J., *Essay*, I.ii].
All ideas originate in experience, which Locke divides into two kinds: *sensation* (the senses providing ideas of external objects) and *reflection* (the mind observing its own operations — thinking, doubting, willing, perceiving). From simple ideas given in experience, the mind constructs *complex ideas* by combination, abstraction, and relation.
Locke's primary/secondary quality distinction (discussed in the previous chapter) is central to his epistemology. We have genuine knowledge only of relations among our own ideas; the extent to which our ideas correspond to mind-independent reality is limited to the primary qualities.
Locke's *Essay* is a monument of systematic empiricism, but it contains significant tensions. His account of substance — the "I know not what" that underlies the qualities we perceive — sits uneasily with his empiricism, since no experience corresponds to substance itself.
## Hume's Fork and Bundle Theory
David Hume pushed empiricism to its systematic conclusions with greater rigour and less embarrassment about where they led. In the *Treatise of Human Nature* (1739-40) and the *Enquiry Concerning Human Understanding* (1748), he developed what has been called the most powerful case in the history of philosophy for the limits of reason and knowledge.
*Hume's fork* divides all genuine claims to knowledge into two classes ^[Hume, D., *Enquiry*, §4]:
1. **Relations of ideas** — propositions knowable a priori by reason alone, whose denials are contradictions (mathematics, logic, conceptual truths). These are certain but tell us nothing about the actual world.
2. **Matters of fact** — propositions about the world, known a posteriori through experience. Their denials are conceivable. They are contingent and can only be known through experience.
Hume's criterion of empirical significance follows: any meaningful claim is either a relation of ideas or a matter of fact. Claims that fit neither category — much of traditional metaphysics, theology, and rationalist philosophy — are, famously, "nothing but sophistry and illusion" ^[Hume, D., *Enquiry*, §12.3].
On the self, Hume is radically deflationary. When he introspects, he finds no impression of a persistent, unified self — only "a bundle or collection of different perceptions, which succeed each other with inconceivable rapidity" ^[Hume, D., *Treatise*, I.iv.6]. The self, on his view, is a fiction constructed from the successive flow of impressions and ideas. Personal identity is a matter of psychological continuity, not a metaphysical substance.
## The Problem of Induction
Hume's most influential contribution to epistemology is his analysis of inductive inference. We routinely infer, from past regularities, what will happen in the future: the sun has risen every morning, so it will rise tomorrow. All known emeralds have been green, so the next emerald will be green. What justifies these inferences?
Not deductive reason: there is no logical contradiction in the sun's failing to rise. Not experience: to justify induction by appeal to the fact that induction has worked before is circular — it assumes the very principle in question ^[Hume, D., *Treatise*, I.iii.6].
Hume's conclusion: our habit of inductive inference is a psychological necessity — we cannot help forming expectations from regularities — but it has no rational justification. This is the *problem of induction*, sometimes called "Hume's guillotine" for the way it cuts off a seemingly obvious route to empirical knowledge.
The problem of induction has proved enormously productive. Karl Popper's falsificationism — the view that science proceeds by bold conjecture and attempted refutation rather than inductive generalisation — was an explicit response ^[Popper, K., *The Logic of Scientific Discovery*, 1934]. Nelson Goodman's "new riddle of induction" showed that the problem was deeper than Hume recognised: even if induction is sometimes reliable, we need a further principle to determine which regularities to project onto the future ^[Goodman, N., *Fact, Fiction and Forecast*, 1955].
## Hume on Causation
Hume's analysis of causation is equally influential. We believe that causes necessitate their effects — that fire *must* produce heat, that one billiard ball *must* move another when struck. But examining our impressions, Hume finds no impression of *necessary connection* between events ^[Hume, D., *Enquiry*, §7]. We observe constant conjunction — event A is always followed by event B — and we observe the spatial and temporal contiguity of cause and effect. But necessity itself is never observed.
Hume's account: the idea of necessary connection is a projection of our own psychological tendency to expect B after A, given repeated experience of their conjunction. The "necessary connection" is in us, not in the world.
This has generated enormous controversy. The *regularity theory* of causation — in Hume's footsteps — holds that causation just is constant conjunction (plus contiguity and temporal priority). Counterfactual theories, mechanistic theories, and probabilistic theories have all been proposed as improvements.
## Empiricism's Legacy
Empiricism as a systematic research programme continues in analytic philosophy. The logical empiricists (Carnap, Schlick, Neurath) developed a sophisticated version oriented toward the philosophy of science. Quine's naturalised epistemology — the view that epistemology is continuous with empirical psychology — is the most radical empiricist programme of the twentieth century ^[Quine, W.V.O., "Epistemology Naturalized", in *Ontological Relativity and Other Essays*, 1969].
The permanent contribution of the classical empiricists is methodological: the insistence that philosophical claims be answerable to experience, and the willingness to follow the logic of that insistence into uncomfortable territory.

View file

@ -0,0 +1,53 @@
---
title: Scepticism and Its Responses
sort: 140
section-id: epistemology
description: Cartesian scepticism, the brain-in-a-vat scenario, contextualism, and relevant alternatives theories.
language: en
---
# Scepticism and Its Responses
Scepticism is the philosophical position that knowledge — or at least, some significant domain of knowledge — is impossible. It has been a central problem in epistemology from antiquity to the present, partly because it is remarkably difficult to refute and partly because attempting to refute it has driven some of philosophy's most creative work.
## Ancient Scepticism
The ancient Greek sceptics — Pyrrho of Elis, and later the Academic sceptics including Arcesilaus and Carneades — argued that for any claim, there is an equally strong case for its denial, leaving the rational response one of *epoché*: suspension of judgment ^[Sextus Empiricus, *Outlines of Pyrrhonism*, I.8-12, c.200 CE]. Suspension of judgment, they argued, brings *ataraxia* — tranquillity, freedom from the anxiety that dogmatic belief produces.
Ancient scepticism was primarily a practical philosophy — a way of living without commitment to metaphysical positions. Modern scepticism takes a different form: it is primarily an epistemological challenge, asking whether knowledge is possible given the limitations of our access to reality.
## Cartesian Scepticism
Descartes' sceptical scenarios, introduced in the *Meditations* as methodological tools, have become the canonical statements of modern epistemological scepticism. The *dreaming argument*: I cannot rule out that I am dreaming right now, and if I might be dreaming, I cannot be certain that anything I currently believe is true ^[Descartes, *Meditations*, AT VII:19].
The *evil demon hypothesis* is more radical: suppose there is an infinitely powerful deceiving demon who ensures that all my beliefs — including the deliverances of reason and mathematics — are false. I cannot disprove this. Therefore, I cannot be certain of anything.
The sceptical strategy exploits what has been called *epistemic closure*: if I know that P entails Q, and I know P, then I know Q. Equivalently: if I don't know Q, and P entails Q, then I don't know P ^[Nozick, R., *Philosophical Explanations*, p.204]. Sceptics argue: if I knew that I have hands, I would know that I am not a brain in a vat; I do not know that I am not a brain in a vat; therefore, I do not know that I have hands.
## The Brain-in-a-Vat Scenario
Hilary Putnam updated Descartes' evil demon into the brain-in-a-vat scenario ^[Putnam, H., *Reason, Truth and History*, 1981, ch.1]. Suppose my brain has been removed from my body, placed in a vat, and is being fed electrical signals by a supercomputer that simulates a complete reality. All my experiences are exactly as they would be in normal embodied life.
Putnam argued — controversially — that this scenario is incoherent. A brain in a vat lacks the causal connections to the external world that are necessary for its terms to refer to external objects. "Water," as a brain-in-a-vat thinks it, does not refer to H₂O — it refers, at most, to the computer simulation. So a brain-in-a-vat thinking "I am not a brain in a vat" is producing a true sentence — because its words do not refer to the things that would make it false. This semantic argument against scepticism has been widely discussed and contested.
## Responses to Scepticism
**Moorean Responses.** G.E. Moore's response to scepticism was blunt: we know more certainly that we have hands than we know any philosophical premise used in the argument for scepticism ^[Moore, G.E., "Proof of an External World", *Proceedings of the British Academy* 25, 1939]. The *modus ponens* can be run in either direction: from the premises to the sceptical conclusion, or from the falsity of the sceptical conclusion to the falsity of one of the premises. Moore insisted the latter is more reasonable.
Wittgenstein developed a related response: certain propositions — "There are physical objects," "The world has existed for many years" — function as *hinges* that cannot be doubted within any practice of inquiry, because doubting them would not be coherent inquiry but something else entirely ^[Wittgenstein, L., *On Certainty*, §341, 1951].
**Relevant Alternatives.** Fred Dretske proposed that knowledge requires ruling out only *relevant* alternatives — possibilities that are live given one's actual situation ^[Dretske, F., "Epistemic Operators", *Journal of Philosophy* 67, 1970]. The possibility that I am a brain in a vat is not a relevant alternative in ordinary contexts; I do not need to rule it out to know that I have hands. Scepticism artificially expands the class of alternatives that must be eliminated.
**Contextualism.** David Lewis and Stewart Cohen developed contextualist responses: the standards for knowledge vary with context ^[Lewis, D., "Elusive Knowledge", *Australasian Journal of Philosophy* 74, 1996; Cohen, S., "How to be a Fallibilist", *Philosophical Perspectives* 2, 1988]. In ordinary contexts, we correctly say we know many things. In sceptical philosophical discussions, where very high standards are in play, those knowledge attributions are false — but this does not undermine ordinary attributions, which operate at a lower standard. Scepticism is a local phenomenon of artificially elevated epistemic standards.
**Externalist Responses.** If knowledge requires reliably produced beliefs (as reliabilism holds), then the sceptical demon scenario involves beliefs that are not reliably produced and therefore do not constitute knowledge. But Descartes' scenario is just that — a scenario where knowledge fails. This does not show that we actually lack knowledge in the real world.
## Closure Denial
Nozick's tracking theory denied epistemic closure, which blocks the sceptical argument at its source ^[Nozick, R., *Philosophical Explanations*, pp.204-211]. On his account, I know I have hands because if I didn't have hands I wouldn't believe I did (the sensitivity condition). But I do not know I am not a brain in a vat — because if I were a brain in a vat, I would still believe I wasn't (the sensitivity condition fails). Yet the failure to know the second proposition does not undermine knowledge of the first, because the inference from "I have hands" to "I am not a brain in a vat" is not knowledge-preserving on Nozick's account.
This is elegant, but the denial of closure is philosophically costly and has not won widespread acceptance.
## The Significance of Scepticism
Scepticism matters not primarily because it is a live hypothesis that reflective people adopt, but because engaging with it illuminates the structure of our knowledge and the character of epistemic justification. The sceptical challenge to close our eyes and demonstrate that we know anything about the external world has driven epistemologists to produce their most careful accounts of justification, reliability, and the conditions for knowledge. Scepticism is philosophy's sharpening stone.

View file

@ -0,0 +1,59 @@
---
title: Theories of Truth
sort: 150
section-id: epistemology
description: Correspondence, coherence, pragmatist, and deflationary theories of truth.
language: en
---
# Theories of Truth
What is truth? The question seems either trivially easy or impossibly hard. Everyone knows that to say something true is to say how things are. And yet when we ask what this "correspondence" between language and world consists in, or whether there might be truths that do not correspond to any independent reality, we find ourselves in deep philosophical waters.
## The Correspondence Theory
The *correspondence theory of truth* is the classical answer: a proposition (or belief, or statement) is true if and only if it corresponds to the facts ^[Aristotle, *Metaphysics*, 1011b26-28]. To say that snow is white is to say something true, because it corresponds to the fact that snow is white.
The appeal of this theory is its fidelity to common sense. When we assert something, we are attempting to describe how things are, and truth is the property of succeeding in that attempt.
The challenge is making "correspondence" precise. Early twentieth-century philosophy developed the *picture theory of meaning* (Wittgenstein's *Tractatus*, Russell's logical atomism) on which propositions picture facts by sharing their logical structure ^[Wittgenstein, L., *Tractatus Logico-Philosophicus*, 1921, §2.15; Russell, B., "The Philosophy of Logical Atomism", *Monist* 28, 1918]. This was technically ambitious but ultimately abandoned.
The central difficulty for correspondence theories is the nature of *facts*. Are facts mind-independent features of the world? If so, what exactly are they, and how do they differ from merely true propositions? The proliferation of suspect entities (negative facts, disjunctive facts, mathematical facts) has made many philosophers wary.
## The Coherence Theory
The *coherence theory* identifies truth with coherence — membership in a system of beliefs that are mutually consistent, mutually supporting, and comprehensive ^[Bradley, F.H., *Essays on Truth and Reality*, 1914]. On this view, a belief is true not because it corresponds to some external fact but because it coheres with the overall system of beliefs.
The coherence theory is associated with British idealism (Bradley, Bosanquet) and has been influential in anti-realist philosophy more generally. Its appeal lies in removing the mysterious "correspondence" relation: truth is an internal relation among beliefs, not a relation between thought and world.
Critics raise two main objections. First, coherence is not sufficient for truth: many consistent, mutually supporting sets of beliefs are simply false. The elaborate beliefs of a deeply mistaken scientific tradition may be perfectly coherent. Second, coherence is not necessary: isolated beliefs can be true without being embedded in a rich coherent system.
## Pragmatist Theories
William James and John Dewey developed *pragmatist* theories of truth that identified truth with what "works" — what is expedient to believe, what guides successful action ^[James, W., "Pragmatism's Conception of Truth", in *Pragmatism*, 1907; Dewey, J., *Logic: The Theory of Inquiry*, 1938].
James's formulation is the most quotable: "True ideas are those that we can assimilate, validate, corroborate and verify. False ideas are those that we cannot."
Pragmatism has been persistently misread as claiming that truth is whatever we find convenient to believe, or that powerful people's beliefs are therefore true. More charitably, pragmatism is the claim that our concept of truth is tied to the role beliefs play in guiding inquiry and action: there is no coherent notion of truth that is entirely divorced from human practice.
The strongest objection is that some truths are inaccessible to human inquiry — truths about the distant past, truths about microscopic phenomena not yet observed. If truth is tied to what we could in principle verify, we seem to be claiming that there are no truths about such matters, which is implausible.
Peirce's more sophisticated version identified truth with what ideal inquiry would converge on in the long run ^[Peirce, C.S., "How to Make Our Ideas Clear", *Popular Science Monthly* 12, 1878]. This avoids the problem of currently inaccessible truths but introduces a counter-factual notion of "ideal inquiry" that is difficult to cash out.
## The Deflationary Theories
*Deflationary theories* — including Ramsey's *redundancy theory*, Quine's *disquotational theory*, and Horwich's *minimalism* — hold that "is true" adds nothing to a proposition ^[Ramsey, F.P., "Facts and Propositions", *Proceedings of the Aristotelian Society* Supp. Vol. 7, 1927; Horwich, P., *Truth*, Blackwell, 1990].
To say "it is true that snow is white" is simply to say "snow is white." The predicate "is true" serves a logical function — enabling us to endorse propositions without repeating them, to quantify over propositions ("everything she said is true") — but there is no deep property of *truth* to be analysed.
The T-schema captures the deflationist insight: for any sentence S, "'S' is true if and only if S." There is nothing more to truth than this biconditional schema.
Deflationists must explain how the truth predicate can do its logical work without denoting a substantive property. Critics also note that scientific realism seems to require a robust notion of truth — the success of science is best explained by the approximate truth of scientific theories — and deflationists have struggled to accommodate this.
## Truth and Language
The relationship between truth and language raises further questions. Alfred Tarski's semantic theory of truth ^[Tarski, A., "The Concept of Truth in Formalized Languages", 1935] provided a technically rigorous account for formal languages: "Snow is white" is true in language L if and only if snow is white. For natural languages, which are semantically open, Tarski's approach encounters the *Liar paradox* ("This sentence is false") and related self-referential difficulties.
Contemporary philosophy of language has explored truth-conditional semantics (meaning just is truth conditions), pluralism about truth (different truth predicates for different domains — factual, moral, mathematical), and relativism (truth relative to standards or contexts).
The debate about truth intersects with debates about realism and anti-realism, metaphysics, and the philosophy of language. It is one of the crossroads of philosophy — a place where multiple independent routes converge.

View file

@ -0,0 +1,81 @@
---
title: Foundations of Ethics
sort: 100
section-id: ethics
description: Metaethics versus normative ethics, the question of moral realism, and why ethical theory matters for practical reasoning.
language: en
---
# Foundations of Ethics
Ethics is the branch of philosophy concerned with questions about value, obligation, and the good life. Before we can adjudicate between competing moral theories — utilitarian, Kantian, Aristotelian — we must first examine what kind of inquiry ethics is. This is the domain of *metaethics*.
## The Metaethical Questions
Metaethics asks: What is the nature of moral claims? When we say "Torturing innocents is wrong," are we:
1. Stating an objective fact about the world?
2. Expressing a subjective attitude?
3. Issuing a kind of command or prescription?
4. Doing something altogether different?
These are not merely academic quibbles. The answer constrains what normative ethics can hope to achieve. If there are no moral facts, then ethical argument collapses into persuasion. If there are moral facts but we cannot know them, then ethical confidence is always epistemically precarious.
## Moral Realism
Moral realists hold that there are objective moral facts — facts that hold independently of what any individual or culture believes. G.E. Moore (1903) argued that "good" is a simple, indefinable, non-natural property that we perceive through a kind of moral intuition.^[Moore, G.E. (1903). *Principia Ethica*. Cambridge University Press.]
Naturalistic moral realists, by contrast, identify moral properties with natural properties. Cornell realists such as Peter Railton and Richard Boyd argue that moral terms refer to natural facts about human flourishing, desire-satisfaction, or social coordination.^[Boyd, R. (1988). "How to be a Moral Realist." In Sayre-McCord (ed.), *Essays on Moral Realism*.]
The **open question argument** (Moore) challenges naturalism: for any natural property N, it is always an open question whether something that is N is thereby good. If "good" just meant "maximises pleasure," then "Is pleasure-maximising action good?" would be a tautology — but it is not. This suggests that moral properties are not identical to natural ones.
## Anti-Realist Positions
### Expressivism
A.J. Ayer's *Language, Truth and Logic* (1936) presented the classic emotivist thesis: moral statements are not truth-apt at all. "Stealing is wrong" means something like "Boo, stealing!" — it expresses disapproval rather than describing a fact.^[Ayer, A.J. (1936). *Language, Truth and Logic*. Gollancz.]
Simon Blackburn developed *quasi-realism* to address the main objection to expressivism: that moral statements appear in contexts (conditionals, embedded clauses) where purely expressive readings are implausible. "If stealing is wrong, then getting your brother to steal for you is also wrong" cannot mean "If boo stealing, then boo getting your brother to steal."
### Error Theory
J.L. Mackie (1977) accepted that moral statements purport to state facts but argued they are systematically false. There are no objective moral properties in the world. We are all making a kind of category error when we assert moral claims.^[Mackie, J.L. (1977). *Ethics: Inventing Right and Wrong*. Penguin.] Mackie's *argument from queerness* claims that objective moral properties would be entities of a very strange kind — utterly unlike anything in the natural world — and our capacity to know them would require an equally strange epistemic faculty.
### Constructivism
Kantian constructivists (Christine Korsgaard, John Rawls) occupy a middle position: moral truths are not mind-independent facts discovered by intuition, but neither are they merely expressions of attitude. They are constructed through procedures of rational reflection or agreement under idealised conditions. Moral facts are *the output* of a normative procedure, not independently existing objects.^[Rawls, J. (1980). "Kantian Constructivism in Moral Theory." *Journal of Philosophy*, 77(9).]
## Normative Ethics: An Overview
Normative ethics asks: what ought we to do, and why? Three traditions dominate:
| Tradition | Central Question | Key Figure |
|---|---|---|
| Consequentialism | What outcomes should we produce? | John Stuart Mill |
| Deontology | What duties bind us regardless of outcome? | Immanuel Kant |
| Virtue Ethics | What kind of person should I be? | Aristotle |
Each tradition is examined in subsequent chapters. Here we note that they often converge in practice while diverging in their theoretical foundations — a useful starting heuristic.
## Moral Epistemology
How do we come to know moral truths (assuming there are any)? Candidates include:
**Moral intuition** — Direct, non-inferential moral knowledge. Strong intuitions (that gratuitous cruelty is wrong) are treated as data points that any adequate theory must accommodate. The method of *reflective equilibrium* (Rawls) involves moving back and forth between intuitions and principles until they cohere.
**Moral perception** — On some realist accounts, we literally perceive moral properties as we perceive colours (though with a different faculty). This view faces difficulty explaining inter-subjective disagreement.
**Reason alone** — Kantians hold that moral knowledge is a priori, derived from pure practical reason. We shall examine this in detail in the chapter on deontology.
## The Fact-Value Distinction
Hume's famous observation — that we cannot derive an "ought" from an "is" — remains one of the most contested claims in metaethics.^[Hume, D. (1740). *A Treatise of Human Nature*, III.i.1.] If no purely factual description of the world entails a moral conclusion, then moral premises are always smuggled into ethical arguments. Naturalists must either deny the is-ought gap or explain why the gap does not undermine their position.
## Relativism and Universalism
*Cultural moral relativism* — the descriptive claim that moral codes vary across cultures — is well-documented. *Moral relativism* — the normative claim that what is right depends on cultural norms — is a separate and far more contested thesis. It generates self-refutation problems: if morality is relative, then the moral principle "we should not impose our moral views on other cultures" is itself only relatively binding.
Universalists hold that certain moral truths — concerning dignity, suffering, basic rights — apply to all humans in all contexts. The debate between particularism and universalism remains unresolved.
## Further Reading
- Parfit, D. (2011). *On What Matters*, Vols. III. Oxford University Press.
- Schroeder, M. (2010). *Noncognitivism in Ethics*. Routledge.
- Sayre-McCord, G. (ed.) (1988). *Essays on Moral Realism*. Cornell University Press.

View file

@ -0,0 +1,67 @@
---
title: Consequentialism
sort: 110
section-id: ethics
description: Utilitarian and consequentialist ethics from Bentham and Mill to Peter Singer and contemporary debates about act versus rule consequentialism.
language: en
---
# Consequentialism
Consequentialism is the family of ethical theories holding that the moral quality of an action is entirely determined by its consequences. The right action is whichever action produces the best outcome. This apparently simple thesis generates a remarkably rich — and contested — philosophical programme.
## Bentham's Utilitarianism
Jeremy Bentham (17481832) founded classical utilitarianism on the *principle of utility*: actions are right insofar as they promote happiness, and wrong insofar as they promote unhappiness. By "happiness," Bentham meant pleasure and the absence of pain.^[Bentham, J. (1789). *Introduction to the Principles of Morals and Legislation*. Payne.]
Bentham proposed the *felicific calculus* — a method for quantifying pleasure and pain along seven dimensions: intensity, duration, certainty, propinquity, fecundity, purity, and extent. The theory is rigorously impartialist: "each to count for one and none for more than one." The pleasure of a street cleaner counts exactly as much as that of an aristocrat.
## Mill's Refinements
John Stuart Mill (18061873) accepted the utilitarian framework but argued that pleasures differ not only in quantity but in quality. *Higher pleasures* — intellectual enjoyment, moral sentiment, aesthetic experience — are intrinsically more valuable than lower, merely sensory pleasures.^[Mill, J.S. (1863). *Utilitarianism*. Parker, Son, and Bourn.] "It is better to be Socrates dissatisfied than a fool satisfied."
Mill also attempted a consequentialist defence of rights and justice: rights protect interests so important that no ordinary gain in welfare could justify violating them. Whether this defence succeeds — whether rights can be grounded in utility without collapsing into mere policy instruments — remains debated.
## Act and Rule Consequentialism
**Act consequentialism** holds that each individual action should be evaluated by its consequences. The right act is the one that, among all available alternatives, produces the greatest aggregate welfare.
**Rule consequentialism** holds that we should follow rules whose general adoption would produce the best consequences. We do not evaluate each act individually but ask: "What rule, if generally followed, would produce the best outcomes?" Rule consequentialism preserves more intuitive commitments about promise-keeping and justice: keeping a promise may not maximise utility on a particular occasion, but a rule requiring promise-keeping generally does.
The objection to act consequentialism is that it seems to justify intuitively monstrous acts whenever the mathematics works out. If torturing an innocent person would prevent a slightly larger number of harms, act consequentialism apparently demands it. The *separateness of persons* objection (Rawls) argues that consequentialism fails to respect the distinction between persons, treating them merely as vessels for welfare rather than as individuals with their own claims.
## Peter Singer and Preference Utilitarianism
Peter Singer (b. 1946) defends a preference utilitarianism that extends moral consideration to all sentient beings capable of having preferences.^[Singer, P. (1979). *Practical Ethics*. Cambridge University Press.] The boundary of the moral community is not species membership but sentience. Singer's argument for animal liberation, global poverty obligations, and euthanasia all follow from applying the impartial preference calculus rigorously.
Singer's *drowning child* argument: if you could save a drowning child at trivial cost to yourself, you are morally required to do so. But the same logic applies to distant strangers dying of preventable diseases. If distance does not diminish moral obligation, affluent people in wealthy nations are obligated to give dramatically more than they typically do.^[Singer, P. (1972). "Famine, Affluence, and Morality." *Philosophy & Public Affairs*, 1(3).]
## Objections
### The Demandingness Objection
Impartial consequentialism seems to demand that we sacrifice almost all personal projects, relationships, and pleasures to maximise aggregate welfare. Bernard Williams argued that this alienates us from our own "ground projects" — the commitments that give our lives meaning.^[Williams, B. (1973). "A Critique of Utilitarianism." In Smart & Williams, *Utilitarianism: For and Against*.]
### The Integrity Objection
Williams' related argument: if consequences are all that matter, then I should be willing to perform any act — including acts I find deeply repugnant — if doing so maximises welfare. This seems to demand that agents violate their own integrity in ways that undermine the coherence of a moral life.
### The Measurement Problem
How do we compare welfare across persons? Cardinal welfare comparisons are notoriously difficult. Preference satisfaction is a proxy, but preferences can be adaptive (the oppressed learn to desire less), malformed, or satisfied in ways that harm the agent.
### Rights Violations
Robert Nozick's side-constraints view: there are moral side-constraints on action — rights — that cannot be overridden even by sufficiently large welfare gains. Using a person merely as a means to aggregate welfare violates their dignity as an end in themselves.^[Nozick, R. (1974). *Anarchy, State, and Utopia*. Basic Books.]
## Sophisticated Consequentialism
Many contemporary consequentialists have developed more sophisticated positions that accommodate common moral intuitions:
- **Indirect consequentialism**: evaluate character traits and dispositions by their consequences, not individual acts
- **Two-level utilitarianism** (Hare): intuitive level rules for everyday decision-making, critical level for theoretical reflection
- **Satisficing consequentialism**: require producing good-enough outcomes rather than maximising
These refinements preserve the spirit of consequentialism while avoiding the most counterintuitive implications.
## Further Reading
- Parfit, D. (1984). *Reasons and Persons*. Oxford University Press.
- Crisp, R. (1997). *Mill on Utilitarianism*. Routledge.
- Kagan, S. (1989). *The Limits of Morality*. Oxford University Press.

View file

@ -0,0 +1,82 @@
---
title: Deontological Ethics
sort: 120
section-id: ethics
description: Kant's categorical imperative, the formulas of universal law and humanity, perfect and imperfect duties, and neo-Kantian developments.
language: en
---
# Deontological Ethics
Deontological ethics holds that certain actions are intrinsically right or wrong regardless of their consequences. The term derives from the Greek *deon* (duty). Immanuel Kant (17241804) constructed the most influential deontological system in the history of ethics, grounding morality entirely in reason rather than sentiment or consequences.
## Kant's Moral Philosophy: Starting Points
Kant begins the *Groundwork of the Metaphysics of Morals* (1785) by identifying the only thing that is good without qualification: a **good will**.^[Kant, I. (1785). *Groundwork of the Metaphysics of Morals*. Trans. Korsgaard, Cambridge UP, 1998.] Intelligence, courage, and even happiness can be used for evil purposes. But a will that acts from duty — that acts because doing so is right, regardless of inclination or consequence — is good unconditionally.
This distinguishes acting *in accordance with* duty (which a prudent merchant might do for self-interested reasons) from acting *from* duty (the only source of genuine moral worth).
## The Categorical Imperative
Kant argues that all genuine moral requirements are *categorical* imperatives — commands that apply unconditionally, regardless of one's desires. "Pay your debts" is categorical: it applies whether or not you want to, whether or not it benefits you. By contrast, "If you want to be trusted, pay your debts" is a *hypothetical* imperative, binding only if you have the relevant desire.
Kant offers three principal formulations of the categorical imperative, claiming they are equivalent:
### Formula of Universal Law (FUL)
> "Act only according to that maxim whereby you can at the same time will that it should become a universal law."
To test whether an action is permissible, extract the maxim (underlying principle) of the action and ask: could I consistently will that everyone act on this maxim? The classic example is lying promises. My maxim: "When in financial difficulty, I will make a false promise to repay a loan." If universalised, the institution of promising collapses — no one would believe promises. The maxim is self-defeating when universalised.
### Formula of Humanity (FH)
> "Act so that you treat humanity, whether in your own person or in that of another, always as an end and never as a means only."
Persons have *dignity* — a value beyond all price. Using someone merely as an instrument for your purposes violates their status as a rational, self-legislating agent. This formula generates more intuitive verdicts than FUL in many cases and grounds a robust conception of human rights.
### Formula of the Kingdom of Ends (FKE)
> "Act according to maxims of a universally legislating member of a merely possible kingdom of ends."
The moral community is a hypothetical kingdom of rational agents who legislate universal laws for themselves and for all. Each person is both subject to and author of the moral law.
## Perfect and Imperfect Duties
Kant distinguishes **perfect duties** (negative, admitting no exceptions) from **imperfect duties** (positive, allowing latitude in how they are fulfilled).
- *Perfect duties*: Do not murder. Do not lie. Do not make false promises. These admit no exceptions.
- *Imperfect duties*: Develop your talents. Help others in need. We must pursue these ends, but have discretion in how.
## The Formula of Universal Law: Applications
Kant tests four cases:
1. **Suicide to escape suffering** — The maxim of self-destruction from self-love contradicts itself when universalised (life-preserving instinct cannot simultaneously mandate destroying life).
2. **False promises** — Universalised, this destroys the institution of promising.
3. **Neglecting one's talents** — Although we can consistently will a world where all neglect their talents, we cannot *rationally* will such a world as members who might need others' developed capacities.
4. **Refusing to aid others** — We cannot rationally will a world with no mutual aid, since we might need it ourselves.
## Objections to Kantian Ethics
### The Problem of Conflicting Duties
What if telling the truth would lead to murder? The notorious example: a murderer asks you where your friend is hiding. Kant's strict application of FUL seems to require telling the truth.^[Kant, I. (1797). "On a Supposed Right to Lie from Philanthropy."] Most critics find this conclusion intolerable. Defenders argue Kant was wrong to apply his own theory in this case.
### The Formalism Objection (Hegel)
Hegel objected that the categorical imperative is empty — too formal to generate determinate moral content. Almost any maxim can be made consistent with FUL through reformulation.^[Hegel, G.W.F. (1821). *Philosophy of Right*, §135.]
### The Rigorism Objection
The absolute prohibition on lying, even to prevent serious harm, seems morally obtuse. A moral theory that ignores consequences entirely cannot be adequate.
### The Humanity Formula and Its Scope
Does FH extend to animals? Kant seems to deny that animals have dignity (since they lack rationality), but this generates counterintuitive implications about the permissibility of animal cruelty.
## Neo-Kantian Developments
**Christine Korsgaard** grounds Kantian ethics in the structure of reflective self-consciousness. When we act, we implicitly endorse a principle. Practical identity — the source of all our obligations — commits us to valuing humanity as an end.^[Korsgaard, C. (1996). *Sources of Normativity*. Cambridge University Press.]
**Thomas Scanlon's contractualism**: An act is wrong if its performance under the circumstances would be disallowed by any set of principles that no one could reasonably reject.^[Scanlon, T.M. (1998). *What We Owe to Each Other*. Harvard University Press.] This grounds moral requirements in what we owe to each other as persons — a broadly Kantian spirit without the metaphysical apparatus.
**W.D. Ross** introduced the concept of *prima facie* duties — duties that are binding unless overridden by stronger competing duties in a given situation. Fidelity, gratitude, non-maleficence, beneficence, and justice are among them. This pluralist deontology avoids the single-minded rigour of Kant while preserving the idea that some actions have moral weight independent of consequences.^[Ross, W.D. (1930). *The Right and the Good*. Oxford University Press.]
## Further Reading
- Korsgaard, C. (1996). *Creating the Kingdom of Ends*. Cambridge University Press.
- O'Neill, O. (1989). *Constructions of Reason*. Cambridge University Press.
- Herman, B. (1993). *The Practice of Moral Judgment*. Harvard University Press.

View file

@ -0,0 +1,82 @@
---
title: Virtue Ethics
sort: 130
section-id: ethics
description: Aristotle's eudaimonia, the virtues, the doctrine of the mean, and contemporary neo-Aristotelian revival in moral philosophy.
language: en
---
# Virtue Ethics
Virtue ethics shifts the primary question of moral theory from "What should I do?" to "What kind of person should I be?" Rather than specifying rules or calculating consequences, virtue ethics focuses on the character traits — the *virtues* — that constitute human excellence and the good life.
## Aristotle's Ethics
The foundational text is Aristotle's *Nicomachean Ethics* (ca. 350 BCE).^[Aristotle. *Nicomachean Ethics*. Trans. Irwin, Hackett, 1999.] Aristotle begins with the observation that every action aims at some good. The highest good — the good for its own sake — he calls *eudaimonia*, usually translated as "happiness" but better rendered as *flourishing* or *living well*.
Eudaimonia is not a feeling but an *activity*: the activity of the soul in accordance with virtue (*arete*). It is not a momentary state but characterises a complete life.
## The Function Argument
Aristotle argues that just as a knife has a function (cutting) and a good knife fulfils its function excellently, human beings have a characteristic function. The human function, unique among animals, is rational activity. *Eudaimonia* is the excellent exercise of our rational capacities.^[*NE* I.7, 1097b241098a20.]
This *ergon* (function) argument has been criticised for assuming that humans have a single, discoverable function. Nonetheless, it provides the teleological framework within which the virtues are defined.
## The Doctrine of the Mean
Virtues are stable dispositions of character that enable us to respond appropriately to situations. They are acquired through habituation: we become courageous by practising courageous acts. The virtuous person does not merely act rightly but does so with pleasure and without painful struggle — virtue has been fully internalised.
Each virtue is a **mean** (*mesotes*) between two extremes — excess and deficiency. Courage is the mean between cowardice (deficiency of boldness) and rashness (excess). Generosity lies between miserliness and prodigality. The mean is not arithmetically fixed but *relative to us* — what counts as appropriate depends on context and the individual.
| Excess | Virtue | Deficiency |
|---|---|---|
| Rashness | Courage | Cowardice |
| Prodigality | Generosity | Miserliness |
| Vanity | Magnanimity | Pusillanimity |
| Obsequiousness | Friendliness | Quarrelsomeness |
| Buffoonery | Wit | Boorishness |
## Practical Wisdom: Phronesis
The master virtue in Aristotle's scheme is *phronesis* — practical wisdom. The person of practical wisdom perceives what a situation requires, deliberates well about how to act, and acts accordingly. Practical wisdom is not reducible to following rules; it requires experience, perception, and judgment.
This distinguishes virtue ethics from rule-based approaches: no finite set of rules can capture what the practically wise person knows. The virtuous person's perceptions and responses are *constitutive* of right action, not mere applications of antecedent principles.
## The Unity of the Virtues
Aristotle holds that the virtues are unified: one cannot genuinely have any virtue without practical wisdom, and practical wisdom requires all the virtues. This *unity thesis* is controversial — it seems possible to be courageous but unjust. Defenders argue that only with the full integration of virtues does one have the "complete" versions; partial virtues are mere natural tendencies, not fully-fledged character traits.
## The Neo-Aristotelian Revival
Virtue ethics experienced a significant revival in twentieth-century analytic philosophy, partly as a reaction to the perceived limitations of both consequentialism and deontology.
**G.E.M. Anscombe** (1958) argued that concepts like "moral obligation" and "duty" are residues of a divine-law framework that has been abandoned; without God, they are incoherent. We should return to Aristotelian concepts of virtue, human nature, and flourishing.^[Anscombe, G.E.M. (1958). "Modern Moral Philosophy." *Philosophy*, 33(124).]
**Philippa Foot** developed a naturalistic virtue ethics grounding virtues in what is good for humans as the kind of organisms we are. *Natural goodness* is a matter of the proper functioning of organisms of a particular natural kind.^[Foot, P. (2001). *Natural Goodness*. Oxford University Press.]
**Alasdair MacIntyre** (*After Virtue*, 1981) argued that contemporary moral discourse is fragmented and incoherent because we have lost the teleological framework within which virtue concepts made sense. The Enlightenment project of grounding morality in individual reason or sentiment was doomed to fail. We need to recover Aristotelian tradition — structured around practices, narrative, and community — to make ethics intelligible.^[MacIntyre, A. (1981). *After Virtue*. University of Notre Dame Press.]
## Contemporary Virtue Ethics
**Rosalind Hursthouse** has developed a virtue-theoretic account of right action: an action is right if it is what a virtuous person would characteristically do in the circumstances.^[Hursthouse, R. (1999). *On Virtue Ethics*. Oxford University Press.] This need not be circular: virtuous persons are those with character traits that constitute human flourishing, and we can characterise flourishing independently.
**Michael Slote** defends agent-based virtue ethics, grounding moral evaluation entirely in the motivational states of agents rather than objective human flourishing.
**Julia Annas** argues that virtue ethics is best understood not as a rival to rule-following but as an account of how we internalise moral requirements through the development of character.
## Objections to Virtue Ethics
### Action Guidance
Critics allege that virtue ethics provides insufficient guidance: when I face a difficult choice, "act as a virtuous person would" tells me little unless I already know what virtue requires. The response is that moral life is not primarily about making hard decisions but about forming character — and character provides guidance of a richer kind than any algorithm.
### Cultural Relativism
If virtues are defined relative to a human *telos* and that *telos* varies across cultures, different cultures will have different, potentially incompatible, lists of virtues. MacIntyre acknowledges this but argues that tradition-internal reasoning can achieve cross-traditional rational dialogue.
### The Problem of the Selfish Gene
If we are products of natural selection, and selection favours genes that promote reproductive fitness, then "human nature" is not a stable, rationally accessible guide to flourishing. The naturalistic programme of Foot and Hursthouse faces this challenge.
## Further Reading
- Annas, J. (2011). *Intelligent Virtue*. Oxford University Press.
- Crisp, R. and Slote, M. (eds.) (1997). *Virtue Ethics*. Oxford University Press.
- Williams, B. (1985). *Ethics and the Limits of Philosophy*. Fontana.

View file

@ -0,0 +1,72 @@
---
title: Applied Ethics
sort: 140
section-id: ethics
description: Ethical theory in practice — bioethics, AI ethics, environmental ethics, and the methodology of applying philosophical principles to real-world problems.
language: en
---
# Applied Ethics
Applied ethics brings philosophical theory to bear on concrete moral problems — questions arising in medicine, technology, environmental policy, business, and law. The movement gained momentum in the 1960s and 1970s, driven partly by rapid advances in medicine and biotechnology that created novel moral dilemmas for which traditional frameworks offered insufficient guidance.
## The Methodology of Applied Ethics
Applied ethics is not mere application of theory to cases, as if ethical systems were algorithms waiting to be run. Several methodological approaches exist:
**Top-down application**: Begin with an ethical theory (utilitarian, Kantian), derive principles, apply to cases. The limitation is that theories are contested; disagreement about foundations propagates into applied questions.
**Case-based reasoning (casuistry)**: Begin with clear, paradigm cases where moral judgement is confident, then reason analogically to harder cases. Associated with clinical ethics consultation. The limitation is that paradigm cases must eventually be justified by something more than intuition.
**Reflective equilibrium**: Move iteratively between principles and case judgements, revising both until achieving coherence. The approach most widely used in practice.
**Specification**: Take general principles (do not harm, respect autonomy) and progressively specify them to handle particular cases without derivation from a complete theory.
## Bioethics
Bioethics addresses moral questions arising in medicine and biological research. The *Georgetown mantra* — four principles identified by Beauchamp and Childress — has become the dominant framework in clinical practice:^[Beauchamp, T. and Childress, J. (2019). *Principles of Biomedical Ethics*, 8th ed. Oxford University Press.]
1. **Autonomy** — Respect for the patient's self-determination. Informed consent is the operational expression of this principle. Patients with decision-making capacity may refuse treatment, even life-saving treatment.
2. **Beneficence** — Act in the patient's best interests. Not merely "do no harm" but actively promote welfare.
3. **Non-maleficence***Primum non nocere* (first, do no harm). Avoid imposing risks disproportionate to benefits.
4. **Justice** — Fair distribution of benefits, risks, and burdens. Includes both procedural fairness and distributive justice in healthcare resource allocation.
### End-of-Life Ethics
The moral permissibility of assisted dying — physician-assisted suicide (PAS) and voluntary euthanasia — is among the most contested issues in bioethics. Arguments in favour appeal to autonomy: competent patients should determine the manner and timing of their deaths. Arguments against cite concerns about palliative care, the potential for coercion, and the symbolic significance of medical killing for the doctor-patient relationship.
Peter Singer and James Rachels defend active euthanasia, arguing that the distinction between killing and letting die is morally irrelevant when intentions and outcomes are the same.^[Rachels, J. (1975). "Active and Passive Euthanasia." *New England Journal of Medicine*, 292(2).] Opponents invoke the doctrine of double effect and the integrity of the medical profession.
### Research Ethics
The Nuremberg Code (1947) and the Declaration of Helsinki (1964) emerged from scandals of medical experimentation on non-consenting subjects. Core requirements: voluntary informed consent, scientific validity, favourable risk-benefit ratio, and independent ethical review. The Belmont Report (1979) added justice as a requirement — the burdens and benefits of research must be distributed fairly.
## AI and Technology Ethics
The rapid development of artificial intelligence creates a new domain for applied ethics. Key issues include:
**Algorithmic bias**: Machine learning systems trained on historical data can encode and amplify existing discriminatory patterns. A loan approval algorithm trained on historical lending data may perpetuate racial discrimination without any discriminatory intent. Fairness criteria (demographic parity, equalised odds, calibration) are mathematically incompatible in general — we cannot satisfy all simultaneously.^[Chouldechova, A. (2017). "Fair Prediction with Disparate Impact." *Big Data*, 5(2).]
**Autonomous systems**: When an autonomous vehicle must choose between killing one pedestrian or five, how should it be programmed? Trolley-problem style dilemmas in algorithmic form raise questions about whether utilitarian calculus should be codified into machines, and who bears moral responsibility for automated decisions.
**AI consciousness and moral status**: If future AI systems develop something like sentience or interests, do they merit moral consideration? The philosophical difficulty of consciousness (see the chapter on philosophy of mind) makes this question genuinely hard.
**Privacy and surveillance**: The collection of vast personal data by states and corporations raises questions about informational privacy as an aspect of autonomy and dignity. Nissenbaum's concept of *contextual integrity* — information flows respect privacy when they match the norms of the context in which they were generated — provides a useful framework.
## Environmental Ethics
Traditional ethics is anthropocentric — it recognises moral obligations only to persons. Environmental ethics asks: do non-human animals, species, ecosystems, or nature as a whole have intrinsic moral value?
**Animal ethics**: Peter Singer's utilitarian case for animal liberation grounds obligations in the capacity for suffering: if pain is bad for humans, it is bad for pigs, who suffer equally.^[Singer, P. (1975). *Animal Liberation*. New York Review Books.] Tom Regan's rights-based account argues that animals who are "subjects of a life" — with beliefs, desires, and a welfare — have inherent value that may not be traded off against aggregate utility.^[Regan, T. (1983). *The Case for Animal Rights*. University of California Press.]
**Biocentric ethics** (Paul Taylor): Every living organism has a good of its own that commands moral respect. We have prima facie duties not to harm living things, override-able only for weighty reasons.^[Taylor, P. (1986). *Respect for Nature*. Princeton University Press.]
**Ecocentric ethics**: Aldo Leopold's *land ethic* — "A thing is right when it tends to preserve the integrity, stability, and beauty of the biotic community" — extends moral consideration to ecosystems and species, not just individuals.^[Leopold, A. (1949). *A Sand County Almanac*. Oxford University Press.]
**Climate ethics** raises questions about intergenerational justice (obligations to future persons), international justice (who bears the costs of mitigation), and the ethics of geoengineering.
## Further Reading
- Rachels, J. and Rachels, S. (2019). *The Elements of Moral Philosophy*, 9th ed. McGraw-Hill.
- Jamieson, D. (2014). *Reason in a Dark Time: Why the Struggle Against Climate Change Failed*. Oxford University Press.
- Floridi, L. (ed.) (2015). *The Onlife Manifesto*. Springer.

View file

@ -0,0 +1,73 @@
---
title: Political Philosophy
sort: 150
section-id: ethics
description: Rawls, Nozick, communitarianism, and contemporary debates about justice, liberty, and the legitimate authority of the state.
language: en
---
# Political Philosophy
Political philosophy investigates the normative foundations of political institutions: the state, law, political authority, rights, and justice. Its central questions include: What justifies political authority? What makes a distribution of benefits and burdens just? What are the limits of individual liberty? What do citizens owe one another?
## The Social Contract Tradition
The dominant tradition in modern political philosophy grounds political authority in a *social contract* — an actual or hypothetical agreement among individuals to establish political institutions. The tradition includes Hobbes, Locke, and Rousseau, but it is John Rawls who gave it its most sophisticated contemporary form.
### Hobbes
Thomas Hobbes (1651) argued that without political authority, life would be "solitary, poor, nasty, brutish, and short."^[Hobbes, T. (1651). *Leviathan*, Ch. XIII.] Rational agents in the state of nature would contract into an absolute sovereign to secure peace. Hobbes's argument is primarily consequentialist in structure: sovereignty is justified by the order it creates.
### Locke
John Locke (1689) grounded political authority in natural rights — rights to life, liberty, and property that individuals possess prior to and independently of the state. Government is legitimate only if it protects these rights; when it systematically violates them, citizens have a right of revolution. Locke's theory provides the philosophical basis for liberal constitutionalism.^[Locke, J. (1689). *Two Treatises of Government*, Second Treatise.]
## Rawls's Theory of Justice
John Rawls (*A Theory of Justice*, 1971) represents the most influential work in twentieth-century political philosophy. Rawls aims to identify principles of justice that free and rational persons would accept in an original position of equality.
### The Original Position and the Veil of Ignorance
The *original position* is a hypothetical decision procedure. We imagine choosing principles of justice behind a *veil of ignorance*: we do not know our place in society, our class position, our natural abilities, our conception of the good, or the generation we belong to. This ensures impartiality — no one can tailor principles to benefit their particular position.^[Rawls, J. (1971). *A Theory of Justice*. Harvard University Press, §3.]
From behind the veil, Rawls argues, rational agents would choose two principles:
1. **The Equal Liberty Principle**: Each person is to have an equal right to the most extensive system of equal basic liberties compatible with a similar system of liberty for all.
2. **The Difference Principle**: Social and economic inequalities are to be arranged so that they are: (a) attached to offices and positions open to all under fair equality of opportunity, and (b) to the greatest benefit of the least advantaged members of society.
The principles are *lexically ordered*: the first has absolute priority over the second. Basic liberties cannot be traded off against economic gains.
### The Difference Principle
The Difference Principle is Rawls's most distinctive and controversial contribution. It permits inequalities only if they maximally benefit the worst-off group. This *maximin* strategy — maximise the minimum position — is what rational agents under uncertainty would choose, according to Rawls.
The argument: since I do not know whether I will be advantaged or disadvantaged, and since the stakes (basic life prospects) are very high, rationality demands choosing the arrangement that makes the worst-case scenario as good as possible.
## Nozick's Libertarianism
Robert Nozick (*Anarchy, State, and Utopia*, 1974) offers a forceful alternative.^[Nozick, R. (1974). *Anarchy, State, and Utopia*. Basic Books.] Beginning from strong Lockean natural rights — that individuals may not be used against their will as means to others' ends — Nozick argues that only a minimal state (limited to protecting against force and fraud) is justified.
Any more extensive state violates individual rights. Redistributive taxation, for Nozick, is morally equivalent to forced labour: it takes the product of a person's labour and transfers it to others without consent.
Nozick's *entitlement theory* of justice: a distribution is just if it arises from just original acquisitions and just transfers. Historical process, not distributional pattern, determines justice. No patterned principle — whether egalitarian or utilitarian — can be maintained without continuous interference with free exchange.
### Wilt Chamberlain Argument
Nozick's celebrated argument: suppose we start from any distribution D1 you consider just. Wilt Chamberlain, a basketball star, charges fans 25 cents per game to see him play. One million fans freely pay. Now Chamberlain has $250,000 more than D1 allowed. Is D2 unjust? But it arose through voluntary exchanges from a just starting point. Any patterned theory must continuously prohibit voluntary exchanges — and this is incompatible with liberty.
## Communitarianism
In the 1980s, a group of philosophers — Michael Sandel, Charles Taylor, Alasdair MacIntyre, and Michael Walzer — challenged liberalism's conception of the self and the priority it gives to justice over the good.
**Sandel's critique of the unencumbered self**: Rawls's original position presupposes a self that is prior to and independent of its ends and social roles. But this is incoherent: we cannot abstract ourselves from the constitutive attachments and community memberships that make us who we are. A more adequate political philosophy would recognise that we are *embedded* in communities whose values and traditions define our identities.^[Sandel, M. (1982). *Liberalism and the Limits of Justice*. Cambridge University Press.]
**Walzer's complex equality**: Justice requires that different social goods — money, political power, medical care, education — be distributed according to their own internal norms, not reduced to a single metric. Injustice is not inequality per se but *dominance*: when one good (typically money) is used to control access to all others.^[Walzer, M. (1983). *Spheres of Justice*. Basic Books.]
## Global Justice
Rawls's *The Law of Peoples* (1999) applied his framework internationally, but controversially limited global distributive obligations to "duty of assistance" to "burdened societies." Cosmopolitan theorists (Thomas Pogge, Charles Beitz) argue that global economic institutions impose injustice on the world's poor, generating stringent obligations to reform them.^[Pogge, T. (2002). *World Poverty and Human Rights*. Polity Press.]
The debate between Rawlsian nationalism and cosmopolitanism turns on whether Rawlsian principles apply only within cooperative schemes (nation-states) or to all human beings as such.
## Further Reading
- Freeman, S. (2007). *Justice and the Social Contract*. Oxford University Press.
- Cohen, G.A. (2008). *Rescuing Justice and Equality*. Harvard University Press.
- Kymlicka, W. (2002). *Contemporary Political Philosophy*, 2nd ed. Oxford University Press.

View file

@ -0,0 +1,136 @@
---
title: Further Reading
sort: 110
section-id: conclusion
description: Annotated bibliography organised by chapter, with commentary on essential secondary texts and resources for continued study.
language: en
---
# Further Reading
This annotated bibliography is organised by chapter. For each topic, the most accessible introductory texts are listed first, followed by more advanced or specialised works. All items marked **[Core]** are considered essential reading; unmarked items represent productive next steps for those wishing to go deeper.
---
## Part I: Epistemology
### What is Knowledge?
**[Core]** Gettier, E.L. (1963). "Is Justified True Belief Knowledge?" *Analysis*, 23(6), 121123. — Three pages that changed epistemology. Required reading.
**[Core]** Chisholm, R. (1977). *Theory of Knowledge*, 2nd ed. Prentice Hall. — The classic textbook on epistemological foundations.
Zagzebski, L. (1994). "The Inescapability of Gettier Problems." *Philosophical Quarterly*, 44(174), 6573. — Shows the structural depth of the problem.
Williamson, T. (2000). *Knowledge and Its Limits*. Oxford University Press. — Defends knowledge as a prime epistemic concept; difficult but rewarding.
### Perception and Reality
**[Core]** Ayer, A.J. (1956). *The Problem of Knowledge*. Penguin. — Accessible and wide-ranging.
Dancy, J. (1985). *Introduction to Contemporary Epistemology*. Blackwell. — Chapter 6 on perception is particularly good.
McDowell, J. (1994). *Mind and World*. Harvard University Press. — Demanding but essential for understanding the conceptualism debate.
### Scepticism
**[Core]** Descartes, R. (1641). *Meditations on First Philosophy*. — The primary source; any good translation suffices.
Stroud, B. (1984). *The Significance of Philosophical Scepticism*. Oxford University Press. — Why scepticism cannot be easily dismissed.
DeRose, K. (2009). *The Case for Contextualism*. Oxford University Press.
### Truth
**[Core]** Horwich, P. (1998). *Truth*, 2nd ed. Oxford University Press. — Deflationary theory, clearly argued.
Lynch, M. (2009). *Truth as One and Many*. Oxford University Press. — Pluralist theory.
---
## Part II: Metaphysics
### Existence and Ontology
**[Core]** Quine, W.V.O. (1948). "On What There Is." *Review of Metaphysics*, 2(5). — The classic statement of Quinean ontology.
**[Core]** van Inwagen, P. (1998). "Meta-Ontology." *Erkenntnis*, 48(23), 233250.
Thomasson, A. (2015). *Ontology Made Easy*. Oxford University Press. — Deflationary approach; valuable counterpoint to heavyweight ontology.
### Identity and Persistence
**[Core]** Parfit, D. (1984). *Reasons and Persons*, Part III. Oxford University Press. — The most influential modern treatment.
Lewis, D. (1976). "Survival and Identity." In Rorty, A. (ed.), *The Identities of Persons*. Berkeley.
Olson, E. (1997). *The Human Animal*. Oxford University Press. — Animalist view.
### Free Will
**[Core]** Kane, R. (1996). *The Significance of Free Will*. Oxford University Press. — Best defence of libertarianism.
**[Core]** Frankfurt, H. (1969). "Alternate Possibilities and Moral Responsibility." *Journal of Philosophy*, 66(23). — Frankfurt cases; only five pages, transformative.
Strawson, P.F. (1962). "Freedom and Resentment." *Proceedings of the British Academy*, 48. — Foundational compatibilist paper.
Fischer, J.M. and Ravizza, M. (1998). *Responsibility and Control*. Cambridge University Press.
### Philosophy of Mind
**[Core]** Nagel, T. (1974). "What Is It Like to Be a Bat?" *Philosophical Review*, 83(4). — The classic statement of the explanatory gap.
**[Core]** Chalmers, D. (1996). *The Conscious Mind*. Oxford University Press. — Comprehensive case for the hard problem.
Dennett, D. (1991). *Consciousness Explained*. Little, Brown. — The physicalist response; readable and provocative.
Jackson, F. (1986). "What Mary Didn't Know." *Journal of Philosophy*, 83(5). — Knowledge argument in five pages.
---
## Part III: Ethics
### Metaethics
**[Core]** Mackie, J.L. (1977). *Ethics: Inventing Right and Wrong*. Penguin. — Error theory; accessible and well-argued.
Blackburn, S. (1998). *Ruling Passions*. Oxford University Press. — Best recent defence of quasi-realism.
Enoch, D. (2011). *Taking Morality Seriously*. Oxford University Press. — Strong defence of robust moral realism.
### Consequentialism
**[Core]** Mill, J.S. (1863). *Utilitarianism*. — Short; read in an afternoon; annotated editions recommended.
**[Core]** Singer, P. (1979). *Practical Ethics*. Cambridge University Press. — Applies utilitarian reasoning to live issues.
Parfit, D. (1984). *Reasons and Persons*, Part IV. — "Repugnant Conclusion" and population ethics; essential.
### Deontological Ethics
**[Core]** Kant, I. (1785). *Groundwork of the Metaphysics of Morals*. Trans. Korsgaard. Cambridge UP. — Use Korsgaard's translation and commentary.
**[Core]** Ross, W.D. (1930). *The Right and the Good*, Chs. 12. Oxford University Press.
Scanlon, T.M. (1998). *What We Owe to Each Other*. Harvard University Press. — Contractualist deontology; rich and rewarding.
### Virtue Ethics
**[Core]** Aristotle. *Nicomachean Ethics*, Books III, X. — Any good translation.
**[Core]** Hursthouse, R. (1999). *On Virtue Ethics*. Oxford University Press.
MacIntyre, A. (1981). *After Virtue*. University of Notre Dame Press. — Polemical and influential.
### Political Philosophy
**[Core]** Rawls, J. (1971). *A Theory of Justice*. Harvard University Press. — Read at minimum Part I.
**[Core]** Nozick, R. (1974). *Anarchy, State, and Utopia*. Basic Books. — Especially Ch. 7 on distributive justice.
Kymlicka, W. (2002). *Contemporary Political Philosophy*, 2nd ed. Oxford University Press. — Best survey text.
---
## General Philosophy Reference
**Stanford Encyclopedia of Philosophy** (plato.stanford.edu) — Freely available online; peer-reviewed, regularly updated. An invaluable first resource for any philosophical topic.
**[Core]** Blackburn, S. (1996). *Oxford Dictionary of Philosophy*, 3rd ed. Oxford University Press. — Concise, reliable reference for key terms.
Craig, E. (ed.) (1998). *Routledge Encyclopedia of Philosophy*. — 10-volume scholarly reference.
---
## On Reading Philosophy
Philosophical texts reward rereading. A text that seems clear at first glance often conceals assumptions that become visible only on the third or fourth reading. Keep a running list of assumptions, note where each argument depends on undefended premises, and always ask: what would need to be true for this argument to fail?
Discussion and disagreement are essential. Read with a philosophical friend or in a seminar. The objections you make and receive in conversation will teach you more than any further reading.

View file

@ -0,0 +1,63 @@
---
title: How to Use This Book
sort: 110
section-id: front-matter
description: Reading guide, chapter dependencies, glossary note, and further reading approach.
language: en
---
![A well-stocked research library](assets/images/library.jpg)
# How to Use This Book
This book is designed to be accessible to readers coming from different starting points and with different purposes. What follows is a brief guide to getting the most from it.
## Sequence and Structure
The book is organised into three parts — Epistemology, Metaphysics, and Ethics — each comprising six chapters. The parts are designed to be read in order, since later discussions often presuppose earlier material. The epistemology chapters, for instance, establish vocabulary and conceptual distinctions (justification, a priori knowledge, reliabilism, coherentism) that recur throughout the metaphysics and ethics discussions.
Within each part, the chapters proceed from foundational questions toward more specific and applied ones. In epistemology, we begin with the basic analysis of knowledge before examining specific faculties (perception, reason) and specific challenges (scepticism). In ethics, we examine the foundations of moral inquiry before turning to the major normative theories and then applied questions.
That said, the book is cross-referenced and many chapters can be read independently by a reader who already has some background. The chapter on free will and determinism (Part II, Chapter 4) can be read alongside the epistemology chapter on scepticism and the ethics chapter on moral responsibility, and is usefully paired with the applied ethics chapter's discussion of criminal justice. The chapter on philosophy of mind (Part II, Chapter 5) is closely connected to the epistemology discussion of perception and knowledge.
## Chapter Structure
Each chapter follows a common pattern:
1. **Introduction** — orienting the problem, explaining why it matters
2. **Main positions** — surveyed with their central arguments
3. **Key arguments and objections** — examined with care
4. **Connections and implications** — how the debate bears on other questions
5. **Inline footnotes** — key citations in the form `^[Author, Year, p.X]`
I have used inline footnotes throughout rather than endnotes. Philosophy students need to learn to read with citations — to understand that positions have sources, that arguments have authors, and that intellectual honesty requires acknowledging where ideas come from.
## The Primary Sources
This is an introductory text. It cannot substitute for reading primary sources, and it is not intended to. The goal is to prepare you to read Descartes, Hume, Kant, and their successors with understanding — to give you the context and vocabulary to follow their arguments, so that when you turn to the *Meditations* or the *Enquiry* or the *Groundwork*, you know what question you are supposed to be thinking about.
At the end of the book you will find an annotated bibliography organised by chapter. Each entry includes a note on what the text contributes and who it is most suitable for. Use it as a starting point for primary reading, not as a substitute for it.
## A Note on Difficulty
Philosophy is hard. It requires holding complex arguments in memory while evaluating their steps, tracking distinctions that initially seem subtle and become crucial, and maintaining intellectual patience through periods of genuine uncertainty. This is not a reason to find the difficulty discouraging — it is a reason to take it seriously.
When you find yourself confused, the first question to ask is not "Am I misunderstanding the argument?" but "Is the argument actually this hard?" Sometimes the answer to the second question is yes, and the apparent confusion reflects something genuinely difficult in the material. At other times, a re-reading or a change of perspective resolves things.
It is worth keeping notes as you read — writing down the main claim of each section, the central argument, and your own questions and objections. Philosophy is better done with a pen in hand than as a purely passive activity.
## On Philosophical Writing
Students who will be writing essays in philosophy should note that philosophical writing is argument, not assertion. The task is not to state what you believe but to give reasons for believing it, to consider and respond to objections, and to be honest about what you do not yet know.
Good philosophical writing is clear, precise, and fair to opposing views. It uses technical vocabulary accurately — not to impress, but because precision matters and ordinary language is often insufficiently precise for philosophical work. It acknowledges the difficulty of the questions rather than pretending to more certainty than is warranted.
These are not stylistic preferences. They are requirements of intellectual honesty, and they are what distinguish good philosophical writing from merely asserting things confidently.
## Glossary
Technical terms are defined when first introduced and collected in the index with page references. Key terms include: *a priori*, *a posteriori*, *analytic*, *synthetic*, *justified true belief*, *internalism*, *externalism*, *substance*, *property*, *supervenience*, *compatibilism*, *libertarianism* (in the metaphysical sense), *hard determinism*, *consequentialism*, *deontology*, *virtue ethics*, *metaethics*, *normative ethics*.
Do not be discouraged if these terms initially seem arbitrary. They acquire meaning through the arguments that use them, and they become tools rather than obstacles once you have enough context to understand why the distinctions they mark are important.
Good reading.

View file

@ -0,0 +1,55 @@
---
title: Existence and Being
sort: 100
section-id: metaphysics
description: Ontology basics, Quine's criterion of ontological commitment, existence as a predicate, and Meinong's jungle.
language: en
---
# Existence and Being
Metaphysics is the study of the most fundamental features of reality — what exists, what kinds of things exist, and what it is for something to exist at all. Ontology, its central division, addresses the question: *what is there?*
The question sounds trivially answerable: there is everything there is, and nothing else. Quine gave this answer in a sentence: "To be is to be the value of a variable" ^[Quine, W.V.O., "On What There Is", *Review of Metaphysics* 2, 1948]. But behind this slogan lies a substantial and contested philosophical methodology.
## Quine's Criterion of Ontological Commitment
Quine argued that the ontological commitments of a theory are revealed by *regimentation* — translating the theory into first-order predicate logic and examining what the variables in the existential quantifiers must range over ^[Quine, W.V.O., "On What There Is"; see also *Word and Object*, MIT Press, 1960, ch.7].
If a true theory says "There are prime numbers between 10 and 20," and the best regimentation of this claim quantifies over numbers, then the theory is committed to the existence of numbers. One cannot simultaneously assert a theory and deny the existence of entities the theory quantifies over.
The Quinean approach transformed ontology from a speculative metaphysical exercise into a semi-technical discipline: the question "Does X exist?" becomes "Does our best overall theory of the world require quantifying over Xs?" This is ontology anchored to science.
**The criterion of ontological commitment** is: a theory is ontologically committed to those entities that, when the theory is put in canonical notation, the bound variables must range over if the theory is true.
## Existence as a Predicate
Can existence be predicated of individuals? Kant famously argued that existence is not a real predicate — it adds nothing to the concept of a thing ^[Kant, I., *Critique of Pure Reason*, A598/B626]. "God is omnipotent" attributes a property; "God exists" does not attribute a property but rather asserts that the concept of God is instantiated.
This view is embedded in the standard logical treatment: in first-order predicate logic, existence is expressed by the existential quantifier (∃x), not by a predicate. "Tigers exist" becomes "there is at least one thing that is a tiger," not "tigers have the property of existence."
If existence is not a predicate, the ontological argument for God's existence — which treats existence as a perfection that maximally great beings must have — fails at the point of treating existence as a property that can be possessed in greater or lesser degree.
## Meinong and Non-Existent Objects
Alexius Meinong argued that the domain of objects is wider than the domain of existents ^[Meinong, A., "On the Theory of Objects", 1904]. There are objects — the golden mountain, the round square, Sherlock Holmes — that do not exist. These non-existent objects nonetheless have properties: the golden mountain is golden and mountainous; the round square is both round and square (and therefore impossible); Sherlock Holmes lived at 221B Baker Street.
Meinong's *Theory of Objects* distinguishes *existence* (the mode of being of concrete things), *subsistence* (the mode of being of abstract objects like numbers and propositions), and mere *Sosein* (having a nature, without any mode of being). Non-existent objects have Sosein without existence.
Russell objected vigorously to this "Meinongian jungle" of non-existent objects, calling it "a failure of that robust sense of reality which ought to be preserved even in the most abstract studies" ^[Russell, B., "Review of Meinong", *Mind* 14, 1905, p.533]. His theory of definite descriptions was designed to handle sentences about non-existents without ontological commitment to them.
## Russell's Theory of Descriptions
Russell's theory of descriptions analyses sentences of the form "The F is G" as: there is exactly one F, and that F is G ^[Russell, B., "On Denoting", *Mind* 14, 1905, pp.479-493]. "The present King of France is bald" is false because there is no present King of France — the uniqueness condition fails. We do not need to posit a non-existent King of France; we only need to recognise that the sentence has a false existential presupposition.
This "logical paraphrase" strategy — finding analyses of problematic sentences that avoid commitment to suspect entities — became a central tool of analytic philosophy. Ockham's razor ("Do not multiply entities beyond necessity") is the methodological principle: ontological economy is a theoretical virtue.
## Contemporary Debates
**Metaontology** — the study of what ontological questions mean and how they should be answered — has become a major area. Carnap distinguished *internal* questions (do numbers exist, within the mathematical framework?) from *external* questions (does the mathematical framework correspond to reality?) and argued that external questions are pragmatic, not factual ^[Carnap, R., "Empiricism, Semantics and Ontology", *Revue Internationale de Philosophie* 4, 1950]. Quine rejected this distinction; contemporary metaontologists debate whether it can be rehabilitated.
**Ontological pluralism** — the view that existence itself is not univocal, but that there are different "modes of being" — has been defended by Kris McDaniel and others, reviving something like the Meinongian project with better tools ^[McDaniel, K., *The Fragmentation of Being*, Oxford UP, 2017].
**Truthmaker theory** holds that truths are made true by features of reality, and asks what those features are for different classes of truths — mathematical truths, modal truths, moral truths ^[Armstrong, D.M., *Truth and Truthmakers*, Cambridge UP, 2004].
Existence and being may seem like the most abstract possible questions. But they have concrete implications: whether numbers exist bears on the foundations of mathematics; whether moral properties exist bears on the nature of moral knowledge; whether merely possible objects exist bears on modal semantics. Ontology is, as Quine put it, where logic and the world meet.

View file

@ -0,0 +1,55 @@
---
title: Identity and Persistence
sort: 110
section-id: metaphysics
description: The Ship of Theseus, personal identity, and four-dimensionalist theories of persistence.
language: en
---
# Identity and Persistence
What makes a thing the same thing over time? The ship that returned to Athens was rebuilt plank by plank during the voyage; no original material remained. Is it the same ship? The person who wakes tomorrow morning shares your memories and psychology — but not your matter, since your cells replace themselves over years. Are they you?
These are not idle puzzles. They bear on personal survival, moral responsibility, and the metaphysics of change, and their answers connect to fundamental questions about what kinds of things exist.
## The Ship of Theseus
The Ship of Theseus is one of philosophy's oldest thought experiments, recorded by Plutarch ^[Plutarch, *Theseus*, ch.23, c.75 CE]. Its philosophical interest lies not in the historical case but in the structure of the puzzle it reveals: ordinary objects persist through gradual material change, but taken to the limit, material continuity seems to dissolve.
Hobbes added a further twist: suppose someone keeps all the original planks and reassembles them. Which is the original ship — the continuously maintained vessel or the reconstructed one? ^[Hobbes, T., *De Corpore*, 1655, II.xi.7]. The puzzle reveals that "same ship" may be determined by different identity criteria (material continuity, spatial-temporal continuity, functional continuity) that can diverge.
This connects to a broader metaphysical debate about *constitution*: when a lump of clay is shaped into a statue, are there two objects (the lump and the statue) that share matter but have different persistence conditions? Or only one? Defenders of constitution theory say two; critics say one (and find the view of two things in the same place implausible) ^[Wiggins, D., *Sameness and Substance*, Blackwell, 1980; Gibbard, A., "Contingent Identity", *Journal of Philosophical Logic* 4, 1975].
## Personal Identity: The Classical View
John Locke offered the first systematic philosophical analysis of personal identity ^[Locke, J., *Essay*, II.xxvii]. He distinguished the identity of *substance* (a material thing, continuous in matter), *organism* (a living thing, continuous in life), and *person* (a thinking conscious being, continuous in *consciousness*).
Persons, for Locke, persist through *psychological continuity* — specifically, memory. What makes the person who did action A the same person as me is my memory of having done A. This allows persons to come apart from both bodies and souls: the prince and the cobbler might "swap" if their consciousnesses were somehow exchanged.
**Objections:** Bishop Butler accused Locke of circularity: memory presupposes personal identity, so it cannot constitute it ^[Butler, J., *The Analogy of Religion*, 1736, Appendix I]. Thomas Reid's *brave officer paradox* sharpened this: an old general remembers his younger self's bravery as a junior officer, but the officer (flogged as a boy) no longer remembers the boy's actions. By transitivity, the general is not the same person as the boy — but this seems absurd ^[Reid, T., *Essays on the Intellectual Powers of Man*, 1785, III.6].
## Neo-Lockean Theories
Derek Parfit developed the most sophisticated neo-Lockean account ^[Parfit, D., *Reasons and Persons*, Oxford UP, 1984, Part III]. He replaced memory with *psychological continuity* — overlapping chains of psychological connections (memories, intentions, beliefs, desires) — and argued that what matters for survival is not strict identity but this continuity relation.
Parfit's striking conclusion: personal identity is not what matters in survival. In cases of fission (where your psychology is duplicated in two people), neither resulting person is strictly you — but both have what matters as much as survival does. We should care about psychological continuity and connectedness, not about identity itself.
This has radical implications for ethics: if personal identity does not matter in itself, many concerns about future persons — including concerns about one's own future self — are impersonal, and the boundaries between persons may be less sharp than we normally assume.
## The Biological Criterion
Eric Olson has argued for *animalism*: we are human animals, and our persistence conditions are those of biological organisms ^[Olson, E., *The Human Animal*, Oxford UP, 1997]. Personal identity is not constituted by psychological continuity; you persist as long as your body's metabolism continues. Brain transplants, on this view, are body transplants.
Animalism avoids neo-Lockean puzzles by refusing to split the person from the organism, but it faces difficulties with cerebral bisection, personal survival, and cases where psychological continuity intuitively tracks identity better than biological continuity does.
## Four-Dimensionalism
The *four-dimensionalist* (or *perdurantist*) view holds that objects persist by having temporal parts at different times, just as they have spatial parts at different locations ^[Lewis, D., *On the Plurality of Worlds*, Blackwell, 1986, pp.202-204; Sider, T., *Four-Dimensionalism*, Oxford UP, 2001]. The person who existed yesterday is a *temporal part* of a four-dimensional object extended through time as well as space.
On this view, there is no problem of persistence through change: different temporal parts can have different properties. The Ship of Theseus problem dissolves: the ship at time t1 and the ship at time t2 are different temporal parts of the same four-dimensional whole, even though their material composition differs.
Four-dimensionalism is technically elegant but counterintuitive: it implies that we never change — what we ordinarily call "my change" is two different temporal parts having different properties. And the multiplication of temporal parts raises concerns about parsimony.
**Three-dimensionalists** (or *endurantists*) hold that ordinary objects persist by being *wholly present* at each moment of their existence. They accept genuine identity through time and must therefore give an account of how the same thing can have different properties at different times (through *temporally modified* property ascription, or relativisation to times).
The persistence debate connects to the metaphysics of time: *presentists*, who hold only the present exists, are naturally endurantists; *eternalists*, who hold past, present, and future equally exist, can more naturally accommodate perdurance.

View file

@ -0,0 +1,61 @@
---
title: Causation
sort: 120
section-id: metaphysics
description: Hume's regularity account of causation, counterfactual theories, mechanistic theories, and causal pluralism.
language: en
---
# Causation
Causation is among the most pervasive features of reality and among the most philosophically contested. We invoke causal relations constantly: the window broke because it was struck by the ball; the fire spread because of the wind; the patient died because of the infection. Causal explanation, causal reasoning, and causal intervention underlie science, medicine, law, and everyday thought. Yet what causation *is* — what it is for one event to cause another — remains deeply controversial.
## Hume's Regularity Theory
Hume's analysis of causation, discussed in the context of empiricism (Chapter 4), remains the starting point for the contemporary debate. Hume distinguished two definitions of cause.
The first, in terms of *constant conjunction*: "An object, followed by another, and where all the objects similar to the first are followed by objects similar to the second" ^[Hume, D., *Enquiry*, §7.2]. Causation, on this view, is nothing over and above regular succession: whenever an event of type A occurs, an event of type B follows.
The second, in terms of *determination*: "An object followed by another, and whose appearance always conveys the thought to that other." This psychological definition reveals that the impression of necessary connection is in us, not in the objects.
The *regularity theory* developed from Hume's first definition. Mill systematised it with his *methods of agreement, difference*, and *concomitant variation* for identifying causal regularities ^[Mill, J.S., *A System of Logic*, 1843, III.viii].
**Objections:** Mere regular succession does not suffice for causation. Day regularly precedes night, but dawn does not cause dusk. Common causes produce correlated effects — thunder correlates with lightning, but neither causes the other. And regularities can be accidental (all gold spheres are smaller than the sun) rather than causal.
## Counterfactual Theories
David Lewis's *counterfactual theory* defines causation in terms of counterfactual dependence: C causes E if and only if, had C not occurred, E would not have occurred ^[Lewis, D., "Causation", *Journal of Philosophy* 70, 1973, pp.556-567].
This approach handles many cases better than regularity theories. The counterfactual "if the ball had not struck the window, the window would not have broken" is true (under normal conditions); hence the striking caused the breaking. Cases of accidental correlation are handled naturally: even if thunder and lightning are regularly correlated, it is not true that if thunder had not occurred, lightning would not have.
**Problems:** *Preemption* — where two potential causes compete and one "preempts" the other — is difficult. If two assassins shoot simultaneously and one bullet arrives first, the first shot caused the death; but it is not clear that the death counterfactually depends on the first shot, since the second would have caused it anyway. Lewis developed increasingly complex responses involving *fragility*, *quasi-dependence*, and *influence* ^[Lewis, D., "Causation as Influence", *Journal of Philosophy* 97, 2000].
*Overdetermination* — where two simultaneous causes each suffice for the effect — creates parallel problems.
## Mechanistic Theories
*Mechanistic* or *process* theories hold that causation consists in the transmission of energy, momentum, or causal influence through a spatiotemporally continuous process ^[Salmon, W., *Scientific Explanation and the Causal Structure of the World*, Princeton UP, 1984; Dowe, P., *Physical Causation*, Cambridge UP, 2000].
Wesley Salmon proposed that causal processes are distinguished from *pseudo-processes* (like shadows) by their ability to transmit a *mark* — an alteration made at one point that propagates forward. Phil Dowe replaced this with a conserved quantity account: a causal process is one that transmits a conserved quantity (energy, charge, momentum).
Mechanistic theories have the advantage of closely tracking scientific practice — physicists and biologists routinely explain by identifying mechanisms. But they face difficulties with causation by absence (the bridge collapsed *because* the engineers failed to inspect it), negative causation, and the causation of absences.
## Interventionist Theories
James Woodward developed an *interventionist* account, appealing to the notion of ideal intervention ^[Woodward, J., *Making Things Happen*, Oxford UP, 2003]. A variable X causes Y if there is a possible ideal intervention on X (one that changes X independently of other causes of Y) that changes Y.
This connects causation to the notion of manipulation or control, and is particularly well-suited to the social and biological sciences. It captures the idea that causal claims are action-guiding: to know that X causes Y is to know that intervening on X will change Y.
Interventionism faces the question of whether the interventionist account is circular — since interventions are themselves causal notions.
## Singular Causation and the Problem of Many Levels
A recurring question: is causation a relation between *types* of events (event-type A regularly precedes event-type B) or between *tokens* — particular, individual events (this striking caused this breaking)?
Token causation matters for law: we want to know whether *this* person's negligence caused *this* accident, not whether negligence-type events generally precede accidents.
The *problem of causal exclusion* (closely connected to philosophy of mind) asks how mental causation is possible if everything is determined at the physical level. If the physical causes of my action fully determine it, what work is left for my mental states to do? ^[Kim, J., *Mind in a Physical World*, MIT Press, 1998]. This challenges non-reductive physicalism about mind.
**Causal pluralism** holds that there is no single analysis of causation that captures all uses of causal vocabulary ^[Hall, N., "Two Concepts of Causation", in *Causation and Counterfactuals*, ed. Collins et al., MIT Press, 2004]. There may be one concept of causation for physics, another for biology, another for the law. Pluralism is comfortable with this; the search for a unified account may be misconceived.
The contemporary causation debate is technically sophisticated and connects to philosophy of science, philosophy of mind, and action theory. What is clear is that Hume was right about one thing: the concept of causation is not simply read off the surface of experience but requires serious philosophical analysis.

View file

@ -0,0 +1,66 @@
---
title: Free Will and Determinism
sort: 130
section-id: metaphysics
description: Hard determinism, libertarianism, compatibilism, and Frankfurt cases.
language: en
---
# Free Will and Determinism
The free will debate is among philosophy's most enduring and personally consequential. It asks whether, in a deterministic universe, human beings can be genuinely free — free in a way that makes praise, blame, punishment, and moral responsibility appropriate. The question matters not just theoretically but practically: legal systems, personal relationships, and our self-understanding all presuppose that people can be held responsible for their actions.
## The Incompatibilist Intuition
The *basic argument* for incompatibilism runs roughly as follows ^[Van Inwagen, P., *An Essay on Free Will*, Oxford UP, 1983, pp.56-105]:
1. Determinism is true: every event, including every human action, is causally necessitated by prior events in conjunction with the laws of nature.
2. If determinism is true, then no one ever could have acted otherwise than they did.
3. Moral responsibility requires the ability to have acted otherwise.
4. Therefore, if determinism is true, no one is morally responsible for anything.
This argument has considerable intuitive force. If my action was causally determined by events that happened before I was born, in what sense was it *my* choice? If the complete causal history of the universe made my decision inevitable, how am I the author of it in any meaningful sense?
## Hard Determinism
*Hard determinists* accept incompatibilism and accept determinism, concluding that free will does not exist and moral responsibility must be radically revised or abandoned.
Derk Pereboom has argued for *hard incompatibilism* with increasing sophistication ^[Pereboom, D., *Living Without Free Will*, Cambridge UP, 2001]. He accepts that moral luck undermines responsibility, that determinism (or indeterminism, for different reasons) threatens desert-based punishment, but argues that a meaningful life can be constructed without the reactive attitudes (blame, indignation, gratitude) that presuppose responsibility.
The practical implications are significant: criminal punishment on retributive grounds is unjustified; therapeutic and quarantine-based reasons for incapacitation remain. Reactive attitudes would be gradually replaced by more forward-looking responses.
## Libertarianism About Free Will
*Libertarians* (in the metaphysical sense, entirely distinct from political libertarianism) accept incompatibilism but reject determinism, maintaining that free will requires — and we have — a form of causation that is not deterministic.
Agent causation: Roderick Chisholm argued that free action requires *agent causation* — a primitive, irreducible capacity of persons as agents to initiate causal chains, not wholly determined by prior events ^[Chisholm, R., "Human Freedom and the Self", in *Free Will*, ed. Watson, Oxford UP, 1982].
The *undetermined choice*: Robert Kane developed an account on which free will-exercising decisions occur at moments of *self-forming actions* — quantum-indeterminate moments of genuine undeterminedness, where the agent's character and reasons could have produced either outcome ^[Kane, R., *The Significance of Free Will*, Oxford UP, 1996].
Libertarianism faces the *luck objection*: if my action was undetermined, then it seems random rather than free. An undetermined choice is one that even I could not have predicted from my own character and reasons — which seems to undermine rather than secure my authorship of the action.
## Compatibilism
*Compatibilists* reject the third premise of the basic argument. Moral responsibility, they argue, does not require the ability to have done otherwise in the libertarian sense. What matters is whether the action was performed for the right kinds of reasons, whether the agent was responsive to reasons, whether the action was voluntary in the relevant sense.
**Classical compatibilism:** Hume, and following him most analytic philosophers of the twentieth century, held that freedom is simply the ability to act in accordance with one's own desires, without external compulsion ^[Hume, D., *Enquiry*, §8]. Coercion, addiction, and phobia compromise freedom; determinism does not.
**Hierarchical compatibilism:** Harry Frankfurt proposed that what matters for freedom is the *structure* of the agent's motivations ^[Frankfurt, H., "Freedom of the Will and the Concept of a Person", *Journal of Philosophy* 68, 1971, pp.5-20]. We have first-order desires (I want to smoke) and second-order desires (I want to want to smoke, or I want not to want to smoke). A free agent is one whose first-order desires align with their second-order volitions — who acts from desires they endorse. This hierarchical structure distinguishes the wanton (who acts on whatever desire is strongest) from the autonomous agent.
**Reasons-responsiveness:** John Martin Fischer and Mark Ravizza developed the view that free agency requires mechanisms that are *reasons-responsive* — mechanisms that would have produced different choices had there been different reasons ^[Fischer, J.M. and Ravizza, M., *Responsibility and Control*, Cambridge UP, 1998]. You are responsible for actions that flow from your own reasons-responsive mechanisms.
## Frankfurt Cases
Harry Frankfurt's most influential contribution is the *Frankfurt case*, designed to challenge the *Principle of Alternative Possibilities* (PAP): a person is morally responsible for their action only if they could have acted otherwise.
The structure: Black wants Jones to perform some action. Black has installed a mechanism in Jones's brain that will, if Jones shows any sign of not performing the action, intervene and ensure Jones performs it. In fact, Jones performs the action on his own, and the mechanism never activates. Jones could not have done otherwise (the mechanism would have prevented it). But, Frankfurt argues, Jones is clearly morally responsible ^[Frankfurt, H., "Alternate Possibilities and Moral Responsibility", *Journal of Philosophy* 66, 1969, pp.829-839].
If Frankfurt cases are sound, PAP is false, and the basic incompatibilist argument loses its third premise. The literature on Frankfurt cases is enormous: compatibilists have used them to argue that alternative possibilities are not required for responsibility; incompatibilists have argued that Frankfurt cases either fail to establish genuine alternative possibilities being closed off, or leave open a "flicker of freedom" ^[Kane, R., "Two Kinds of Incompatibilism", *Philosophy and Phenomenological Research* 50, 1989].
## Strawson's Reactive Attitudes
P.F. Strawson argued that the debate misses what is most important about responsibility: our *reactive attitudes* ^[Strawson, P.F., "Freedom and Resentment", *Proceedings of the British Academy* 48, 1962]. Resentment, gratitude, indignation, and love are the appropriate responses to the quality of an agent's will toward us. These attitudes constitute — rather than presuppose — our moral practices. The question of whether determinism is true is largely beside the point; what matters is whether we are the kinds of creatures to whom it is appropriate to hold reactive attitudes, and we clearly are.
Strawson's insight is that moral responsibility is essentially an interpersonal, practice-constituted phenomenon, not primarily a metaphysical one.
The free will debate has not converged. Compatibilism is the most widely held view among professional philosophers, but libertarian and hard incompatibilist positions retain significant defenders. The question of what freedom requires — and whether we have it — remains genuinely open.

View file

@ -0,0 +1,61 @@
---
title: Philosophy of Mind
sort: 140
section-id: metaphysics
description: Dualism, functionalism, physicalism, qualia, and the hard problem of consciousness.
language: en
---
# Philosophy of Mind
Philosophy of mind asks what the mind is, how mental states relate to physical states, and whether consciousness can be explained by the natural sciences. It is a meeting point of metaphysics, epistemology, cognitive science, and neuroscience — and at its centre lies what David Chalmers called *the hard problem of consciousness*, the question of why there is subjective experience at all.
## Substance Dualism
Descartes' *substance dualism* holds that mind and body are distinct substances: the body is extended substance (res extensa), governed by mechanical laws; the mind is thinking substance (res cogitans), unextended and not subject to physical laws ^[Descartes, R., *Meditations*, AT VII:78-80; *The Passions of the Soul*, AT XI:330].
Substance dualism captures the intuition that mental life — the experience of pain, the feeling of red, the taste of coffee — is radically different in kind from the physical world. No description in purely physical terms seems to capture what it is like to be in pain.
The central objection: *causal interaction*. If mind and body are distinct substances, how do they causally interact? How does my decision to raise my arm cause my arm to rise? Descartes' attempted answer — via the pineal gland — was never convincing. Occasionalism (Malebranche) and pre-established harmony (Leibniz) were developed as alternatives, both of which deny genuine causal interaction and invoke God.
## Behaviourism and Its Failures
*Logical behaviourism* — associated with Ryle and early Wittgenstein — held that mental concepts are analysable in terms of behavioural dispositions, not inner states ^[Ryle, G., *The Concept of Mind*, 1949]. To believe that it will rain is to be disposed to carry an umbrella, to seek shelter, and so on. There is no "ghost in the machine" — mentality just is the complex of behavioural dispositions.
Hilary Putnam argued that behaviourism fails because behavioural dispositions are mediated by other mental states ^[Putnam, H., "Brains and Behavior", 1963]. The pain-disposition to withdraw from stimuli requires the desire to avoid pain; the belief-disposition to seek shelter requires the desire to stay dry. No purely behavioural analysis of a mental state can avoid this regress.
## Identity Theory
*Identity theory* — associated with Place and Smart — held that mental states are identical to brain states: pain is a type of neural activity ^[Place, U.T., "Is Consciousness a Brain Process?", *British Journal of Psychology* 47, 1956; Smart, J.J.C., "Sensations and Brain Processes", *Philosophical Review* 68, 1959].
**Multiple realisability objection:** Putnam argued that mental states are *multiply realisable* — they can be realised in many different physical substrates ^[Putnam, H., "Psychological Predicates", 1967]. If octopuses (with radically different nervous systems) can be in pain, pain cannot be identical to any specific neural state. This counts against *type* identity theory, though not *token* identity (this instance of pain is identical to this neural event).
## Functionalism
*Functionalism* — Putnam's alternative — holds that mental states are defined by their *functional role*: their causal relations to sensory inputs, behavioural outputs, and other mental states ^[Putnam, H., "The Nature of Mental States", 1967]. Pain is whatever state is typically caused by tissue damage, causes withdrawal behaviour and the desire to relieve it, and interacts with other states in characteristic ways.
Functionalism accommodates multiple realisability: what makes something a pain is its functional role, regardless of whether it is implemented in neurons, silicon, or anything else. It is the dominant view in philosophy of mind and cognitive science.
**Objections:** The *inverted qualia* argument: two people might have their functional roles entirely aligned while having inverted phenomenal experiences (what's red to you is green to me). Functionalism, which is defined by function, cannot distinguish them. The *absent qualia* argument: a system could have all the right functional relations while having no phenomenal experience at all — a philosophical zombie ^[Block, N., "Troubles with Functionalism", *Minnesota Studies in the Philosophy of Science* 9, 1978; Chalmers, D., *The Conscious Mind*, 1996, ch.3].
## Physicalism and Its Varieties
Contemporary philosophy of mind is broadly physicalist: mental states are physical states or at least entirely dependent on physical states. The question is *how* they are dependent.
*Supervenience physicalism*: mental properties supervene on physical properties — any two individuals physically identical are mentally identical ^[Kim, J., *Supervenience and Mind*, Cambridge UP, 1993].
*Non-reductive physicalism*: mental properties are real but not reducible to physical properties, even though they supervene on them.
*Reductive physicalism*: mental properties can ultimately be explained in physical terms.
The *causal exclusion argument* (Kim) poses a serious problem for non-reductive physicalism: if physical events have sufficient physical causes, and mental events are supposed to cause behaviour, then either mental events are physical events (reductivism) or mental events are causally redundant ^[Kim, J., *Mind in a Physical World*, 1998, ch.2-3].
## The Hard Problem
Chalmers distinguished the *easy problems* of consciousness — explaining cognitive access, attention, introspection, sleep/waking cycles (these problems are hard, but they admit of functional-explanatory solutions) — from *the hard problem*: why is there subjective experience at all? ^[Chalmers, D., "Facing Up to the Problem of Consciousness", *Journal of Consciousness Studies* 2, 1995].
Even a complete physical and functional account of what the brain does would leave open why any of this processing is *experienced* — why it feels like something to be a brain. The explanatory gap between physical descriptions and phenomenal experience seems irreducible.
Responses range from *type-B physicalism* (the gap is a conceptual illusion, not a real explanatory gap) to *property dualism* (phenomenal properties are real, non-physical properties that supervene on physical ones) to *panpsychism* (consciousness is a fundamental feature of reality, found at all levels of physical organisation) ^[Goff, P., *Galileo's Error*, 2019].
The hard problem has not been solved. Whether it is solvable within a physicalist framework, or whether it requires revising our fundamental ontology, remains one of philosophy's most contested open questions.

View file

@ -0,0 +1,61 @@
---
title: The Nature of Time
sort: 150
section-id: metaphysics
description: A-series and B-series, presentism, eternalism, and the growing block theory of time.
language: en
---
# The Nature of Time
Time is both utterly familiar and deeply puzzling. We live in it, measure it, experience its passage. Yet when we ask what time is, whether the past and future exist, whether time flows or merely seems to, we find ourselves quickly in some of philosophy's most difficult territory.
## McTaggart's A-Series and B-Series
J.M.E. McTaggart introduced the most influential framework for the philosophy of time in his 1908 paper "The Unreality of Time" ^[McTaggart, J.M.E., "The Unreality of Time", *Mind* 17, 1908, pp.457-474].
The *B-series* is the ordering of events as earlier, simultaneous, and later. Every event stands in a fixed B-relation to every other: the Battle of Hastings is earlier than the French Revolution; your birth is earlier than your reading this sentence. These relations are *permanent*: if A is earlier than B, it always has been and always will be.
The *A-series* orders events as *past*, *present*, and *future*. Unlike B-relations, A-properties change: what is now future becomes present and then past. The event of your reading this sentence was future, is now present, and will be past.
McTaggart argued that the A-series is essential to time — without the genuine distinction between past, present, and future, there would be no temporal becoming, no flow of time, and time would not be genuinely real. But the A-series is contradictory: every event has all three properties (past, present, future), which are mutually exclusive. Attempts to resolve the contradiction by saying "the event is present *now*, past *at later times*, future *at earlier times*" invoke further temporal moments and generate a vicious infinite regress. McTaggart concluded that time is unreal.
Most philosophers reject the conclusion but accept that McTaggart identified a genuine structural puzzle.
## Presentism
*Presentism* holds that only present entities exist ^[Crisp, T., "Presentism", in *Oxford Handbook of Metaphysics*, 2003]. The past is gone; the future is not yet here. "There were dinosaurs" is true, but dinosaurs do not now exist in any sense — they existed and no longer do.
Presentism is the view that most naturally fits everyday temporal experience. The past seems gone; the future seems open.
**The problem of cross-temporal relations:** Many true claims seem to relate present entities to past ones: Caesar crossed the Rubicon *before* you were born. If Caesar does not now exist, what makes this claim true? Presentists have responded with *truthmakers* that exist presently — facts about the present state of the world — and with *temporal ersatzism* (abstract representations of past times).
**Reconciling presentism with special relativity:** Relativity implies there is no absolute simultaneity — different reference frames carve the four-dimensional spacetime differently. This is deeply problematic for presentism, which requires a distinguished present ^[Putnam, H., "Time and Physical Geometry", *Journal of Philosophy* 64, 1967].
## Eternalism (The Block Universe)
*Eternalism* — the view associated with Einstein's spacetime — holds that past, present, and future entities all equally exist, merely at different temporal locations ^[Sider, T., *Four-Dimensionalism*, Oxford UP, 2001, ch.2]. The universe is a four-dimensional "block" in which all events co-exist; the distinction between past, present, and future is merely perspectival, like the distinction between here and there.
Eternalism accommodates special relativity naturally: there is no privileged present, and the temporal order of events can be frame-relative.
**The problem of temporal passage:** Eternalism seems to make temporal experience mysterious. If past and future are equally real, why does time seem to pass? Why do we experience becoming rather than merely co-existing with our past and future selves in a static four-dimensional block?
Some eternalists accept this implication and argue that the *passage* of time is an illusion — a product of our temporal perspective, not a feature of reality. Others have argued that passage can be accommodated within an eternalist framework through the *moving spotlight* theory.
## The Growing Block Theory
C.D. Broad proposed an intermediate view: the growing block ^[Broad, C.D., *Scientific Thought*, 1923, pp.66-68]. The past and present are real and fixed; the future does not yet exist. As time passes, new events come into existence and the block grows. This preserves the reality of temporal becoming without committing to a privileged present.
**Objections:** If the growing block is correct, and past moments are real and fixed, how do we know we are in the present rather than in some past moment (which is, after all, equally real)? This generates a peculiar form of scepticism about our temporal location ^[Tooley, M., *Time, Tense, and Causation*, Oxford UP, 1997].
## The Direction of Time
Physics, at the fundamental level, is largely time-symmetric: the laws of mechanics, electromagnetism, and quantum mechanics (with minor exceptions) work equally in both temporal directions. Yet our experience of time is strongly asymmetric: we remember the past and not the future; causes precede effects; entropy increases.
Boltzmann argued that the thermodynamic arrow of time — the increase of entropy — explains the asymmetry ^[Boltzmann, L., *Lectures on Gas Theory*, 1896-98]. We are in a low-entropy region of a vastly larger, mostly high-entropy universe; the Second Law of Thermodynamics is a statistical fact about macroscopic systems, not a fundamental law.
The *causal theory* of time holds that the direction of time is grounded in the direction of causation: the future is the direction in which causal processes flow. But if causation itself is time-asymmetric, this seems circular.
**Temporal experience:** Husserl's phenomenology of time-consciousness distinguished *retention* (the immediate just-past), *primal impression* (the now), and *protention* (the immediate just-future) as three interlocking structures of temporal awareness ^[Husserl, E., *On the Phenomenology of the Consciousness of Internal Time*, 1928]. The experience of time as flowing may be constituted by this structure rather than being evidence of metaphysical flow.
The nature of time remains one of the most contested and most fundamental questions in metaphysics, connecting to physics, the theory of causation, personal identity, and the phenomenology of experience.

View file

@ -0,0 +1,48 @@
---
title: Preface
sort: 100
section-id: front-matter
description: Professor Okafor's preface — why philosophy matters, how to use this book, and acknowledgements.
language: en
---
![Library and scholarly atmosphere](assets/images/hero.jpg)
# Preface
Philosophy begins where certainty ends. It is the sustained, rigorous attempt to think carefully about questions that do not yield to experiment, calculation, or common sense alone — questions about the nature of knowledge, the structure of reality, the foundations of morality, and the meaning of a human life. These questions are not trivial or marginal. They are, in many respects, the questions that underlie everything else we do. The scientist who believes her experiments can yield knowledge presupposes an account of knowledge and justification. The judge who sentences a criminal presupposes an account of moral responsibility and desert. The citizen who argues for a just social arrangement presupposes an account of fairness, rights, and legitimate authority.
Philosophy is the discipline that examines these presuppositions — that takes them seriously enough to interrogate them rather than quietly relying on them.
This book is an introduction to three of philosophy's central subdisciplines: epistemology (the theory of knowledge), metaphysics (the study of the fundamental nature of reality), and ethics (the systematic examination of morality). These three areas are not the whole of philosophy — there is also philosophy of language, philosophy of science, political philosophy in its own right, philosophy of mathematics, philosophy of religion, and many others — but they constitute what has traditionally been called the core of the discipline, and they are deeply interconnected. Our account of knowledge bears on our account of what exists. Our account of what exists bears on our account of moral facts. The connections run in every direction.
## On This Book
*Foundations of Modern Philosophy* is written for advanced undergraduates and first-year graduate students who have some acquaintance with philosophical questions but no prior systematic training. It assumes intellectual seriousness but not technical background. The goal is not to survey every debate in every area — that would require a library, not a textbook — but to provide rigorous introductions to the central issues, equip the reader with the conceptual vocabulary needed to engage with primary sources, and convey something of the excitement of philosophy as a living intellectual enterprise.
Each chapter introduces a major area of inquiry, presents the central arguments with the care they deserve, and points toward the ongoing debates that the reader will encounter if they pursue the subject further. The book is designed to be read sequentially, but chapters can also be read independently: those with a specific interest in, say, philosophy of mind or free will can begin there and follow the cross-references.
I have tried to write with clarity without sacrificing rigour, and with genuine intellectual engagement without pretending that the questions have been settled. They have not been settled. Some of them may not be settleable. This is not a reason for despair but for sustained attention.
## Philosophy and Disagreement
One thing that surprises new students of philosophy is how much disagreement persists among careful, intelligent, well-informed people. In mathematics, experts eventually converge. In philosophy, they often do not. This is sometimes taken as evidence that philosophy makes no progress, or that philosophical questions are somehow empty.
I believe this is wrong, for two reasons.
First, philosophical progress is real, even when consensus is absent. We understand the problems more precisely than we did. The conceptual terrain has been mapped. Certain paths have been shown to be dead ends. The space of viable positions has been narrowed, even if it has not collapsed to a single point.
Second, sustained intelligent disagreement is not a failure — it is what happens when the questions are genuinely hard and the standards of evidence genuinely exacting. The fact that Kant, Mill, and Aristotle disagree about the foundations of morality does not mean there are no good arguments in ethics. It means the questions are difficult enough that even great minds approach them from different directions and reach different conclusions.
Students who enter philosophy looking for certainty will be disappointed. Students who enter looking for difficult questions asked well, with intellectual honesty and genuine rigour, will find that and more.
## Acknowledgements
This book has accumulated debts over many years of teaching. Students at the University of Lagos, Cambridge, and Princeton asked the questions that forced me to think more carefully; colleagues in seminars and conference rooms challenged positions I held too comfortably; many generations of undergraduates reminded me, by their confusion and their insight alike, what it is actually like to encounter these ideas for the first time.
I am grateful to my research assistants, Amara Osei-Bonsu and Lars Eriksson, for careful reading of the manuscript. My editor, who has the good philosopher's gift of asking exactly the right question at exactly the wrong moment, improved the book considerably.
My deepest debts are to the philosophers whose work is discussed in these pages. I have tried to present their arguments with fairness. Where I have failed, the failure is mine.
*James Okafor*
*Lagos / Princeton, 2026*

View file

@ -0,0 +1,77 @@
---
title: Synthesis and Open Questions
sort: 100
section-id: conclusion
description: How epistemology, metaphysics, and ethics connect, and ten open problems that define the frontiers of contemporary philosophy.
language: en
---
# Synthesis and Open Questions
We have traversed three great branches of philosophy — epistemology, metaphysics, and ethics — as if they were distinct territories. They are not. In this concluding chapter we trace some of the connections between them, then identify ten open problems that represent the active frontiers of contemporary philosophical inquiry.
## How the Three Branches Connect
### Epistemology and Metaphysics
Epistemology and metaphysics are entangled at their foundations. The question "What is there?" (ontology) is inseparable from "How can we know what there is?" (epistemology). Kant made this explicit: our knowledge of reality is always knowledge of reality as structured by our cognitive apparatus. The thing-in-itself — the world independent of our categories — is unknowable.
The debate between realism and anti-realism in metaphysics has a direct epistemological dimension: scientific realists hold that our best theories give us genuine knowledge of the unobservable structure of reality; anti-realists (van Fraassen's constructive empiricism) hold that empirical adequacy, not truth, is the goal of science.
Scepticism is both an epistemological and a metaphysical thesis: if we cannot rule out the brain-in-a-vat hypothesis, then our beliefs about the external world may be systematically false. Responding to scepticism requires both an epistemological account of what knowledge requires and a metaphysical account of what makes beliefs true.
### Metaphysics and Ethics
Free will and determinism (Chapter 9) is the clearest intersection of metaphysics and ethics. Moral responsibility — the foundation of our entire ethical and legal practice — presupposes that agents could have done otherwise. If determinism is true (and it may be), then whether this condition is satisfied becomes a metaphysical question with profound ethical and social consequences.
Personal identity (Chapter 7) connects to ethics in multiple ways. Derek Parfit argued that the correct view of personal identity — that what matters in survival is not identity per se but psychological continuity — has far-reaching implications for distributive justice, self-interest, and our concern for future persons. If I am only loosely connected to my future self, do I have the same reasons for prudence?
### Epistemology and Ethics
Moral epistemology asks whether we can have knowledge of ethical truths, and if so, how. Empiricists about ethics hold that moral judgements are answerable to experience; rationalists hold that some moral truths are knowable a priori. The methodology of ethics — intuitions as data, reflective equilibrium, thought experiments — is itself a set of epistemological commitments.
The is-ought gap (Hume) is an epistemological constraint on ethical argument: purely factual premises cannot entail normative conclusions. This shapes what counts as a valid argument in ethics.
## Ten Open Problems in Philosophy
### 1. The Hard Problem of Consciousness
Why is there subjective experience at all? Why does processing information feel like anything? David Chalmers's formulation of the hard problem remains without consensus resolution. Physicalist accounts explain the functional and structural properties of mind but seem to leave out the *what it is like*.
### 2. The Nature of Mathematical Objects
Are mathematical objects abstract entities that exist independently of minds (Platonism), or are they mental constructs (constructivism), or merely useful fictions (fictionalism)? The unreasonable effectiveness of mathematics — its uncanny applicability to physical reality — demands explanation on any view.
### 3. The Reference of Natural Kind Terms
Kripke and Putnam argued that natural kind terms ("water," "gold") refer rigidly to their physical essences, not to descriptive clusters. But what determines reference? The causal-historical picture has unresolved problems for highly theoretical kinds (fields, virtual particles) and social/biological categories.
### 4. The Status of Modality
What grounds claims about necessity and possibility? Are possible worlds Lewisian concrete universes, abstract maximally consistent sets of propositions, or something else? The ontological costs of modal realism seem high; the alternatives face their own problems.
### 5. Personal Identity Over Time
Despite extensive philosophical work, we lack a fully satisfactory account of what makes you the same person you were ten years ago — and what this should matter for ethics. Parfit's reductionism remains controversial.
### 6. The Foundations of Probability
The three major interpretations — frequentist, Bayesian (subjective), and propensity — each face serious objections. The role of probability in quantum mechanics compounds the difficulty: are quantum probabilities objective features of reality or epistemic representations of our uncertainty?
### 7. The Demarcation Problem
What distinguishes science from non-science, or good science from pseudo-science? Popper's falsifiability criterion is widely acknowledged to be insufficient. String theory and cosmological multiverse theories generate empirical predictions only under contested conditions. Philosophy of science lacks an agreed criterion.
### 8. Moral Realism and Evolutionary Debunking
Sharon Street's evolutionary debunking argument: if our moral faculties were shaped by natural selection for fitness rather than moral truth, then we have no reason to trust that our moral intuitions track mind-independent moral facts.^[Street, S. (2006). "A Darwinian Dilemma for Realist Theories of Value." *Philosophical Studies*, 127(1).] Moral realists must either deflect this argument or explain why adaptive pressure would align our intuitions with moral truth.
### 9. The Epistemology of Disagreement
When two equally well-informed, well-reasoned individuals reach opposing conclusions, what should each do? The *conciliationist* view says each should move towards the other; the *steadfast* view says you can maintain your view if you have independent reasons. The answer has implications for political philosophy, scientific consensus, and religious belief.
### 10. The Grounds of Normativity
Why does reason bind us? Kant's answer — that rational nature is the source of all value — faces both metaphysical challenges (what is rational nature?) and sceptical challenges (why should I care about what reason prescribes?). Korsgaard's attempt to ground normativity in reflective self-endorsement has been contested. This may be the most fundamental question in all of philosophy.
## Conclusion
Philosophy does not progress by solving problems and discarding them. The questions examined in this book — about knowledge, reality, and value — are perennial because they arise from the structure of human thought itself. What philosophy offers is not definitive answers but greater clarity about what the questions are, greater rigour in evaluating proposed answers, and greater sensitivity to the hidden assumptions that shape all inquiry.
The student who completes this introduction will find these questions following them into every discipline they pursue — into science, into law, into medicine, into politics, into ordinary life. That is not a failure of philosophy to resolve itself. It is philosophy doing exactly what it should.
## Further Reading
- Chalmers, D. (2023). *Reality+: Virtual Worlds and the Problems of Philosophy*. Penguin.
- Parfit, D. (1984). *Reasons and Persons*. Oxford University Press.
- Nagel, T. (1986). *The View from Nowhere*. Oxford University Press.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,46 @@
# mdcms theme — Foundations of Modern Philosophy
light:
accent: "#744210"
background: "#FFFFF8"
nav-background: "#F7F3E9"
text: "#1A202C"
text-muted: "#718096"
dark:
accent: "#F6AD55"
background: "#1A1611"
nav-background: "#211D18"
text: "#EDF2F7"
text-muted: "#A0AEC0"
colours-semantic:
info: "#2B6CB0"
warning: "#C05621"
success: "#276749"
error: "#C53030"
callouts:
info:
icon: info
primary-colour: "#2B6CB0"
background-colour: "#2B6CB0"
warning:
icon: warning
primary-colour: "#C05621"
background-colour: "#C05621"
success:
icon: success
primary-colour: "#276749"
background-colour: "#276749"
error:
icon: error
primary-colour: "#C53030"
background-colour: "#C53030"
font-body: "bunny:Source Serif 4:400"
font-heading: "bunny:Source Serif 4:700"
font-size: 1.0
line-height: 1.85
main-width: 72em
nav-width: 20em

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1,6 @@
# mdcms v0.3 | DO NOT REMOVE THIS COMMENT
sitename: NeuralDB
sitedescription: AI-native database with vector and relational capabilities
navigation: topbar
search: true
footer: "© 2026 NeuralDB, Inc. Documentation licensed under CC BY 4.0."

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,240 @@
# nav.yml — generated by mdcms.py
sections:
- code: overview
defaultname: Overview
sort: 100
pagesvisibility: visible
- code: installation
defaultname: Installation
sort: 200
pagesvisibility: visible
- code: configuration
defaultname: Configuration
sort: 300
pagesvisibility: visible
- code: query-language
defaultname: Query Language
sort: 400
pagesvisibility: visible
- code: client-sdks
defaultname: Client SDKs
sort: 500
pagesvisibility: visible
- code: operations
defaultname: Operations
sort: 600
pagesvisibility: visible
pages:
- file: pages/index.md
title: What is NeuralDB?
section-id: overview
sort: 100
variants: [en]
titles:
en: What is NeuralDB?
- file: pages/concepts.md
title: Core Concepts
section-id: overview
sort: 110
variants: [en]
titles:
en: Core Concepts
- file: pages/architecture.md
title: Architecture
section-id: overview
sort: 120
variants: [en]
titles:
en: Architecture
- file: pages/comparison.md
title: Comparison
section-id: overview
sort: 130
variants: [en]
titles:
en: Comparison
- file: pages/install-docker.md
title: Docker Install
section-id: installation
sort: 100
variants: [en]
titles:
en: Docker Install
- file: pages/install-kubernetes.md
title: Kubernetes
section-id: installation
sort: 110
variants: [en]
titles:
en: Kubernetes
- file: pages/install-cloud.md
title: Cloud Managed
section-id: installation
sort: 120
variants: [en]
titles:
en: Cloud Managed
- file: pages/install-local.md
title: Local Development
section-id: installation
sort: 130
variants: [en]
titles:
en: Local Development
- file: pages/config-server.md
title: Server Config
section-id: configuration
sort: 100
variants: [en]
titles:
en: Server Config
- file: pages/config-auth.md
title: Authentication
section-id: configuration
sort: 110
variants: [en]
titles:
en: Authentication
- file: pages/config-storage.md
title: Storage Config
section-id: configuration
sort: 120
variants: [en]
titles:
en: Storage Config
- file: pages/config-replication.md
title: Replication
section-id: configuration
sort: 130
variants: [en]
titles:
en: Replication
- file: pages/nql-basics.md
title: NQL Basics
section-id: query-language
sort: 100
variants: [en]
titles:
en: NQL Basics
- file: pages/nql-vectors.md
title: Vector Queries
section-id: query-language
sort: 110
variants: [en]
titles:
en: Vector Queries
- file: pages/nql-hybrid.md
title: Hybrid Queries
section-id: query-language
sort: 120
variants: [en]
titles:
en: Hybrid Queries
- file: pages/nql-aggregations.md
title: Aggregations
section-id: query-language
sort: 130
variants: [en]
titles:
en: Aggregations
- file: pages/nql-transactions.md
title: Transactions
section-id: query-language
sort: 140
variants: [en]
titles:
en: Transactions
- file: pages/sdk-python.md
title: Python SDK
section-id: client-sdks
sort: 100
variants: [en]
titles:
en: Python SDK
- file: pages/sdk-javascript.md
title: JavaScript SDK
section-id: client-sdks
sort: 110
variants: [en]
titles:
en: JavaScript SDK
- file: pages/sdk-go.md
title: Go SDK
section-id: client-sdks
sort: 120
variants: [en]
titles:
en: Go SDK
- file: pages/sdk-rest.md
title: REST API
section-id: client-sdks
sort: 130
variants: [en]
titles:
en: REST API
- file: pages/ops-monitoring.md
title: Monitoring
section-id: operations
sort: 100
variants: [en]
titles:
en: Monitoring
- file: pages/ops-backup.md
title: Backup & Restore
section-id: operations
sort: 110
variants: [en]
titles:
en: Backup & Restore
- file: pages/ops-scaling.md
title: Scaling
section-id: operations
sort: 120
variants: [en]
titles:
en: Scaling
- file: pages/ops-migration.md
title: Migration
section-id: operations
sort: 130
variants: [en]
titles:
en: Migration
- file: pages/ops-troubleshooting.md
title: Troubleshooting
section-id: operations
sort: 140
variants: [en]
titles:
en: Troubleshooting

View file

@ -0,0 +1,188 @@
---
title: Architecture
sort: 120
section-id: overview
keywords: architecture, storage engine, query planner, replication, WAL, HNSW
description: NeuralDB internal architecture — storage engine, query planner, and replication
language: en
---
# Architecture
![NeuralDB Architecture](assets/images/architecture.jpg)
NeuralDB is built on a custom storage engine that co-locates relational and vector data, with a query planner that understands both SQL predicates and vector similarity operations natively.
## High-Level Architecture
```
Client (psql / SDK / REST)
┌─────────────────────────────────────────┐
│ Connection Layer │
│ (PostgreSQL Wire Protocol compatible) │
└───────────────────┬─────────────────────┘
┌──────────▼──────────┐
│ Query Parser │
│ (SQL + NQL ext.) │
└──────────┬──────────┘
┌──────────▼──────────┐
│ Semantic Planner │◄── Statistics + Index Metadata
│ (hybrid cost model) │
└──────────┬──────────┘
┌──────────▼──────────┐
│ Execution Engine │
│ ┌────────────────┐ │
│ │ SQL Executor │ │
│ └────────────────┘ │
│ ┌────────────────┐ │
│ │ ANN Executor │ │
│ └────────────────┘ │
└──────────┬──────────┘
┌──────────▼──────────┐
│ Storage Engine │
│ ┌────────────────┐ │
│ │ Row Store │ │◄── SST Files (columnar)
│ └────────────────┘ │
│ ┌────────────────┐ │
│ │ Vector Store │ │◄── HNSW Graph Files
│ └────────────────┘ │
│ ┌────────────────┐ │
│ │ WAL │ │◄── Write-Ahead Log
│ └────────────────┘ │
└─────────────────────┘
```
## Storage Engine
### Row Store
NeuralDB's row store uses a Log-Structured Merge-tree (LSM) architecture inspired by RocksDB. Data is written to an in-memory write buffer (MemTable), which is periodically flushed to sorted string tables (SSTables) on disk. Background compaction merges SSTables and reclaims space.
Key properties:
- **Write-optimised**: writes are sequential, not random — excellent NVMe utilisation
- **Columnar format**: SSTables store data column-by-column for fast analytical scans
- **Compression**: LZ4 by default, Zstd for archival storage — typically 36× compression ratio
### Vector Store
Vectors are stored separately from rows in a Vector Store. The Vector Store maintains:
1. **Raw vector data** — the float32 arrays, stored in compressed pages
2. **HNSW graph** — the in-memory navigation graph for ANN search
The HNSW graph is loaded into memory on startup and kept warm. Memory required ≈ `num_vectors × dimensions × 4 bytes × 1.3` (1.3× overhead for the graph structure).
For a 10M-row table with 1536-dimensional embeddings: `10M × 1536 × 4 × 1.3 ≈ 80 GB`. Plan memory accordingly.
### Write-Ahead Log (WAL)
All writes (row and vector) are first written to the WAL before being applied to the storage engine. The WAL provides:
- **Durability**: committed transactions survive crashes
- **Replication**: replicas apply the WAL stream from the primary
- **Point-in-time recovery (PITR)**: archive the WAL to recover to any point in time
WAL segments are 128 MB by default and are archived to the configured storage backend (local disk, S3, GCS) upon rotation.
## Query Planner
The Semantic Planner extends a PostgreSQL-compatible query planner with understanding of vector operations.
### Hybrid Cost Model
For hybrid queries (vector + relational), the planner considers two physical plans:
**Plan A: Pre-filter**
```
Filter(price < 100) ANN(embedding, k=10)
```
Cost: selectivity × full_scan_cost + ANN_cost(filtered_set)
**Plan B: Post-filter**
```
ANN(embedding, k=10×estimated_filter_ratio) → Filter(price < 100)
```
Cost: ANN_cost(full_index) + filter_cost
The planner uses column statistics (histogram, null fraction, distinct values) and vector index parameters to estimate costs. It picks the plan with the lower estimated cost.
### Index Types
NeuralDB supports the following index types:
| Index | Data | Purpose |
|-------|------|---------|
| B-tree | Scalar columns | Equality, range queries |
| Hash | Scalar columns | Equality only (faster than B-tree) |
| GIN | JSON, arrays | Containment queries |
| HNSW | VECTOR columns | Approximate nearest neighbour |
| IVF-Flat | VECTOR columns | High-recall exact-ish search |
| BRIN | Timestamp columns | Range scans on append-only data |
## Replication
NeuralDB uses streaming replication. The primary continuously ships WAL segments to replicas, which apply them in order.
### Synchronous vs Asynchronous Replication
```sql
-- Set replication mode per-transaction
SET synchronous_commit = 'on'; -- wait for WAL to reach all sync replicas (safest)
SET synchronous_commit = 'local'; -- wait for local WAL flush only (faster)
SET synchronous_commit = 'off'; -- don't wait (fastest, small durability window)
```
### Read Replicas
Replicas accept `SELECT` queries. Direct read-heavy workloads to replicas:
```
primary: write queries + critical reads
replica-1: analytical queries, reporting
replica-2: search API traffic
```
The client SDK supports automatic read/write splitting:
```python
client = NeuralDB(
primary="primary.example.com:5432",
replicas=["replica1.example.com:5432", "replica2.example.com:5432"],
read_from="replicas",
replica_selection="round-robin",
)
```
## Memory Architecture
NeuralDB divides available memory into three pools:
| Pool | Purpose | Default |
|------|---------|---------|
| `shared_buffers` | Row store page cache | 25% of RAM |
| `vector_buffer` | HNSW graph warm cache | 40% of RAM |
| `work_mem` | Per-query sort and hash buffers | 64 MB |
Tune these in `neuraldb.conf`:
```ini
shared_buffers = 8GB
vector_buffer = 16GB
work_mem = 128MB
```
## Consistency Model
NeuralDB provides **strong consistency** for primary reads and **eventual consistency** for replica reads (with a configurable replication lag threshold).
Reads on the primary always see the latest committed data. Reads on replicas may lag behind the primary by the `max_replication_lag` setting (default: 1 second). To force a replica to wait until it is caught up:
```sql
SELECT pg_wait_for_replica_replay('0/1234ABCD');
```

View file

@ -0,0 +1,102 @@
---
title: Comparison
sort: 130
section-id: overview
keywords: comparison, Postgres, pgvector, Pinecone, Weaviate, MongoDB, alternatives
description: How NeuralDB compares to Postgres+pgvector, Pinecone, Weaviate, and MongoDB Atlas Vector Search
language: en
---
# Comparison
This page provides an honest comparison of NeuralDB against the most common alternatives for applications that need vector search.
## NeuralDB vs PostgreSQL + pgvector
pgvector is the most popular way to add vector search to an existing PostgreSQL deployment. If you already run Postgres, the low barrier to entry is attractive. Here is where the two diverge:
| Feature | NeuralDB | Postgres + pgvector |
|---------|---------|---------------------|
| Vector index algorithm | HNSW (native) | HNSW, IVFFlat |
| Max dimensions | 65,536 | 16,000 |
| Index build speed | Native Rust (fast) | C extension (moderate) |
| Parallel index builds | Yes | Limited |
| Vector data memory isolation | Dedicated vector buffer pool | Shared with row pages |
| Horizontal sharding | Built-in | Manual (Citus, Patroni) |
| Automatic embeddings | Yes | No |
| Multi-modal vectors | Yes | No (one VECTOR column type) |
| Streaming ingestion | Yes | No |
| Read replica for vector queries | Automatic routing | Manual |
**When to choose Postgres + pgvector:** You already have a Postgres deployment, your dataset is under 10M vectors, and you do not need automatic embeddings or horizontal scaling. The operational overhead of a new database system is not worth it.
**When to choose NeuralDB:** Your vector dataset exceeds 10M rows, you need horizontal sharding, you want automatic embedding pipelines, or you are starting a new project and want a purpose-built system.
## NeuralDB vs Pinecone
Pinecone is a fully managed, purpose-built vector database. It excels at pure vector search at massive scale.
| Feature | NeuralDB | Pinecone |
|---------|---------|---------|
| Relational data | Full SQL | Metadata filters only |
| Hybrid queries | Single query, query planner | Metadata post-filter |
| ACID transactions | Yes | No |
| SQL interface | Yes | Proprietary API |
| Self-hosted option | Yes | No |
| Pricing model | Infrastructure cost | Per-request + storage |
| Latency (p99, 1M vectors) | ~5ms | ~10ms (managed) |
| Data gravity | Stays in your infra | Vendor-managed |
**When to choose Pinecone:** You need a fully managed solution with no operational overhead, your workload is pure vector search with simple metadata filtering, and you are comfortable with a vendor-specific API and pricing model.
**When to choose NeuralDB:** You need relational data co-located with vectors, ACID transactions, SQL compatibility, self-hosting, or lower total cost of ownership at scale.
## NeuralDB vs Weaviate
Weaviate is an open-source vector database with a GraphQL-based query language and built-in module support for embedding generation.
| Feature | NeuralDB | Weaviate |
|---------|---------|---------|
| Query language | SQL (NQL) | GraphQL |
| Relational joins | Yes | No |
| ACID transactions | Yes | Eventually consistent |
| SQL wire compatibility | PostgreSQL wire protocol | Proprietary |
| Embedding modules | Yes | Yes (vectorizers) |
| BM25 hybrid search | Yes | Yes |
| Multi-tenancy | Row-level, schema-level | Class-level |
| Replication | Sync + async | Eventual |
**When to choose Weaviate:** You want an open-source solution with a rich ecosystem of vectorizer modules and a GraphQL interface. If your team is more comfortable with graph-shaped queries than SQL, Weaviate is a natural fit.
**When to choose NeuralDB:** You need SQL, transactional guarantees, relational joins between your vector data and other structured data, or PostgreSQL wire protocol compatibility (so existing tools like dbt, Metabase, and psql work out of the box).
## NeuralDB vs MongoDB Atlas Vector Search
MongoDB Atlas added vector search as an extension to its document model. It is a convenient choice if you already run Atlas.
| Feature | NeuralDB | MongoDB Atlas Vector Search |
|---------|---------|---------------------------|
| Data model | Relational + vector | Document + vector |
| Query language | SQL | MQL (MongoDB Query Language) |
| ACID transactions | Yes (all operations) | Yes (within a session) |
| Horizontal scaling | Native sharding | Atlas sharding |
| Vector index type | HNSW | ENN (exact), HNSW |
| Full-text + vector hybrid | Yes | Yes (Atlas Search) |
| Self-hosted | Yes | Atlas only |
**When to choose MongoDB Atlas Vector Search:** Your application already uses MongoDB and you want to add vector search without changing your data model or infrastructure. The document model maps well to semi-structured data.
**When to choose NeuralDB:** You need relational data integrity, SQL, lower query latency, or the ability to self-host. If your data is inherently tabular (rather than document-shaped), NeuralDB's relational model will be a better fit.
## Performance Benchmarks
The following benchmarks were run against 10M 1536-dimensional vectors on equivalent hardware (32 vCPU, 128 GB RAM, NVMe SSD):
| System | QPS (recall@95%) | p50 latency | p99 latency | Index build time |
|--------|-----------------|-------------|-------------|-----------------|
| NeuralDB 1.0 | 8,400 | 1.2ms | 4.8ms | 22 min |
| pgvector 0.7 | 3,100 | 2.9ms | 12ms | 45 min |
| Pinecone (s1) | 5,200 | 1.8ms | 8ms | Managed |
| Weaviate 1.24 | 4,600 | 2.1ms | 9ms | 31 min |
Benchmarks are inherently workload-dependent. Run your own benchmarks against your specific data and query patterns before making infrastructure decisions.

View file

@ -0,0 +1,171 @@
---
title: Core Concepts
sort: 110
section-id: overview
keywords: concepts, vectors, embeddings, hybrid queries, nodes, HNSW, ANN
description: Core NeuralDB concepts — vectors, embeddings, hybrid queries, and the node model
language: en
---
# Core Concepts
Understanding these fundamental concepts will help you use NeuralDB effectively and make good architectural decisions for your application.
## Vectors and Embeddings
A **vector** is an ordered list of floating-point numbers — a point in high-dimensional space. In NeuralDB, vectors are used to represent the semantic meaning of data.
An **embedding** is a vector produced by a machine learning model that encodes the semantic meaning of its input. Similar inputs produce vectors that are close together in the embedding space. For example, the sentences "I love dogs" and "I adore canines" will produce embeddings that are close to each other, even though they share no words.
NeuralDB stores embeddings as `VECTOR(n)` columns, where `n` is the dimensionality (the number of float32 values). Common dimensionalities:
| Model | Dimensions |
|-------|-----------|
| OpenAI text-embedding-3-small | 1536 |
| OpenAI text-embedding-3-large | 3072 |
| Cohere embed-english-v3.0 | 1024 |
| Google text-embedding-004 | 768 |
| BAAI/bge-m3 | 1024 |
## Distance Metrics
NeuralDB computes similarity between two vectors using one of three distance metrics:
### Cosine Similarity
Measures the angle between two vectors. Ranges from -1 (opposite) to 1 (identical). Ideal for text embeddings produced by models trained with cosine objectives:
```
cosine_similarity(a, b) = (a · b) / (|a| × |b|)
```
In NQL, use the `<=>` operator or `COSINE_SIMILARITY()` function.
### Dot Product
Measures the product of vector magnitudes and the angle between them. Used when vectors are not normalised and magnitude carries information (e.g., collaborative filtering):
```
dot_product(a, b) = Σ(aᵢ × bᵢ)
```
In NQL, use the `<#>` operator or `DOT_PRODUCT()` function.
### Euclidean Distance (L2)
Measures straight-line distance between two points. Lower is more similar. Useful for spatial data and image embeddings:
```
l2_distance(a, b) = √(Σ(aᵢ - bᵢ)²)
```
In NQL, use the `<->` operator or `L2_DISTANCE()` function.
## Vector Indexes
NeuralDB builds vector indexes using the **HNSW (Hierarchical Navigable Small World)** algorithm. HNSW provides:
- Sub-linear approximate nearest neighbour (ANN) search
- Configurable trade-off between speed and recall
- Incremental updates (no full rebuild needed when inserting)
### HNSW Parameters
When creating a vector index:
```sql
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
```
| Parameter | Description | Default |
|-----------|-------------|---------|
| `m` | Number of bi-directional links per node. Higher = better recall, more memory | 16 |
| `ef_construction` | Size of candidate set during index construction. Higher = better quality, slower build | 64 |
| `ef_search` | Size of candidate set at query time. Set per-query with `SET hnsw.ef_search = 100` | 40 |
### Exact vs Approximate Search
By default, vector queries use the HNSW index (approximate). For exact results (slower but 100% recall), use:
```sql
SET neuraldb.vector_scan = 'exact';
SELECT * FROM documents ORDER BY embedding <=> :query LIMIT 10;
```
## Hybrid Queries
A hybrid query combines vector similarity with relational predicates in a single query plan. The NeuralDB query planner evaluates two strategies and picks the cheaper one:
1. **Pre-filter then search** — apply relational filters first to reduce the candidate set, then run ANN search on the filtered set
2. **Post-filter** — run ANN search to get top-k candidates, then apply relational filters
NeuralDB automatically selects the optimal strategy based on selectivity estimates. You can hint the planner:
```sql
SELECT * FROM documents
WHERE /*+ PREFILTER */ category = 'news'
ORDER BY embedding <=> :query
LIMIT 10;
```
## Tables and Schemas
NeuralDB is schema-based, like PostgreSQL. Everything lives inside a database → schema → table hierarchy:
```sql
CREATE DATABASE my_app;
\c my_app
CREATE SCHEMA vectors;
CREATE SCHEMA metadata;
CREATE TABLE vectors.documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
embedding VECTOR(1536),
schema_id TEXT REFERENCES metadata.schemas(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
```
## Nodes
NeuralDB is a distributed system. A **node** is a single NeuralDB process. Nodes have one of three roles:
| Role | Responsibilities |
|------|----------------|
| **Primary** | Accepts reads and writes, coordinates transactions |
| **Replica** | Accepts reads, replicates writes from primary |
| **Index** | Maintains vector indexes for shard(s), offloads ANN queries |
In a single-node deployment, one process takes all three roles.
## Sharding
NeuralDB shards data by `shard_key`. By default, the primary key is used as the shard key:
```sql
CREATE TABLE events (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
event_type TEXT,
embedding VECTOR(768)
) SHARD BY tenant_id;
```
All rows with the same `tenant_id` are guaranteed to reside on the same shard, which enables efficient tenant-scoped queries without cross-shard joins.
## Transactions
NeuralDB uses multi-version concurrency control (MVCC) for transaction isolation, identical to PostgreSQL:
```sql
BEGIN;
INSERT INTO documents (content, embedding) VALUES ($1, $2);
UPDATE document_counts SET count = count + 1 WHERE id = $3;
COMMIT;
```
Both the vector data and the relational data are committed atomically. If the transaction rolls back, neither the row nor the vector index entry is committed.

View file

@ -0,0 +1,223 @@
---
title: Authentication
sort: 110
section-id: configuration
keywords: authentication, API keys, mTLS, RBAC, SSO, pg_hba, security
description: Configuring NeuralDB authentication — API keys, mTLS, role-based access control, and SSO
language: en
---
# Authentication
NeuralDB supports multiple authentication methods, from simple password auth to mutual TLS and enterprise SSO. This page explains how to configure each.
## Host-Based Authentication (pg_hba.conf)
The `pg_hba.conf` file controls which clients can connect and how they must authenticate. It is evaluated top-to-bottom and the first matching rule applies.
```
# pg_hba.conf
# FORMAT: TYPE DATABASE USER ADDRESS METHOD
# Local Unix socket connections (no password needed for postgres superuser)
local all neuraldb trust
local all all md5
# IPv4 connections from local network
host all all 127.0.0.1/32 scram-sha-256
host all all 10.0.0.0/8 scram-sha-256
# Reject all other connections
host all all 0.0.0.0/0 reject
```
Reload after changes:
```sql
SELECT pg_reload_conf();
```
## Authentication Methods
| Method | Security | Use case |
|--------|---------|---------|
| `trust` | None | Local socket, trusted environments |
| `md5` | Weak | Legacy compatibility |
| `scram-sha-256` | Strong (default) | Standard password auth |
| `cert` | Very strong | Mutual TLS authentication |
| `ldap` | Strong | Enterprise LDAP/AD integration |
| `radius` | Strong | RADIUS server |
| `gss` | Strong | Kerberos |
| `jwt` | Strong | Token-based auth |
### Enabling scram-sha-256
```ini
# neuraldb.conf
password_encryption = scram-sha-256
```
Set passwords for users:
```sql
CREATE USER app_user WITH PASSWORD 'secure-random-password';
ALTER USER app_user PASSWORD 'new-secure-password';
```
## Role-Based Access Control (RBAC)
### Creating Roles
```sql
-- Application read-only role
CREATE ROLE app_readonly;
GRANT CONNECT ON DATABASE myapp TO app_readonly;
GRANT USAGE ON SCHEMA public TO app_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO app_readonly;
-- Application read-write role
CREATE ROLE app_readwrite;
GRANT app_readonly TO app_readwrite;
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT INSERT, UPDATE, DELETE ON TABLES TO app_readwrite;
-- Vector operations role (needed for ANN searches)
CREATE ROLE vector_user;
GRANT USAGE ON SCHEMA public TO vector_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO vector_user;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO vector_user;
-- Application user
CREATE USER my_app WITH PASSWORD 'app-password';
GRANT app_readwrite TO my_app;
GRANT vector_user TO my_app;
```
### Row-Level Security (RLS)
For multi-tenant applications, use Row-Level Security to enforce tenant isolation at the database level:
```sql
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Tenants can only see their own documents
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.tenant_id')::UUID);
-- Admin can see everything
CREATE POLICY admin_access ON documents
TO admin_role
USING (true);
```
In your application, set the tenant context before querying:
```python
with connection.cursor() as cursor:
cursor.execute("SET app.tenant_id = %s", [tenant_id])
cursor.execute("SELECT * FROM documents ORDER BY embedding <=> %s LIMIT 10", [embedding])
```
## API Key Authentication
NeuralDB supports an API key table for token-based authentication — useful for microservices and CI pipelines that cannot use password auth:
```sql
-- Enable the API key extension
CREATE EXTENSION neuraldb_apikeys;
-- Create an API key for a service account
SELECT neuraldb_apikeys.create_key(
label => 'my-service',
role => 'app_readwrite'
);
-- Returns: ndb_live_abcdefghij123456...
```
Clients authenticate by passing the key in the connection string:
```
postgresql://apikey:ndb_live_abc123@host:5432/mydb
```
Revoke a key:
```sql
SELECT neuraldb_apikeys.revoke_key('ndb_live_abc123');
```
## Mutual TLS (mTLS)
For the highest security, require clients to present a certificate signed by your CA.
### 1. Generate a CA
```bash
# Generate CA key and certificate
openssl genrsa -out ca.key 4096
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 \
-subj "/CN=NeuralDB CA/O=My Org"
```
### 2. Issue a Server Certificate
```bash
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/CN=neuraldb.example.com"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365
```
### 3. Configure NeuralDB
```ini
# neuraldb.conf
ssl = on
ssl_cert_file = '/etc/neuraldb/ssl/server.crt'
ssl_key_file = '/etc/neuraldb/ssl/server.key'
ssl_ca_file = '/etc/neuraldb/ssl/ca.crt'
ssl_crl_file = '/etc/neuraldb/ssl/crl.pem' # optional certificate revocation list
```
### 4. Require Client Certificates
```
# pg_hba.conf
hostssl all all 0.0.0.0/0 cert clientcert=verify-full
```
## LDAP / Active Directory
```
# pg_hba.conf
host all all 0.0.0.0/0 ldap \
ldapserver=ldap.example.com \
ldapport=636 \
ldaptls=1 \
ldapbasedn="dc=example,dc=com" \
ldapbinddn="cn=neuraldb,dc=example,dc=com" \
ldapbindpasswd=bindpassword \
ldapsearchfilter="(sAMAccountName=%s)"
```
## Auditing
Enable connection and statement auditing:
```sql
CREATE EXTENSION pgaudit;
```
```ini
# neuraldb.conf
pgaudit.log = 'ddl, write, role'
pgaudit.log_catalog = on
pgaudit.log_client = on
pgaudit.log_level = log
```
Audit logs are written to the standard NeuralDB log file in a structured format, suitable for ingestion by SIEM systems.

View file

@ -0,0 +1,185 @@
---
title: Replication
sort: 130
section-id: configuration
keywords: replication, primary, replica, streaming replication, multi-region, consistency
description: Configuring NeuralDB replication — primary/replica setup, multi-region, and consistency levels
language: en
---
# Replication
NeuralDB replication is based on streaming replication: the primary continuously ships WAL records to replicas, which apply them in real time. This page explains how to set up and configure replication.
## Prerequisites
- The primary must have `wal_level = replica` or higher
- `max_wal_senders` must be greater than the number of replicas
- A replication user must exist
## Setting Up a Primary
Configure `neuraldb.conf` on the primary:
```ini
# neuraldb.conf (primary)
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 1GB
hot_standby_feedback = on # prevents primary from vacuuming rows still needed by replicas
```
Create a replication user:
```sql
CREATE USER replicator WITH REPLICATION PASSWORD 'repl-password';
```
Allow the replica to connect:
```
# pg_hba.conf (primary)
host replication replicator replica-ip/32 scram-sha-256
```
## Setting Up a Replica
On the replica server, use `pg_basebackup` to clone the primary:
```bash
# On the replica server
pg_basebackup \
--host=primary.example.com \
--port=5432 \
--username=replicator \
--pgdata=/var/lib/neuraldb/data \
--wal-method=stream \
--checkpoint=fast \
--progress \
--write-recovery-conf
```
The `--write-recovery-conf` flag creates a `standby.signal` file and writes connection info to `postgresql.auto.conf`, which tells NeuralDB to start in standby mode.
Configure `neuraldb.conf` on the replica:
```ini
# neuraldb.conf (replica)
hot_standby = on # allow read queries
hot_standby_feedback = on # send feedback to primary
wal_receiver_timeout = 60s
recovery_min_apply_delay = 0 # apply WAL immediately (increase for delayed replicas)
```
Start the replica:
```bash
systemctl start neuraldb
```
Verify replication is working:
```sql
-- On the primary
SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn,
(sent_lsn - replay_lsn) AS replication_lag_bytes
FROM pg_stat_replication;
```
## Replication Slots
Replication slots ensure the primary retains WAL until the replica has consumed it. This prevents the replica from falling too far behind, but also prevents WAL from being cleaned up if the replica disconnects.
```sql
-- Create a replication slot
SELECT pg_create_physical_replication_slot('replica_1');
-- List slots and their lag
SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots;
-- Drop a slot (do this if a replica is permanently removed)
SELECT pg_drop_replication_slot('replica_1');
```
**Warning:** Monitor slot lag. An inactive slot with large lag will cause unbounded WAL accumulation and can fill your disk.
## Synchronous Replication
By default, replication is asynchronous — the primary does not wait for replicas to acknowledge writes. For zero data loss, configure synchronous replication:
```ini
# neuraldb.conf (primary)
synchronous_standby_names = 'FIRST 1 (replica1, replica2)'
# ^ Wait for at least 1 of the listed standbys to acknowledge each commit
```
Modes:
- `FIRST n (list)` — wait for the first n standbys in the list
- `ANY n (list)` — wait for any n standbys from the list
- `*` — wait for all standbys
Per-transaction override:
```sql
SET synchronous_commit = 'local'; -- this transaction doesn't wait for replicas
```
## Multi-Region Replication
For global deployments, replicate to remote regions. The configuration is identical to local replication, but network latency affects synchronous commit performance.
Recommended approach for multi-region:
```
Primary (us-east-1)
├── Sync replica (us-east-1-az2) ← HA within region, ~2ms latency
├── Async replica (eu-west-1) ← EU reads, ~80ms latency
└── Async replica (ap-northeast-1) ← APAC reads, ~170ms latency
```
```ini
# Synchronous only within region; async to remote regions
synchronous_standby_names = 'FIRST 1 (local_replica)'
```
Configure the remote replicas with a `primary_conninfo` pointing to the primary:
```ini
# standby.signal (on replica)
primary_conninfo = 'host=primary.us-east-1.example.com port=5432 user=replicator password=repl-password sslmode=require'
```
## Failover
NeuralDB does not include automatic failover out of the box. Use one of:
- **Patroni** — industry-standard HA manager for PostgreSQL-compatible databases
- **NeuralDB HA Operator** — Kubernetes operator with automatic failover (see [Kubernetes docs](install-kubernetes.md))
- **repmgr** — lightweight failover manager
Manual failover:
```bash
# On the replica that will become the new primary
neuraldb-cli -c "SELECT pg_promote();"
```
After promotion, update `primary_conninfo` on all other replicas to point to the new primary.
## Monitoring Replication
```sql
-- Replication lag in bytes and seconds
SELECT client_addr, state,
pg_size_pretty(sent_lsn - replay_lsn) AS lag_bytes,
now() - pg_last_xact_replay_timestamp() AS lag_time
FROM pg_stat_replication;
-- On a replica: check its own lag
SELECT now() - pg_last_xact_replay_timestamp() AS lag,
pg_is_in_recovery() AS is_replica;
```
Set up an alert when replication lag exceeds 30 seconds.

View file

@ -0,0 +1,228 @@
---
title: Server Config
sort: 100
section-id: configuration
keywords: neuraldb.conf, server configuration, settings, parameters, tuning
description: Complete reference for neuraldb.conf — all server configuration settings explained
language: en
---
# Server Config
NeuralDB is configured through `neuraldb.conf`, a key-value text file. This page documents all configuration parameters.
## Locating the Config File
```bash
# Show the active config file path
neuraldb-cli -c "SHOW config_file;"
```
Default locations:
- Linux: `/etc/neuraldb/neuraldb.conf`
- macOS Homebrew: `$(brew --prefix)/etc/neuraldb/neuraldb.conf`
- Docker: `/var/lib/neuraldb/data/neuraldb.conf`
Changes to `neuraldb.conf` require a reload (for most parameters) or a restart:
```bash
# Reload without restart (applies most parameters)
neuraldb-cli -c "SELECT pg_reload_conf();"
# Full restart (required for listen_addresses, port, shared_buffers, etc.)
systemctl restart neuraldb
```
## Connection Settings
```ini
# Network interface to listen on
# '*' = all interfaces, 'localhost' = local only
listen_addresses = '*'
# TCP port
port = 5432
# Maximum simultaneous connections
# Each connection uses ~5 MB of memory
max_connections = 100
# Unix domain socket directory (Linux/macOS only)
unix_socket_directories = '/var/run/neuraldb'
# Superuser reserved connections
# Reserves this many connections exclusively for superusers
superuser_reserved_connections = 3
```
## Memory Settings
These are the most impactful parameters for performance.
```ini
# Page cache for relational data (row store)
# Recommended: 25% of available RAM
shared_buffers = 4GB
# Memory for HNSW vector indexes
# Recommended: 40-60% of available RAM for vector-heavy workloads
# Must be large enough to fit all active HNSW graphs
vector_buffer = 8GB
# Per-query working memory (sorts, hash joins)
# Recommended: 64MB256MB for OLTP, more for analytical queries
# Be conservative: (max_connections × work_mem) should fit in RAM
work_mem = 128MB
# Memory for DDL maintenance (CREATE INDEX, VACUUM, etc.)
maintenance_work_mem = 2GB
# Shared memory for parallel query workers
parallel_query_mem = 512MB
```
## Vector Settings
```ini
# Default HNSW ef_search parameter (candidates evaluated per query)
# Higher = better recall, slower queries
hnsw.ef_search = 40
# Maximum number of vectors per shard before auto-splitting
vector_shard_size = 10000000
# Enable approximate nearest neighbour by default (true = HNSW index)
# Set to 'exact' to always use exact search (ignores vector indexes)
vector_scan = 'approximate'
# Number of parallel threads for HNSW index builds
hnsw.build_threads = 4
# Compression algorithm for stored vector data
# 'none', 'lz4', 'scalar_quantize' (lossy but 4× smaller)
vector_compression = 'lz4'
```
## WAL Settings
```ini
# WAL logging level
# 'minimal': minimum for crash recovery
# 'replica': enables streaming replication (default)
# 'logical': enables logical replication and CDC
wal_level = replica
# WAL segment size (change requires initdb)
wal_segment_size = 128MB
# Maximum number of concurrent WAL sender processes
max_wal_senders = 10
# Number of WAL segments to keep for standby catching up
wal_keep_size = 1GB
# Synchronise WAL to disk before acknowledging commit
# 'on': safest; 'local': only local disk; 'off': async (fastest, tiny durability risk)
synchronous_commit = on
# Interval between WAL checkpoints
checkpoint_timeout = 5min
checkpoint_completion_target = 0.9
max_wal_size = 4GB
```
## Replication Settings
```ini
# Comma-separated list of standby names that must acknowledge writes
# Leave empty for asynchronous replication
synchronous_standby_names = ''
# Maximum lag allowed before primary throttles writes
max_standby_lag = 30s
# Hot standby: allow reads on replicas
hot_standby = on
```
## Query Planner
```ini
# Estimated cost of a random page fetch (tune based on SSD vs HDD)
# Lower values favour index scans; higher values favour sequential scans
random_page_cost = 1.1 # NVMe SSD (default is 4.0 for HDD)
# Effective size of the disk cache (affects planner estimates)
effective_cache_size = 24GB # ~75% of RAM
# Enable parallel query
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
max_worker_processes = 16
# Hybrid query planner behaviour
# 'auto': planner decides pre-filter vs post-filter
# 'pre-filter': always pre-filter
# 'post-filter': always post-filter
vector_hybrid_strategy = 'auto'
```
## Logging
```ini
# Log destination
log_destination = 'stderr' # 'stderr', 'csvlog', 'jsonlog', 'syslog'
# Minimum severity to log
# 'DEBUG5' (verbose) → 'INFO' → 'WARNING' → 'ERROR' → 'FATAL'
log_min_messages = WARNING
# Log all SQL statements with duration above this threshold (ms)
# -1 = disable; 0 = log everything; 250 = log slow queries only
log_min_duration_statement = 250
# Log query parameters
log_parameters = off
# Log connection events
log_connections = on
log_disconnections = on
# Log lock waits longer than this (ms)
deadlock_timeout = 1s
log_lock_waits = on
```
## Full Configuration Example
```ini
# neuraldb.conf — Production settings for a 32 vCPU / 128 GB server
listen_addresses = '*'
port = 5432
max_connections = 500
superuser_reserved_connections = 5
shared_buffers = 32GB
vector_buffer = 64GB
work_mem = 128MB
maintenance_work_mem = 4GB
wal_level = replica
max_wal_senders = 10
wal_keep_size = 2GB
synchronous_commit = on
checkpoint_timeout = 10min
max_wal_size = 8GB
random_page_cost = 1.1
effective_cache_size = 96GB
max_parallel_workers_per_gather = 8
max_parallel_workers = 16
hnsw.ef_search = 80
vector_compression = lz4
log_min_duration_statement = 500
log_connections = on
```

View file

@ -0,0 +1,221 @@
---
title: Storage Config
sort: 120
section-id: configuration
keywords: storage, memory, disk, SSD tiers, compression, tablespace, IOPS
description: Configuring NeuralDB storage — memory tiers, disk layout, compression, and tablespaces
language: en
---
# Storage Config
NeuralDB's performance depends heavily on storage configuration. This page covers how to optimise disk layout, configure memory tiers, enable compression, and use tablespaces for data tiering.
## Disk Layout Recommendations
Separate the data directory, WAL, and vector files onto different physical disks (or at least different volumes with guaranteed IOPS):
| Directory | Recommended storage | IOPS requirement |
|-----------|--------------------|--------------------|
| `$DATADIR/base/` | NVMe SSD | High random read/write |
| `$DATADIR/wal/` | NVMe SSD (separate) | High sequential write |
| `$DATADIR/vectors/` | NVMe SSD or RAM disk | High random read |
| `$DATADIR/archive/` | HDD or object storage | Low (sequential write) |
Configure separate paths in `neuraldb.conf`:
```ini
# Separate WAL onto a dedicated volume
wal_directory = '/mnt/nvme-wal/neuraldb/wal'
# Separate vector storage
vector_data_directory = '/mnt/nvme-fast/neuraldb/vectors'
# Archive destination for WAL shipping
archive_status_directory = '/var/lib/neuraldb/archive_status'
```
## Memory Tiers
NeuralDB has three distinct memory pools:
### shared_buffers (Row Store Cache)
The page cache for relational data. Sized at 25% of available RAM for a dedicated database server:
```ini
shared_buffers = 32GB # for a 128 GB server
```
### vector_buffer (Vector Index Cache)
Holds the HNSW graph in memory. The entire active HNSW graph must fit in `vector_buffer` for optimal performance. When the graph doesn't fit, NeuralDB falls back to disk-based graph traversal, which is 1050× slower.
Calculate the required size:
```
vector_buffer = num_vectors × dimensions × 4 bytes × hnsw_overhead_factor
```
Where `hnsw_overhead_factor` ≈ 1.3 for default HNSW parameters (m=16).
```sql
-- Check current vector index memory usage
SELECT table_name, index_name,
pg_size_pretty(hnsw_graph_size_bytes) AS graph_memory,
pg_size_pretty(vector_data_size_bytes) AS data_size
FROM neuraldb_stat_vector_indexes;
```
### work_mem (Per-Query Buffer)
Used for in-memory sorts, hash joins, and bitmap operations. Set conservatively — each query can allocate multiple `work_mem` buffers:
```ini
# For 200 connections with typical 2 buffers per query:
# Max memory consumption: 200 × 2 × 128MB = 51 GB
work_mem = 128MB
```
Override per-session for analytical queries:
```sql
SET work_mem = '2GB';
SELECT ... complex analytical query ...
```
## Compression
### Row Store Compression
NeuralDB compresses SSTables on disk. Choose the algorithm based on your priorities:
```ini
# Compression algorithm for row data
# 'none': no compression (fastest reads/writes, most disk)
# 'lz4': fast, moderate compression ratio (~2-3×) — default
# 'zstd': slower compression, better ratio (~3-5×)
# 'zstd-9': high compression for archival (slow, ~6-8×)
storage_compression = lz4
# Compression level (for zstd only), 1-19
storage_compression_level = 3
```
### Vector Compression
```ini
# Vector data compression
# 'none': full precision, largest storage
# 'lz4': fast, minimal precision loss
# 'scalar_quantize': reduce to 8-bit (4× smaller, ~1% recall loss)
# 'product_quantize': very high compression, higher recall loss
vector_compression = lz4
```
To enable scalar quantisation for a specific index:
```sql
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (quantization = 'scalar');
```
Scalar-quantised indexes use 4× less memory and may be faster due to better cache utilisation, at a typical recall cost of 0.52%.
## Tablespaces
Use tablespaces to store different tables or indexes on different volumes:
```sql
-- Create tablespaces pointing to different mount points
CREATE TABLESPACE fast_ssd LOCATION '/mnt/nvme-fast';
CREATE TABLESPACE bulk_hdd LOCATION '/mnt/hdd-storage';
-- Create a table on fast SSD
CREATE TABLE hot_documents (
id UUID PRIMARY KEY,
content TEXT,
embedding VECTOR(1536)
) TABLESPACE fast_ssd;
-- Move an index to a specific tablespace
CREATE INDEX ON hot_documents USING hnsw (embedding vector_cosine_ops)
TABLESPACE fast_ssd;
-- Move old partitions to cheaper storage
ALTER TABLE documents_2024_q1 SET TABLESPACE bulk_hdd;
```
## Table Partitioning
Partition large tables by time or tenant to improve query performance and manageability:
```sql
-- Partition documents by month
CREATE TABLE documents (
id UUID NOT NULL DEFAULT gen_random_uuid(),
content TEXT,
embedding VECTOR(1536),
tenant_id UUID,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);
CREATE TABLE documents_2026_01
PARTITION OF documents
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01')
TABLESPACE fast_ssd;
CREATE TABLE documents_2025_archive
PARTITION OF documents
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01')
TABLESPACE bulk_hdd;
```
## VACUUM and Autovacuum
NeuralDB inherits PostgreSQL's MVCC-based VACUUM system:
```ini
# Autovacuum settings
autovacuum = on
autovacuum_naptime = 1min
autovacuum_vacuum_threshold = 50
autovacuum_vacuum_scale_factor = 0.05 # vacuum when 5% of rows are dead
autovacuum_analyze_threshold = 50
autovacuum_analyze_scale_factor = 0.02 # analyse when 2% of rows change
# For high-write tables, increase autovacuum aggressiveness
autovacuum_vacuum_cost_delay = 2ms # default: 20ms
autovacuum_vacuum_cost_limit = 400 # default: 200
```
Manually vacuum a table:
```sql
VACUUM ANALYZE documents; -- reclaim space + update statistics
VACUUM FULL documents; -- full rewrite (blocking, very thorough)
```
## Monitoring Disk Usage
```sql
-- Table sizes
SELECT table_name,
pg_size_pretty(pg_total_relation_size(quote_ident(table_name))) AS total,
pg_size_pretty(pg_relation_size(quote_ident(table_name))) AS table,
pg_size_pretty(pg_total_relation_size(quote_ident(table_name))
- pg_relation_size(quote_ident(table_name))) AS indexes
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY pg_total_relation_size(quote_ident(table_name)) DESC;
-- Vector index sizes
SELECT * FROM neuraldb_stat_vector_indexes
ORDER BY hnsw_graph_size_bytes DESC;
-- Bloat estimate
SELECT tablename, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
```

View file

@ -0,0 +1,125 @@
---
title: What is NeuralDB?
sort: 100
section-id: overview
keywords: NeuralDB, introduction, AI database, vector database, overview
description: What NeuralDB is, its key use cases, and a high-level architecture overview
language: en
---
# What is NeuralDB?
![NeuralDB Platform](assets/images/hero.jpg)
NeuralDB is an AI-native database that unifies vector storage, semantic search, and relational data management in a single, horizontally scalable system. It is designed for applications that need to combine structured data queries with the semantic understanding that modern AI models provide.
Traditional databases store and retrieve data based on exact matches and range queries. NeuralDB adds a third dimension: **semantic proximity**. You can ask "find the 20 products most similar to this description" at the same time as "where inventory > 0 and price < 100" in a single, atomic query with full ACID guarantees.
## Why NeuralDB Exists
The AI application stack of 20242026 exposed a fundamental tension in data architecture. Teams building retrieval-augmented generation (RAG) systems, recommendation engines, and semantic search needed:
- A **vector database** (Pinecone, Weaviate, Qdrant) for embedding storage and similarity search
- A **relational database** (PostgreSQL, MySQL) for structured metadata and transactions
- A **synchronisation layer** to keep the two in sync — a constant source of bugs and operational overhead
NeuralDB replaces all three with a single system. Vectors and relational data share the same storage engine, the same transaction log, and the same query planner. There is no synchronisation problem because there is no synchronisation needed.
## Key Capabilities
### Hybrid Vector + Relational Queries
```sql
SELECT id, name, price, SIMILARITY(embedding, :query_embedding) AS score
FROM products
WHERE category = 'electronics'
AND price BETWEEN 50 AND 500
AND stock > 0
ORDER BY score DESC
LIMIT 10;
```
This query uses a vector index for semantic ranking and a B-tree index for the relational filters, with the query planner choosing the optimal execution order.
### Multiple Vector Index Types
NeuralDB supports three similarity metrics out of the box:
| Metric | Use case |
|--------|----------|
| Cosine similarity | Text embeddings, normalised vectors |
| Dot product | Recommendation systems (unnormalised) |
| Euclidean (L2) | Image embeddings, spatial data |
### Full ACID Transactions
Unlike most vector databases, NeuralDB provides full ACID guarantees — including transactional upserts of both the relational row and the vector embedding simultaneously.
### Automatic Embedding Updates
Configure NeuralDB to call an embedding API whenever a text column changes:
```sql
ALTER TABLE documents
ADD COLUMN embedding VECTOR(1536)
GENERATED ALWAYS AS EMBEDDING OF content
USING openai_ada_002;
```
NeuralDB handles the embedding pipeline automatically — no application-level embedding code required.
### Multi-Modal Vectors
Store and query vectors from any modality — text, image, audio, or custom — in the same table:
```sql
CREATE TABLE media_items (
id UUID PRIMARY KEY,
title TEXT,
text_embedding VECTOR(1536),
image_embedding VECTOR(512),
created_at TIMESTAMP DEFAULT NOW()
);
```
## Use Cases
**Retrieval-Augmented Generation (RAG)** — Store your document corpus with embeddings. Query the most relevant chunks in a single round-trip and inject them into your LLM prompt.
**Semantic Product Search** — Replace keyword search with semantic search. Find products matching "comfortable running shoes for flat feet" even if those exact words don't appear in any product description.
**Recommendation Engines** — Store user preference vectors alongside item vectors. Compute collaborative-filtering recommendations with a single NQL query.
**Anomaly Detection** — Flag records whose vectors are distant from the cluster of "normal" data.
**Duplicate Detection** — Find near-duplicate records across millions of rows using approximate nearest-neighbour (ANN) search.
**Knowledge Graphs** — Store entity embeddings alongside relationship metadata for graph-enhanced retrieval.
## NeuralDB vs Traditional Approaches
| Capability | NeuralDB | Postgres + pgvector | Pinecone + Postgres |
|-----------|---------|---------------------|---------------------|
| Vector search | Native, HNSW | Extension, limited | Native |
| Relational queries | Full SQL | Full SQL | None (separate DB) |
| Hybrid queries | Single query | Single query | Application-layer join |
| ACID transactions | Yes | Yes | Partial |
| Horizontal sharding | Built-in | Manual (Citus) | Managed |
| Automatic embeddings | Yes | No | No |
| Streaming ingestion | Yes | No | Partial |
## Getting Started
The fastest way to try NeuralDB is with Docker:
```bash
docker run -p 5432:5432 -e NEURALDB_PASSWORD=mypassword neuraldb/neuraldb:latest
```
Connect with any PostgreSQL-compatible client:
```bash
psql -h localhost -U neuraldb -d neuraldb
```
Then read the [Core Concepts](concepts.md) to understand the NeuralDB data model, or jump to [NQL Basics](nql-basics.md) to start writing queries.

View file

@ -0,0 +1,186 @@
---
title: Cloud Managed
sort: 120
section-id: installation
keywords: cloud, managed, NeuralDB Cloud, regions, tiers, SaaS
description: Setting up NeuralDB Cloud — the fully managed service with global regions and flexible tiers
language: en
---
# Cloud Managed
NeuralDB Cloud is the fully managed version of NeuralDB. It handles provisioning, patching, backups, monitoring, and scaling — so you can focus on building your application rather than managing database infrastructure.
## Getting Started
### 1. Create an Account
Sign up at [cloud.neuraldb.io](https://cloud.neuraldb.io). You can authenticate with Google, GitHub, or an email address.
### 2. Create a Cluster
Click **New Cluster** and configure:
- **Region**: choose the cloud region closest to your application servers
- **Tier**: select based on your workload requirements (see tier comparison below)
- **Storage**: initial storage allocation (can be scaled later)
- **High Availability**: enable for production workloads
### 3. Connect
Once the cluster is provisioned (typically under 3 minutes), your connection string appears in the dashboard:
```
postgresql://neuraldb:[password]@[cluster-id].cloud.neuraldb.io:5432/[database]?sslmode=require
```
Use this with any PostgreSQL-compatible driver or psql:
```bash
psql "postgresql://neuraldb:mypassword@abc123.cloud.neuraldb.io:5432/mydb?sslmode=require"
```
## Available Regions
NeuralDB Cloud is available in the following regions:
| Region | Cloud Provider | Availability |
|--------|---------------|-------------|
| us-east-1 (N. Virginia) | AWS | GA |
| us-west-2 (Oregon) | AWS | GA |
| eu-west-1 (Ireland) | AWS | GA |
| eu-central-1 (Frankfurt) | AWS | GA |
| ap-northeast-1 (Tokyo) | AWS | GA |
| ap-southeast-1 (Singapore) | AWS | GA |
| us-central1 (Iowa) | GCP | Beta |
| europe-west4 (Netherlands) | GCP | Beta |
| eastus (Virginia) | Azure | Beta |
Multi-region replication (primary in one region, read replicas in others) is available on Business and Enterprise tiers.
## Pricing Tiers
### Starter
Free tier for development and experimentation.
| Resource | Limit |
|---------|-------|
| Storage | 5 GB |
| Vector dimensions | Up to 1536 |
| Max connections | 10 |
| PITR | No |
| HA | No |
| SLA | No |
The Starter tier automatically suspends after 7 days of inactivity and resumes on the next connection (cold start: ~30 seconds).
### Developer
$29/month — for side projects and pre-production environments.
| Resource | Limit |
|---------|-------|
| vCPU | 2 dedicated |
| RAM | 8 GB |
| Storage | 100 GB NVMe SSD |
| Connections | 100 |
| PITR | 7 days |
| HA | No |
### Business
$199/month — for production workloads.
| Resource | Limit |
|---------|-------|
| vCPU | 8 dedicated |
| RAM | 32 GB |
| Storage | 500 GB NVMe SSD (expandable) |
| Connections | 500 |
| PITR | 30 days |
| HA | Yes (1 standby) |
| Read replicas | Up to 3 |
| SLA | 99.95% |
### Business Plus
$599/month — for large-scale production workloads.
| Resource | Limit |
|---------|-------|
| vCPU | 32 dedicated |
| RAM | 128 GB |
| Storage | 2 TB NVMe SSD (expandable) |
| Connections | 2,000 |
| PITR | 30 days |
| HA | Yes (2 standbys) |
| Read replicas | Up to 10 |
| Multi-region replicas | Yes |
| SLA | 99.99% |
### Enterprise
Custom pricing — for mission-critical applications with specific compliance requirements.
- Dedicated infrastructure (no multi-tenancy)
- Custom SLAs
- Private endpoints (AWS PrivateLink, GCP Private Service Connect)
- SOC 2 Type II, HIPAA, and ISO 27001 compliance
- Dedicated support with SLA
## Connecting from Your Application
### Connection Pooling
NeuralDB Cloud includes PgBouncer-based connection pooling. Use the pooler endpoint for serverless and short-lived connections:
```
postgresql://neuraldb:[password]@[cluster-id]-pooler.cloud.neuraldb.io:5432/[database]
```
| Connection type | Endpoint suffix | Use for |
|----------------|----------------|---------|
| Direct | (none) | Long-lived connections, COPY operations |
| Transaction pooling | `-pooler` | Serverless, short-lived connections |
| Session pooling | `-session-pooler` | Prepared statements |
### IP Allow-listing
Restrict access to specific IP ranges in the **Security** tab of your cluster dashboard. Enter your application servers' CIDR ranges (e.g., `10.0.0.0/8` for private VPC, or specific public IPs).
### SSL/TLS
All connections require TLS. Download the cluster CA certificate from the dashboard and verify it in your connection string:
```
sslmode=verify-full&sslrootcert=/path/to/ca.pem
```
## Monitoring and Alerting
NeuralDB Cloud includes a built-in monitoring dashboard with:
- Query throughput and latency percentiles (p50, p95, p99)
- Connection count
- Storage usage
- Vector index size
- Replication lag
Configure alerts for:
- Storage > 80% full
- Average query latency > 500ms
- Replication lag > 30s
- Failed connections
Metrics are also available via the NeuralDB Cloud API for ingestion into your own monitoring stack (Datadog, Grafana Cloud, New Relic).
## Branching (Database Branches)
NeuralDB Cloud supports **branching** — create an instant copy-on-write clone of your production database for development, testing, or migrations:
```bash
neuraldb-cloud branch create staging --from production
```
Branches share storage pages with the parent until they diverge (copy-on-write). A branch of a 100 GB database costs storage only for the pages that change.

View file

@ -0,0 +1,204 @@
---
title: Docker Install
sort: 100
section-id: installation
keywords: Docker, install, docker run, docker-compose, volumes, container
description: Installing NeuralDB using Docker — single container and docker-compose setups
language: en
---
# Docker Install
Docker is the fastest way to run NeuralDB locally or in a single-server deployment. NeuralDB's official Docker image is published to Docker Hub as `neuraldb/neuraldb`.
## Quick Start
Run a single NeuralDB instance:
```bash
docker run -d \
--name neuraldb \
-p 5432:5432 \
-e NEURALDB_PASSWORD=mypassword \
-e NEURALDB_DB=mydb \
-v neuraldb_data:/var/lib/neuraldb/data \
neuraldb/neuraldb:latest
```
Connect with psql:
```bash
psql -h localhost -p 5432 -U neuraldb -d mydb
# Password: mypassword
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `NEURALDB_PASSWORD` | required | Password for the `neuraldb` superuser |
| `NEURALDB_USER` | `neuraldb` | Superuser username |
| `NEURALDB_DB` | `neuraldb` | Default database name |
| `NEURALDB_PORT` | `5432` | TCP port |
| `NEURALDB_MAX_CONNECTIONS` | `100` | Maximum concurrent connections |
| `NEURALDB_SHARED_BUFFERS` | `256MB` | Row store page cache |
| `NEURALDB_VECTOR_BUFFER` | `512MB` | Vector index memory |
| `NEURALDB_WAL_LEVEL` | `replica` | WAL level (`minimal`, `replica`, `logical`) |
## Available Tags
| Tag | Description |
|-----|-------------|
| `latest` | Latest stable release |
| `1.0` | Specific major version |
| `1.0.3` | Specific patch version |
| `nightly` | Nightly build from main branch |
| `1.0-alpine` | Alpine-based image (smaller, less glibc compat) |
## Volumes
NeuralDB stores data in `/var/lib/neuraldb/data` inside the container. Always mount a named volume or bind mount to persist data:
```bash
# Named volume (recommended)
docker volume create neuraldb_data
docker run -v neuraldb_data:/var/lib/neuraldb/data neuraldb/neuraldb:latest
# Bind mount
docker run -v /srv/neuraldb:/var/lib/neuraldb/data neuraldb/neuraldb:latest
```
The data directory includes:
- `base/` — table and index data
- `vectors/` — HNSW graph files and raw vector data
- `wal/` — write-ahead log segments
- `neuraldb.conf` — runtime configuration (editable)
## docker-compose Setup
A production-grade docker-compose file for NeuralDB with automatic backup:
```yaml
# docker-compose.yml
version: '3.9'
services:
neuraldb:
image: neuraldb/neuraldb:1.0
container_name: neuraldb
restart: unless-stopped
ports:
- "127.0.0.1:5432:5432" # bind to localhost only — use nginx for external access
environment:
NEURALDB_PASSWORD: ${NEURALDB_PASSWORD}
NEURALDB_USER: ${NEURALDB_USER:-neuraldb}
NEURALDB_DB: ${NEURALDB_DB:-neuraldb}
NEURALDB_SHARED_BUFFERS: "4GB"
NEURALDB_VECTOR_BUFFER: "8GB"
NEURALDB_MAX_CONNECTIONS: "200"
volumes:
- neuraldb_data:/var/lib/neuraldb/data
- neuraldb_wal:/var/lib/neuraldb/wal
- ./neuraldb.conf:/etc/neuraldb/neuraldb.conf:ro # optional custom config
shm_size: '2gb' # increase shared memory for large sort operations
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${NEURALDB_USER:-neuraldb}"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 16G
reservations:
memory: 8G
neuraldb-backup:
image: neuraldb/neuraldb-backup:latest
environment:
NEURALDB_HOST: neuraldb
NEURALDB_PASSWORD: ${NEURALDB_PASSWORD}
S3_BUCKET: ${BACKUP_S3_BUCKET}
S3_PREFIX: backups/neuraldb/
SCHEDULE: "0 2 * * *" # 2am daily
depends_on:
neuraldb:
condition: service_healthy
volumes:
neuraldb_data:
driver: local
driver_opts:
type: none
o: bind
device: /srv/neuraldb/data
neuraldb_wal:
driver: local
driver_opts:
type: none
o: bind
device: /srv/neuraldb/wal
```
Start with:
```bash
echo "NEURALDB_PASSWORD=$(openssl rand -base64 32)" > .env
docker-compose up -d
```
## Initialisation Scripts
Place `.sql` or `.sh` scripts in `/docker-entrypoint-initdb.d/` to run them on first startup:
```bash
docker run \
-v ./init-scripts:/docker-entrypoint-initdb.d:ro \
-e NEURALDB_PASSWORD=mypassword \
neuraldb/neuraldb:latest
```
```sql
-- init-scripts/01-schema.sql
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
embedding VECTOR(1536),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
```
## Memory Tuning
For production, size the container memory based on your dataset:
```
Recommended memory = shared_buffers + vector_buffer + (max_connections × work_mem) + OS overhead
```
For a typical RAG application (5M documents, 1536 dimensions):
- `vector_buffer` ≈ 5M × 1536 × 4B × 1.3 = ~40 GB
- `shared_buffers` = 8 GB
- `work_mem` × connections = 128MB × 50 = 6.4 GB
- **Total**: ~56 GB — provision a 64 GB container
## Upgrading
```bash
# Pull the new image
docker pull neuraldb/neuraldb:1.1
# Stop the current container (data is safe in the volume)
docker stop neuraldb && docker rm neuraldb
# Start with the new image
docker run -d --name neuraldb \
-v neuraldb_data:/var/lib/neuraldb/data \
-e NEURALDB_PASSWORD=mypassword \
neuraldb/neuraldb:1.1
# Run any pending migrations
docker exec neuraldb neuraldb-migrate
```

View file

@ -0,0 +1,232 @@
---
title: Kubernetes
sort: 110
section-id: installation
keywords: Kubernetes, Helm, StatefulSet, PVC, k8s, cluster, deployment
description: Deploying NeuralDB on Kubernetes using the official Helm chart and StatefulSets
language: en
---
# Kubernetes
The recommended way to run NeuralDB on Kubernetes is via the official Helm chart. The chart deploys NeuralDB as a StatefulSet with persistent volume claims, and supports both standalone and high-availability configurations.
## Prerequisites
- Kubernetes 1.27+
- Helm 3.x
- A storage class that supports `ReadWriteOnce` PVCs (most cloud providers support this)
- At least 4 CPU cores and 8 GB RAM per NeuralDB node
## Installing the Helm Chart
```bash
# Add the NeuralDB Helm repository
helm repo add neuraldb https://charts.neuraldb.io
helm repo update
# Create a namespace
kubectl create namespace neuraldb
# Install the chart
helm install neuraldb neuraldb/neuraldb \
--namespace neuraldb \
--set auth.password=mysecretpassword \
--set persistence.size=100Gi
```
## Chart Configuration
Create a `values.yaml` file for production settings:
```yaml
# values.yaml
image:
repository: neuraldb/neuraldb
tag: "1.0"
pullPolicy: IfNotPresent
auth:
# Set via --set auth.password=... or a pre-existing secret
existingSecret: ""
secretKey: "neuraldb-password"
replicaCount: 1 # primary nodes (use 1 for standalone)
readReplicaCount: 2 # read replicas
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "8"
memory: "32Gi"
persistence:
enabled: true
storageClass: "fast-ssd" # use a fast SSD storage class
size: 500Gi
walSize: 50Gi # separate PVC for WAL
vectorBuffer: "16Gi" # memory for HNSW index
sharedBuffers: "8Gi" # row store page cache
maxConnections: 200
service:
type: ClusterIP
port: 5432
# High-availability configuration
ha:
enabled: true
replication:
mode: synchronous # 'synchronous' or 'asynchronous'
synchronousCommit: "on"
backup:
enabled: true
schedule: "0 2 * * *"
s3:
bucket: my-neuraldb-backups
region: us-east-1
existingSecret: aws-credentials
monitoring:
enabled: true
serviceMonitor:
enabled: true # requires Prometheus Operator
```
Apply the values:
```bash
helm install neuraldb neuraldb/neuraldb \
--namespace neuraldb \
-f values.yaml \
--set auth.password=$(openssl rand -base64 32)
```
## StatefulSet Details
The chart deploys a `StatefulSet` with:
- One pod per replica (primary + read replicas)
- Two PVCs per pod: data volume and WAL volume
- An init container that configures replication on startup
```yaml
# Example pod spec (simplified)
spec:
containers:
- name: neuraldb
image: neuraldb/neuraldb:1.0
ports:
- containerPort: 5432
resources:
requests:
memory: "8Gi"
cpu: "2"
volumeMounts:
- name: data
mountPath: /var/lib/neuraldb/data
- name: wal
mountPath: /var/lib/neuraldb/wal
livenessProbe:
exec:
command: ["pg_isready", "-U", "neuraldb"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["pg_isready", "-U", "neuraldb"]
initialDelaySeconds: 5
periodSeconds: 5
```
## Services
The chart creates three Kubernetes services:
| Service | Type | Port | Description |
|---------|------|------|-------------|
| `neuraldb-primary` | ClusterIP | 5432 | Primary — reads + writes |
| `neuraldb-replica` | ClusterIP | 5432 | Read replicas — reads only |
| `neuraldb-headless` | Headless | 5432 | For StatefulSet pod discovery |
Connect to the primary:
```bash
kubectl port-forward svc/neuraldb-primary 5432:5432 -n neuraldb
psql -h localhost -U neuraldb
```
## Persistent Volume Claims
Each pod gets two PVCs:
```yaml
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 500Gi
- metadata:
name: wal
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
```
Use a **fast-ssd** storage class (AWS `gp3`, GCP `pd-ssd`, Azure `Premium_LRS`) for the data and WAL volumes. Spinning disks are not supported in production.
## Secrets Management
Store the NeuralDB password in a Kubernetes secret:
```bash
kubectl create secret generic neuraldb-credentials \
--namespace neuraldb \
--from-literal=password=$(openssl rand -base64 32)
```
Reference it in `values.yaml`:
```yaml
auth:
existingSecret: neuraldb-credentials
secretKey: password
```
For larger installations, use an external secrets manager (HashiCorp Vault, AWS Secrets Manager) with the External Secrets Operator.
## Scaling Read Replicas
Scale the number of read replicas without downtime:
```bash
helm upgrade neuraldb neuraldb/neuraldb \
--namespace neuraldb \
--set readReplicaCount=4
```
The new replica pods will join the replication stream automatically.
## Upgrading
```bash
helm repo update
helm upgrade neuraldb neuraldb/neuraldb \
--namespace neuraldb \
-f values.yaml \
--set auth.existingSecret=neuraldb-credentials
```
The upgrade performs a rolling update — replicas are updated first, then the primary.

View file

@ -0,0 +1,211 @@
---
title: Local Development
sort: 130
section-id: installation
keywords: local, development, binary, homebrew, winget, install, macOS, Linux, Windows
description: Installing NeuralDB locally for development using binaries, Homebrew, or winget
language: en
---
# Local Development
For local development, you can run NeuralDB as a native binary without Docker. This provides lower latency for development workflows and avoids container overhead.
## System Requirements
| Platform | Minimum | Recommended |
|----------|---------|-------------|
| macOS | 13.0 (Ventura) | 14.x+ |
| Linux | Ubuntu 22.04 / RHEL 9 | Ubuntu 24.04 |
| Windows | Windows 10 22H2 | Windows 11 |
| CPU | x86-64 or ARM64 | ARM64 (Apple Silicon) |
| RAM | 4 GB | 16 GB+ |
| Disk | 2 GB free | SSD recommended |
## macOS
### Homebrew (Recommended)
```bash
brew tap neuraldb/tap
brew install neuraldb
# Start as a service (auto-restart on login)
brew services start neuraldb
# Or start manually (foreground)
neuraldb start
# Check status
neuraldb status
```
The Homebrew formula installs:
- `neuraldb` — the server binary
- `neuraldb-cli` — an enhanced SQL shell (psql-compatible)
- Configuration at `$(brew --prefix)/etc/neuraldb/neuraldb.conf`
- Data directory at `$(brew --prefix)/var/neuraldb`
### Direct Binary
```bash
# Apple Silicon (M1/M2/M3/M4)
curl -LO https://releases.neuraldb.io/1.0/neuraldb-macos-arm64.tar.gz
tar -xzf neuraldb-macos-arm64.tar.gz
sudo mv neuraldb /usr/local/bin/
sudo mv neuraldb-cli /usr/local/bin/
# Intel Mac
curl -LO https://releases.neuraldb.io/1.0/neuraldb-macos-amd64.tar.gz
tar -xzf neuraldb-macos-amd64.tar.gz
sudo mv neuraldb /usr/local/bin/
sudo mv neuraldb-cli /usr/local/bin/
```
## Linux
### Ubuntu / Debian
```bash
# Add the NeuralDB APT repository
curl -fsSL https://packages.neuraldb.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/neuraldb-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/neuraldb-keyring.gpg] https://packages.neuraldb.io/apt stable main" \
| sudo tee /etc/apt/sources.list.d/neuraldb.list
sudo apt update
sudo apt install -y neuraldb
# Start and enable the service
sudo systemctl enable --now neuraldb
```
### RHEL / Fedora / CentOS
```bash
# Add the NeuralDB YUM repository
sudo rpm --import https://packages.neuraldb.io/gpg
sudo tee /etc/yum.repos.d/neuraldb.repo <<'EOF'
[neuraldb]
name=NeuralDB Repository
baseurl=https://packages.neuraldb.io/rpm/stable
enabled=1
gpgcheck=1
gpgkey=https://packages.neuraldb.io/gpg
EOF
sudo dnf install -y neuraldb
sudo systemctl enable --now neuraldb
```
### Direct Binary (Linux)
```bash
# x86-64
curl -LO https://releases.neuraldb.io/1.0/neuraldb-linux-amd64.tar.gz
tar -xzf neuraldb-linux-amd64.tar.gz
sudo mv neuraldb neuraldb-cli /usr/local/bin/
```
## Windows
### winget
```powershell
winget install NeuralDB.NeuralDB
```
This installs NeuralDB and registers it as a Windows Service. It starts automatically after installation.
### Chocolatey
```powershell
choco install neuraldb
```
### MSI Installer
Download the MSI installer from [neuraldb.io/download](https://neuraldb.io/download) and run it. The installer:
1. Installs `neuraldb.exe` and `neuraldb-cli.exe` to `C:\Program Files\NeuralDB\`
2. Adds them to `PATH`
3. Creates a `NeuralDB` Windows Service
4. Initialises the data directory at `%APPDATA%\NeuralDB\data`
## First-Time Setup
After installation, initialise the database:
```bash
# Linux / macOS
neuraldb init
neuraldb start
# Connect
neuraldb-cli
# psql prompt: neuraldb=#
```
### Change the Default Password
```sql
ALTER USER neuraldb PASSWORD 'your-new-password';
```
### Create a Development Database
```sql
CREATE DATABASE myapp;
\c myapp
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT,
embedding VECTOR(1536)
);
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
```
## Configuration
The default configuration file locations:
| Platform | Path |
|----------|------|
| macOS (Homebrew) | `$(brew --prefix)/etc/neuraldb/neuraldb.conf` |
| Linux | `/etc/neuraldb/neuraldb.conf` |
| Windows | `%PROGRAMDATA%\NeuralDB\neuraldb.conf` |
For local development, create a `neuraldb.conf` in your project directory and point to it:
```bash
neuraldb start --config ./neuraldb.conf
```
Useful development settings:
```ini
# neuraldb.conf (development)
listen_addresses = 'localhost'
port = 5432
max_connections = 50
shared_buffers = 256MB
vector_buffer = 1GB
log_min_duration_statement = 100 # log slow queries (>100ms)
log_statement = 'all' # log all SQL (useful for debugging)
```
## Uninstalling
```bash
# macOS
brew services stop neuraldb
brew uninstall neuraldb
# Ubuntu
sudo systemctl stop neuraldb
sudo apt remove neuraldb
# Windows
winget uninstall NeuralDB.NeuralDB
# Or: Settings → Apps → Installed Apps → NeuralDB → Uninstall
```

View file

@ -0,0 +1,229 @@
---
title: Aggregations
sort: 130
section-id: query-language
keywords: aggregations, GROUP BY, COUNT, SUM, vectors, AVG, centroid, analytics
description: Aggregating data in NQL including GROUP BY, COUNT, SUM, and vector-specific aggregation functions
language: en
---
# Aggregations
NQL supports the full SQL aggregation toolkit, extended with vector-specific aggregate functions for centroid computation, clustering, and semantic analytics.
## Standard Aggregations
All standard SQL aggregate functions work as expected:
```sql
-- Count documents by category
SELECT category, COUNT(*) AS doc_count
FROM documents
GROUP BY category
ORDER BY doc_count DESC;
-- Average price by category
SELECT category,
COUNT(*) AS products,
AVG(price) AS avg_price,
MIN(price) AS min_price,
MAX(price) AS max_price,
SUM(stock * price) AS inventory_value
FROM products
WHERE available = true
GROUP BY category
ORDER BY inventory_value DESC;
```
## Vector Aggregations
### `AVG(embedding)` — Centroid Computation
Compute the centroid (average vector) of a group:
```sql
-- Centroid of all "technology" documents
SELECT AVG(embedding) AS centroid
FROM documents
WHERE category = 'technology';
```
Use centroids to find documents representative of a cluster:
```sql
WITH centroid AS (
SELECT AVG(embedding) AS c FROM documents WHERE category = 'technology'
)
SELECT id, title, 1 - (embedding <=> centroid.c) AS similarity_to_centroid
FROM documents, centroid
WHERE category = 'technology'
ORDER BY embedding <=> centroid.c
LIMIT 10;
```
### `vector_centroid(embedding)` — Weighted Centroid
Compute a weighted centroid using a score column:
```sql
-- Weighted centroid by rating (higher-rated items pull more)
SELECT vector_centroid(embedding, rating) AS weighted_centroid
FROM products
WHERE category = 'electronics';
```
### `vector_agg_concat(embedding)` — Vector Array
Collect vectors into an array for downstream processing:
```sql
SELECT category, vector_agg_concat(embedding) AS all_embeddings
FROM documents
GROUP BY category;
```
## GROUP BY with Vector Search
Find the best document in each category for a given query:
```sql
SELECT DISTINCT ON (category)
id, category, title, 1 - (embedding <=> $1) AS similarity
FROM documents
WHERE embedding IS NOT NULL
ORDER BY category, embedding <=> $1;
```
Or using a lateral join for more control:
```sql
SELECT cat.category, top_doc.id, top_doc.title, top_doc.similarity
FROM (SELECT DISTINCT category FROM documents) cat,
LATERAL (
SELECT id, title, 1 - (embedding <=> $1) AS similarity
FROM documents
WHERE category = cat.category
ORDER BY embedding <=> $1
LIMIT 1
) top_doc;
```
## Window Functions
Use window functions to rank results within partitions:
```sql
-- Rank documents by similarity within each category
SELECT
id, title, category,
1 - (embedding <=> $1) AS similarity,
RANK() OVER (
PARTITION BY category
ORDER BY embedding <=> $1
) AS rank_in_category
FROM documents
WHERE 1 - (embedding <=> $1) > 0.5
ORDER BY category, rank_in_category;
```
Rolling average similarity over time:
```sql
SELECT
date_trunc('day', created_at) AS day,
AVG(1 - (embedding <=> $1)) AS avg_daily_similarity,
AVG(AVG(1 - (embedding <=> $1))) OVER (
ORDER BY date_trunc('day', created_at)
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS rolling_7d_avg
FROM documents
GROUP BY day
ORDER BY day;
```
## Clustering with GROUP BY
Perform k-means style clustering by assigning documents to their nearest centroid:
```sql
-- Given pre-computed centroids in a centroids table:
SELECT d.id, d.content,
c.cluster_id,
(d.embedding <=> c.centroid) AS distance_to_centroid
FROM documents d
CROSS JOIN LATERAL (
SELECT cluster_id, centroid
FROM centroids
ORDER BY d.embedding <=> centroid
LIMIT 1
) c;
```
## HAVING with Vector Conditions
```sql
-- Categories where the average intra-category similarity is high (tight clusters)
SELECT category,
COUNT(*) AS doc_count,
1 - AVG(embedding <=> (SELECT AVG(e2.embedding) FROM documents e2 WHERE e2.category = e.category)) AS cohesion
FROM documents e
GROUP BY category
HAVING COUNT(*) > 10
ORDER BY cohesion DESC;
```
## Time-Series Analytics
Analyse how semantic content shifts over time:
```sql
-- Daily semantic drift: how different is today's content from last week's?
WITH weekly_centroids AS (
SELECT
date_trunc('week', created_at) AS week,
AVG(embedding) AS centroid
FROM documents
GROUP BY week
)
SELECT
w1.week,
1 - (w1.centroid <=> w2.centroid) AS similarity_to_prev_week
FROM weekly_centroids w1
LEFT JOIN weekly_centroids w2
ON w2.week = w1.week - INTERVAL '1 week'
ORDER BY w1.week;
```
## JSON Aggregation with Vectors
Combine JSON aggregation with vector results:
```sql
SELECT
category,
COUNT(*) AS total,
AVG(price) AS avg_price,
JSON_AGG(
JSON_BUILD_OBJECT('id', id, 'name', name, 'similarity', 1 - (embedding <=> $1))
ORDER BY embedding <=> $1
) FILTER (WHERE ROW_NUMBER() OVER (PARTITION BY category ORDER BY embedding <=> $1) <= 3)
AS top_3_per_category
FROM products
WHERE available = true
GROUP BY category;
```
## ROLLUP and CUBE
Standard SQL ROLLUP and CUBE work for hierarchical aggregation:
```sql
SELECT
region,
category,
COUNT(*) AS count,
AVG(price) AS avg_price
FROM products
GROUP BY ROLLUP(region, category)
ORDER BY region NULLS LAST, category NULLS LAST;
```

View file

@ -0,0 +1,239 @@
---
title: NQL Basics
sort: 100
section-id: query-language
keywords: NQL, NeuralDB Query Language, SQL, syntax, basics, queries
description: Introduction to NeuralDB Query Language (NQL) — syntax, data types, and basic operations
language: en
---
# NQL Basics
NQL (NeuralDB Query Language) is a superset of standard SQL. Every valid SQL statement is also valid NQL. NQL adds extensions for vector operations, embedding generation, and semantic search primitives.
If you know SQL, you already know most of NQL. This page covers the NQL-specific additions and the data types introduced for AI workloads.
## Connecting
NeuralDB speaks the PostgreSQL wire protocol. Connect with any PostgreSQL client:
```bash
# psql
psql -h localhost -p 5432 -U neuraldb -d mydb
# neuraldb-cli (enhanced interactive shell)
neuraldb-cli -h localhost
```
Connection string format:
```
postgresql://[user[:password]@][host][:port][/dbname][?param=value...]
```
## Data Types
### VECTOR(n)
The core NQL extension. Stores a fixed-length array of 32-bit floats representing a vector embedding:
```sql
-- Declare a vector column with 1536 dimensions (OpenAI ada-002 output)
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
embedding VECTOR(1536)
);
```
Insert a vector by providing a bracketed float array:
```sql
INSERT INTO documents (content, embedding)
VALUES ('Hello, world', '[0.1, 0.2, 0.3, ...]'); -- 1536 values
```
### HALFVEC(n)
A 16-bit float variant of VECTOR. Half the memory, slight precision loss. Useful when vector_buffer is a constraint:
```sql
embedding HALFVEC(1536)
```
### SPARSEVEC(n)
Sparse vector representation — stores only non-zero elements. Efficient for high-dimensional but sparse vectors (e.g., BM25 term-frequency vectors):
```sql
bm25_vector SPARSEVEC(30000)
```
## Basic CRUD
### Creating Tables
```sql
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
category TEXT,
price DECIMAL(10, 2),
stock INTEGER DEFAULT 0,
embedding VECTOR(1536),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
```
### Inserting Data
```sql
INSERT INTO products (name, description, category, price, stock, embedding)
VALUES (
'Wireless Headphones',
'Premium noise-cancelling wireless headphones with 30-hour battery',
'electronics',
299.99,
150,
'[0.023, -0.187, 0.412, ...]' -- 1536 floats from your embedding model
);
```
### Reading Data
Standard SQL SELECT works as expected:
```sql
SELECT id, name, price FROM products WHERE category = 'electronics';
SELECT * FROM products WHERE price BETWEEN 50 AND 300 AND stock > 0;
```
### Updating Data
```sql
UPDATE products SET price = 279.99, embedding = '[...]' WHERE id = $1;
```
When updating the `embedding` column, the HNSW index is updated atomically.
### Deleting Data
```sql
DELETE FROM products WHERE id = $1;
```
## Creating Vector Indexes
Without an index, vector similarity queries perform exact linear scans (O(n)). Create an HNSW index for sub-linear performance:
```sql
-- Cosine similarity (most common for text embeddings)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
-- Euclidean distance
CREATE INDEX ON documents USING hnsw (embedding vector_l2_ops);
-- Dot product (for recommendation systems)
CREATE INDEX ON documents USING hnsw (embedding vector_ip_ops);
```
Build an index on an existing large table in parallel:
```sql
SET max_parallel_maintenance_workers = 8;
CREATE INDEX CONCURRENTLY ON documents USING hnsw (embedding vector_cosine_ops);
```
## Basic Vector Queries
### Find Similar Documents
```sql
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
ORDER BY embedding <=> $1
LIMIT 10;
```
The `<=>` operator is cosine distance (lower = more similar). Subtract from 1 for a similarity score (higher = more similar).
### Distance Operators
| Operator | Distance metric | Index ops |
|----------|----------------|-----------|
| `<=>` | Cosine distance | `vector_cosine_ops` |
| `<->` | Euclidean (L2) distance | `vector_l2_ops` |
| `<#>` | Negative dot product | `vector_ip_ops` |
### Distance Threshold
```sql
-- Only return results with cosine similarity > 0.8
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
WHERE 1 - (embedding <=> $1) > 0.8
ORDER BY embedding <=> $1
LIMIT 20;
```
**Note:** The `WHERE 1 - (embedding <=> $1) > 0.8` condition is evaluated after the ANN search, not before. Use `LIMIT` generously enough to capture all relevant results before the threshold filter.
## NQL Functions
### `to_vector(text)`
Convert a string literal to a VECTOR:
```sql
SELECT to_vector('[0.1, 0.2, 0.3]')::VECTOR(3);
```
### `vector_dims(v)`
Return the number of dimensions:
```sql
SELECT vector_dims(embedding) FROM documents LIMIT 1;
-- Returns: 1536
```
### `vector_norm(v)`
Return the L2 norm of a vector:
```sql
SELECT vector_norm(embedding) FROM documents LIMIT 5;
```
### `cosine_similarity(a, b)`, `l2_distance(a, b)`, `dot_product(a, b)`
Named function alternatives to the operators:
```sql
SELECT cosine_similarity(embedding, $1) AS similarity
FROM documents
ORDER BY similarity DESC
LIMIT 10;
```
## Transactions
NQL supports full ACID transactions:
```sql
BEGIN;
INSERT INTO documents (content, embedding) VALUES ($1, $2);
UPDATE document_stats SET total_count = total_count + 1;
COMMIT;
```
On error, roll back:
```sql
BEGIN;
-- ... operations ...
ROLLBACK;
```

View file

@ -0,0 +1,216 @@
---
title: Hybrid Queries
sort: 120
section-id: query-language
keywords: hybrid queries, vector, relational, filters, combined, semantic search, metadata
description: Combining vector similarity and relational filters in NQL hybrid queries
language: en
---
# Hybrid Queries
Hybrid queries combine vector similarity search with relational filter predicates in a single SQL statement. The NeuralDB query planner handles the execution strategy — you write normal SQL with vector operators.
## Basic Hybrid Query
Find the 10 most semantically similar products that are in the "electronics" category and in stock:
```sql
SELECT id, name, price, 1 - (embedding <=> $1) AS similarity
FROM products
WHERE category = 'electronics'
AND stock > 0
AND price < 500
ORDER BY embedding <=> $1
LIMIT 10;
```
NeuralDB automatically determines whether to:
1. **Pre-filter**: apply relational conditions first, then search the filtered set
2. **Post-filter**: run ANN search, then apply conditions to the top-k results
The decision is based on the selectivity of the relational predicates.
## Query Planner Hints
Override the planner's strategy:
```sql
-- Force pre-filter (good when relational filter is very selective)
SELECT /*+ PREFILTER */ id, name, score
FROM (
SELECT id, name, 1 - (embedding <=> $1) AS score
FROM products
WHERE category = 'electronics' -- very selective: 2% of rows
) sub
ORDER BY score DESC
LIMIT 10;
-- Force post-filter (good when relational filter is weakly selective)
SELECT /*+ POSTFILTER */ id, name, 1 - (embedding <=> $1) AS score
FROM products
WHERE price < 500 -- weakly selective: 80% of rows
ORDER BY embedding <=> $1
LIMIT 10;
```
## Filtering by Multiple Conditions
```sql
SELECT id, name, description,
1 - (embedding <=> $1) AS similarity,
price,
rating
FROM products
WHERE category = ANY($2) -- multi-category filter
AND price BETWEEN $3 AND $4
AND rating >= 4.0
AND discontinued = false
AND created_at > NOW() - INTERVAL '1 year'
ORDER BY embedding <=> $1
LIMIT 20;
-- $1 = query embedding
-- $2 = ['electronics', 'computers']
-- $3 = 50, $4 = 1000
```
## Hybrid Full-Text + Vector (BM25)
Combine traditional full-text search with vector similarity using Reciprocal Rank Fusion (RRF):
```sql
WITH vector_search AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $1) AS rank
FROM documents
ORDER BY embedding <=> $1
LIMIT 100
),
fts_search AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(tsv, query) DESC) AS rank
FROM documents, to_tsquery('english', $2) query
WHERE tsv @@ query
ORDER BY ts_rank_cd(tsv, query) DESC
LIMIT 100
),
rrf AS (
SELECT
COALESCE(v.id, f.id) AS id,
(COALESCE(1.0 / (60 + v.rank), 0) + COALESCE(1.0 / (60 + f.rank), 0)) AS rrf_score
FROM vector_search v
FULL OUTER JOIN fts_search f ON v.id = f.id
)
SELECT d.id, d.content, rrf.rrf_score
FROM rrf
JOIN documents d ON d.id = rrf.id
ORDER BY rrf_score DESC
LIMIT 10;
```
NQL also provides a built-in `HYBRID_SEARCH` function:
```sql
SELECT id, content, score
FROM HYBRID_SEARCH(
table := 'documents',
vector_column := 'embedding',
tsv_column := 'tsv',
query_vector := $1,
query_text := $2,
top_k := 10,
rrf_k := 60,
vector_weight := 0.6,
text_weight := 0.4
);
```
## Joining Vector Results with Other Tables
```sql
SELECT
p.id,
p.name,
p.price,
c.name AS category_name,
u.display_name AS seller,
1 - (p.embedding <=> $1) AS similarity
FROM products p
JOIN categories c ON c.id = p.category_id
JOIN users u ON u.id = p.seller_id
WHERE p.available = true
AND c.slug = ANY($2)
AND u.verified = true
ORDER BY p.embedding <=> $1
LIMIT 15;
```
## Subquery Vectors
Use a subquery to dynamically compute a query vector from existing data:
```sql
-- Find products similar to product #123
SELECT id, name, 1 - (embedding <=> ref.embedding) AS similarity
FROM products,
(SELECT embedding FROM products WHERE id = $1) ref
WHERE id != $1
ORDER BY embedding <=> ref.embedding
LIMIT 10;
```
## Tenant-Scoped Search
For multi-tenant applications, always include tenant filters:
```sql
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
WHERE tenant_id = $2 -- partition pruning if SHARD BY tenant_id
AND embedding IS NOT NULL
ORDER BY embedding <=> $1
LIMIT 10;
```
If the table is sharded by `tenant_id`, this query runs entirely on the correct shard without cross-shard coordination.
## Composite Scoring
Combine vector similarity with relational signals:
```sql
SELECT
id,
name,
price,
rating,
-- Weighted composite score: 70% semantic, 20% rating, 10% recency
(0.7 * (1 - (embedding <=> $1))
+ 0.2 * (rating / 5.0)
+ 0.1 * (1 - EXTRACT(DAYS FROM NOW() - created_at) / 365.0)
) AS composite_score
FROM products
WHERE available = true
AND price < $2
ORDER BY composite_score DESC
LIMIT 20;
```
## Pagination
Cursor-based pagination for vector results:
```sql
-- Page 1
SELECT id, name, (embedding <=> $1) AS dist
FROM products
WHERE available = true
ORDER BY dist, id -- secondary sort by id for stable pagination
LIMIT 20;
-- Page 2 (cursor: last dist and id from page 1)
SELECT id, name, (embedding <=> $1) AS dist
FROM products
WHERE available = true
AND ((embedding <=> $1) > $2 OR ((embedding <=> $1) = $2 AND id > $3))
ORDER BY dist, id
LIMIT 20;
```

View file

@ -0,0 +1,237 @@
---
title: Transactions
sort: 140
section-id: query-language
keywords: transactions, ACID, isolation levels, MVCC, BEGIN, COMMIT, ROLLBACK
description: ACID transactions in NeuralDB — isolation levels, MVCC, savepoints, and advisory locks
language: en
---
# Transactions
NeuralDB provides full ACID transactions with MVCC (Multi-Version Concurrency Control). Unlike most vector databases, NeuralDB guarantees atomicity across both relational and vector data within a single transaction.
## ACID Guarantees
| Property | Guarantee |
|----------|---------|
| **Atomicity** | All operations in a transaction succeed or all are rolled back — including vector index updates |
| **Consistency** | Constraints (foreign keys, unique indexes, not null) are enforced at commit time |
| **Isolation** | Concurrent transactions do not see each other's uncommitted changes |
| **Durability** | Committed transactions survive crashes via the WAL |
## Basic Transaction Syntax
```sql
BEGIN;
-- Your operations here
INSERT INTO documents (content, embedding) VALUES ($1, $2);
UPDATE document_stats SET total_count = total_count + 1;
INSERT INTO audit_log (action, data) VALUES ('insert', $3);
COMMIT;
```
On error, roll back:
```sql
BEGIN;
INSERT INTO documents (content, embedding) VALUES ($1, $2);
-- Something went wrong
ROLLBACK;
```
## Isolation Levels
NeuralDB supports four isolation levels. Set them with `SET TRANSACTION ISOLATION LEVEL`:
```sql
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- ... your queries ...
COMMIT;
```
### Read Committed (Default)
Each statement sees only rows committed before that statement began. Two successive reads within the same transaction may see different data if another transaction commits between them.
```sql
BEGIN;
-- Sees all rows committed before this SELECT
SELECT COUNT(*) FROM documents; -- Returns 1000
-- Another transaction inserts and commits a row here
-- Sees the new row (non-repeatable read)
SELECT COUNT(*) FROM documents; -- Returns 1001
COMMIT;
```
### Repeatable Read
A transaction sees only rows committed before the transaction began. Reads are stable throughout the transaction.
```sql
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT COUNT(*) FROM documents; -- Returns 1000
-- Another transaction inserts and commits
SELECT COUNT(*) FROM documents; -- Still 1000 — repeatable read
COMMIT;
```
### Serializable
The strictest level. Transactions execute as if they ran serially one after another. NeuralDB uses Serializable Snapshot Isolation (SSI) — it allows concurrent execution but detects and aborts transactions that would produce a non-serializable outcome.
```sql
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- ... complex read-modify-write patterns ...
COMMIT;
-- May raise: ERROR: could not serialize access — retry the transaction
```
### Read Uncommitted
NeuralDB maps this to Read Committed (it does not implement dirty reads).
## Retry Logic
Serializable transactions can fail with serialization errors. Always retry:
```python
from psycopg2 import errors
MAX_RETRIES = 5
for attempt in range(MAX_RETRIES):
try:
with conn.cursor() as cur:
cur.execute("BEGIN ISOLATION LEVEL SERIALIZABLE")
# ... your operations ...
cur.execute("COMMIT")
break
except errors.SerializationFailure:
conn.rollback()
if attempt == MAX_RETRIES - 1:
raise
time.sleep(0.1 * (2 ** attempt)) # exponential backoff
```
## Savepoints
Savepoints allow partial rollbacks within a transaction:
```sql
BEGIN;
INSERT INTO documents (content, embedding) VALUES ($1, $2);
SAVEPOINT after_insert;
-- Risky operation
UPDATE document_stats SET count = count + 1 WHERE id = $3;
-- Oh no, something went wrong — roll back to the savepoint
ROLLBACK TO SAVEPOINT after_insert;
-- The INSERT is still pending — we can try a different approach
UPDATE document_stats SET count = count + 1 WHERE id = $4;
COMMIT;
```
## Vector Transactions
Vector index updates are transactional in NeuralDB. An HNSW index entry is added atomically with the row:
```sql
BEGIN;
-- Both the row and the vector index entry are inserted atomically
INSERT INTO documents (id, content, embedding) VALUES ($1, $2, $3);
-- If we ROLLBACK, neither the row nor the index entry will exist
ROLLBACK;
-- After rollback, a similarity search will NOT find $1
SELECT id FROM documents ORDER BY embedding <=> $3 LIMIT 1;
-- $1 is not returned
```
## Long-Running Transactions
Avoid long-running transactions — they:
- Hold row-level locks, blocking other writes
- Prevent VACUUM from reclaiming dead rows (bloat)
- Increase the risk of deadlocks
Set a statement timeout to kill runaway queries:
```sql
SET statement_timeout = '30s';
```
Set a transaction timeout:
```sql
SET idle_in_transaction_session_timeout = '5min';
```
## Deadlock Detection
NeuralDB automatically detects deadlocks and aborts one of the transactions:
```
ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process 99999.
Hint: See server log for query details.
```
Minimise deadlock risk by always acquiring locks in the same order across all transactions.
## Advisory Locks
For application-level locking (e.g., ensuring only one worker processes a job):
```sql
-- Acquire a session-level advisory lock (blocks until acquired)
SELECT pg_advisory_lock(42);
-- Try to acquire (returns false if already held)
SELECT pg_try_advisory_lock(42); -- returns boolean
-- Release
SELECT pg_advisory_unlock(42);
-- Transaction-level (auto-released at commit/rollback)
SELECT pg_advisory_xact_lock(42);
```
## Two-Phase Commit (2PC)
For distributed transactions spanning multiple systems:
```sql
-- Phase 1: Prepare
BEGIN;
-- ... operations ...
PREPARE TRANSACTION 'my-distributed-txn-id';
-- Phase 2: Commit or rollback
COMMIT PREPARED 'my-distributed-txn-id';
-- or
ROLLBACK PREPARED 'my-distributed-txn-id';
```
Check pending prepared transactions:
```sql
SELECT gid, prepared, owner, database
FROM pg_prepared_xacts;
```

View file

@ -0,0 +1,215 @@
---
title: Vector Queries
sort: 110
section-id: query-language
keywords: vector queries, NEAREST, SIMILAR, cosine, dot product, euclidean, ANN
description: Writing vector similarity queries in NQL — NEAREST, SIMILAR, distance operators, and recall tuning
language: en
---
# Vector Queries
NQL extends standard SQL with operators and functions for vector similarity search. This page covers every method for querying vectors, from basic nearest-neighbour lookups to advanced recall tuning.
## Distance Operators
NQL provides three distance operators that double as index-acceleration hints:
```sql
-- Cosine distance (returns 0 to 2, lower = more similar)
embedding <=> query_vector
-- Euclidean (L2) distance (returns 0 to ∞, lower = more similar)
embedding <-> query_vector
-- Negative dot product (higher inner product = more similar → negate for ORDER BY)
embedding <#> query_vector
```
Always pair `ORDER BY` with `LIMIT` when using distance operators — the planner uses the HNSW index only when there is an explicit `ORDER BY ... LIMIT`:
```sql
-- ✅ Uses HNSW index
SELECT id, content FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 10;
-- ❌ Full scan (no ORDER BY ... LIMIT)
SELECT id, content FROM documents
WHERE (embedding <=> '[0.1, 0.2, ...]') < 0.3;
```
## NEAREST Clause
NQL provides a syntactic alternative to `ORDER BY ... LIMIT` for nearest-neighbour queries:
```sql
SELECT id, content, score
FROM documents
NEAREST TO embedding = '[0.1, 0.2, ...]' USING COSINE
TOP 10;
```
This is equivalent to:
```sql
SELECT id, content, 1 - (embedding <=> '[0.1, 0.2, ...]') AS score
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'
LIMIT 10;
```
The `NEAREST TO` clause is more readable and allows NeuralDB to apply additional optimisations.
### Distance Metrics in NEAREST
```sql
NEAREST TO embedding = $1 USING COSINE TOP 10
NEAREST TO embedding = $1 USING EUCLIDEAN TOP 10
NEAREST TO embedding = $1 USING DOT_PRODUCT TOP 10
```
## SIMILAR Clause
`SIMILAR` returns results above a similarity threshold rather than a fixed count. Because the threshold is checked after the ANN search, NeuralDB must retrieve an initial candidate set. Use `LIMIT` to cap the candidates:
```sql
SELECT id, content, score
FROM documents
SIMILAR TO embedding = $1 USING COSINE THRESHOLD 0.75
LIMIT 100;
```
This returns up to 100 documents with cosine similarity ≥ 0.75.
## Returning Scores
Include the distance or similarity score in results:
```sql
-- Distance (lower = more similar)
SELECT id, content, (embedding <=> $1) AS distance
FROM documents
ORDER BY embedding <=> $1
LIMIT 10;
-- Similarity (higher = more similar, cosine)
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
ORDER BY embedding <=> $1
LIMIT 10;
```
## Querying With a Vector Literal
Pass vectors as SQL parameters (recommended) or literals:
```sql
-- Parameterised (prevents injection, preferred)
SELECT id, content FROM documents
ORDER BY embedding <=> $1
LIMIT 10;
-- $1 = '[0.023, -0.187, 0.412, ...]'
-- Inline literal (useful in SQL shells)
SELECT id, content FROM documents
ORDER BY embedding <=> '[0.023, -0.187, 0.412]'::VECTOR(3)
LIMIT 5;
```
## Recall Tuning
The HNSW index trades recall for performance. By default, `hnsw.ef_search = 40`, which provides ~95% recall at ~1ms latency for 10M vectors.
Increase `ef_search` for higher recall:
```sql
-- Set for the current session
SET hnsw.ef_search = 200;
-- Set for the current transaction
BEGIN;
SET LOCAL hnsw.ef_search = 200;
SELECT * FROM documents ORDER BY embedding <=> $1 LIMIT 10;
COMMIT;
```
Typical recall vs performance trade-off (10M 1536-dim vectors, 32 vCPU):
| ef_search | Recall@10 | p50 latency | QPS |
|-----------|-----------|-------------|-----|
| 20 | 89% | 0.7ms | 12,000 |
| 40 | 95% | 1.2ms | 8,400 |
| 80 | 98% | 2.1ms | 4,800 |
| 200 | 99.5% | 4.8ms | 2,100 |
| exact | 100% | 45ms | 220 |
## Exact Search
Force exact (brute-force) nearest-neighbour search, ignoring the HNSW index:
```sql
SET neuraldb.vector_scan = 'exact';
SELECT * FROM documents ORDER BY embedding <=> $1 LIMIT 10;
RESET neuraldb.vector_scan;
```
Use exact search when:
- You need 100% recall (e.g., de-duplication, exact compliance checks)
- The table has fewer than ~100k rows (exact is competitive)
- You are benchmarking ANN recall
## Bulk Vector Operations
### Batch Insert
Use `COPY` for high-throughput ingestion:
```bash
# Format: id\tcontent\tembedding
psql -c "\COPY documents (id, content, embedding) FROM '/data/vectors.tsv'"
```
### Updating Embeddings in Bulk
```sql
UPDATE documents
SET embedding = new_embeddings.embedding
FROM (VALUES
('uuid-1', '[...]'::VECTOR(1536)),
('uuid-2', '[...]'::VECTOR(1536))
) AS new_embeddings(id, embedding)
WHERE documents.id = new_embeddings.id::UUID;
```
## Multi-Vector Queries
Find documents closest to ANY of multiple query vectors (OR semantics):
```sql
WITH queries AS (
SELECT UNNEST(ARRAY['[...]'::VECTOR(1536), '[...]'::VECTOR(1536)]) AS qv
),
ranked AS (
SELECT d.id, d.content, MIN(d.embedding <=> q.qv) AS best_distance
FROM documents d, queries q
GROUP BY d.id, d.content
)
SELECT * FROM ranked
ORDER BY best_distance
LIMIT 20;
```
## Vector Arithmetic
NQL supports vector arithmetic for query expansion and centroid computation:
```sql
-- Average embedding of a result set (cluster centroid)
SELECT AVG(embedding) FROM documents WHERE category = 'technology';
-- Find documents similar to the average
SELECT id, content FROM documents
ORDER BY embedding <=> (SELECT AVG(embedding) FROM documents WHERE category = 'tech')
LIMIT 10;
```

View file

@ -0,0 +1,227 @@
---
title: Backup & Restore
sort: 110
section-id: operations
keywords: backup, restore, snapshot, WAL archiving, PITR, point-in-time recovery
description: Backup and restore strategies for NeuralDB — snapshots, WAL archiving, and point-in-time recovery
language: en
---
# Backup & Restore
A comprehensive backup strategy for NeuralDB combines base snapshots with continuous WAL archiving, enabling point-in-time recovery (PITR) to any moment within your retention window.
## Backup Strategies
| Strategy | Recovery point objective | Recovery time | Storage |
|----------|--------------------------|---------------|---------|
| Snapshot only | Time of last snapshot | Fast | Medium |
| WAL archiving only | Continuous (any point) | Slow | High |
| Snapshot + WAL | Best of both | Fast | High |
**Recommendation:** Use snapshot + WAL archiving in production. Take daily base snapshots and archive WAL continuously.
## Physical Snapshot (pg_basebackup)
`pg_basebackup` creates a consistent physical copy of the data directory:
```bash
# Full backup — local filesystem
pg_basebackup \
--host=localhost \
--port=5432 \
--username=backup_user \
--pgdata=/backups/neuraldb/$(date +%Y%m%d) \
--wal-method=stream \
--checkpoint=fast \
--compress=lz4 \
--progress \
--verbose
# Full backup — tar format (smaller, easier to upload to S3)
pg_basebackup \
--host=localhost \
--pgdata=- \
--format=tar \
--wal-method=stream \
--compress=lz4 \
| aws s3 cp - s3://my-backups/neuraldb/base-$(date +%Y%m%d).tar.lz4
```
Create a dedicated backup user:
```sql
CREATE USER backup_user WITH REPLICATION PASSWORD 'backup-password';
GRANT CONNECT ON DATABASE neuraldb TO backup_user;
```
## WAL Archiving
WAL archiving copies each WAL segment to a secure location as it is completed. Combined with a base snapshot, this enables PITR.
Enable WAL archiving:
```ini
# neuraldb.conf
wal_level = replica
archive_mode = on
archive_command = 'aws s3 cp %p s3://my-backups/neuraldb/wal/%f'
archive_timeout = 60 # archive at least every 60 seconds even if no WAL activity
```
Verify archiving is working:
```sql
SELECT last_archived_wal, last_archived_time,
last_failed_wal, last_failed_time,
archived_count, failed_count
FROM pg_stat_archiver;
```
### S3 Archive Command
```bash
#!/bin/bash
# /usr/local/bin/neuraldb-archive.sh
# Usage: %p = source file path, %f = file name
set -e
SOURCE="$1"
DEST_FILE="$2"
S3_BUCKET="${ARCHIVE_S3_BUCKET}"
S3_PREFIX="${ARCHIVE_S3_PREFIX:-neuraldb/wal/}"
aws s3 cp "$SOURCE" "s3://${S3_BUCKET}/${S3_PREFIX}${DEST_FILE}" \
--storage-class STANDARD_IA \
--sse aws:kms
```
```ini
archive_command = '/usr/local/bin/neuraldb-archive.sh %p %f'
```
## Automated Backups with pgBackRest
pgBackRest is the recommended tool for production NeuralDB backups:
```bash
# Install
sudo apt install pgbackrest
# Configure
sudo tee /etc/pgbackrest/pgbackrest.conf <<'EOF'
[global]
repo1-path=/var/lib/pgbackrest
repo1-retention-full=7
repo1-retention-diff=14
repo1-type=s3
repo1-s3-bucket=my-neuraldb-backups
repo1-s3-endpoint=s3.amazonaws.com
repo1-s3-region=us-east-1
compress-type=lz4
start-fast=y
backup-standby=y
[neuraldb]
pg1-path=/var/lib/neuraldb/data
pg1-port=5432
pg1-user=backup_user
EOF
# Initialise
sudo -u postgres pgbackrest --stanza=neuraldb stanza-create
# Full backup
sudo -u postgres pgbackrest --stanza=neuraldb backup --type=full
# Differential backup (only changes since last full)
sudo -u postgres pgbackrest --stanza=neuraldb backup --type=diff
# Incremental (only changes since last backup of any type)
sudo -u postgres pgbackrest --stanza=neuraldb backup --type=incr
```
Schedule backups with cron:
```cron
# /etc/cron.d/neuraldb-backup
0 1 * * 0 postgres pgbackrest --stanza=neuraldb backup --type=full
0 1 * * 1-6 postgres pgbackrest --stanza=neuraldb backup --type=diff
```
## Point-in-Time Recovery (PITR)
To restore to a specific point in time:
```bash
# Stop NeuralDB
systemctl stop neuraldb
# Restore a base backup
pgbackrest --stanza=neuraldb restore \
--target="2026-05-15 14:30:00+00" \
--target-action=promote \
--delta
# Or restore to just before a specific transaction
pgbackrest --stanza=neuraldb restore \
--target-name="before_accidental_delete" \
--target-action=promote
# Start NeuralDB — it will replay WAL up to the target point
systemctl start neuraldb
```
Create named restore points before risky operations:
```sql
-- Before running a migration
SELECT pg_create_restore_point('before_migration_20260515');
```
## Logical Backup (pg_dump)
For smaller databases or table-level backups, `pg_dump` provides a logical backup:
```bash
# Dump entire database
pg_dump -h localhost -U neuraldb mydb | \
lz4 | \
aws s3 cp - s3://my-backups/neuraldb/logical-$(date +%Y%m%d).sql.lz4
# Dump specific table
pg_dump -h localhost -U neuraldb -t documents mydb > documents-backup.sql
# Dump in custom format (best compression, selective restore)
pg_dump -Fc -h localhost -U neuraldb mydb > mydb-$(date +%Y%m%d).dump
```
**Note:** Logical backups do not include vector index data — only the raw vector column values. After restore, recreate indexes manually.
## Restoring from pg_dump
```bash
# Restore entire database
lz4 -d backup.sql.lz4 | psql -h localhost -U neuraldb -d mydb_restore
# Restore custom format
pg_restore -h localhost -U neuraldb -d mydb_restore --jobs=8 mydb.dump
# Restore a single table
pg_restore -h localhost -U neuraldb -d mydb -t documents mydb.dump
```
## Testing Backups
Never trust backups you haven't tested. Automate monthly restore tests:
```bash
#!/bin/bash
# Test backup restore in a separate environment
pgbackrest --stanza=neuraldb restore --pg1-path=/tmp/restore-test --delta
pg_ctl -D /tmp/restore-test start
psql -h /tmp/restore-test -c "SELECT COUNT(*) FROM documents;" neuraldb
pg_ctl -D /tmp/restore-test stop
rm -rf /tmp/restore-test
echo "Restore test passed: $(date)"
```

View file

@ -0,0 +1,250 @@
---
title: Migration
sort: 130
section-id: operations
keywords: migration, import, Postgres, Pinecone, Weaviate, data migration, ETL
description: Migrating data to NeuralDB from PostgreSQL, Pinecone, Weaviate, and other sources
language: en
---
# Migration
This guide covers migrating data into NeuralDB from common sources: PostgreSQL (with or without pgvector), Pinecone, and Weaviate.
## From PostgreSQL (without vectors)
If you are migrating a standard PostgreSQL database to NeuralDB, the simplest path is a logical dump and restore:
```bash
# 1. Dump from source Postgres
pg_dump \
-h source-host \
-U source-user \
-d source-database \
--format=custom \
--compress=9 \
> source-backup.dump
# 2. Create the target database in NeuralDB
psql -h neuraldb-host -U neuraldb -c "CREATE DATABASE myapp;"
# 3. Restore into NeuralDB
pg_restore \
-h neuraldb-host \
-U neuraldb \
-d myapp \
--jobs=8 \
--no-owner \
source-backup.dump
```
### Adding Vector Columns Post-Migration
After restoring the schema and data, add vector columns and generate embeddings:
```sql
-- Add the vector column
ALTER TABLE documents ADD COLUMN embedding VECTOR(1536);
-- Create the index (do this before backfilling on large tables)
CREATE INDEX CONCURRENTLY documents_embedding_idx
ON documents USING hnsw (embedding vector_cosine_ops);
```
Then backfill embeddings in batches:
```python
import openai
from neuraldb import NeuralDB
client = NeuralDB(connection_string)
openai_client = openai.OpenAI()
BATCH_SIZE = 100
while True:
rows = client.query("""
SELECT id, content FROM documents
WHERE embedding IS NULL
LIMIT %s
""", [BATCH_SIZE])
if not rows:
break
texts = [row['content'] for row in rows]
response = openai_client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
updates = [
(response.data[i].embedding, rows[i]['id'])
for i in range(len(rows))
]
client.executemany(
"UPDATE documents SET embedding = %s WHERE id = %s",
updates
)
print(f"Backfilled {len(rows)} rows")
```
## From PostgreSQL + pgvector
pgvector uses the same `VECTOR` type as NeuralDB. Migration is a direct dump and restore with minimal adjustments.
```bash
# Dump — exclude pgvector extension (NeuralDB has native vector support)
pg_dump \
-h source-host -U source-user -d source-db \
--format=custom \
--exclude-extension=vector \
> pgvector-backup.dump
pg_restore \
-h neuraldb-host -U neuraldb -d myapp \
--jobs=8 \
pgvector-backup.dump
```
### Re-create HNSW Indexes
pgvector HNSW indexes are not transferred. Recreate them in NeuralDB:
```sql
-- Drop pgvector-created indexes
DROP INDEX IF EXISTS documents_embedding_idx;
-- Create NeuralDB HNSW index (same syntax, better performance)
CREATE INDEX CONCURRENTLY documents_embedding_idx
ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
```
## From Pinecone
Pinecone stores vectors with metadata. Export using the Pinecone SDK and ingest into NeuralDB:
```python
import pinecone
from neuraldb import NeuralDB, BulkIngestor
# Source: Pinecone
pc = pinecone.Pinecone(api_key=os.environ["PINECONE_API_KEY"])
index = pc.Index("my-index")
# Target: NeuralDB
client = NeuralDB(os.environ["NEURALDB_URL"])
# Create target table
client.execute("""
CREATE TABLE IF NOT EXISTS pinecone_migration (
id TEXT PRIMARY KEY,
embedding VECTOR(1536),
metadata JSONB,
migrated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)
""")
client.execute("""
CREATE INDEX IF NOT EXISTS pinecone_migration_emb_idx
ON pinecone_migration USING hnsw (embedding vector_cosine_ops)
""")
# Paginate through all Pinecone vectors
ingestor = BulkIngestor(client, table="pinecone_migration", batch_size=500)
with ingestor as ing:
for ids_batch in paginate_pinecone_ids(index, batch_size=1000):
fetch_response = index.fetch(ids=ids_batch)
for vector_id, vector_data in fetch_response.vectors.items():
ing.add({
"id": vector_id,
"embedding": vector_data.values,
"metadata": vector_data.metadata or {}
})
print(f"Migrated {ingestor.total_inserted} vectors")
```
### Mapping Pinecone Metadata to Columns
Flatten commonly-queried metadata fields into dedicated columns for better query performance:
```python
# Instead of: metadata JSONB
# Create typed columns for common filter fields:
client.execute("""
ALTER TABLE pinecone_migration
ADD COLUMN IF NOT EXISTS category TEXT GENERATED ALWAYS AS (metadata->>'category') STORED,
ADD COLUMN IF NOT EXISTS created_date DATE GENERATED ALWAYS AS ((metadata->>'date')::DATE) STORED;
CREATE INDEX ON pinecone_migration (category);
CREATE INDEX ON pinecone_migration (created_date);
""")
```
## From Weaviate
Export Weaviate data using the Weaviate client SDK:
```python
import weaviate
from neuraldb import NeuralDB, BulkIngestor
weaviate_client = weaviate.connect_to_local()
neuraldb_client = NeuralDB(os.environ["NEURALDB_URL"])
collection = weaviate_client.collections.get("Document")
# Create target schema
neuraldb_client.execute("""
CREATE TABLE weaviate_documents (
id UUID PRIMARY KEY,
content TEXT,
category TEXT,
source TEXT,
embedding VECTOR(1536)
);
CREATE INDEX ON weaviate_documents USING hnsw (embedding vector_cosine_ops);
""")
ingestor = BulkIngestor(neuraldb_client, table="weaviate_documents", batch_size=500)
with ingestor as ing:
for item in collection.iterator(include_vector=True):
ing.add({
"id": str(item.uuid),
"content": item.properties.get("content", ""),
"category": item.properties.get("category"),
"source": item.properties.get("source"),
"embedding": item.vector.get("default"),
})
weaviate_client.close()
print(f"Migrated {ingestor.total_inserted} objects")
```
## Verifying Migration
After any migration, verify data integrity:
```sql
-- Row count comparison
SELECT COUNT(*) FROM documents;
-- Sample vector similarity (should match source)
SELECT id, content, 1 - (embedding <=> (SELECT embedding FROM documents LIMIT 1)) AS sim
FROM documents
ORDER BY embedding <=> (SELECT embedding FROM documents LIMIT 1)
LIMIT 5;
-- Check for null embeddings
SELECT COUNT(*) FROM documents WHERE embedding IS NULL;
-- Index health
SELECT index_name, hnsw_in_memory, estimated_recall
FROM neuraldb_stat_vector_indexes;
```

View file

@ -0,0 +1,214 @@
---
title: Monitoring
sort: 100
section-id: operations
keywords: monitoring, Prometheus, Grafana, metrics, alerts, observability, dashboards
description: Monitoring NeuralDB with Prometheus metrics, Grafana dashboards, and alert configuration
language: en
---
# Monitoring
![NeuralDB Dashboard](assets/images/dashboard.jpg)
Observability is critical for database operations. NeuralDB exposes Prometheus-compatible metrics and provides an official Grafana dashboard for real-time monitoring.
## Prometheus Metrics
NeuralDB exposes metrics at `http://localhost:9187/metrics` (via the bundled exporter).
Enable the metrics exporter:
```ini
# neuraldb.conf
metrics.enabled = true
metrics.port = 9187
metrics.path = /metrics
```
Or run the standalone exporter:
```bash
neuraldb_exporter \
--web.listen-address=:9187 \
--db.uri="postgresql://monitor:password@localhost:5432/neuraldb?sslmode=disable"
```
### Key Metrics
#### Connection Metrics
| Metric | Type | Description |
|--------|------|-------------|
| `neuraldb_connections_total` | Gauge | Current connections by state |
| `neuraldb_connections_max` | Gauge | `max_connections` setting |
| `neuraldb_connection_pool_waiting` | Gauge | Queries waiting for a connection |
#### Query Metrics
| Metric | Type | Description |
|--------|------|-------------|
| `neuraldb_queries_total` | Counter | Total queries by database and status |
| `neuraldb_query_duration_seconds` | Histogram | Query duration (p50, p95, p99) |
| `neuraldb_slow_queries_total` | Counter | Queries exceeding `log_min_duration_statement` |
| `neuraldb_deadlocks_total` | Counter | Deadlocks detected |
#### Vector Metrics
| Metric | Type | Description |
|--------|------|-------------|
| `neuraldb_vector_queries_total` | Counter | Vector similarity queries by index |
| `neuraldb_vector_query_duration_seconds` | Histogram | ANN query latency |
| `neuraldb_hnsw_index_size_bytes` | Gauge | In-memory size of HNSW graphs |
| `neuraldb_hnsw_build_duration_seconds` | Histogram | Time to build HNSW indexes |
| `neuraldb_vector_recall_ratio` | Gauge | Estimated recall for ANN queries |
#### Replication Metrics
| Metric | Type | Description |
|--------|------|-------------|
| `neuraldb_replication_lag_bytes` | Gauge | WAL lag per replica |
| `neuraldb_replication_lag_seconds` | Gauge | Time lag per replica |
| `neuraldb_wal_size_bytes` | Gauge | Current WAL on-disk size |
#### Storage Metrics
| Metric | Type | Description |
|--------|------|-------------|
| `neuraldb_database_size_bytes` | Gauge | Total database size |
| `neuraldb_table_size_bytes` | Gauge | Size per table |
| `neuraldb_bloat_ratio` | Gauge | Estimated dead row ratio |
| `neuraldb_checkpoint_duration_seconds` | Histogram | Checkpoint write time |
## Prometheus Configuration
```yaml
# prometheus.yml
scrape_configs:
- job_name: 'neuraldb'
static_configs:
- targets: ['localhost:9187']
scrape_interval: 15s
metrics_path: /metrics
```
## Grafana Dashboard
Import the official NeuralDB dashboard from Grafana.com (Dashboard ID: **18921**):
```bash
# Import via Grafana API
curl -X POST \
http://admin:password@localhost:3000/api/dashboards/import \
-H "Content-Type: application/json" \
-d '{ "gnetId": 18921, "overwrite": true, "inputs": [{"name": "DS_PROMETHEUS", "type": "datasource", "pluginId": "prometheus", "value": "Prometheus"}] }'
```
The dashboard includes panels for:
- Query rate and error rate
- Query latency percentiles (p50, p95, p99)
- Active connections vs max connections
- Vector index memory usage
- Replication lag
- Database and table sizes
- Cache hit ratio
- Checkpoint frequency
## Alerting Rules
Create Prometheus alerting rules for critical conditions:
```yaml
# neuraldb-alerts.yml
groups:
- name: neuraldb
rules:
- alert: NeuralDBConnectionsHigh
expr: neuraldb_connections_total{state="active"} / neuraldb_connections_max > 0.85
for: 2m
labels:
severity: warning
annotations:
summary: "NeuralDB connections above 85%"
description: "{{ $value | humanizePercentage }} of max connections in use"
- alert: NeuralDBConnectionsExhausted
expr: neuraldb_connections_total{state="active"} / neuraldb_connections_max > 0.98
for: 30s
labels:
severity: critical
annotations:
summary: "NeuralDB connections nearly exhausted"
- alert: NeuralDBHighQueryLatency
expr: histogram_quantile(0.99, rate(neuraldb_query_duration_seconds_bucket[5m])) > 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "P99 query latency above 1 second"
- alert: NeuralDBReplicationLagHigh
expr: neuraldb_replication_lag_seconds > 30
for: 1m
labels:
severity: warning
annotations:
summary: "Replication lag above 30 seconds"
- alert: NeuralDBDiskSpaceHigh
expr: (neuraldb_database_size_bytes / disk_total_bytes) > 0.80
for: 5m
labels:
severity: warning
annotations:
summary: "Database storage above 80% capacity"
- alert: NeuralDBVectorBufferExhausted
expr: neuraldb_hnsw_index_size_bytes > (neuraldb_vector_buffer_size_bytes * 0.90)
for: 5m
labels:
severity: warning
annotations:
summary: "HNSW indexes using >90% of vector_buffer"
```
## Built-In Query Statistics
```sql
-- Top 10 slowest queries
SELECT query,
calls,
round(mean_exec_time::numeric, 2) AS avg_ms,
round(total_exec_time::numeric, 2) AS total_ms,
round(stddev_exec_time::numeric, 2) AS stddev_ms
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- Cache hit ratio (should be >99%)
SELECT
sum(blks_hit) * 100.0 / sum(blks_hit + blks_read) AS cache_hit_ratio
FROM pg_stat_database
WHERE datname != 'template0';
-- Lock waits
SELECT pid, query, state, wait_event_type, wait_event, query_start
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
ORDER BY query_start;
```
## Log-Based Alerting
Forward slow query logs to your SIEM or log aggregation system:
```ini
# neuraldb.conf
log_destination = 'jsonlog'
log_min_duration_statement = 500 # log queries slower than 500ms
log_line_prefix = '%t [%p] %u@%d '
```
Parse JSON logs in Loki or Elasticsearch and alert when the rate of slow queries exceeds a threshold.

View file

@ -0,0 +1,201 @@
---
title: Scaling
sort: 120
section-id: operations
keywords: scaling, sharding, read replicas, horizontal scaling, capacity planning, performance
description: Scaling NeuralDB horizontally with sharding, read replicas, and capacity planning
language: en
---
# Scaling
NeuralDB is designed to scale horizontally. This page covers adding read replicas for query throughput, sharding for data volume, and capacity planning to avoid resource exhaustion.
## Vertical Scaling (Scale Up)
Before adding nodes, ensure you have maximised single-node performance:
### Memory
The biggest lever for NeuralDB performance is memory. Ensure:
- `vector_buffer` is large enough to hold all active HNSW graphs
- `shared_buffers` is set to 25% of RAM
- `work_mem` is appropriate for your query patterns
```sql
-- Check if vectors are being served from disk (slow) vs memory (fast)
SELECT index_name, hnsw_graph_size_bytes, hnsw_in_memory
FROM neuraldb_stat_vector_indexes
ORDER BY hnsw_graph_size_bytes DESC;
```
If `hnsw_in_memory = false`, increase `vector_buffer`.
### CPU
Vector ANN searches are CPU-bound. Enable parallel query:
```ini
max_parallel_workers_per_gather = 8
max_parallel_workers = 16
```
```sql
-- Allow parallel ANN queries for large tables
SET max_parallel_workers_per_gather = 8;
SELECT * FROM large_table ORDER BY embedding <=> $1 LIMIT 10;
```
### Storage I/O
Use NVMe SSDs with high IOPS. Configure the OS:
```bash
# Increase read-ahead for sequential I/O
sudo blockdev --setra 1024 /dev/nvme0n1
# Use deadline/mq-deadline I/O scheduler
echo "mq-deadline" | sudo tee /sys/block/nvme0n1/queue/scheduler
# Disable transparent huge pages (reduces latency variability)
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
```
## Read Replicas
Add read replicas to distribute query load.
### Setting Up Read Replicas
Follow the [Replication guide](config-replication.md) to add replicas. Each replica can independently serve `SELECT` queries, including vector similarity searches.
### Client-Side Read Splitting
Configure your application to route reads to replicas:
**Python:**
```python
from neuraldb import NeuralDB
primary = NeuralDB("postgresql://neuraldb:pass@primary:5432/mydb")
replica = NeuralDB("postgresql://neuraldb:pass@replica:5432/mydb")
def search(query_vector):
# Read goes to replica
return replica.query("SELECT * FROM docs ORDER BY embedding <=> %s LIMIT 10", [query_vector])
def insert(content, embedding):
# Write goes to primary
return primary.execute("INSERT INTO docs (content, embedding) VALUES (%s, %s)", [content, embedding])
```
**Connection string with `target_session_attrs`:**
```
postgresql://neuraldb:pass@primary:5432,replica:5432/mydb?target_session_attrs=prefer-standby
```
### Read Replica Scaling Targets
| Replicas | Approximate peak QPS (1536-dim, 10M vectors) |
|---------|----------------------------------------------|
| 1 primary | 8,000 |
| 1 primary + 2 replicas | 24,000 |
| 1 primary + 4 replicas | 48,000 |
| 1 primary + 8 replicas | 96,000 |
## Horizontal Sharding
For datasets exceeding single-node capacity (>50M vectors or >5 TB), shard across multiple primary nodes.
### Shard Configuration
```sql
-- Create a sharded cluster (requires NeuralDB Cluster Edition)
SELECT neuraldb_cluster.init_cluster(
shards => 8,
replication_factor => 2
);
-- Create a sharded table
CREATE TABLE documents (
id UUID NOT NULL DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
content TEXT,
embedding VECTOR(1536)
) SHARD BY tenant_id;
-- Each shard holds ~1/8 of the data
-- All rows with the same tenant_id are colocated on the same shard
```
### Cross-Shard Queries
Cross-shard queries (where the filter doesn't align with the shard key) are automatically parallelised across shards:
```sql
-- This query executes on all 8 shards in parallel
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
ORDER BY embedding <=> $1
LIMIT 10;
-- Results are merged and re-ranked by the coordinator
```
Performance with 8 shards: near-linear scaling. An 8-shard cluster serves ~8× the QPS of a single node for cross-shard searches, with ~20% overhead for coordination.
### Shard Rebalancing
When adding new shard nodes, rebalance data:
```sql
-- Rebalance shards (online, non-blocking)
SELECT neuraldb_cluster.rebalance_shards();
-- Monitor progress
SELECT * FROM neuraldb_cluster.rebalance_status;
```
## Capacity Planning
### Storage Capacity
Estimate required storage:
```
Row data ≈ avg_row_size_bytes × num_rows × 1.3 (index overhead)
Vector data ≈ dimensions × 4 bytes × num_vectors
HNSW graph ≈ dimensions × 4 bytes × num_vectors × 1.3
WAL ≈ daily_write_volume × wal_retention_days
Total ≈ row_data + vector_data + HNSW_graph + WAL + 20% buffer
```
Example: 100M rows, 1536 dimensions, 500 bytes average row size:
- Row data: 500B × 100M × 1.3 ≈ **65 GB**
- Vector data: 1536 × 4B × 100M ≈ **614 GB**
- HNSW graph: 614 GB × 1.3 ≈ **800 GB** (must fit in `vector_buffer`)
- WAL (7 days): 10 GB/day × 7 = **70 GB**
- **Total: ~1.6 TB storage, 800 GB RAM for HNSW**
### Connection Capacity
```
max_connections = max_app_connections + pgbouncer_pool_size + replication_slots + 3 (superuser)
```
For 500 app connections through PgBouncer with pool size 20:
```
max_connections = 20 + 10 (replicas) + 3 = 33
```
PgBouncer multiplexes 500 app connections → 20 database connections.
### Alert Thresholds
| Resource | Warning | Critical |
|---------|---------|---------|
| Connections | 80% of max | 95% of max |
| Storage | 70% full | 85% full |
| vector_buffer utilisation | 80% | 90% |
| Replication lag | 30s | 120s |
| Query p99 latency | 500ms | 2000ms |

View file

@ -0,0 +1,226 @@
---
title: Troubleshooting
sort: 140
section-id: operations
keywords: troubleshooting, errors, diagnostics, FAQ, common problems, debug
description: Common NeuralDB errors, diagnostic techniques, and frequently asked questions
language: en
---
# Troubleshooting
This page covers common NeuralDB errors and how to diagnose and resolve them.
## Connection Issues
### `FATAL: password authentication failed for user "neuraldb"`
The password is incorrect or the user doesn't exist.
```bash
# Reset the password as the OS postgres user
sudo -u neuraldb neuraldb-cli
```
```sql
ALTER USER neuraldb PASSWORD 'new-password';
```
Check `pg_hba.conf` — ensure the correct authentication method is used for the client's IP address.
### `FATAL: no pg_hba.conf entry for host "x.x.x.x", user "neuraldb"`
The client's IP is not in `pg_hba.conf`:
```
# Add to pg_hba.conf
host all all x.x.x.x/32 scram-sha-256
```
Reload: `SELECT pg_reload_conf();`
### `could not connect to server: Connection refused`
NeuralDB is not running on the expected host/port:
```bash
# Check process
systemctl status neuraldb
# or
ps aux | grep neuraldb
# Check listening port
ss -tlnp | grep 5432
# Check logs
journalctl -u neuraldb -n 50
```
### `FATAL: remaining connection slots are reserved for non-replication superuser connections`
All available connections are consumed. Use PgBouncer to pool connections, or increase `max_connections`:
```sql
-- Check current connections
SELECT count(*), state, wait_event_type FROM pg_stat_activity GROUP BY state, wait_event_type;
-- Kill idle connections
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE state = 'idle' AND state_change < NOW() - INTERVAL '10 minutes';
```
## Vector Query Issues
### Slow Vector Searches
If vector queries are slow, check whether the HNSW index is being used:
```sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, embedding <=> '[...]' AS dist
FROM documents
ORDER BY embedding <=> '[...]'
LIMIT 10;
```
Look for `Index Scan using documents_embedding_idx` in the plan. If you see `Seq Scan`, the planner may have decided the index is not beneficial.
Common causes:
1. **Missing LIMIT clause**: The planner only uses the HNSW index for `ORDER BY ... LIMIT` queries.
2. **Too few rows**: For small tables, a sequential scan may be faster.
3. **HNSW graph not in memory**: Check `SELECT * FROM neuraldb_stat_vector_indexes` — if `hnsw_in_memory = false`, increase `vector_buffer`.
4. **ef_search too low**: Increase for better recall at the cost of speed.
```sql
-- Force index use for debugging
SET enable_seqscan = off;
EXPLAIN ANALYZE SELECT ... ORDER BY embedding <=> $1 LIMIT 10;
SET enable_seqscan = on;
```
### `ERROR: expected 1536 dimensions, not 768`
The vector you are inserting has a different number of dimensions than the column definition:
```sql
-- Check column definition
\d documents
-- embedding column shows VECTOR(1536)
-- You are inserting a 768-dimensional vector — check your embedding model
```
Ensure your embedding model is consistent. If you need to change models, you must re-embed all existing data.
### Low Recall on ANN Queries
If approximate queries are not returning expected results:
```sql
-- Increase ef_search for higher recall
SET hnsw.ef_search = 200;
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
ORDER BY embedding <=> $1
LIMIT 10;
```
Compare against exact search:
```sql
SET neuraldb.vector_scan = 'exact';
SELECT id, content FROM documents ORDER BY embedding <=> $1 LIMIT 10;
```
If exact search finds results that approximate search misses, increase `ef_search` or rebuild the index with a higher `m` value.
## Performance Issues
### High Memory Usage
```sql
-- Check vector index memory consumption
SELECT index_name, pg_size_pretty(hnsw_graph_size_bytes) AS graph_memory
FROM neuraldb_stat_vector_indexes
ORDER BY hnsw_graph_size_bytes DESC;
-- Check for shared_buffers usage
SELECT name, setting, unit FROM pg_settings
WHERE name IN ('shared_buffers', 'vector_buffer', 'work_mem');
```
### Disk Space Exhaustion
```sql
-- Identify large tables and indexes
SELECT tablename, pg_size_pretty(pg_total_relation_size(tablename::regclass)) AS size
FROM pg_tables WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(tablename::regclass) DESC
LIMIT 20;
-- Check WAL accumulation (often caused by idle replication slots)
SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;
```
Drop inactive replication slots:
```sql
SELECT pg_drop_replication_slot('orphaned_slot_name');
```
### Long-Running Queries
```sql
-- Find queries running longer than 30 seconds
SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state
FROM pg_stat_activity
WHERE state != 'idle'
AND (now() - pg_stat_activity.query_start) > INTERVAL '30 seconds'
ORDER BY duration DESC;
-- Terminate a specific query
SELECT pg_cancel_backend(pid); -- send SIGINT (graceful)
SELECT pg_terminate_backend(pid); -- send SIGTERM (forceful)
```
## Replication Issues
### Replication Lag Growing
```sql
-- Check lag on primary
SELECT client_addr, state, sent_lsn - replay_lsn AS lag_bytes
FROM pg_stat_replication;
-- On replica: check replay lag
SELECT now() - pg_last_xact_replay_timestamp() AS lag;
```
Causes: high write load, slow replica hardware, network saturation. Solutions: increase replica hardware, add more replicas, use async replication.
### `FATAL: the database system is not accepting connections — the database system is starting up`
NeuralDB is replaying WAL after a crash. Wait for it to complete. Check progress:
```bash
tail -f /var/log/neuraldb/neuraldb.log
```
## FAQ
**Q: Can I use NeuralDB as a drop-in replacement for PostgreSQL?**
Yes. NeuralDB implements the PostgreSQL wire protocol. Any psql-compatible client, ORM, or tool that works with PostgreSQL will work with NeuralDB.
**Q: How do I know if my HNSW index is working correctly?**
Run `EXPLAIN ANALYZE` on your vector query and look for `Index Scan using ... hnsw`. Also check `neuraldb_stat_vector_indexes` for `estimated_recall`.
**Q: What should `vector_buffer` be set to?**
Set it large enough to hold the sum of all HNSW graph sizes. Query `SELECT SUM(hnsw_graph_size_bytes) FROM neuraldb_stat_vector_indexes` to see the current total.
**Q: Can I store vectors from different embedding models in the same table?**
Only if they have the same dimensionality. Otherwise, use separate columns (e.g., `embedding_openai VECTOR(1536)` and `embedding_cohere VECTOR(1024)`).
**Q: Is NeuralDB compatible with pgvector extensions?**
NeuralDB includes native support for all pgvector data types (`VECTOR`, `HALFVEC`, `SPARSEVEC`) and operators (`<=>`, `<->`, `<#>`). Applications written for pgvector work without modification.

View file

@ -0,0 +1,286 @@
---
title: Go SDK
sort: 120
section-id: client-sdks
keywords: Go, Golang, SDK, client, connection pool, query builder, pgx
description: The NeuralDB Go SDK — installation, connection pooling, and vector query builder
language: en
---
# Go SDK
The NeuralDB Go SDK provides idiomatic Go bindings built on top of `pgx`, the high-performance PostgreSQL driver for Go.
## Installation
```bash
go get github.com/neuraldb/neuraldb-go
```
Requires Go 1.21+.
## Connecting
### Single Connection
```go
package main
import (
"context"
"fmt"
"log"
"github.com/neuraldb/neuraldb-go"
)
func main() {
ctx := context.Background()
client, err := neuraldb.Connect(ctx, "postgresql://neuraldb:password@localhost:5432/mydb")
if err != nil {
log.Fatal("connection failed:", err)
}
defer client.Close(ctx)
var count int64
err = client.QueryRow(ctx, "SELECT COUNT(*) FROM documents").Scan(&count)
if err != nil {
log.Fatal(err)
}
fmt.Println("Documents:", count)
}
```
### Connection Pool (Recommended)
```go
import (
"github.com/neuraldb/neuraldb-go"
"github.com/jackc/pgx/v5/pgxpool"
)
func NewPool(ctx context.Context) (*neuraldb.Pool, error) {
config, err := pgxpool.ParseConfig(os.Getenv("NEURALDB_URL"))
if err != nil {
return nil, err
}
config.MaxConns = 20
config.MinConns = 5
config.MaxConnIdleTime = 30 * time.Minute
config.MaxConnLifetime = time.Hour
return neuraldb.NewPool(ctx, config)
}
```
## Working with Vectors
### Defining Vector Types
```go
package main
import "github.com/neuraldb/neuraldb-go/types"
// Create a vector from a float32 slice
v := types.NewVector([]float32{0.023, -0.187, 0.412})
// Create from float64 (auto-converted)
v2 := types.NewVectorFromFloat64([]float64{0.023, -0.187, 0.412})
// Access the underlying data
floats := v.Slice() // []float32
dims := v.Dims() // int
```
### Inserting a Document with a Vector
```go
type Document struct {
ID string
Content string
Embedding types.Vector
}
func InsertDocument(ctx context.Context, pool *neuraldb.Pool, doc Document) error {
_, err := pool.Exec(ctx,
`INSERT INTO documents (id, content, embedding) VALUES ($1, $2, $3)`,
doc.ID, doc.Content, doc.Embedding,
)
return err
}
```
### Vector Similarity Search
```go
func SemanticSearch(ctx context.Context, pool *neuraldb.Pool, queryEmbedding []float32, limit int) ([]SearchResult, error) {
qv := types.NewVector(queryEmbedding)
rows, err := pool.Query(ctx, `
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
WHERE embedding IS NOT NULL
ORDER BY embedding <=> $1
LIMIT $2
`, qv, limit)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
defer rows.Close()
var results []SearchResult
for rows.Next() {
var r SearchResult
err := rows.Scan(&r.ID, &r.Content, &r.Similarity)
if err != nil {
return nil, err
}
results = append(results, r)
}
return results, rows.Err()
}
```
## Query Builder
The SDK includes an optional query builder for type-safe query construction:
```go
import "github.com/neuraldb/neuraldb-go/qb"
// Build a hybrid query
query, args := qb.New().
Select("id", "name", "price").
Expr("1 - (embedding <=> $?) AS similarity", queryVector).
From("products").
Where(qb.Eq("category", "electronics")).
Where(qb.GTE("price", 50)).
Where(qb.LTE("price", 500)).
Where(qb.GT("stock", 0)).
OrderByExpr("embedding <=> $?", queryVector).
Limit(20).
Build()
rows, err := pool.Query(ctx, query, args...)
```
## Batch Operations
```go
import "github.com/jackc/pgx/v5"
func BatchInsert(ctx context.Context, pool *neuraldb.Pool, docs []Document) error {
batch := &pgx.Batch{}
for _, doc := range docs {
batch.Queue(
`INSERT INTO documents (content, embedding) VALUES ($1, $2)`,
doc.Content, doc.Embedding,
)
}
results := pool.SendBatch(ctx, batch)
defer results.Close()
for range docs {
_, err := results.Exec()
if err != nil {
return fmt.Errorf("batch insert error: %w", err)
}
}
return results.Close()
}
```
## Transactions
```go
func TransactionalInsert(ctx context.Context, pool *neuraldb.Pool, docs []Document) error {
return pool.BeginTxFunc(ctx, pgx.TxOptions{
IsoLevel: pgx.ReadCommitted,
}, func(tx pgx.Tx) error {
for _, doc := range docs {
_, err := tx.Exec(ctx,
`INSERT INTO documents (content, embedding) VALUES ($1, $2)`,
doc.Content, doc.Embedding,
)
if err != nil {
return err // auto-rolled back by BeginTxFunc
}
}
_, err := tx.Exec(ctx,
`UPDATE stats SET doc_count = doc_count + $1`,
len(docs),
)
return err
})
}
```
## Scanning Results into Structs
```go
import "github.com/jackc/pgx/v5/pgxscan"
type Product struct {
ID string `db:"id"`
Name string `db:"name"`
Price float64 `db:"price"`
Similarity float64 `db:"similarity"`
}
func SearchProducts(ctx context.Context, pool *neuraldb.Pool, qv types.Vector) ([]Product, error) {
var products []Product
err := pgxscan.Select(ctx, pool, &products, `
SELECT id, name, price, 1 - (embedding <=> $1) AS similarity
FROM products
WHERE available = true
ORDER BY embedding <=> $1
LIMIT 10
`, qv)
return products, err
}
```
## Context and Cancellation
All SDK methods accept a `context.Context`. Use it for timeout and cancellation:
```go
// Set a 5-second query deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := pool.Query(ctx, "SELECT ...", args...)
```
## Error Handling
```go
import (
"github.com/jackc/pgx/v5/pgconn"
"errors"
)
_, err := pool.Exec(ctx, "INSERT INTO ...", args...)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "23505": // unique_violation
return ErrDuplicate
case "23503": // foreign_key_violation
return ErrForeignKey
default:
return fmt.Errorf("database error %s: %s", pgErr.Code, pgErr.Message)
}
}
return err
}
```

View file

@ -0,0 +1,277 @@
---
title: JavaScript SDK
sort: 110
section-id: client-sdks
keywords: JavaScript, TypeScript, SDK, Node.js, browser, npm, client
description: The NeuralDB JavaScript/TypeScript SDK for Node.js and browser environments
language: en
---
# JavaScript SDK
The NeuralDB JavaScript SDK provides a fully typed client for Node.js and browser environments. It is built on `pg` (node-postgres) for Node.js and a custom HTTP adapter for edge and browser environments.
## Installation
```bash
npm install @neuraldb/client
# or
yarn add @neuraldb/client
# or
pnpm add @neuraldb/client
```
## Basic Setup
### Node.js
```typescript
import { NeuralDB } from '@neuraldb/client';
const client = new NeuralDB({
connectionString: process.env.NEURALDB_URL!,
// or individual options:
host: 'localhost',
port: 5432,
user: 'neuraldb',
password: 'password',
database: 'mydb',
ssl: { rejectUnauthorized: true },
});
await client.connect();
```
### Edge / Serverless (HTTP mode)
For Cloudflare Workers, Vercel Edge, and browser environments, use the HTTP adapter:
```typescript
import { NeuralDB, HttpAdapter } from '@neuraldb/client';
const client = new NeuralDB({
adapter: new HttpAdapter({
url: process.env.NEURALDB_HTTP_URL!,
apiKey: process.env.NEURALDB_API_KEY!,
}),
});
```
### Connection Pool
```typescript
import { NeuralDBPool } from '@neuraldb/client';
const pool = new NeuralDBPool({
connectionString: process.env.NEURALDB_URL!,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
```
## Executing Queries
```typescript
// Simple query — returns QueryResult
const result = await client.query('SELECT id, content FROM documents LIMIT 10');
const rows = result.rows; // typed as any[]
// Parameterised query
const { rows } = await client.query<{ id: string; content: string }>(
'SELECT id, content FROM documents WHERE source = $1 LIMIT $2',
['web-scraper', 20]
);
```
## Vector Operations
### Inserting Vectors
```typescript
import { toVector } from '@neuraldb/client';
const embedding = [0.023, -0.187, 0.412, /* 1536 values */];
await client.query(
'INSERT INTO documents (content, embedding) VALUES ($1, $2)',
['My document content', toVector(embedding)]
);
```
### Similarity Search
```typescript
import OpenAI from 'openai';
import { toVector } from '@neuraldb/client';
const openai = new OpenAI();
async function semanticSearch(query: string, limit = 10) {
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query,
});
const queryVector = embeddingResponse.data[0].embedding;
const { rows } = await client.query<{
id: string;
content: string;
similarity: number;
}>(
`SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
WHERE embedding IS NOT NULL
ORDER BY embedding <=> $1
LIMIT $2`,
[toVector(queryVector), limit]
);
return rows;
}
```
### Hybrid Search
```typescript
async function hybridSearch(query: string, filters: Record<string, unknown>, limit = 10) {
const queryVector = await generateEmbedding(query);
const conditions: string[] = [];
const params: unknown[] = [toVector(queryVector)];
Object.entries(filters).forEach(([key, value]) => {
params.push(value);
conditions.push(`${key} = $${params.length}`);
});
const whereClause = conditions.length > 0
? 'WHERE ' + conditions.join(' AND ')
: '';
params.push(limit);
const { rows } = await client.query(
`SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM documents
${whereClause}
ORDER BY embedding <=> $1
LIMIT $${params.length}`,
params
);
return rows;
}
```
## High-Level Document API
The SDK includes a higher-level document management API:
```typescript
import { DocumentStore } from '@neuraldb/client';
const store = new DocumentStore(client, {
table: 'documents',
embeddingColumn: 'embedding',
contentColumn: 'content',
embeddingModel: {
provider: 'openai',
model: 'text-embedding-3-small',
apiKey: process.env.OPENAI_API_KEY!,
},
});
// Add documents (auto-generates embeddings)
await store.add([
{ content: 'First document', metadata: { source: 'web' } },
{ content: 'Second document', metadata: { source: 'pdf' } },
]);
// Search
const results = await store.search('query text', {
limit: 10,
filter: { source: 'web' },
minSimilarity: 0.7,
});
// Delete
await store.delete({ filter: { source: 'web' } });
```
## Transactions
```typescript
const pgClient = await pool.connect();
try {
await pgClient.query('BEGIN');
await pgClient.query(
'INSERT INTO documents (content, embedding) VALUES ($1, $2)',
['Content', toVector(embedding)]
);
await pgClient.query(
'UPDATE stats SET doc_count = doc_count + 1 WHERE id = $1',
[statsId]
);
await pgClient.query('COMMIT');
} catch (error) {
await pgClient.query('ROLLBACK');
throw error;
} finally {
pgClient.release();
}
```
## Streaming Results
For large result sets, stream rows to avoid loading all data into memory:
```typescript
const stream = client.queryStream(
'SELECT id, content, embedding FROM documents WHERE source = $1',
['web-scraper']
);
for await (const row of stream) {
await processDocument(row);
}
```
## Type Definitions
```typescript
import type { Vector, QueryResult, PoolClient } from '@neuraldb/client';
interface Document {
id: string;
content: string;
embedding: Vector | null;
created_at: Date;
}
const { rows } = await client.query<Document>(
'SELECT * FROM documents WHERE id = $1',
[id]
);
```
## Error Handling
```typescript
import { NeuralDBError, ConnectionError, QueryError } from '@neuraldb/client';
try {
await client.query('SELECT * FROM nonexistent_table');
} catch (error) {
if (error instanceof QueryError) {
console.error('SQL error:', error.message, 'Code:', error.code);
} else if (error instanceof ConnectionError) {
console.error('Connection failed:', error.message);
}
}
```

View file

@ -0,0 +1,277 @@
---
title: Python SDK
sort: 100
section-id: client-sdks
keywords: Python, SDK, client, connection, CRUD, vector operations, psycopg
description: Installing and using the NeuralDB Python SDK — connection, CRUD, and vector operations
language: en
---
# Python SDK
The NeuralDB Python SDK provides a high-level client for Python applications. It is built on top of `psycopg3` (the PostgreSQL adapter) with NeuralDB-specific helpers for vector operations, embedding generation, and batch ingestion.
## Installation
```bash
pip install neuraldb
# or
pip install neuraldb[asyncio] # includes async support
pip install neuraldb[all] # includes all optional extras
```
### Requirements
- Python 3.10+
- libpq (PostgreSQL client library)
On Ubuntu: `sudo apt install libpq-dev`
On macOS: `brew install libpq`
## Connecting
### Synchronous Client
```python
from neuraldb import NeuralDB
# From connection string
client = NeuralDB("postgresql://neuraldb:password@localhost:5432/mydb")
# From parameters
client = NeuralDB(
host="localhost",
port=5432,
user="neuraldb",
password="password",
database="mydb",
sslmode="require",
)
# Context manager (auto-closes connection)
with NeuralDB("postgresql://...") as client:
result = client.query("SELECT 1")
```
### Async Client
```python
import asyncio
from neuraldb import AsyncNeuralDB
async def main():
async with AsyncNeuralDB("postgresql://neuraldb:password@localhost/mydb") as client:
result = await client.query("SELECT 1")
print(result)
asyncio.run(main())
```
### Connection Pool
```python
from neuraldb import NeuralDBPool
pool = NeuralDBPool(
"postgresql://neuraldb:password@localhost/mydb",
min_size=5,
max_size=20,
)
with pool.acquire() as client:
result = client.query("SELECT COUNT(*) FROM documents")
```
## Schema Operations
```python
# Create a table with a vector column
client.execute("""
CREATE TABLE IF NOT EXISTS documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
source TEXT,
embedding VECTOR(1536),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
)
""")
# Create a vector index
client.execute("""
CREATE INDEX IF NOT EXISTS documents_embedding_idx
ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64)
""")
```
## CRUD Operations
### Insert
```python
from neuraldb import Vector
# Insert with pre-computed embedding
client.execute(
"INSERT INTO documents (content, source, embedding) VALUES (%s, %s, %s)",
("My document content", "web-scraper", Vector([0.023, -0.187, 0.412, ...]))
)
# Insert many (batched for efficiency)
docs = [
("Content A", "source-1", Vector([...])),
("Content B", "source-2", Vector([...])),
("Content C", "source-1", Vector([...])),
]
client.executemany(
"INSERT INTO documents (content, source, embedding) VALUES (%s, %s, %s)",
docs
)
```
### Query
```python
# Standard query — returns list of Row objects
rows = client.query("SELECT id, content FROM documents WHERE source = %s", ("web-scraper",))
for row in rows:
print(row["id"], row["content"])
# As dicts
rows = client.query(
"SELECT * FROM documents LIMIT 10",
row_factory="dict"
)
# As named tuples
rows = client.query(
"SELECT id, content FROM documents LIMIT 10",
row_factory="namedtuple"
)
```
### Vector Search
```python
import openai
# Generate query embedding
query_text = "high-performance wireless headphones"
query_embedding = openai.embeddings.create(
model="text-embedding-3-small",
input=query_text
).data[0].embedding
# Semantic search
results = client.query("""
SELECT id, content, 1 - (embedding <=> %s) AS similarity
FROM documents
WHERE embedding IS NOT NULL
ORDER BY embedding <=> %s
LIMIT 10
""", (Vector(query_embedding), Vector(query_embedding)))
for row in results:
print(f"{row['similarity']:.3f}: {row['content'][:100]}")
```
### Using the High-Level Search API
```python
from neuraldb import VectorSearch
searcher = VectorSearch(client, table="documents", embedding_column="embedding")
results = searcher.search(
query_vector=query_embedding,
limit=10,
filters={"source": "web-scraper"},
metric="cosine",
)
```
### Update
```python
client.execute(
"UPDATE documents SET content = %s, embedding = %s WHERE id = %s",
("Updated content", Vector(new_embedding), doc_id)
)
```
### Delete
```python
client.execute("DELETE FROM documents WHERE id = %s", (doc_id,))
```
## Transactions
```python
with client.transaction():
client.execute("INSERT INTO documents (content, embedding) VALUES (%s, %s)", (content, Vector(embedding)))
client.execute("UPDATE stats SET count = count + 1")
# Auto-commits on exit, rolls back on exception
```
Explicit control:
```python
with client.transaction() as txn:
try:
client.execute("INSERT ...")
client.execute("UPDATE ...")
txn.commit()
except Exception:
txn.rollback()
raise
```
## Bulk Ingestion
For high-throughput ingestion, use the `BulkIngestor`:
```python
from neuraldb import BulkIngestor
ingestor = BulkIngestor(
client,
table="documents",
columns=["content", "source", "embedding"],
batch_size=1000, # insert in batches of 1000
embedding_model="openai/text-embedding-3-small", # auto-generate embeddings
embedding_column="embedding",
text_column="content",
)
docs = [
{"content": "Document text here", "source": "source-1"},
{"content": "Another document", "source": "source-2"},
# ... thousands more
]
with ingestor as ing:
for doc in docs:
ing.add(doc)
# Flushes remaining rows and commits on context exit
print(f"Ingested {ingestor.total_inserted} documents")
```
## Type Handling
The SDK provides type adapters for NeuralDB types:
```python
from neuraldb.types import Vector, HalfVector, SparseVector
# Dense vector
v = Vector([0.1, 0.2, 0.3])
# Half-precision vector (less memory)
hv = HalfVector([0.1, 0.2, 0.3])
# Sparse vector
sv = SparseVector({0: 0.5, 15: 0.3, 200: 0.8}, dimensions=384)
```

Some files were not shown because too many files have changed in this diff Show more