2 个版本

0.0.3 2019年11月25日
0.0.2 2019年11月21日

#765 in 科学

ISC 或 GPL-3.0 及后续版本

195KB
4K SLoC

retriever

Retriever 是 Rust 应用的嵌入式、内存、面向文档的数据存储。它以类似 NoSQL 数据库的方式存储普通的 Rust 数据类型。

当你需要按多个属性索引集合时,当你在集合的元素之间需要多种关系时,或者当你需要维护集合的摘要统计信息时,Retriever 是理想的。

Callie, a golden retriever puppy. (图片由维基媒体用户 MichaelMcPhee 提供,展示一只金毛寻回犬 Callie。)

特性

  • 面向文档的存储和检索。
  • 可按无限数量的次要键索引。
  • 根据需要创建索引并在不再需要时删除它们。
  • 惰性索引。在查询索引时支付重新索引的成本,而不是在此之前。
  • 选择借用或计算的(动态)键(使用 Cow)。
  • 如果需要,可以进行类似 map-reduce 的操作。
  • 分块:同一块中所有记录都存储在同一个 Vec 中。
  • 100% 安全的 Rust,无默认依赖。
  • 超过 60 个测试、文档测试和基准测试(需要更多)
  • 大量全功能示例供您入门!

Retriever 没有下面这些功能

  • 并行。这是一个“待办事项”。
  • 持久性。您可以为任何块访问原始数据并将其传递给 serde 进行序列化。请参阅 Storage::raw() 中的示例。
  • 网络。Retriever 像任何其他 crate 一样嵌入到您的应用程序中。它不通过网络访问任何内容,也不能通过网络访问。
  • 新颖性。我已经尽力使 Retriever 尽可能简单和明显,希望人们能够轻松上手并使用它(甚至为其做出贡献),学习曲线很小。在有许多类型参数的地方,我会尝试通过适当的文档来揭示它们。

示例

use retriever::prelude::*;
use std::borrow::Cow;
use chrono::prelude::*;  // Using rust's Chrono crate to handle date/time
                         // (just for this example, you don't need it)
use std::collections::HashSet;

// This example is going to be about a puppy rescue agency
struct Puppy {
  name: String,
  rescued_date: Date<Utc>,
  adopted_date: Option<Date<Utc>>,
  breed: HashSet<String>,
  parents: HashSet<Id<i32,String>>,
}

// Some convenience functions for describing puppies
impl Puppy {
  fn new(name: &str, rescued_date: Date<Utc>) -> Puppy {
    Puppy {
      name: String::from(name),
      rescued_date,
      adopted_date: None,
      breed: HashSet::default(),
      parents: HashSet::default(),
    }
  }

  fn with_adopted_date(mut self, adopted_date: Date<Utc>) -> Puppy {
    self.adopted_date = Some(adopted_date);
    self
  }

  fn with_breeds(mut self, breeds: &[&str]) -> Puppy {
    self.breed.extend(breeds.iter().map(|breed| String::from(*breed)));
    self
  }

  fn with_parent(mut self, year: i32, name: &str) -> Puppy {
    self.parents.insert(ID.chunk(year).item(String::from(name)));
    self
  }
}

// We need to implement Record for our Puppy type.
// We choose the year the puppy was rescued as the chunk key,
// and the name of the puppy as the item key.
// Because of this design, we can never have two puppies with same name
// rescued in the same year. They would have the same Id.
impl Record<i32,str> for Puppy {
  fn chunk_key(&self) -> Cow<i32> {
    Cow::Owned(self.rescued_date.year())
  }

  fn item_key(&self) -> Cow<str> {
    Cow::Borrowed(&self.name)
  }
}

// Let's create a storage of puppies.
let mut storage : Storage<i32,str,Puppy> = Storage::new();

// Add some example puppies to work with
storage.add(
  Puppy::new("Lucky", Utc.ymd(2019, 3, 27))
    .with_adopted_date(Utc.ymd(2019, 9, 13))
    .with_breeds(&["beagle"])
);

storage.add(
  Puppy::new("Spot", Utc.ymd(2019, 1, 9))
    .with_breeds(&["labrador", "dalmation"])  // See below for correct spelling.
    .with_parent(2010, "Yeller")
);

storage.add(
  Puppy::new("JoJo", Utc.ymd(2018, 9, 2))
    .with_adopted_date(Utc.ymd(2019, 5, 1))
    .with_breeds(&["labrador","shepherd"])
    .with_parent(2010, "Yeller")
);

storage.add(
  Puppy::new("Yeller", Utc.ymd(2010, 8, 30))
    .with_adopted_date(Utc.ymd(2013, 12, 24))
    .with_breeds(&["labrador"])
);

// Get all puppies rescued in 2019:
let q = Chunks([2019]);
let mut rescued_2019 : Vec<_> = storage.query(&q)
  .map(|puppy: &Puppy| &puppy.name).collect();
rescued_2019.sort();  // can't depend on iteration order!
assert_eq!(vec!["Lucky","Spot"], rescued_2019);

// Get all puppies rescued in the last 3 years:
let q = Chunks(2017..=2019);
let mut rescued_recently : Vec<_> = storage.query(&q)
  .map(|puppy: &Puppy| &puppy.name).collect();
rescued_recently.sort();
assert_eq!(vec!["JoJo","Lucky","Spot"], rescued_recently);

// Get all puppies rescued in march:
let q = Everything.filter(|puppy: &Puppy| puppy.rescued_date.month() == 3);
let mut rescued_in_march : Vec<_> = storage.query(&q)
  .map(|puppy| &puppy.name).collect();
rescued_in_march.sort();
assert_eq!(vec!["Lucky"], rescued_in_march);

// Fix spelling of "dalmatian" on all puppies:
let q = Everything.filter(|puppy : &Puppy| puppy.breed.contains("dalmation"));
storage.modify(&q, |mut editor| {
  let puppy = editor.get_mut();
  puppy.breed.remove("dalmation");
  puppy.breed.insert(String::from("dalmatian"));
});
assert_eq!(0, storage.iter().filter(|x| x.breed.contains("dalmation")).count());
assert_eq!(1, storage.iter().filter(|x| x.breed.contains("dalmatian")).count());

// Set up an index of puppies by their parent.
// In SecondaryIndexes, we always return a collection of secondary keys.
// (In this case, a HashSet containing the Ids of the parents.)
let mut by_parents = SecondaryIndex::new(&storage,
  |puppy: &Puppy| Cow::Borrowed(&puppy.parents));

// Use an index to search for all children of Yeller:
let yeller_id = ID.chunk(2010).item(String::from("Yeller"));
let q = Everything.matching(&mut by_parents, Cow::Borrowed(&yeller_id));
let mut children_of_yeller : Vec<_> = storage.query(&q)
  .map(|puppy: &Puppy| &puppy.name).collect();
children_of_yeller.sort();
assert_eq!(vec!["JoJo","Spot"], children_of_yeller);

// Remove puppies who have been adopted more than five years ago.
let q = Chunks(0..2014).filter(|puppy: &Puppy|
  puppy.adopted_date.map(|date| date.year() <= 2014).unwrap_or(false));
assert!(storage.get(&yeller_id).is_some());
storage.remove(&q, std::mem::drop);
assert!(storage.get(&yeller_id).is_none());

与其他数据库(SQL、MongoDB等)的比较

与大多数数据库不同,Retriever将您的数据存储在堆内存中的普通Rust数据类型中。(具体来说,每个块都有一个Vec,用于存储该块的所有数据。)它不支持从多个客户端通过网络访问。

与传统的数据库一样,Retriever拥有灵活的索引和查询系统,可以模拟记录之间的多对多关系。

与ECS(实体-组件-系统)框架的比较

Retriever可以作为可服务组件存储使用,因为具有相同键的记录很容易相互交叉引用。但Retriever并非专门为游戏项目设计,它试图在程序员舒适度、可靠性和性能之间取得平衡。

ECS使用低基数索引以非常快的速度完成大量工作。Retriever使用高基数索引以尽可能避免工作。

如果您知道需要使用面向数据设计,那么您可能会考虑像specslegion这样的ECS。

入门指南

  1. 创建一个Rust结构体或枚举,代表您想要存储的数据项。
  2. 为您的记录的每个实例选择一个块键项键
    • 许多记录可以共享相同的块键。
    • 同一块中的两个记录不得具有相同的项键。
    • 所有键都必须满足Clone + Debug + Eq + Hash + Ord。请参阅ValidKey
    • 如果您不想使用分块或不确定要选择哪种类型的块键,请使用()作为块键。分块是一个旨在帮助您的功能——您不必使用它。
  3. 为您的记录、块键和项键类型实现Record特质。
  4. 使用Storage::new()创建一个新的空Storage对象。
  5. 使用Storage::add()Storage::iter()Storage::query()Storage::modify()Storage::remove()在您的存储中实现CRUD操作。
  6. 如果您想的话,可以使用SecondaryIndex::new()创建一些二级索引。通过编写一个将记录映射到零个或多个二级键的单个闭包来定义二级索引。
  7. 如果您想,可以使用 Reduction::new() 创建一些缩减。通过编写两个闭包来定义缩减:(1) 从记录到摘要的映射,和(2) 将多个摘要折叠成一个单一摘要。使用 Reduction::reduce() 将整个存储缩减为一个单一摘要,或使用 Reduction::reduce_chunk() 将单个块缩减为一个单一摘要。

更多关于如何选择一个好的块键的信息

  • 一个好的块键会将相关的记录放在一起;查询通常应该一次只操作几个块。
  • 一个好的块键是可预测的;理想情况下,在寻找记录之前,你应该知道记录所在的块。
  • 一个好的块键可能与持久存储相对应,例如文件系统中的一个单个文件。作为一个块轻松加载和卸载数据块。
  • 对于表示地理或空间信息的存储,一个好的块键可能代表一个网格方块或其他细分策略。
  • 对于时间序列数据库,一个好的块键可能代表一个时间段。
  • 在GUI框架中,每个窗口可能都有自己的块,每个小部件可能是该块中的一个记录。
  • 如果您只想对存储的一部分执行 Reduction,则该部分必须定义为单个块。未来,我希望实现可以映射到零个或多个块的卷积缩减。

关于Cow

Retriever大量使用 Cow 来表示各种索引键。使用 Cow 允许检索器桥接广泛的用例。

一个 Cow<T> 通常要么是 Cow::Owned(T),要么是 Cow::Borrowed(&T)。泛型参数指的是借用形式,因此 Cow<str> 要么是 Cow::Owned(String),要么是 Cow::Borrowed(&str)。每次您看到像 ChunkKeyItemKeyIndexKey 这样的泛型参数时,这些键也应该是借用形式。

这些都是好的

  • 记录<i64,str>
  • 记录<i64,&'static str>
  • 记录<i64,Arc<String>>

这在大多数情况下都会起作用,但有点奇怪

  • 记录<i64,String>

许可证

Retriever根据您的选择受ISC许可证(一种宽松的许可证)或AGPL v3.0或更新版(一种强力的copyleft许可证)的许可。

小狗的照片由维基媒体用户MichaelMcPhee拍摄。 Creative Commons Attribution 3.0 Unported。 (来源)

贡献

除非您明确说明,否则您提交给Retriever以供包含的任何贡献都应按ISC或AGPL-3.0或更新版许可,不附加任何额外的条款或条件。

如何帮助

在此阶段,任何关于错误报告或文档不清晰的问题都非常有价值。如果我不能立即回复,请耐心等待。我对任何有助于进一步简化代码库的建议也感兴趣。

待办事项:(我希望有这些功能,但它们尚未实现)

  • 并行处理(可能通过rayon功能标志实现)
  • 排序索引/范围查询
  • 布尔查询(并集、交集、差集等--注意:您可以通过链式查询操作来执行交集查询)
  • 外部可变迭代器(目前只支持内部迭代进行修改)
  • 在某些我预期会对其产生影响的场所进行更多的小向量优化
  • 需要严格测试空间使用(目前没有努力缩小存储或索引向量,这可能是当前的首要任务)
  • 惰性项键索引或禁用项键可能是一个性能提升。
  • 总结零个或多个源块的卷积缩减。
  • 想法:数据元素可以存储在持久化数据结构中,这可能使得在单独修改元素的同时遍历元素成为可能。这个想法需要研究。
  • 理论上,我预计检索器的性能将在大约1600万个16百万元素的块超过16百万块时下降,而对于低基数数据,二级索引根本不可扩展。如果有人以某种方式合法地获得了这一级别的硬件,我希望检索器最终能够扩展到“宇宙中每个电子”。

许可证:ISC 或 GPL-3.0 或更新版本

依赖项