仙那度计划 (Project Xanadu) | 双链简史
本文正式开始之前,让我们回到故事的原点——追溯双向链接的历史,找寻永恒记忆宫殿的遗址
上都启示录:超链接(Hypertext)缘起
一切的一切,始于信息技术先驱 Ted Nelson(泰德 · 尼尔森)内心深处的愿景——一个能够在数字世界关联起一切事物的电脑文件系统。最初,Nelson 的构思是想建造一个数字资源库——以承载全世界范围内的所有电子出版物。
那之后,他将心目中关于这一想法的企划正式命名为 Project Xanadu「上都计划」。
1965 年,Nelson 把他的想法诉诸于论文之上,并提交给了计算机协会(ACM)。
范式:Xanadoc | Xanadu Space
失落的仙那度 | 万维网(World-Wide-Web)远非世外桃源
不难看出,Nelson 本人并不认可互联网时代下的万维网——他认为所谓的「所见即所得」,不过是局限在传统打印文件里的说法。
也就是说,一切计算机文档都是无互动性可言的,既不能被标注,也不允许插入(外部链接)——好比压在玻璃下的报纸,是可望而不可即的。
当下的人们亟需一个可关联所有文档的笔记工具,以满足信息时代下日益增长的个人知识储备。
Roam Research:属于前方朝圣者的福音 | Project Xanadu 原教旨之十二
1.每一位用户都是独一无二且属安全认证的
在 Roam 里,每一个云端库(Hosted Graph)都是独有且私有的。
ps:仅在创建图库阶段,可以将其设置为独立加密类型
2.每一位用户可自行搜索,检索,创建以及存储文档
每一位用户(编辑权限)可自行搜索,检索,创建以及存储区块(Block)和页面(Page)。
3.每一份文档可由任何数据类型的任意数量部分组成
Roam 的 Block 可由相当数量的不同数据文件组成,类型如下:
- 文本(text)、图像(image),音视频(audio/video)
- PDF文件、网页(web page)
- 看板(kanban)、表格(table)
- 画板(drawing board)
- 白板工具(excalidraw)
- 思维导图(diagram)
4.每一份文档可包含任意类型的链接,包括系统所有者可访问的任何文档的虚拟拷贝(转包)
每一个区块可包含任意类型块的引用,包括图库所有者可访问的任何区块的虚拟拷贝(嵌入)
5.链接是可见,且可从所有端点处出发跟踪
块引用是公开的,且能从所有引用过原区块的区块处反向跟踪
6.每一份文档都是独一无二且安全认证的
每一个区块都是独一无二且由双重标识(Private ID/Public ID)进行识别——每一页 Page 在命名空间(Namespace)的标题(Title)都是唯一的
对于云端加密库而言,每一个块(页面)引用都经由自动加密处理
特殊地,桌面端应用上的离线本地库无法加密由用户上传的图像等媒体附件
7.每一份文档都具备安全的访问控制
每一份页面都可以被共享给任何人,或具备阅读/编辑(read/edit)的账号邮箱
特殊地,共享独立页面将加载部分图库(Graph)的数据到任何一位访问者的浏览器上
对于一位别有用心且精于钻研的人来说,他将可以从某一页面访问图库
8.每一份文档都可以进行快速搜索,存储以及检索,并且无须考虑其物理意义上的存储位置
每一个区块(文本/字符串)都可以进行快速搜索,存储(仅限于 Page)以及检索,并且无须考虑后端数据库的存储问题
9.每一份文档根据来自任意地点的访问频率,自动迁移到最合适的物理存储区
每一个区块涉及到的最近编辑数据,都以 EDN 格式扁平化存储为一系列简易数据集,并储存在本地缓存中
除此之外,对于无加密的云端库来说,还能够以离线的形式直接访问
警告:删除本地缓存将导致尚未同步的数据丢失
而对于任何共享了访问权限(阅读/编辑)的人来说,图库数据将加载至他们的浏览器上
10.每一份文档都有作自动冗余存储,即使在意外情况下也能保持可用性
仅对本地库而言,如果尝试清除本地存储,那么此举将即刻删除掉该浏览器上的所有图库
建议:用户方应自行设置好定期备份,以避免重要数据的损失
对于任何共享了访问权限(阅读/编辑)的人来说,都可以将图库整体导出为以下格式:MarkDown/MarkDown、JSON以及EDN
11.每一项事务都是保密且可由组成事务的部分(文档源)进行单独审计的
每一项事务都默认存储在与之对于的属性列表当中;每一项事务都仅可由共享相同事务ID的区块进行审计查询
此外,仅限于云端库而言,所有区块的类型内容(含图像等媒体附件)同数据集(非必要的元数据外)一样,均可经由加密处理
常见的上传附件 (Uploads):图像、音视频、还有 PDF 文件;
一些非必要元数据 (Non-Necessary Metadata) 不会被加密:
- 节点形式(无序/有序/文档)
- 标题水平以及文本调整情况(居左/居中/居右)等
所有 Blocks 的关键数据将被加密:
- 各个用户的昵称/邮箱
- 创建该区块的用户信息(昵称/邮箱)
- 还有页面标题,图像链接等
12.Xanadu 客户端通讯协议是符合开源公布标准的;因此,涉及到第三方软件的开发与集成是被允许且支持的
在 Roam 中,涉及到第三方插件的开发和集成式被允许且支持的
用户方甚至可以直接在特定的页面或区块下方,自行修改定义图库样式 (Graph's CSS), 以及 JS;举例来说:油管视频(嵌入式)的 Comment Button 的样式修改
{
background-color: white;
right: 0;
left: 98%;
top: 95%;
bottom: 0;
z-index: 2;
opacity: .8;
position: absolute;
border-top-left-radius: 7px;
}
类似的设置,都是可以直接在 Graph 里由用户方自行定义的
知识图谱 Roam Graph:非线性大纲笔记——打破传统文件柜的桎梏
对于非线性的网状形成,不只是页面与页面之间的联系,更要有任意段落的粒度参与。
跳出文档柜的层级藩篱,打造第二大脑的数字城堡
不只是树叶的繁荣,也要有花朵般的点缀
每一页 Page 都是 Graph 这座森林中的一棵树,而所有的 Blocks 则充当枝干和树叶的形式。
[[Page]]
* Branch
* Branch
* Leaf
* Leaf
* Leaf
* Branch
* Branch
* Leaf
* Branch
* Leaf
...
事实上,Page 和 Block 并没有本质上的区别,Page 只不过是一类特殊的 Block ——作为视图 (Graph Overview) 上独立且直观的节点;换句话说,Page 即是不存在父级节点 (Parent Node) 的区块。
更形象地说,Pages 与 Blocks 之间的关系基本可视作为正方形与长方形(前者为独立的个体,通常由后者组成)。
标题树 [[title]]
使用 [[]]
(双括号)引用的 Pages,具备相对宽泛的节点粒度;如同森林里的树木,高矮胖瘦,各有不同。
顺便说一句,区块 (Blocks) 在粒度的定义上理应是“最自由自在的”。
标签垛 #tag
紧挨着 #
(井号)的 Page 引用通常具备较小的节点粒度,这类标签常用于标记 Block(s) 在内容上可能涉及到的概念/范畴。
堆垛 #tag/sub-tag
对于递进式的标签链接,它们更接近于「堆垛」的状态。
只有当该标签(含子标签)相对于独立标签是不可分割的,才以“多步标签 (Multi-step label)”的形式进行引用。
如果还是不能很好的 Get 到这一块的内容,可通过以下内容来理解:
如图,两种分子都属于是氧元素的单质类型。不同的点在于臭氧分子比之氧分子多出了一个氧原子——因此说臭氧在形态上更进一步。
属性核 attribute::
attribute::
与 #tag
相似,但在细粒程度上更加具体,普遍用来概括(涵盖)一类拥有共同特征(特性)的的人/事/物。
例如:作者/邮箱 (Author/Email)
Author:: Mac Arthur
Email:: macarthur@gmail.com
种子/果核 attribute::{kwargs}
简单来说,
attribute::
不仅可以用于描述一类具体的属性,还能用来概括具备同一特点的不同事物。
Forest
* Apple:: This is an apple
* AppleCore:: An apple core
* Banana::
* BananaSeed:: A banana seed
* BananaSeed:: A banana seed
* BananaSeed:: A banana seed
* Pear:: This is a pear
通用格式:
- attribute:: outline
- sub-attr-1:: detail
- sub-attr-2:: detail
- sub-attr-3:: detail
简而言之,一切与属性相关的数据,更接近于“元数据 (Metadata)”的概念。
用集合的表达式来理解的话,attribute::
更接近于右侧对于 φ 的属性定义(特点/类别);左侧部分则是双冒号 ::
之后具体的(多数)对象。
而在 Roam 中,具备相同(近似)属性/属于相同(近似)类型,则可由 attribute:: 来定义或包含。
内联标题 [[[[inline]]outline]]
标题中的标题:洋葱头,番石榴,柚子
除了上述三类引用,Page 还可以作为任意粒度的独立节点(相对于 Block 而言)——此类 Page 可以在标题的命名空间 (Namespace) 处内联其他 Page 的 Title,来进一步「增殖」该 Page 的节点粒度。
Guava: [[[[seed]][[flesh]]]]
可见,Guava 通过前后内联其他 Page,使得原本的节点粒度进一步「增生」。
根系发达,枝头发芽
枝头即标题段落 Headings,需要区别于页面标题 Titles
世界上不存在两片完全相同的叶子
如果说,每一页 Page 都是一棵树,那么它的根须将可以与不同的 Pages 相连——它们之中的 Blocks 既是具备营养的花蜜果肉;也是充当养分的化肥颗粒;更是洞见灵感 (Insight) 的花种果核。
而为了保证这样一幅“营养-养分”网络系统的自身稳定,Roam 不仅为每一个 Block (包括 Page 在内) 分配有独一无二的双重 ID,还在页面标题的命名上,采取了「非唯一则合并」的维稳手段。
对于页面标题的(重)命名,Roam 只允许同时存在一份同名 Page,并在内容上合并二者。
良好的开端,是成功的一半
每一页 Page 下的 Top-level Blocks,通常作为 Headings 存在。
正如本章开头所提及的,Graph 本身像是一座大林场。每一页 Page 即是一棵 Tree,而这些 Top-level Blocks 则是树上的主要枝干,起到统领全文,提纲挈领的重要作用。
它们常以各级标题的形式出现,如一级/二级/三级标题:
而这些 Top-level Blocks 都将 Page 视作唯一的父级节点;且 Top-level Blocks 不只是作为 Page 的直属(后代)节点,同时还是各段落的起首节点。
尽管标题水平 (Heading Level) 与节点当前所处的层级无关,但还是十分建议将 Heading Blocks 与线性的层级关系相匹配。
注:heading 仅作为一个 Blocks 的可选项(相对于 Pages 的 Title)
附庸的附庸不是我的附庸
一般来说,当把 ((block ref))
置于某处时,该引用仅显示当前块的命名空间 (Namespace) 里的内容。
那么块引用
((block ref))
与嵌入{{embed:((block ref))}}
间的区别又何在呢?
全文式索引的块引用
通俗地说,块引用是仅包含自身的;除非嵌入该引用,否则节点将视作从原本的层级关系中「抽离」出来而变得无法展开的。
只有当块引用被置于嵌入块内,即 {{embed:((block ref))}}
,才能够展开/收缩其下属的后代节点——之所以将块引用称作【全文式索引】,正因为 ((block ref))
总是指向被引用块的位置以及包含一个直属子级节点的属性列表。
换句话说,只有被嵌入的 Block(s),才能「串联」起各个层级的后代节点,进而形成完整的 Branches 分支系列。
定位原区块的导航栏
左键位于左侧的 Bullet Node,将 Zoom-in 到当前区块(段落),或者在按住 Shift 键的同时左键节点,则可在右侧 Sidebar 处打开当前区块的“视窗口”;
而不管使用哪一种方式打开,都始终存在一个 Breadcrumb 导航栏指向当前节点的先代节点——这是因为所有节点的 ((block ref))
都将默认存储在其所有后代的属性列表当中。
引用上下文的位置框
同样,左键位于视窗口顶部的(任一)先代节点,即可展示对应层级的上下文内容——此时,会出现一个“黄色边框的盒子”来「突出」原区块的位置。
简而言之,你进入的层级越高,那么你将看到越多的上下文。
半场总结
综上所述,Roam 的双向链接 (Bi-Directional Liking) 足以证明涉及到整个 Graph 中的不同 Page/Block(s) 之间的链接是高度可靠的,且能在足够宽泛的粒度范畴内深度「反向追踪」。
【可溯源性】在设计上应当高于【延展性】。
概要
现在,让我们再次回顾一下目前提及的功能特性
- Page 对应不同节点粒度区间的基本引用形式:
[[title]]/#tag/attribute::
- Page Title 命名的全局唯一性——Block Ref 同样具有潜在的唯一性
- Block Ref 引用的全文式索引
- 任一 Block 的 Breadcrumb 导航栏
可以说,Roam 在节点内容的粒度上没有任何限制,也支持上传各种类型的资源文件——除了文本,块引用本身还能展示图像,音视频等外部链接的内容。
在下文中,我们将继续探索 Roam 中的功能块 (Func-Blocks)。
奥林匹克格言:更快,更高,更强
前端式数据处理——基于 Datomic 数据库
Embedding:嵌入在 Graph | 跃然于纸上
镜像 {{embed: ((block ref))}
在 Roam 里,可以通过复制粘贴的形式,将块引用嵌入到 Graph 中的任何一个 Block 内。
通过粘贴该嵌入块 {{embed: ((block ref))}}
到任何一处 Block,即可复现与原区块完全一致的“镜像版本”。
镜中镜
嵌入,嵌入,再嵌入
即便是涉及到对同一区块的「多重引用」,Roam 同样能够保证 100% 的同步率以及近乎 0ms 延迟的编辑体验。
任意门 {{embed: [[title]]}}
将嵌入块 (Embed-Block) 中的 ((block ref))
替换成 [[title]]
,以实现对于整一页 Page 的嵌入——这样一来,被嵌入的 Page 将看起来不再那么“独立”,而是变成了某一个区块的子级节点 (Children Node)。
实际上,对于 Page 的嵌入形式并不会使其原本“独立”的节点关系发生任何改变——你可以将这类嵌入页面 (Embed-Page) 仅看作是某种伪类的子级节点 (Pseudo-Children Node)
Searching 或 Querying:这是一个 Mention
无论是搜索 (Searching) 还是查询 (Querying),都应该以「可检索 (Retrieving) 原文并提供上下文背景」为标准来衡量返回结果 (Returns)。
搜索 {{[[search]]: {keyword}}}
搜索包含输入(关键词)的一切区块
在 Roam 中,你可以在某一页 Page 任意一处 Block 嵌入搜索块 (Search-Block);然后输入关键词,即可返回所有包含「此关键词」的 Block(s)。
你还可以按一下 Show More,以展示更多返回的内容。
理论上说,Search Block 能够返回该 Graph 中所有涉及到「此关键词」的 Block(s)
查询 {{[[query]]: {and:[[title]]}}}
查询与之相关的一切区块
查询块 (Query-Block) 的功能类似,但查询的形式比起单纯的关键词搜索更加精确;只需设置几个简单的查询条件,即可轻易返回与之相关的 Block(s):
下面来简单说明一下 Query-Block 的四类基本条件:
- Query (and): 包括 A,B
- Query (or): 包括 A 或 B
- Query (and or): 包括A,包括 B 或 C
- Query (and not): 包括A,不包括 B
1、2 命令很好理解:如果说 Query (and) 条件是要求 A 与 B 位于同一 Block 中;那么 Query (or) 则只要求二者中的一个位于某一 Block 中,即可返回满足条件的 Block(s)。
3、4 命令类似,其中 Query (and or) 比较特殊:它要求 A 位于某一 Block 的前提下,B 或 C 两者之一也位于该 Block;而 4 命令 Query (and not) 里的 not 则是把 B 设置成排除项。
感兴趣的朋友可以自行了解:How Queries Work in Roam Research
不难发现,Query-Block 内输入的正是 [[title]]
形式的 Page 引用。
而当我们把标题引用
[[title]]
替换为块引用((block ref))
时,Query-Block 的查询返回将同样有效!
提及 {{[[mentions]]: [[title]]/((block ref))}}
返回提及某一引用的一切区块
如果说,Search-Block 是内嵌了一个搜索框 (Search Box) 在某一 Page 内,那么 Mention-Block 则是拓展了关于其他 Page(Block) 的引用联系。
与 Query-Block 类似,Mention-Block 也可以通过输入
((block ref))
,用作「针对」Block 级别的查询返回。
原生应用的备份导出 - 简约主义的资源管理
EDN: 充分完备的导出格式
对于那些习惯 All-in-One 的个人来说,无论是面对跨笔记库 (Cross-Graph) 时的数据迁移;还是为了避免云端(本地)存储遇到意外而导致的数据损失——EDN 格式的备份导出,几乎成了唯一的最佳选项!
于 Roam 而言,包含 Graph 所有笔记的数据信息,全部被存储在同一份 EDN 格式文件当中——这也就意味着,最近时间的 EDN 文件即包含 Graph 在导出时刻的完整备份;至于之前时间(日期)的,则可看作是该 Graph 的历史版本。
也就是说,仅需导入最近时间的那一份 EDN 备份文件,即可复原整个 Graph 的全部数据信息。
即导即出 | 镜像迁移
EDN 作为应用程序的原生格式,无论是本地库还是云端库(包括加密库类型在内)的数据存储,都会加载到当前 Browser 的存储 (Storage) 内——真正做到了「存储在本地」的“就近原则”;因此,在导出 EDN 格式备份时,桌面端 (Desktop App) 和网页端 (Web App) 都能实现「即导即出」的即时备份。
反过来说,在新建的 Graph 中导入其他 Graph 的完整备份(仅支持 EDN 格式)时,仅有 EDN 格式的备份文件能做到最大程度上的数据复原——即在保真度上,EDN 格式备份不但可重建 Graph 整体的关系结构(各 Page/Block 之间的引用关系),还能够保留所有 Blocks 的非必要元数据 (Non-Necessary Metadata)。
整个导入三个数量级(100<N<1000)的笔记 (Page) 节点来算,所需的时间通常在一分钟以内——甚至不到 30 秒。
全自动备份 | 历史版本
通过 Roam Depot 里的插件 Page History,甚至而且更进一步的实现对单一 Page 的历史版本保留。
Hosting | 图床托管的灵活性
Roam Graph 通过对节点数据 (Page/Blocks) 与资源文件 (Uploads) 的分离式存储,既保证了 Graph 在加载到本地时不受媒体附件的影响,又维持了 Graph 自身数据结构的简致性——即无论上传或引入的媒体附件多少,都不会影响到 Graph 整体的结构关系。
Roam 本身提供创建本地库和云端库 (Local Graph/Hosted Graph):
- Local Graph 支持 Uploads 存储到本地目录文件下方;
- Hosted Graph 则通过 Firebase 的网络传输上传至 Google Cloud 作云端存储。
对于 Hosted Graph 而言,挂载在 Google Cloud 的资源文件(即媒体附件)必须在联网的前提下才能下传到本地展示;Roam 则在设置 (Settings) 处提供了后台式的图床管理服务。
一次外链,多处引用
在 Roam 里,你只需上传一次资源文件,即可在异地 Block(s) 引用同一份附件并实现无损化复现。
扁平化的资源管理
在各种资源文件的页面 ![/video/audio/PDF
下方 (Linked References) 即可查看到所有对应类型的资源文件。
![
是 image 类 URL 用作「展示」的<充分文本>;实际应用上,则可通过创建.jpg/.png/.gif
等更精确的 Page 来统一查看相应格式的媒体附件。
事实上,Roam 对于文本(字符串)以外的数据均作区分化存储,使得资源文件(媒体附件)的上下传几乎不受 Graph 自身的任何影响。
任何挂载在线托管网站的可访问资源文件,Roam 都能快速加载至本地浏览;
配合 Roam Depot 插件 Query Builder 设置简单的语法条件,甚至还能建立起高效的查询块(机制)——可锚定该资源 URL 的<网址域名>作更精细化的检索(如:查询挂载在 XX 图床上的所有图像)。
可以说,Roam 在节点内容的粒度上没有任何限制,也支持上传各种类型的资源文件——除了文本,块引用本身还能展示图像,音视频等外部链接的内容;
也就是说,对于同一份媒体文件的「复现」,Roam 能做到百分之百的利用率,进而使得整个 Graph 高度精简化。
展望未来:一个更大的愿景 | 人人为之贡献的社区型知识图谱
在 Roam 中,每一个 Block 都会保存与之相关的编辑信息。
在导出的 JSON 文件里,甚至可以查看到「创建账户-创建时间/编辑账户-最近编辑时刻」的相关信息;
从软件开发的角度来看,存储这类数据似乎并没有什么意义——在 Graph 里的数据不都是属于用户(订阅邮箱)本人的吗?
让我们换个角度思考,Graph 何必拘泥于个人笔记内容,一个真正有助于实现个人/他人价值的社区型 Graph 应该是可以共享的,且满足于多人协作的编辑场景。
协作者的充分编辑权限
全权编辑许可
设置为 edit 权限的共享(协作)邮箱若默认不启用 Immutable blocks 选项,协作者(编辑邮箱)将可以随意编辑/删除由 Graph 所有者(订阅邮箱)创建的全部 Blocks。
限定导出授权
关于导出权限,Graph 所有者(订阅邮箱)还可以设置为仅限于协助者(编辑邮箱)。
在「全权编辑许可」与「限定导出授权」的双重加持下,协作者(编辑邮箱)可被视作 Graph 所有者以外访问权限最高的用户。
信任与非信任协作
信任协作场景下的版本控制
在共享(编辑权限)用户可信任的协作场景下,各用户可自行添加当前块的版本 (Add version),以实现对同一位置的 Block 进行多版本编辑。
基于不可变动块的非信任协作
在启用 Immutable blocks 的前提下,各个用户(包括订阅邮箱)都不可以编辑或移除由其他人创建的 Blocks/Pages;但能在 Immutable block 下方创建 Block(s) 并通过缩进,或直接拖动至该区块下方,使其作为附属的(后代)节点。
除此之外,用户之间可正常引用(相对于自己)不可变动的 Blocks。
基于 Query Builder 的内容审计
还可以通过添加查询选项 (Add Selection) 来进一步查询关于编辑的时间信息:
协作者的参与度
在 Roam 里,协作者除了可以参与到 Graph 内容上的创作,还能通过 Slider 功能块 {{[[slider]]}}
与内置的评论页面 [[roam/comments]]
体现用户的活跃程度。
Slider 评分机制
滑动块 (Slider-Block) 本身可用于多种评分目的,比如让社区用户「共同表明对某一观点的认可程度」:
Comment 评论
每一位(编辑邮箱)用户都可以对某一特定的区块内容作出评论,Roam 会自动为每一位用户生成对应的评论块 (Comment-Block)。
结尾
至此,关于 Roam Research 主要内容的介绍就基本完毕了。
总结
在关系结构上,Graph 能够高效联络起各 Page 之间的 Blocks;而从数据存储的角度出发,Roam 对资源文件 (Files) 的管理又是相当明智的——将用户上传的媒体附件作分离式存储,且允许对同一份 Upload 的(引用)复现;
最终在实现 Graph 内部高度维稳的同时,还保证了应用程序在数据处理方面的高效运行。
感言
诚然,入门 Roam 是存在一定门槛的,无论是(对于国内用户)全英界面带来的「不适感」;还是初见 Roam 时的异常「空旷感」;又或是在第一次面对 Sidebar 时的不知所措——以上种种,都有可能成为新人用户「自我劝退」的缘由。
而当你真正上手后,便会不自觉的陷入其中,对于 Roam Research 的使用场景也会随着时间的推移,自然而然迎来新的开拓与发掘。
可令人遗憾的是,Roam 官方高昂的订阅价格实在是令人望而却步的——几乎成为了大多数人放弃(或离开) Roam 的主要原因。
对此,笔者想说:无论怎样优秀的笔记工具,最终都是要由人去使用的。
不过笔者始终坚信,Roam 真正要做的绝不只是一个 Note-taking tool 那么简单,而是要让人们在创建/编辑笔记的过程中尽可能得跳脱出"盲目的整理怪圈"。
人们应当认识到这一点:关于信息的分类本应是多维度,多模态的。
与其事先定义好一个个「层层嵌套的文件柜」去收纳文档;倒不如让笔记在输入-输出的过程中“自行生长”——以实现对知识图谱的网络模型整合,最终建立起长期的思维认知体系。
正如 Roam 团队在官网介绍的那样:
最后的最后,请允许笔者用一句话作为全文的总结:
人类的生命是短暂的,但知识却是永恒的,让我们在有限的人生里致力于无限的事业!