我第一次把地图文字搜索接到页面上时,做完最显眼的部分就停了:输入固定关键词,点按钮,拿到一串地点名称,按接口返回的先后渲染出来。列表出现的那一刻,我还挺满意。可同事看了一眼就问:“第一条为什么排在前面?”
我答不上来。
更准确地说,我当时并没有证据说明它为什么在前面。我只看到了一个数组,顺手把数组下标写成了#1、#2、#3。页面看起来像有排序,用户却只能猜排序规则。查询词固定为“华为”,位置固定在北京中心点,半径是 50000 米。哪怕这些条件都摆在那里,列表依然可能让人感觉像一把纸条被风吹过,落下来后恰好排成了现在的样子。
这篇只复盘一件小事:当搜索服务返回结果时,怎样把结果项里已有的相关性线索留在页面上;当服务没有返回时,又怎样老老实实停在状态和错误上。它不讨论地点是不是“正确答案”,也不讨论地图服务是否已经稳定可用。当前工程里,真实服务仍取决于认证与运行环境,不能拿一张列表的设计稿替它下结论。
现象很简单,误会也很容易发生
最初页面只展示名称和地址时,我会把第一个元素自然叫作“首条命中”。这句话听起来没有问题,实际上悄悄多走了一步:从“接口数组第一个元素”走到了“最值得信任的地点”。如果用户拿它去做门店跳转、地址回填,后半句就不该由我替服务说出口。
回到现有页面,SearchEvidenceItem明确留了rank、siteId、name、address和可选的reliability。这组字段很克制。rank只说明该项在这次展示数组中的位置;siteId让同名地点不至于混为一谈;address提供人工辨认的文字;reliability则是服务返回时可以显示的一条相关性线索。它们拼起来,才让“为什么先看到这条”有了可以继续检查的入口。
interface SearchEvidenceItem { rank: number; siteId: string; name: string; address: string; reliability?: number; } @State totalCount: string = '尚未返回'; @State searchItems: Array<SearchEvidenceItem> = [];我一开始的错误猜测是,排序看着别扭,多半是ForEach渲染时把元素弄乱了。这个怀疑并非完全荒唐,列表 key 写得不好时确实会有视觉错位。但看回代码后,展示用的是${item.rank}-${item.siteId},而rank又是在sites.forEach中按index + 1生成的。页面没有另做排序,也没有悄悄按名称、距离重新排列。它只是把服务返回的数组逐条装进searchItems,再把这个数组显示出来。
因此,真正的空缺不是“排序代码错了”,而是我没有把返回项里的reliability读出来,也没有向使用者说明它只是线索。
先把调用路径摆在桌面上
runSearch()开始时先把页面写成“第 N 轮搜索中”,再清空上一次的搜索错误。随后组装固定参数,调用site.searchByText。返回成功时,代码遍历response.sites,而不是凭空生成样本;每一项的item.reliability被原样放进SearchEvidenceItem。如果该字段没有提供,页面通过reliabilityText显示“未提供”,没有用 0、100 或所谓默认分去填空。
这张图里最容易被忽略的是totalCount。我原本只关心页面展示了几条,后来才发现这会制造另一个假象:接口报告的总数与本页展示的五条并不是同一个概念。参数固定pageIndex: 1、pageSize: 5,因此列表项数最多代表这一页被展示出来的数量;totalCount是响应中单独返回的文字化记录。两者需要并排出现,不能拿“页面只有五条”推断“地图只找到五个地方”,也不能拿一个总数去替每一项解释先后次序。
const response = await site.searchByText(getContext(this), params); const sites = response.sites ?? []; const items: Array<SearchEvidenceItem> = []; sites.forEach((item: site.Site, index: number) => { items.push({ rank: index + 1, siteId: item.siteId, name: item.name ?? '未提供名称', address: item.formatAddress ?? '未提供地址', reliability: item.reliability }); }); this.totalCount = `${response.totalCount}`; this.searchItems = items;当我把这段顺着读完,第二个猜测也被排除了:不是页面在用某个神秘规则洗牌。页面既不计算相关性,也不把它解释为距离,更不承诺数值越大就必然更适合业务。它做的事是保留服务给出的字段,并让结果列表不至于只有“第几条”这一种信息。
相关性不是装饰性的紫色文字
列表里那行reliability:${this.reliabilityText(item.reliability)},看上去只是辅助信息。可在排查时,它决定了我会不会把不确定的结果说得太满。
假如某次真实响应给出了若干项,我能记录:固定查询词是什么、当前是第几轮、页面本轮展示多少项、响应声明的totalCount是多少,以及每个地点是否携带reliability。这已经足够支持“排序有返回字段可供检查”这句话。它不支持“第一条一定离我最近”“第一条一定是用户要找的公司”“分数等于某个固定阈值就可以自动下单”。那些都需要额外业务规则、地点事实与运行材料,当前页面没有写。
反过来,若字段是undefined,reliabilityText返回“未提供”。我以前总觉得这种文案不够漂亮,想补一个 0 让卡片整齐。后来想明白了:0 是一个数值结论,未提供是信息没有到达。把后者改成前者,会让排查者误以为服务明确给了最低分,实际上代码只知道字段缺席。地图搜索里最危险的不是空白,而是把空白涂成一个很像答案的数字。
服务没有回来时,别把页面做成会讲故事的人
当前代码的失败分支很直白:searchItems = [],totalCount = '调用失败,未返回',searchState写第 N 轮搜索失败,searchErrorMessage保存格式化后的真实错误。页面还把首条名称改为“调用失败,未返回”,并把相关性设回undefined。这是一种比“给你一组演示地点”更有用的失败表达,因为它告诉我:此刻没有结果可以解释。
} catch (error) { this.searchRound = nextRound; this.searchItems = []; this.totalCount = '调用失败,未返回'; this.searchState = `第 ${nextRound} 轮搜索失败`; this.searchErrorMessage = formatRuntimeError(error as Error); this.evidenceState = { query: this.evidenceState.query, resultName: '调用失败,未返回', reliability: undefined, markerLongClickCount: this.evidenceState.markerLongClickCount, poiLongClickCount: this.evidenceState.poiLongClickCount, lastEvent: `第 ${nextRound} 轮搜索失败,已保留真实错误` }; }我会把这里的页面复测分成两段。第一段不依赖外部结果,只观察动作后的状态转换:点击后是否先出现搜索中,失败后是否清掉旧列表,错误文本有没有留在searchErrorMessage。第二段需要认证、网络和真实 Map 服务响应,才可以记录sites、totalCount与reliability的实际值。第一段通过,不能冒充第二段已经完成;第二段没有材料,也不该反过来否定失败分支的页面处理。
这个流程看起来比“有列表就截图”麻烦一点,却把问题拆开了。若searchState没进“搜索中”,先查按钮和函数;若进了搜索中又失败,查错误文本与环境;若有返回,再查字段是否被保留。每一步都有落点,至少不会因为最后空白就胡乱改样式。
复测后我留下的三条判断
第一,列表顺序本身是一个现象,不是理由。rank只记录这次展示的先后,不替服务解释业务含义。
第二,reliability有值时可以作为检查线索,未提供时就显示未提供。无论哪一种,都不能从页面字段直接推导“这个地点是真的”或“这个地点一定适合用户”。
第三,searchItems的长度和totalCount是两种不同记录。前者是当前页实际装入并展示的项目,后者是响应返回的总结果数。把它们混成一个数字,后面无论做分页还是人工核对都会很别扭。
我现在再看到“地图搜索看起来乱”的反馈,不会马上讨论算法,也不会急着给第一条加一个“推荐”角标。我会先问,页面有没有显示能解释结果的原始线索;再问,这一轮到底有没有真实响应。若答案还是没有,那就把状态和错误留在页面上,等环境补齐。地图搜索最朴素的诚实,是承认我们此刻只看到了什么,也承认还没有看到什么。
我还排除了三个看似合理的解释
第一个是“固定北京中心点,所以肯定按距离排”。页面确实构造了location: this.center,也设置了radius: 50000,但当前代码没有读取距离字段,也没有在客户端比较坐标,更没有sort。位置和半径是请求条件,不能被我偷换成已证实的排序规则。即使用户感觉某一项“应该更近”,那也只能成为进一步核对的理由,不是页面已经解释了顺序的证据。
第二个是“名称相同,第一条自然就是目标”。恰恰相反,同名是我更不敢直接采纳的原因。页面保留siteId与formatAddress,说明名称不足以独立承担辨认任务。把名称、地址和 ID 同时摆出来,目的不是把卡片做复杂,而是让人工在返回真的存在时能区分两个名称相近的候选。这里没有地址确认按钮,也没有业务表单回填,因此我不能把任何一个siteId写成最终地点。
第三个是“反正本页只展示五条,相关性不显示也无所谓”。这会让诊断失去方向。五条是pageSize: 5的展示上限,而非质量结论。没有reliability时,我只能说这一页按返回数组显示了五项;有该字段时,我可以额外记录服务给出的线索。两种情况都比给用户一个没有解释的序号更清楚,但谁也不能代替服务文档或实际运行材料解释分值算法。
这些排除有个共同点:不从请求参数、显示序号或单个字段跳到“排序已经被证明”。工程里常见的误会不是完全没有数据,而是刚有一点数据就把它解释得比它本身更多。回到代码的好处,是它会把我拉回事实边界:当前页保存了哪些字段,又明确没有保存哪些字段。
状态区为什么不能被结果卡片遮住
我还做过一次很容易误导人的操作:先尝试搜索,得到一个错误;接着改了页面样式,只留下结果卡片区域。这样一来,空列表看上去和“没有符合条件的地点”一模一样。直到我把searchState与searchErrorMessage恢复到状态区,才知道二者根本不是同一件事。
searchState负责描述这轮请求处在什么阶段,searchErrorMessage负责保留失败的原因文本。前者可能是“调用成功,但 sites 为空”,后者仍是“暂无错误”;也可能前者是“搜索失败”,后者有格式化的运行错误。若只保留一个“暂无结果”,就会把成功空结果和调用失败混在一起。排查人会去修改关键词或半径,真正的问题却可能是认证或网络。
this.searchState = `第 ${nextRound} 轮搜索中`; this.searchErrorMessage = '暂无错误'; this.markOperation(`开始第 ${nextRound} 轮固定条件搜索`); // 成功后写入返回结果;失败时清空列表并保留格式化错误。我现在复测时会先截状态区,再看结果区。因为结果区只能告诉我“现在展示了什么”,状态区才告诉我“为什么会是这个样子”。对于真实服务不可用的环境,这个顺序尤其重要:没有地点卡片不是文章的遗憾,而是页面应当留下的真实状态。
一次可复用的人工核对记录
如果以后环境拿到了真实响应,我不会只保存“首条名称”。我会在同一份记录里写下查询词、中心点、半径、语言、页码和 pageSize,再写searchRound、totalCount、本页searchItems.length。每一项则记录 rank、name、siteId、address、reliability 是否提供。这样做不是为了制造一张漂亮表格,而是避免下次有人换了参数后,仍拿旧截图讨论排序。
这里还要把时间维度放稳。当前页记录了lastTriggerTime,它可帮助定位最近一次页面动作,但并没有为每个结果项生成独立时间戳。若需要比较多轮实际响应,应在外部测试记录中按轮次保存,而不是让页面当前这一份状态替我保存历史。页面展示的是现场仪表盘,不是历史仓库;不承认这点,后面最容易把上一轮结果错挂到下一轮。
所以我的复测结论很窄:页面在成功响应时能将数组位置、总数和reliability的提供情况显示出来;响应失败时能清空项目并显示错误。至于排序是否符合某个业务预期、地点是否真实、分数具体意味着什么,必须由合法服务响应、文档和人工判断共同补齐。把结论收窄不是退缩,而是让下一次真正拿到数据时,知道应该补哪一块。
再往前走一步,我会检查同一轮的页面文案是否自洽:有项目时不应仍显示“尚未返回”,失败时不应保留旧项目,字段未提供时不应用一个数值冒充。这些不是排序算法检查,却是排序解释进入页面之前最基本的卫生条件。状态干净,后续的真实响应才有被正确阅读的可能。
还有一点很容易漏:response.sites ?? []把未提供的数组收成空数组,并不自动说明“服务找到零个地点”。只有结合searchState、错误文本和实际响应,空列表才有可解释的语境。把空数组当零命中,是我过去最轻率的一种快捷判断。
这也要求我在截图时保留状态摘要,而不只截取列表区域;否则一张空白图会把不同原因压成同一种沉默。
本文依据当前MapSearchLongClickPage中的runSearch、searchItems与totalCount状态撰写。真实搜索排序、实际相关性值与地点结果,仍需在合法认证、网络可用的运行环境中单独复核。