公司动态
Map:有了搜索分数仍不敢直接把第一条当答案
搜索列表里出现reliability后我有过一个很诱人的念头既然首条排在最前旁边又有相关性分数那就给它一个“直接采用”的按钮。做门店查询、报修地址、访客目的地时少一次人工确认页面看起来也更干脆。幸好我没有把这个念头直接写进页面。因为我认真看了现有代码后发现reliability在这里是搜索返回项的一条线索searchState和searchErrorMessage才告诉我这次请求到底进行到哪里operationSequence则把“刚刚哪一次动作”留在屏幕上。若跳过这些状态只盯住排名和一个数字就像拿着候选名单直接盖章。名单可以帮助找人却不能替人完成核对。这篇不重复讲列表怎样保存相关性字段而是只讲一个更容易越界的误判排序线索为什么不能被改写成地点事实。当前工程并没有真实 POI 命中材料也没有“用户确认此地址”的业务状态。服务不可用时页面应当停在等待或错误而不是编一条看起来合理的地址给用户。那个看似省事的错误推理我当时脑中的推理很短第一条在前面分数也有于是第一条就是答案。它的问题不在代码语法而在于把三个不同层面的事情揉成了一件事。第一层是请求状态有没有开始搜索、是否成功返回、还是调用失败。第二层是候选排序响应中的项目按什么先后到达页面是否附带reliability。第三层才是地点事实这是不是用户要的那个具体地点地址是否应被业务采纳。现有MapSearchLongClickPage覆盖前两层的一部分观察信息并没有替第三层作出断言。否是不能跳过不能直接等同用户点击搜索请求状态有真实响应吗searchState 与 searchErrorMessage候选列表与 reliability排序线索人工结合名称 地址 场景判断地点事实或业务采纳这张图改变了我的排查顺序。以前我会在列表一出现就研究首条现在先看searchState。它初始是“尚未搜索”点击后是“第 N 轮搜索中”有项目时会写“第 N 轮返回 X 条展示结果”成功但sites为空也有独立文案出错则是“第 N 轮搜索失败”。这些词不是为了把界面写得热闹而是避免上一轮的画面被误当成本轮结果。State searchState: string 尚未搜索; State searchErrorMessage: string 暂无错误; State operationSequence: number 0; State lastOperationId: string 未生成; private markOperation(event: string): void { this.operationSequence 1; this.lastOperationId MAP-${this.operationSequence}; this.lastTriggerTime timestamp(); // 同时保存最近一次动作说明 }我最初还怀疑searchState只是文案和真假没什么关系。读到失败分支才知道它正是页面不乱下结论的一部分失败时searchItems清空totalCount写“调用失败未返回”searchErrorMessage保存真实错误。也就是说页面不会在新的请求失败后继续摆着旧候选诱导我把旧结果说成刚刚得到的答案。回到代码分数被保留判断没有被伪造runSearch成功返回后会取items[0]放到evidenceState.resultName并记录首条的reliability。我第一次看到这个位置时差点误读成“首条已经被选中”。其实它只是状态摘要让页面能显示本轮结果的第一项与相关性值没有selectedSiteId没有“确认地址”更没有把这一项发送给业务表单的代码。const first items.length 0 ? items[0] : undefined; this.searchState items.length 0 ? 第 ${nextRound} 轮返回 ${items.length} 条展示结果 : 第 ${nextRound} 轮调用成功但 sites 为空; this.evidenceState { query: this.evidenceState.query, resultName: first?.name ?? 未返回结果, reliability: first?.reliability, markerLongClickCount: this.evidenceState.markerLongClickCount, poiLongClickCount: this.evidenceState.poiLongClickCount, lastEvent: 第 ${nextRound} 轮搜索完成首条 reliability${this.reliabilityText(first?.reliability)} };这里的first是“数组第一项”不是“地点真相”。两者只差几个字工作里差得很远。同名园区、不同分店、模糊查询、地址简称都可能让人工仍需看siteId、地址与实际任务上下文。页面可以告诉我“这一轮的第一项是什么”和“它有没有携带相关性值”却没有资格替用户说“你要找的就是它”。因此我把错误猜测逐一排掉不是相关性字段无用也不是分数越高越危险问题是使用它时越过了页面拥有的信息。字段的意义是帮人阅读候选排序而不是替代确认动作。一个值再漂亮也不能弥补请求失败、地址缺失或用户目的不明。为什么操作编号也要参与搜索复盘有人会问搜索解释为什么要看operationSequence我后来发现它很实用。点击搜索前markOperation会将序号加一生成如MAP-1的lastOperationId同时记录“开始第 N 轮固定条件搜索”。状态区将最近操作与触发时间放在一起。这样页面上至少能分清眼前的搜索中提示属于哪次动作还是某个后续 Marker 或 POI 操作已经把最近操作覆盖了。这并不是在声称页面拥有生产级的异步拒绝机制。当前代码只是递增计数和展示 ID没有对返回做序号比对也没有网络时序的实测材料。它的价值更朴素为连续点击留下可读的编号别让我盯着一张静态页面把两次操作混成一次。失败成功但为空成功且有项目第 N 次点击搜索markOperation 生成 MAP-NsearchState: 第 N 轮搜索中调用结果searchState: 第 N 轮搜索失败searchErrorMessage 保留错误searchState: 调用成功但 sites 为空展示候选和 reliability人工核对名称 地址 siteId不把排名直接改写为地点事实有了这个编号我在复测时会刻意做两次连续点击。第一下只确认状态从“尚未搜索”进入“搜索中”第二下观察lastOperationId是否递增状态里有没有对应的轮次描述。若服务失败错误信息应该属于当前轮的失败状态候选区域应为空而不是显示一个来源不明的旧地点。若有真实响应才记录本轮候选与字段仍然不把首项改成自动选择。复测不是给第一名发奖状我的测试步骤现在很固定。先进入页面确认searchState是“尚未搜索”、searchErrorMessage是“暂无错误”、lastOperationId还未生成。点击一次真实搜索先抓“搜索中”的状态。此时不要提前判断地点因为还没有返回。随后按结果分流。发生错误时检查错误有没有显示列表是否清空totalCount是否明确说明未返回。服务返回但没有sites时页面应说明调用成功但列表为空不能塞一个默认地址。只有服务返回项目时才核对每个卡片的名称、地址、siteId与reliability是否来自响应首条仍只是候选首项。失败成功且为空成功且有候选否是打开页面确认尚未搜索 暂无错误 未生成 ID点击一次搜索确认 MAP 序号递增且显示搜索中后续状态检查 searchErrorMessage检查列表为空和未返回提示检查调用成功但 sites 为空检查名称 地址 siteId reliability是否有独立人工确认信息仅保留候选 不采纳地点由业务流程确认 不由排名代替这份流程故意把“有没有候选”与“是否可以采用”拆开。前者是搜索状态和返回字段的问题后者需要业务页面设计、用户意图甚至现场核对。工程师最容易犯的错是因为前者看起来已经很顺就默认后者也不需要人了。我后来给自己留的边界第一searchState不是可有可无的提示它用来限定页面当前能说的话。尚未搜索、搜索中、失败、成功空列表、成功有候选这五种状态不能混成“搜到了/没搜到”两句话。第二searchErrorMessage不该被一个友好但空泛的“请重试”盖掉。重试提示可以有原始格式化错误也要留着至少让调试时能判断是认证、网络还是服务端没有回应。没有真实响应时我只写状态和错误不写地点。第三operationSequence和lastOperationId是观察连续动作的标签不是地点确认器。它们能减少我把旧页面状态当成新动作结果的概率却不能证明某次网络请求如何先后返回。搜索分数最容易让人产生“机器已经替我判断好了”的错觉。我的这次踩坑恰好相反分数出现之后反而更应该把它放回合适的位置。它是排序线索是候选阅读的一盏小灯不是盖在地点事实上的印章。能把这句话留在代码和页面边界里后面即使接入更完整的业务确认也不会从一开始就把错误前提写死。首条摘要不是选中状态让我最警惕的一点是页面确实把首条结果写进了evidenceState.resultName。如果只看字段名很容易把它理解成“当前结果名称”再顺势把它理解成“当前选择”。但当前MapEvidenceState同时还保存 query、两个长按计数、最近事件和可选的相关性。它更像一块页面摘要并不存在selectedSiteId、confirmedAddress或userAccepted这样的确认状态。这个区别在实际开发里很重要。摘要的职责是告诉我本轮返回里第一项是什么选择的职责是说明用户或业务规则已经明确采纳哪一项。两者之间少的不是一行赋值而是一个需要看清名称、地址、ID、任务上下文的决定。若把摘要字段直接回填工单后面发生错误时连“是谁做出的选择”都说不清。interface MapEvidenceState { query: string; resultName: string; reliability?: number; markerLongClickCount: number; poiLongClickCount: number; lastEvent: string; } // 当前页面没有 selectedSiteId 或确认地点的状态。我也排除了“可以用相关性阈值代替确认”的念头。源码没有阈值常量没有根据分数筛掉候选的分支更没有把不同数值映射成可信、可用或不可用。硬加一个阈值表面上能自动化实际会让页面承担没有来源的业务判断。分数缺失时怎么办、同分时怎么办、地址不完整怎么办都会在这条捷径的尽头重新找上门。错误信息不是失败后的附属品还有一次我把“第 N 轮搜索失败”当成足够的提示准备把searchErrorMessage隐藏免得用户看到技术文本。后来发现对外可以换一种表达页面内部却不能把真实错误丢掉。因为失败不是一个统一状态认证问题、网络不可达、参数不被服务接受都可能最终落到“失败”两个字上。没有错误详情我无法判断下一步应该检查配置、连接还是请求条件。当前代码在每次搜索开始时先把错误恢复为“暂无错误”这是为了避免上一轮失败的文本挂在下一轮搜索中旁边但这不等于遗忘上一轮历史。它只是让当前页面状态不自相矛盾。若要追查多轮失败需要在测试记录中把operationSequence、轮次、时间和错误另行记下。一个实时页面不能自动成为完整日志这点我以前很容易忽略。this.searchItems []; this.totalCount 调用失败未返回; this.searchState 第 ${nextRound} 轮搜索失败; this.searchErrorMessage formatRuntimeError(error as Error);从这里也能看出失败后清空候选并不是“体验不够友好”。它防止的是上一轮成功候选继续挂在页面上让我以为它们属于本轮。尤其在连续搜索时旧列表比空列表更危险空列表会促使人检查状态旧列表则会安静地诱导人继续做地点判断。连续点击时我怎样不把两轮混为一谈我会把连续操作拆成可观察的四个问题。第一次点击后operationSequence是否增长并生成一个 MAP 编号searchState是否变为第 1 轮搜索中第二次点击后编号和轮次是否再次增长最后无论成功或失败状态描述是否仍带着相应轮次这些检查并不要求我假定请求会如何并发返回只检查页面为每次发起动作留下的标签。如果第二次点击发生在第一次结果尚未出现时当前代码并没有把不同请求的返回与 ID 绑定比较。因此我不会声称它能拒绝第一次的迟到结果。这里能做到的是在观察和记录上把“开始第几轮”明确写出来真正要做旧结果保护需要以后单独增加请求令牌和回写前比较。把这两层分开才不会把一个观察编号误宣传成并发控制。错误候选点击第 1 次搜索记录 MAP-1 与第 1 轮搜索中再次点击搜索记录新的 MAP 编号与第 2 轮搜索中页面是否有结果或错误记录该轮 searchErrorMessage记录该轮首条仅为候选不使用旧列表替代本轮结果不把首条摘要写成确认地点复测到这里我会停下来问一个很朴素的问题页面此刻显示的是“搜索能力给出的候选”还是“业务已经确认的地点”当前答案只能是前者。没有人工确认入口就不要把演示状态误读成完成动作。这个停顿能避开很多后来很难解释的自动回填。最后留给使用者的话当真实服务返回候选时最稳妥的页面语言是“第 N 轮返回若干条展示结果”而不是“已经找到地点”。当服务失败时最稳妥的语言是“调用失败错误已保留”而不是“附近没有地点”。当首项有相关性时最稳妥的语言是“可用于理解排序的线索”而不是“系统确认”。这些句子不够抢眼却能给后续人工判断留出正确的位置。我也因此更珍惜operationSequence的存在。它不是魔法字段却提醒每一个看页面的人刚才这一步是一次操作不要用它替别的操作背书。搜索状态、错误信息、编号和候选字段各自说一件事组合起来才能让人不急着把第一条当答案。这层克制也让后续改造有清晰起点先补独立确认状态再讨论自动化不能反过来。本文依据当前页面的searchState、searchErrorMessage、operationSequence与lastOperationId撰写。真实搜索结果、相关性值和地点确认仍依赖合法认证、网络以及独立的业务核对流程。