1个不稳定版本
0.1.0 | 2021年8月18日 |
---|
#2278 在 数据库接口
在 assemblage_view 中使用
220KB
3.5K SLoC
用于连接页面的分布式文档/图数据库
AssemblageDB 是一个事务性的高级数据库,用于连接的页面、笔记、文本和其他媒体网络。将其视为一个 个人网络,但易于编辑,比网络有更多的连接和更好的导航。它在高级意义上的定义是一个类似于HTML但简单得多,并且具有图形化双向链接而不是树形单向跳转链接的文档模型。数据模型是
- 面向文档的:支持嵌套文档,没有固定模式
- 基于图:文档可以有多个父级,形成一个有向的、可能为循环的图
特性
- 版本控制:旧值在合并前仍然可访问
- 事务性:通过 MVCC 隔离快照
- 存储无关:支持本地(文件)和wasm(IndexedDB)目标
- 索引:维护一个自动索引以进行相似度/重叠搜索
- 分布式:节点可以作为远程广播进行发布/订阅
数据模型
AssemblageDB 中的节点可以是原子的(例如一行文本)或嵌套的,可以是包含多个子项的 列表 或仅包含单个子项的 样式 节点。 List
节点有一个 布局,它控制子项之间的相对布局,而 Styled
节点有零个或多个 块样式 或 行样式,它们控制其(可能嵌套的)子项的显示方式。布局和样式的示例
Layout::Chain
:将子项布局为连续的行内跨度链。对于两个文本子项 "foo" 和 "bar",链将被显示为 "foobar"。Layout::Page
:将子项布局为页面上的块,垂直分隔新行。对于两个文本子项 "foo" 和 "bar",页面将被显示为两行,第一行包含 "foo",第二行包含 "bar"。SpanStyle::Italic
:一个行内样式,将子项 "foo" 显示为 "foo"BlockStyle::Heading
:一种块样式,将子元素“foo”显示在其自己的块中,字体大小更大。
节点始终是span或block。默认情况下,文本节点被视为span,如果使用如SpanStyle::Italic
或SpanStyle::Bold
等span样式进行样式化,则保持为span。然而,一组样式中的单个块样式(如BlockStyle::Heading
)始终是“传染性”的,并将同时使用SpanStyle::Italic
和BlockStyle::Heading
样式的文本节点“foo”转换为块。同样,布局控制列表是否以span或block显示:Layout::Chain
将列表转换为span,而Layout::Page
将列表转换为一系列块。
强制警告
AssemblageDB仍处于实验阶段。不言而喻,它不是一个经过实战考验的生产就绪数据库,并且可能随时会吃掉您的所有数据。如果您需要持久化生产数据,请使用真正的数据库。
示例
let storage = FileStorage::open("db1").await?;
let db = Db::open(storage).await?;
// Nodes support layouts and styles, for example as a page of blocks...
let page1_id = tx!(|db| {
db.add(Node::list(
Layout::Page,
vec![
Node::styled(BlockStyle::Heading, Node::text("A Heading!")),
Node::text("This is the first paragraph."),
Node::text("Unsurprisingly this is the second one..."),
],
))
.await?
});
// ...or as inline spans that are chained together:
let page2_id = tx!(|db| {
db.add(Node::list(
Layout::Page,
vec![Node::list(
Layout::Chain,
vec![
Node::text("And this is the "),
Node::styled(SpanStyle::Italic, Node::text("last")),
Node::text(" paragraph..."),
],
)],
))
.await?
});
// Documents can form a graph, with nodes keeping track of all parents:
tx!(|db| {
db.add(Node::list(Layout::Page, vec![page1_id, page1_id])).await?;
assert_eq!(db.parents(page1_id).await?.len(), 2);
assert_eq!(db.parents(page2_id).await?.len(), 0);
});
// All text is indexed, the DB supports "overlap" similarity search:
tx!(|db| {
let paragraph1_id = db.get(page1_id).await?.unwrap().children()[1].id()?;
let paragraph3_id = db.get(page2_id).await?.unwrap().children()[0].id()?;
let overlaps_of_p1 = db.overlaps(paragraph1_id).await?;
assert_eq!(overlaps_of_p1.len(), 1);
assert_eq!(overlaps_of_p1[0].id, paragraph3_id);
assert!(overlaps_of_p1[0].score() > 0.5);
});
// Nodes (with all their descendants) can be published globally...
let broadcast = tx!(|db| { db.publish_broadcast(page1_id).await? });
// ...and the broadcast can then be fetched in another remote DB:
let other_storage = FileStorage::open("db2").await?;
let db2 = Db::open(other_storage).await?;
tx!(|db2| {
let paragraph_id = db2
.add(Node::text("This is the first paragraph, right?"))
.await?;
db2.add(Node::list(Layout::Page, vec![paragraph_id]))
.await?;
// The DB is empty except for this paragraph, so no overlaps:
assert_eq!(db2.overlaps(paragraph_id).await?.len(), 0);
// But when the broadcast paragraph is fetched, there is an overlap:
db2.fetch_broadcast(&broadcast.broadcast_id).await?;
assert_eq!(db2.overlaps(paragraph_id).await?.len(), 1);
});
依赖项
~3–17MB
~220K SLoC