· engineering· 约 25 分钟精读

Offline Register Search Facet Selection Boundary — Technical Analysis | KeyRotate Technology

核心要点速览 · TL;DR 概要

现场要做的其实只有两个动作:把手里这件设备找出来,把某一批设备一起看一遍。本文以密旋科技 TagWorks 与 SlingGuard 两套离线台账为样本,把“哪些设备显示在屏幕上”这条链路逐段拆开:状态如何被压成单值枚举、分面数字算的是什么范围、空列表到底该归因给谁、勾选集为什么可以越出可见集,以及没有索引时这笔计算账落在哪里。

Offline Register Search Facet Selection Boundary — Technical Analysis | KeyRotate Technology

一、现场要的只有两个动作

在地下配电间做便携式电器的定期检测,或者在港口堆场做吊索具的定期检验,作业人员手上真正要完成的事情只有两个:把手里这件东西在台账里找出来;把某一批东西一起看一遍。前者是检索,后者是筛选。

这两件事都不发生在服务器上。地下室没有信号,堆场边缘也没有,台账就在设备里。密旋科技的两套现场工具各自回答了这两个动作的一部分:TagWorks 面向电气安全,按 AS/NZS 3760 的检测周期维护便携式电器台账;SlingGuard 面向吊装索具,按 LOLER 1998 与 OSHA 29 CFR 1910.184 维护索具的检验与放行记录。两者都是端侧离线:数据落本机,没有账号,也没有远端索引。

本文只处理一个很窄的问题:屏幕上究竟该显示哪些行。这条链路看起来只是“把不匹配的滤掉”,但真正落到代码里,它牵出四件事——状态怎么算、计数算的是什么范围、空列表怎么解释、勾选与可见之间是什么关系。

二、两条链路的形状不一样

TagWorks 的筛选是一个四选一的分面:全部、需要关注、三十天内到期、合规。它没有检索框。要找一件具体设备,路径是先按分面缩小范围,再滚动,或者直接扫标签进详情页。站点是第二条路径:仪表盘按站点聚合,点进去看到的是该站点的只读清单。

SlingGuard 的筛选是六个状态分面加一个自由检索框。检索覆盖四个字段:标签号、名称、位置、出厂编号。两个条件取交集:文本命中,且状态相等。

维度TagWorksSlingGuard
筛选谓词来源单一状态分面,四选一状态分面六选一
自由检索无检索框四个字段,任一包含即命中
组合方式分面,或站点下钻(二者不同时生效)检索与分面取交集
分面计数不显示计数,仪表盘另有汇总胶囊上显示计数
列表顺序登记顺序,新件追加在末尾登记顺序,新件插入在头部
站点结构化下钻入口仅作为一个可检索字段

先把两条链路画成一张图。左侧是共同的四段:登记册快照、文本谓词、状态分面、可见集;右侧是这条链路上四处容易说错的地方。

登记册的检索链路:可见集如何被算出来两套离线台账的筛选路径对照,求值全部在本机完成一、登记册快照全量资产在内存二、文本谓词四字段任一命中三、状态分面单值枚举精确相等四、可见集 V求值结果不落盘选择集 S 与可见集 VV∩S勾选不随筛选收缩,S 越出 V 的部分仍计入批量四处语义缺口计数作用域:分面数字按全量算,不随查询收缩空态归因:文案只判检索词,不判选中分面顺序来源:屏幕按登记顺序,导出按标签号归一化:检索不做,判重才做筛选只决定显示什么;到期与放行结论仍由本机规则独立计算

这张表里最容易被忽略的是最后一行。TagWorks 的站点清单来自资产里的自由文本字段,去掉空值、排序后成为导航入口;SlingGuard 没有站点导航,位置只是字符串。于是“同一批设备”在两套工具里的定义并不相同:前者靠字符串相等,后者靠字符串包含。

三、状态是单值枚举,不是布尔集合

分面能够成立,前提是每件资产在任一时刻只属于一个状态。两套工具都把状态做成单值枚举,并各自写了一条优先级链。

TagWorks 的枚举有六个取值:逾期、三十天内到期、合规、失败、不完整、未初检。判定顺序是固定的:最新一次检验若判为失败,直接落“失败”,与日期无关;其次,若最新记录既没有建立 PASS 也不算判失败,落“不完整”;再次,若从未检验过,落“未初检”;最后才按到期日分三种——已过期为逾期,三十天以内为临期,其余为合规。

SlingGuard 的枚举也有六个取值:在用、临期、逾期、隔离、报废、未检验。顺序同样固定:手工报废标记或存在报废记录,落“报废”;存在未释放且证据完整的隔离记录,或者存在手工隔离标记,落“隔离”;从未检验过,落“未检验”;其后按下一次检验到期日分三种。这里有一处值得单独说明:后来的一次 PASS 不会自动清掉已经记录的隔离,解除隔离必须走一条独立的、与隔离记录显式关联的放行记录。

状态值判定条件(按优先级从上到下)归属分面
失败最新检验未通过需要关注
不完整最新记录未建立 PASS,也未判失败需要关注
未初检台账内无任何检验记录需要关注
逾期到期日已过需要关注
三十天内到期到期日在三十天窗口内三十天内到期
合规到期日在窗口之外合规

这张表说明一件事:分面是状态的投影,不是另一个独立事实。TagWorks 把四个值合成一个“需要关注”,所以点进去会看到四种不同颜色的行内徽章;SlingGuard 是一对一分面,点进去只有一种。两者都不算错,但它们对“这一批”的定义粒度不同——前者的分面回答“哪些要处理”,后者的分面回答“哪些属于同一类”。

四、分面数字算的是什么范围

SlingGuard 的分面胶囊上带数字。数字的算法很直接:对每一个状态,遍历全部资产求一次状态,再计数。这里有三处后果。

第一,数字是全量口径,不随检索词收缩。输入框里已经有关键词时,胶囊上的数字仍然是整个台账的计数。操作者看到“逾期 12”,点进去只看到两行,因为另外十行被检索词挡住了。

第二,计数为零的分面不渲染。某个状态曾经存在、如今归零,胶囊直接消失。这省了空间,代价是操作者无法通过“点进去看到空列表”来确认这一类真的为零——空列表本身也是一种证据,而它被优化掉了。

第三,计数在每次视图求值时重算,且每个分面各遍历一遍。六个分面就是六遍遍历,同一件资产的状态在一次求值里被重复计算六次。

TagWorks 没有胶囊计数,但仪表盘顶部的四个数字来自同一套状态函数:资产总数、合规数、三十天内到期数、需要关注数(失败、不完整、逾期、未初检之和)。它与筛选分面口径一致。一致这一点很实际:屏幕上的“需要关注 7”与点进分面后数出来的行数不会打架,复核时不需要解释第二套算法。

五、空列表该归因给谁

空列表至少有三种来路:台账本来就是空的;分面命中了零条;检索词命中了零条。三种来路对操作者意味着完全不同的下一步,所以空态文案必须能被分辨。

TagWorks 的空态分两支:台账为空时提示“还没有资产,先添加第一台设备”;台账非空但当前无匹配时提示“没有资产符合这个筛选条件”。分支键是台账是否为空,识别的是第一种来路;第二种来路被合并处理,站点下钻为空时也落在这句通用文案上,不会指出是哪个站点为空。

SlingGuard 的空态也分两支,但分支键是检索词是否为空:检索词为空时提示“点加号登记第一件索具”,检索词非空时提示“没有设备符合你的检索”。问题在于,第二个可空维度(选中的分面)没有进入判断。

场景TagWorks 文案SlingGuard 文案归因
台账为空,无筛选还没有资产点加号登记第一件准确
台账非空,分面无命中没有资产符合筛选点加号登记第一件归因不足
台账非空,检索无命中不适用没有设备符合检索准确
分面已选但计数在两次求值之间归零没有资产符合筛选点加号登记第一件归因不足

需要说清楚的是,SlingGuard 的第三行并不是常规路径:胶囊只在计数大于零时出现,所以“选了分面却看到空列表”通常要等到状态在两次求值之间发生变化——比如跨过午夜,一件设备的到期判定翻转了。这是归因粒度的问题,不是显示错误。但它暴露了一个结构:空态文案的判定维度与可空维度没有对齐,两个可空条件只判了一个。

六、选择集可以越出可见集

SlingGuard 的批量检验依赖一个勾选集合。这个集合独立于检索词与分面:切换分面、输入关键词、清空关键词,都不会改变已经勾选的项。底部的按钮写着“批量检验(N)”,这个 N 是勾选数,不是当前可见行数。退出批量模式才会清空勾选;批量模式下侧滑删除不执行;删除一件资产会同时清掉它的检验记录与放行记录。

于是会出现这样一幕:操作者先在“逾期”分面里勾了五件,再切到“全部”、输入另一个词,屏幕上只剩两行,底部按钮仍写着“批量检验(5)”。这是刻意的取舍——勾选是操作者的意图,不应被视角变化抹掉——但它要求操作者能看见自己勾了什么,而当前没有一份“已选清单”视图。选择集与可见集分离是合理的,分离之后缺少一个可查看的交集窗口,才是缺口。

两处侧滑删除用的都是同一个模式:偏移量作用在“当场重新求值出来的数组”上,而不是“上一帧渲染的那个数组”。标签按日期算出的状态在跨过午夜时会翻转,两帧之间数组的成员与顺序都可能变化,此时按索引删除有可能落到相邻行。这不是高频路径,但它说明按索引定位行、按标识符删除资产这两件事需要分开:索引是渲染的产物,资产标识符才是稳定键。

七、没有索引的代价

两套存储都是内存数组加 JSON 整体快照落盘:没有索引,没有查询引擎,状态也不是存储字段。每次显示都要由记录现算——最新一次检验是哪一条、有没有未解决的失败、复检与放行是否构成了有效的解除链。

以 TagWorks 为例,单件资产的状态要反复触及记录表:一次取最新检验记录,一次检查未解决的失败(这一次还要把该资产的历史排序),而判定时又要再取一次最新结论,内部再各扫一遍。按代码结构推算,单件资产约四遍全表触及。SlingGuard 更重一些:报废判定、隔离判定、有效放行集合各扫一遍,再回落到日期分三种。

环节计算口径量级
TagWorks 一次分面筛选400 件 × 1600 条记录 × 约 4 遍约 256 万次元素访问
TagWorks 可见行重算惰性列表,只算可见的十余行约 8 万次
SlingGuard 分面胶囊6 个分面 × 400 件 × 1600 条 × 约 4 遍约 1536 万次
SlingGuard 选中分面再过滤400 件 × 1600 条 × 约 4 遍约 256 万次

这张表里的数字是按代码结构推出来的量级,不是 Instruments 采样值;列表是惰性的,行内重算只发生在可见行,所以真正的成本集中在“全量遍历”那两层。结论不是“慢”,而是一个比例:分面计数的代价是选中过滤的六倍,而它在每次键入时都会重跑一遍。

可落地的收敛并不涉及语义变更:一次求值里先算一张“资产标识符到状态”的表,让分面计数、选中过滤、行内徽章都查这张表;状态只与记录集合有关,记录变动时递增一个版本号即可让表失效。这一步把六遍扫描压成一遍,且不改变任何一条判定规则。

八、三个顺序同时存在

屏幕上的顺序是登记顺序:TagWorks 把新资产追加到末尾,SlingGuard 把新资产插到头部。两者都没有排序控件,也没有“按到期日排列”这个选项。

纸面上的顺序是另一套:导出登记册时,TagWorks 按站点与名称拼起来的键排序,SlingGuard 按标签号排序。

报告里的顺序是第三套:两份 PDF 都按站点或位置分组,空值归入一个合成的“未分配”分组,组内再排一次序。

三套顺序并存,对复核对话的影响很直接:口头说“第 12 行”没有任何一处能对齐。可行的口径是以标签号为准,或者把排序做成可见可选项——但在端侧,任何新增的排序维度都要同时写进导出与报告,否则又会多出第四套顺序。

拼接排序键还有两个细节。TagWorks 直接用站点字符串加名称字符串作为比较键,中间没有分隔符,站点“AB”配名称“C”与站点“A”配名称“BC”会得到同一个键;这不会丢数据,但会让这几行的相对顺序退化为不稳定。另一处是命名碰撞:PDF 把空站点合成一个名为“未分配”的分组,如果台账里真有一个站点叫这个名字,两部分内容会落进同一节。两处都不严重,但都属于“导出件与屏幕不完全同构”的范畴,写进交付说明比留给人去猜要好。

九、检索面与归一化

SlingGuard 的检索覆盖四个字段,其余字段一概不参与:制造商、设备类型、链条等级、额定载荷数值、检验备注、证书编号都不在匹配范围内。这意味着搜一个品牌名能不能命中,取决于它有没有被写进名称字段;搜“80 级”不会命中链条等级字段;搜一句缺陷描述不会命中检验备注。

可检索不可检索
标签号制造商
名称设备类型
位置链条等级
出厂编号额定载荷数值、证书编号、检验备注

匹配用的接口是大小写不敏感且区域感知的包含判断。它的文档承诺只有这两点,没有承诺变音符、全半角、连字符或空白的归一。所以“SL-101”与“SL101”互不命中,全角写法的标签号同样不命中。对扫码入场的设备这不是问题——扫出来的字符串与台账里存的是同一串;对手工键入的设备,这就是一个每天都会遇到的小摩擦。

TagWorks 走了另一条路:它有归一化,但只做在入口。扫码得到的字符串会去掉首尾空白、按竖线切分、取出标识字段,再与台账比对;新增与判重都经过这一层。可是筛选侧的站点比较是字符串相等,没有去空白,也没有大小写折叠;站点清单直接取原始字符串去空后排序。结果是“Yard A”与“yard a”算两个站点,只含空格的站点会变成一个看不见名字的入口。

于是两套工具各缺一半:一套把归一化做在入口却没做在检索,另一套做了检索却没做归一化。补法并不复杂——让检索与分面共用同一个归一化函数(大小写、首尾空白、连字符、全半角),把归一化后的值作为比较键,原始字符串留给显示与导出。这样既不会改变台账里的既有记录,也不会让检索结果依赖于操作者的输入习惯。

十、能力边界

第一条,检索不命中不等于台账里没有这件设备,只说明那四个字段里没有包含这个词。二者在界面上长得很像,在责任上完全不同。

第二条,筛选不改变任何判定。可见集是显示层的东西;到期、失败、隔离、放行的结论各自由本机规则独立计算,与当前选中哪个分面无关。把可见集当成结论集会得出错误的合规判断。

第三条,分面数字不是可见集的行数。前者是全量口径,后者是检索与分面取交集之后的结果;两者在有关键词时必然不同。

第四条,选择集可以越出可见集。这是有意为之,但需要一条让操作者看得见的通道;没有这条通道,批量操作的对象就只能靠记忆。

第五条,无索引意味着规模有上限。四百件资产、一千六百条记录还在可接受区间;到数千件、上万条记录时,每次键入重算六遍分面会成为可感知的延迟。端侧离线用“数据不外流”换来的,就是这类必须自己承担的工程账——没有远端索引可以借,也没有服务端缓存可以加。

十一、结语

把“显示哪些行”这件事拆开之后,能落成条款的东西并不多:状态是单值枚举且有固定优先级;分面是状态的投影,计数有明确的作用域;空态要按可空维度给归因;勾选集与可见集是两个集合,允许分离但要能查看;没有索引时,成本集中在全量遍历那一层,收敛方式是先算一张状态表。

这些都不涉及新能力,只是把已有的东西说得更准。密旋科技在这两套工具里选择了端侧离线,代价与收益都写在同一个地方:数据不出设备,于是所有与数据规模相关的工程问题也一并留在设备上。把这条链路的形状交代清楚,比给它加一个更好看的分面更有用。

参考文献

[1] Standards Australia / Standards New Zealand. AS/NZS 3760:2022 — In-service safety inspection and testing of electrical equipment. https://www.standards.org.au/standards-catalogue/sa-snz/electrotechnology/el-036

[2] UK Legislation. Lifting Operations and Lifting Equipment Regulations 1998 (LOLER), SI 1998/2307. https://www.legislation.gov.uk/uksi/1998/2307/contents

[3] U.S. Occupational Safety and Health Administration. 29 CFR 1910.184 — Slings. https://www.osha.gov/laws-regs/regulations/standardnumber/1910/1910.184

[4] Apple Developer Documentation. NSString — localizedCaseInsensitiveContains. https://developer.apple.com/documentation/foundation/nsstring/localizedcaseinsensitivecontains(_:)

[5] Apple Developer Documentation. NSString — localizedStandardContains. https://developer.apple.com/documentation/foundation/nsstring/localizedstandardcontains(_:)

[6] Apple Developer Documentation. SwiftUI — searchable(text:placement:prompt:). https://developer.apple.com/documentation/swiftui/view/searchable(text:placement:prompt:)

[7] ISO. ISO 55000:2014 — Asset management: Overview, principles and terminology. https://www.iso.org/standard/55088.html

[8] W3C. Web Content Accessibility Guidelines (WCAG) 2.2. https://www.w3.org/TR/WCAG22/

Share:
Back to Blog