先别急着把模型都下载下来
这次遇到的是一个看起来很具体、实际上很容易失控的任务:核实 5 个公开 3D 数据集到底有多少条记录,字段怎么组织,哪些对象带关节标注,数据体积多大,发布许可能不能支持商用;同时还要在 2GB 以内做一个本地浏览器,让人可以按类别和关节情况筛选,并旋转查看模型。
一开始,最自然的想法是先把模型下载到本地。只有文件在眼前,预览页才有东西可显示,数量和字段也可以顺手从本地文件里统计。但我很快意识到,这个顺序会把几个不同的问题搅在一起:全库到底有多少条,是事实核查;哪些模型值得看,是取样决策;每个对象的许可是否清楚,是权利判断。它们不应该由同一批大文件来承担。
面对超大 3D 数据集,我会先建立全库的可检索事实,再决定哪些对象值得下载预览。
如果不处理这个边界,后面会出现很实际的后果:下载量先超过预算,数量统计却还没有完成;预览页看起来很丰富,但缺失字段被默认为“没有”,许可范围也被模型是否能打开替代。到那时,返工不只是删文件,而是要重新区分哪些结论有证据、哪些只是因为本地碰巧有样本。
“10万条”不是一个可以直接拿来用的数字
这次核对的第一个结果,是官方口径合计为 106094 条,确实超过了用户印象里的“10 万个”。这个数字有用,但它并不意味着 106094 个对象都能被同一种方式理解。
比如,AutoMate 的 3169 条里,包含 872 个装配体和 2297 个零件。如果只在页面上显示一个总数,读者很容易把它理解成 3169 个完整装配案例。实际上,对象层级已经不同了。全库统计必须保留原始数据集的定义,不能为了让数字更整齐,就把零件和装配体混成一种记录。
类别也不是统一的。CAD 有 200 个数字形状族,Articraft 有 238 个类别,而 AutoMate 的类别字段又存在全为空的真实缺失。这里最容易误判的是:字段为空不等于可以根据文件名或经验补一个类别。页面显示“未标注类别”,反而比制造一个看似合理的分类更可靠。
这件事让我重新意识到,数量核实不是把几个网页上的数字相加,而是先问每个数字的统计单位是什么。记录、零件、装配体、类别族,它们都可能出现在同一套数据里,却不承担同一种含义。索引如果没有保留这些差异,搜索越方便,误读就越快。
全量元数据先进入事实底座
我的处理顺序是先读取官方元数据,用列范围读取的方式把全量记录放进 SQLite,只保留搜索和核对需要的字段。这样做的重点不是数据库本身,而是把“全库可检索”从“模型文件已经下载”里拆出来。
索引里保留数据集来源、原始标识、类别、关节标注情况、原始格式、许可信息、署名要求和哈希等字段。缺预览图的记录仍然入库,缺参数定义的记录也仍然保留,只在相应字段上明确标注缺失。缺失值应该成为可见的事实,而不是被下载流程悄悄过滤掉的空白。
这样一来,搜索层可以回答“全库里有多少条带关节标注的记录”“哪些记录没有类别”“某个数据集的原始格式是什么”,而不会因为本地只缓存了一部分模型,就把样本数冒充全库数。用户先在索引中发现对象,再决定是否需要拉取文件,下载因此变成一个有条件的动作。
这一步还给许可证判断留下了位置。此次核对中,Objaverse 子集只含 CC BY 和 CC0,Zero-To-CAD 为 Apache-2.0,SimReady 与 Articraft 为 CC BY 4.0,AutoMate 的上游发布许可为 CC0。它们可以作为发布层面的证据,但不能自动推出每个上游设计的原作者权利都已经清理完毕。
尤其是 CADFS 和 AutoMate,虽然公开发布许可允许一定范围的商用使用,具体 Onshape 设计的原作者权利边界仍要单独标出。公开发布许可说明的是发布者给出的使用条件,不等于每一个上游对象都完成了独立的权利清理。 许可证、署名和对象来源必须分别留证,不能用一个“可商用”标签覆盖所有不确定性。
预览层只承担它能承担的事情
有了全量索引后,第二层才是本地预览。我的做法是按每个数据集的 family 类别抽样,每类尽量取 10 个,同时覆盖有关节和无关节的对象;如果某个类别本身不足 10 条,就全部保留。全量扫描后共有 500 个类别组,其中 16 个类别不足 10 条。按这个规则,样本大约 4900 条,原始 USD 约 1.05GB,仍在 2GB 预算以内。
这个数字需要和全库数量严格分开。4900 条是受控样本,不是 106094 条的缩小版统计结果。它服务于浏览器中的模型检查、格式转换和筛选体验,不能反过来代表数据集的完整分布。小文件更容易被下载和转换,因此样本天然可能偏向体积较小、结构较简单的对象;这个偏差要写在结果里,而不是等用户从预览页自行猜出来。
预览时我保留原始 USD 和标注,把转换得到的 GLB 明确标成静态几何预览。浏览器可以旋转模型,但这不表示 GLB 保留了原始关节行为,也不表示转换后的文件具备完整编辑能力。数据集里的程序不直接执行,预览只处理下载后的受控文件。这样做牺牲了一些自动化想象空间,却把本地浏览的安全边界和数据边界都留在可解释的位置。
这里的错误捷径很明显:为了做出可旋转预览,把所有模型先下载到本地,再事后补数量、许可和缺失字段。这个办法表面上省了一次设计,实际上把最重要的判断推迟到了最昂贵的环节。如果转换过程中才发现字段定义不一致,或者某些对象的许可需要单独查看,已经花掉的下载量和处理时间都很难追回。
用两层结果处理两种不同的不确定性
全库索引和样本预览分开以后,浏览器的页面结构也变得简单了。搜索、类别筛选和关节筛选作用于完整索引;只有点击进入预览时,页面才显示该记录是否已缓存、原始格式是什么、是否存在可用的转换文件。没有预览图的记录不会从搜索结果中消失,只会显示“暂无本地预览”。
我选择这种设计,是因为“找得到”与“看得到”是两种不同的不确定性。全库索引回答数据是否存在、字段是否齐全、许可证据在哪里;样本预览回答某类对象在本地是否能正常打开、转换后的几何大致是什么样。把这两件事合在一个状态里,用户就无法分辨“没有这个对象”和“这个对象还没下载”。
验证也按这两层进行。全库侧检查数量与官方数据对齐,检查关节统计和缺失值是否保留;预览侧检查模型是否正常显示、页面是否有 JavaScript 错误、GLB 结构是否有效、筛选结果是否对应索引字段。首轮无头浏览器实测显示模型正常显示,页面无 JavaScript 错误,金属材质也能在本地环境中出现反射。这个结果只能说明预览链路通过,不能替代全库事实核验。
还有一个容易被忽略的边界:下载和转换可以并行,但不能把“任务正在处理”当成“事实已经确认”。当时先完成了约 2700 个样本缓存和约 1000 个转换,尚未发现转换失败;随后仍要完成剩余覆盖、哈希校验、断网浏览和筛选验收。一个流程成熟的标志,不是它能同时跑很多任务,而是它能在并行时仍然分清哪些结果已经确认、哪些只是进行中。
下次我会怎么做
下次再遇到超大数据集,我会先把问题写成两张表:一张是全库事实表,回答数量、字段、层级、许可和缺失值;另一张是预览采样表,回答抽样规则、预算、原始文件、转换格式和验收结果。两张表共享稳定的对象标识,但不共享“是否已下载”这种会误导统计的状态。
我会先核对官方元数据的统计单位,再建立全量只读索引;确认索引可以搜索后,才按类别、标注情况和下载预算抽样。许可证会按发布许可、署名要求和上游权利边界分栏记录,原始格式与预览格式也不混为一谈。任何缺失字段都保留并显式显示,任何样本数都注明它只是样本。
这件事让我重新意识到,真正难的不是把 3D 模型显示在页面上,而是让页面没有因为显示了一部分模型,就假装自己理解了全部数据。表面上我是在做一个浏览器,其实我是在给事实、样本和权利判断划边界。当下载预算、数据规模和版权判断同时存在时,最稳妥的顺序永远是先索引全库,再受控预览。
我更愿意把它看成一种工作习惯,而不是一个 3D 技巧:先留下完整、可追溯的判断底座,再让昂贵的动作只服务于已经明确的问题。这样下一次换数据集、换格式或换浏览器时,真正需要重做的只是样本和转换,不必从下载堆里重新寻找事实。