《数据集》(2018)中的有趣想法

2020-10-20 02:23:33

Datasette(以前)是我用于浏览和发布结构化数据的开源工具。在Datasette中嵌入了很多想法。我意识到我并没有把很多东西写下来。

Datasette为您的数据提供只读API。它不尝试处理写入。完全避免写入是众多有趣属性的基础,其中许多属性将在下面进一步展开。简而言之:

在2018年,托管没有读/写持久性要求的web应用非常便宜--通常是免费的(Zeit Now和Heroku都有慷慨的免费层级)。这是一件大事:即使每月支付几美元也足以对共享数据进行统计,因为现在你必须找出谁来支付,并确保未来的支付不会过期。

因为是只读的,所以很难扩展:只需添加更多实例,每个实例都有自己的数据副本。在扩展Web应用程序时,与可写数据存储相关的所有难题都可以完全跳过。

由于数据库文件是使用SQLite的不可变模式打开的,因此我们可以接受任意SQL查询,而不会有损坏数据的风险。

每当您的数据发生更改时,您都需要发布整个数据库的全新副本。有了合适的主机,这很容易做到:在现有实时部署的同时部署全新的数据和应用程序副本,然后在负载平衡器级别将传入的HTTP流量切换到您的API。Heroku和Zeit现在都支持这一开箱即用的策略。

由于数据是只读的,并且封装在单个二进制SQLite数据库文件中,因此我们可以将数据捆绑为应用程序的一部分。这意味着我们可以轻松地创建和发布Docker镜像,这些镜像既可以提供数据,也可以提供访问它的API和UI。我们还可以发布到允许我们运行Python应用程序的任何宿主提供程序,而不需要提供可变数据库。

Datasette Package命令获取一个或多个SQLite数据库,并将它们与Datasette应用程序捆绑在单个Docker映像中,随时可以部署到可以运行Docker容器的任何位置。

关系数据库很棒:一旦您知道如何使用它们,就可以使用精心设计的模式来表示您能想象到的任何数据。

如果数据过于非结构化,不适合关系模式怎么办?SQLite包括对JSON数据的出色支持-因此,如果您不能塑造适合表模式的数据,您可以将其存储为JSON的文本BLOB-并使用SQLite的JSON函数来过滤或提取特定字段。

那么二进制数据呢?即使是这样,SQLite也会很高兴地存储二进制blob。我的Datasette-Render-Image插件(这里是实时演示)是一个处理存储在SQLite blob中的二进制图像数据的工具示例。

如果我的数据太大怎么办?Datasette不是一个“大数据”工具,但是如果您对大数据的定义不适合RAM,那么这个门槛会一直在增加(单个AWS实例上2TB的RAM现在的成本不到4美元/小时)。

我个人从多GB SQLite数据库和Datasette中获得了很好的结果。单个SQLite数据库的理论最大大小约为140TB。

SQLite还内置了对好得出奇的全文搜索的支持,并且由于模块的可扩展性,它以SpatiaLite扩展的形式提供了出色的地理空间功能。Datasette从这个更广泛的生态系统中获益良多。

大多数开发人员避免将SQLite用于生产Web应用程序的原因是,它不能出色地处理大量并发写入。由于数据集是只读的,我们可以完全忽略此限制。

既然Datasette实例中的数据永远不会更改,为什么不永远缓存对它的调用呢?

Datasette在每个API响应中发送一个遥远的HTTP缓存过期标头。这意味着浏览器只会在第一次访问特定URL时获取数据,如果您在CDN(如Fastly或Cloudflare)后面托管Datasette,则每个唯一的API调用只会访问Datasette一次,然后CDN基本上会永远缓存它。

这意味着使用廉价托管的Datasette支持的API将JavaScript应用程序部署到即使是高流量站点的首页也是安全的-CDN将很容易承担负载。

早在7月份,Zeit就将Cloudflare添加到了每个部署中(甚至包括他们的免费层),因此,如果您在那里托管,就可以免费获得CDN好处。

如果您重新发布数据的更新副本,该怎么办?Datasette也涵盖了这一点。您可能已经注意到,每个DataSet数据库在部署时都会自动获得一个散列后缀:

此后缀基于整个数据库文件内容的SHA256散列,因此对数据的任何更改都将导致新的URL。如果您查询以前的后缀,Datasette会通知您并将您重定向到新的后缀。

如果您知道要更改数据,则可以根据无后缀的URL构建应用程序。这将不会被高速缓存,并且将总是302重定向到正确的版本(并且这些重定向非常快)。

重定向发送一个HTTP/2Push报头,这样,如果您在理解推送的CDN(比如Cloudflare)后面运行,您的浏览器就不必发出两个请求来跟踪重定向。您可以使用Chrome DevTools查看实际操作:

最后,如果出于某种原因需要退出HTTP缓存,您可以在URL查询字符串中包含?_ttl=0,以针对每个请求禁用它。-例如,如果您想要返回复仇者联盟的随机成员,则缓存响应没有意义:

Datasette的目标是尽可能减少在网上发布有趣数据的摩擦。

#部署到Herokudatasette发布Heroku mydatabase ase.db#或部署到Zeit NowDataette立即发布mydatabase.db。

这些命令获取一个或多个SQLite数据库,将它们上传到主机提供商,配置一个Datasette实例为它们提供服务,并返回新部署的应用程序的公共URL。

开箱即用,Datasette现在既可以发布到Heroku,也可以发布到Zeit。PUBLISH_SUBMAND插件钩子意味着可以通过编写插件来支持其他提供程序。

Datasette认为,只要有可能,数据都应该伴随着源信息和许可证。可以与您的数据捆绑在一起的metadata.json文件支持这些功能。您还可以在运行数据集发布时提供源和许可证信息:

当您使用这些选项时,Datasette将在部署过程中为您创建相应的metadata.json文件。

我真的很喜欢分面搜索:每当我想要开始理解数据集合时,它都是我求助的第一个工具。我在Solr、Elasticsearch和PostgreSQL的基础上构建了多方面搜索引擎,许多我最喜欢的工具(如Splunk和Datadog)都将其作为核心功能。

Dataset自动尝试计算每个表的面。你可以在这里阅读关于Datasette facets特性的更多信息--作为一个巨大的分面搜索迷,它是我一直以来最喜欢的项目特性之一。现在我可以将SQLite添加到我用来构建分面搜索的技术列表中了!

CSV是迄今为止最常用的在线共享和发布数据格式。几乎每个有用的数据工具都可以导出到它,并且它仍然是电子表格导入和导出的通用语言。

它有许多缺陷:它不能很容易地表示嵌套的数据结构,包含逗号的值的转义规则实现不一致,并且它没有表示字符编码的标准方式。

Datasette旨在将SQLite提升为更好的发布数据的默认格式。我更愿意下载一个充满预结构化数据的.db文件,而不是下载一个.csv,然后不得不将其重新组织为一项单独的工作。

但与庞大的CSV生态系统良好互动是至关重要的。Datasette具有强大的CSV导出功能:您可以看到的任何数据都可以导出-包括任意SQL查询的结果。如果您的查询可以分页,Datasette可以向下传输单个CSV文件中的每一页。

Datasette的姊妹工具CSV-to-sqlite处理等式的另一边:将数据从CSV导入SQLite表。此外,Datasette Publish Web应用程序允许用户上传其CSV,并将其直接部署到自己的全新Datasette实例中-不需要命令行。

现在很多人都对GraphQL感到兴奋,因为它允许API客户端准确地请求他们需要的数据,包括在单个查询中遍历到相关对象。

大多数API不允许人们向它们传递任意SQL查询的原因有很多:

性能:如果有人发送的意外(或故意)昂贵的查询耗尽了我们的资源,该怎么办?

隐藏实现细节:如果人们针对我们的API编写SQL,我们就永远不能更改数据库表的结构。

在安全性方面:数据是只读的,使用SQLite的不可变模式。您不能用查询插入来破坏它,更新只会抛出无害的错误。

在性能方面:SQLite有一种机制来取消耗时超过特定阈值的查询。默认情况下,Datasette将其设置为1秒,但如果需要,您可以更改该配置(在笔记本电脑上浏览多GB数据时,我经常将其设置为10秒)。

关于隐藏的实现细节:因为我们发布的是静态数据,而不是维护不断发展的API,所以我们基本上可以忽略这个问题。如果您确实担心这个问题,您可以利用预录查询和SQL视图定义将精心选择的向前兼容视图公开到您的数据中。

我在上面提到了Datasette的SQL时间限制。它们不仅仅是为了避免恶意查询:“乐观SQL求值”的概念已经融入到Datasette的一些核心特性中。

考虑建议的方面-Datasette检查您查看的任何表,并尝试建议值得针对其进行方面的列。

其工作方式是Datasette遍历表中的每一列,并运行查询以查看该列的唯一值是否少于20个。在大型表上,这可能会花费令人望而却步的时间,因此Datasette对这些查询设置了激进的超时:只有50ms。如果查询在该时间内运行失败,它将被静默删除,并且该列不会作为建议的方面列出。

Datasette的JSON API为JavaScript应用程序提供了使用相同模式的机制。如果将?_TimeLimit=20添加到任何Datasette API调用,底层查询将只有20ms的运行时间。如果成功,您将从API获得非常快速的错误响应。这意味着您可以设计自己的功能,尝试乐观地运行昂贵的查询,而不会损害应用程序的性能。

使用OFFSET/LIMIT的SQL分页有一个致命的缺陷:如果以每页20的速度请求页码300,底层SQL引擎需要计算和排序前面的所有6000行,然后才能返回您请求的20行。

键集分页(通常也称为其他名称,包括基于指针的分页)是一种更有效的数据分页方法。它对有序数据起作用。每页返回一个表示您最后看到的记录的令牌,然后当您请求下一页时,引擎只需过滤大于该标记化值的记录,并扫描接下来的20条记录即可。

(实际上,它扫描21个。通过请求比您打算显示的记录多一条记录,您可以检测是否存在另一页结果-如果您请求21条记录,但得到的结果少于或等于20条,则您知道您是在最后一页。)。

数据集默认为按主键(或SQLite行ID)排序。这非常适合进行高效的分页:对主键列运行SELECT以查找大于X的值是任何数据库都能支持的最快的范围扫描查询之一。这允许用户根据自己的喜好进行分页,而无需支付偏移/限制性能损失。

这也是“将所有行导出为CSV”选项的工作方式:当您选择该选项时,Datasette会向您的浏览器打开一个流,并在内部开始对整个表进行键集分页。这样即使在流回数百万行的情况下也能保持对资源使用的控制。

这就是Datasette的用武之地:它还处理任何其他排序顺序的键集分页。如果按任何列排序并单击“下一步”,您将请求上一次看到的值之后的下一组行。这甚至适用于包含重复值的列:如果您按这样的列排序,Datasette实际上会结合主键按该列排序。它生成的“NEXT”分页标记对排序值和主键都进行了编码,这样当您单击链接时,它就可以正确地为您提供下一页。

尝试单击此页面上的“下一步”,查看针对已排序列的键集分页操作。

我喜欢互动演示。我决定,如果Datasette的每个版本都有一个永久性的交互式演示来说明它的功能,那将会很有用。

多亏了Zeit Now,这很容易设置。实际上,我已经更进一步:在GitHub上成功推送到Master的每一次推送都会部署到一个永久的URL。

Https://latest.datasette.io/-对数据集主设备的最新提交。您可以查看https://latest.datasette.io/-/versions上当前部署的提交散列,并将其与https://github.com/simonw/datasette/commits进行比较。

Https://v0-25.datasette.io/是0.25标签版本数据集的永久网址。另请参阅https://v0-24.datasette.io/和https://v0-23-2.datasette.io/。

此演示使用的数据库与Datasette的单元测试装置创建的数据库完全相同。单元测试已经设计用于执行每个功能,因此重用它们作为实时演示非常有意义。

通过从GitHub签出完整的Datasette存储库并运行以下命令,您可以在自己的计算机上查看此测试数据库:

下面是Datasette Travis CI配置中的代码,它为每个提交和发布的标记部署一个实时演示。

Datasette的单元测试包括一些断言,以确保文档中提到每个插件挂钩、配置设置和底层视图类。在不更新文档(或至少确保文档中有相应的标题)的情况下添加或修改这些内容的提交或拉入请求将无法通过其测试。

Datasette的文档现在相当不错,ChangeLog提供了我添加到项目中的新功能的详细概述。我在8月份的PyBay会议上展示了Datasette,我已经发表了那次演讲的注释幻灯片。我在5月份为Changelog播客接受了关于Datasette的采访,我在那次对话中的笔记包括了一些我最喜欢的演示。

Datasette现在有了一个官方Twitter账号-你可以在那里关注@Datetteproj,以获得关于该项目的最新消息。