1. 页面概览
1.1 是什么
语义检索页面(SemanticSearchPage.vue)是 LightFoundry 提供的结构化语义检索入口:用户输入一段自然语言查询(如「营收」「订单」「金额」),后端在三个语义域——指标(metric)、对象(object)、属性(property)——中做文本匹配,按命中字段权重打分,返回相似度从高到低的结构化结果卡片。
它走的是 POST /ontology/semantic-search 契约(设计规格 design_overview §3.3 ①),该接口同时被 LightAIP OAG 与 LightSwift 风控消费,是一个跨产品复用的检索能力。与「对象查询」(/objects/:id/query 精确过滤)不同,语义检索面向「不知道确切的词、只记得大概说法」的场景——例如输入「订单的总销售额是多少?」这类中文整句,也能命中名称里包含「总销售额」的指标。
页面特点还包括:
- scopes 多选:限定只在指标/对象/属性某一类或几类里搜,不选则全部搜;
- 相似度可视化:每条结果带 score 进度条(0~100%)与命中字段标签;
- 对象级可见性:检索对象时按当前用户的
ontology:<对象名>:read权限过滤,越权对象直接不出现在结果里(fail-closed); - payload 透明:每条结果可展开 payload 明细 JSON,看到数据源 id、物理表、属性数、动作列表等上下文。
1.2 核心价值
| 维度 | 说明 |
|---|---|
| 低门槛 | 中文整句/关键词都能搜,无需知道确切 api_name |
| 多域覆盖 | 一次搜索同时命中指标、对象、属性三类语义资产 |
| 可解释 | 命中字段 + 权重分 + 进度条,清楚说明「为什么命中」 |
| 安全兜底 | 对象级可见性过滤越权结果,未授权对象不泄漏存在性 |
| 跨产品复用 | 同一接口被 AIP OAG 与 Swift 风控消费,语义一致 |
1.3 一句话总结
2. 访问入口
2.1 路由与菜单
- 路由:
/foundry/search - 路由名称:
FoundrySearch - 菜单位置:Foundry 左侧边栏「语义检索」(
FoundryLayout.vue菜单第 13 项,位于「数据质量」之后、「对象查询」之前) - 源码文件:
action/web/src/views/SemanticSearchPage.vue(402 行)
2.2 认证与权限
- 需要登录:是(路由
meta.requiresAuth = true) - 登录方式:AIP 统一登录,
localStorage.aip_token作为 Bearer Token 自动附带 - 页面本身无角色限制,但对象检索受对象级可见性过滤:后端按当前用户对
ontology:<对象名>:read的授权结果决定对象是否出现在结果中;admin 角色经AuthorizeUser内置通配自动通过,普通用户只能搜到自己有读权限的对象 - 404 排错:若访问
/foundry/search出现 404,先确认 Foundry 后端(端口 18081)已启动、前端路由已注册(router/index.js第 251~255 行)
2.3 端口与 API 前缀
- 后端端口:18081(Foundry)
- 前端 API 前缀:
/api/v1(client.js,超时 30 秒) - 注意:该接口的响应是
{code, data, meta}(data里才是{items, total}),没有统一外层再包一层,前端取res.data.data
3. 界面布局
页面纵向分为 4 个板块(从上到下):
┌──────────────────────────────────────────────────────────────┐ │ ① 页头:标题「语义检索」 │ │ ② 操作结果提示条(alert,失败红,可关闭) │ │ ③ 检索条件卡片 │ │ ├─ 查询语句 query *(输入框,回车可搜) │ │ ├─ limit(上限 50) 与 data_source 过滤(可选)两输入框 │ │ ├─ 检索范围 scopes 多选:[指标(metric)] [对象(object)] │ │ │ [属性(property)](不选=全部) │ │ └─ [检索] 按钮 │ │ ④ 检索结果卡片(共 N 条命中,展示 M 条) │ │ ├─ 诚实提示:接口无 offset/total 分页参数,仅返回前 limit 条 │ │ ├─ 渲染层分页条(命中数 > 10,最小档):第 x/y 页 + 上/下一页 │ │ │ + 每页 10/20/50 条下拉 │ │ ├─ 命中项列表:type 徽章 + name + display_name + score 进度条 │ │ ├─ 描述 + 命中字段标签 │ │ └─ payload 明细(可展开 JSON) │ └──────────────────────────────────────────────────────────────┘
各板块职责:
- ① 页头:标识本页能力。
- ② 提示条:检索失败时红条提示(如 query 为空、后端报错)。
- ③ 检索条件:所有检索参数入口。query 必填,limit 钳制 1~50,data_source 可选数字过滤,scopes 决定检索范围。
- ④ 检索结果:展示命中项、相似度、命中字段与 payload 明细;命中数超过 10(页大小最小档)时提供渲染层分页条(本地切分已返回结果);无命中时给出调整建议文案。
4. 交互元素详解
4.1 查询语句 query 输入框
| 属性 | 值 |
|---|---|
| 控件 | <input> 文本输入框,placeholder「输入要检索的内容,回车搜索(如:营收、订单、金额)」 |
| 必填 | 是(handleSearch 中 query.trim() 为空时提示「请输入查询语句」并中止) |
| 快捷操作 | @keyup.enter 直接触发检索,无需点按钮 |
| 提交处理 | 提交前 trim(),防首尾空格影响匹配 |
4.2 limit 输入框
| 属性 | 值 |
|---|---|
| 控件 | <input type=number>,min=1 max=50,绑定 v-model.number |
| 默认 | 20(空时前端回退 20) |
| 前端钳制 | Math.min(Math.max(limit || 20, 1), 50),超界自动收进 1~50 |
| 后端兜底 | 后端 Search 同样处理:<=0 取 20,>50 钳 50 |
4.3 data_source 过滤输入框
| 属性 | 值 |
|---|---|
| 控件 | <input type=number>,placeholder「数据源 id」,v-model.number |
| 可选 | 是(留空不过滤) |
| 提交处理 | 仅当 dataSource != null && dataSource !== ''(hasFilters computed)时才在请求体加 filters: { data_source: <id> } |
| 后端能力 | filters.data_source 支持数字 id 或名称;传名称时需后端注入 DataSourceNameResolver 反查(本服务已注入,按 data_sources 表解析) |
4.4 检索范围 scopes 多选
| 属性 | 值 |
|---|---|
| 控件 | 三个 checkbox(.row-check),绑定 v-model 数组 form.scopes |
| 选项 | 「指标(metric)」「对象(object)」「属性(property)」 |
| 语义 | 不选 = 全部三类都搜(后端 normalizeScopes 空数组回退全类);选中的类型才参与检索 |
| 去重 | 后端对 scope 做 strings.ToLower 去重保序,非法值忽略 |
4.5 「检索」按钮
| 属性 | 值 |
|---|---|
| 按钮文字 | 「检索」(检索中变「检索中...」并禁用) |
| 触发 | handleSearch:校验 query → 组装 {query, scopes, limit, filters?} → POST /ontology/semantic-search |
| 失败 | 红条提示「语义检索失败:<error>」,结果清空为 null |
| 成功 | result = {items, total},searched = true,渲染结果卡片 |
4.6 检索结果卡片
| 元素 | 说明 |
|---|---|
| 标题 | 「检索结果」+「共 N 条命中,展示 M 条」(total 为命中总数,items 为截断后展示数) |
| 诚实提示 | 常驻一行提示「后端接口无 offset/total 分页参数,仅返回前 limit 条(上限 50);下方分页为渲染层分页,只切分已返回结果、不减少请求数据量」 |
| 渲染层分页条 | 仅当 items.length > 10(10 为页大小最小档)时显示:第 x / y 页 + 「上一页」「下一页」(首/末页禁用)+ 每页条数下拉(10/20/50,默认 10)。页大小切到 20/50 后若命中数不足一页,分页条与页大小下拉仍保留,便于切回更小页大小;否则切大后条会整条消失、无法恢复。这是对已返回 items 的本地切片,不是服务端翻页 |
| 空态 | 「未检索到相关内容,请调整查询语句或检索范围后重试。」 |
| 未检索态 | searched 为 true 但无 result 时显示「请先输入查询语句,然后点击"检索"。」 |
4.7 渲染层分页(补)
| 项 | 说明 |
|---|---|
| 状态 | page(当前页,默认 1)、pageSize(每页条数,默认 10) |
| 显示阈值 | items.length > 10(页大小最小档):命中数超过最小一页即常驻分页条与页大小下拉,避免页大小切大后整条消失无法切回 |
| 派生 | totalPages = max(1, ceil(items.length / pageSize));pagedItems = items.slice((page-1)*pageSize, page*pageSize) |
| 复位 | watch(result) 与 watch(pageSize) 都回到第 1 页(新检索 / 改每页条数不残留页码) |
| 边界夹紧 | setPage(p) 把页码夹到 [1, totalPages] |
| 服务端口径 | 不改请求:limit 仍参与后端「score 降序截断」(后端 Search 只接受 limit,见 5.5) |
4.8 命中项卡片
| 元素 | 说明 |
|---|---|
| type 徽章 | type-metric(紫)/type-object(绿)/type-property(黄),对应 item.type |
| name | 加粗展示(api_name) |
| display_name | 右侧灰色显示,为空显示 - |
| score 进度条 | .score-bar 内 .score-fill 宽度按 scorePercent(item.score) 填充(蓝→绿渐变),右侧显示百分比整数(Math.round(score*100)+'%') |
| 描述 | item.description 存在时显示「描述:...」,title 悬浮全文 |
| 命中字段 | matched_fields 逐个渲染为 dim-tag 蓝色小标签,如 metric_name/object_category/property_synonyms |
| payload 明细 | <details> 折叠「payload 明细」,展开显示 JSON.stringify(payload, null, 2),超长内容 260px 滚动 |
5. 后端关联
5.1 API 客户端
client.js:baseURL: /api/v1,超时 30 秒;请求拦截器附Authorization: Bearer <aip_token>;响应拦截器 401 清理并跳/login(登录页内不重复跳)。- 页面直接
apiClient.post('/ontology/semantic-search', body),无独立 API 文件。
5.2 端点表
| 方法 | 路径 | 请求体 | 超时 |
|---|---|---|---|
| POST | /ontology/semantic-search | {query, scopes, limit, filters?} | 30 秒 |
请求示例:
{
"query": "订单的总销售额是多少?",
"scopes": ["metric", "object"],
"limit": 20,
"filters": { "data_source": 1 }
}
5.3 响应结构
响应为 {code, data, meta}(无统一外层二次包装,前端取 res.data.data):
{
"code": 0,
"data": {
"items": [
{
"type": "object",
"id": "o_orders",
"name": "orders",
"display_name": "订单",
"description": "订单主表",
"matched_fields": ["object_display_name", "object_name"],
"score": 0.9,
"payload": {
"category": "交易",
"data_source_id": 1,
"base_table": "order",
"object_type_id": 3,
"property_count": 12,
"link_count": 4,
"action_count": 2,
"actions": ["create_order", "cancel_order"]
}
}
],
"total": 5
},
"meta": { "trace_id": "t_<uuid>" }
}
错误响应:{code: "VALIDATION_ERROR", error: "query 不能为空"} 等,HTTP 状态取 ierr 映射。
5.4 关联模块表
| 后端包 | 职责 |
|---|---|
foundry/semantic/search.go | SemanticSearchService:检索主流程、按 scope 分发、打分与过滤、对象级可见性回调 |
foundry/server/semantic_handlers.go | handleSemanticSearch:注入当前用户(WithSearchUser + username 兜底)后调用服务 |
foundry/server/server.go | 组装:注入 ObjectTypeProvider、MetricSearchProvider、DataSourceNameResolver、WithObjectAuthorizer(RBAC 对象级闸门) |
foundry/metric | GetSearchMetrics 适配提供指标检索条目 |
foundry/ontology | OntologyService.ListObjectTypes/GetObjectType 提供对象/属性数据 |
5.5 关键机制
打分规则(score = 命中字段最高权重,保留两位小数):
| 域 | 字段权重 |
|---|---|
| object | name 1.0 / display_name 0.9 / category 0.7 / description 0.3 |
| property | name 1.0 / display_name 0.9 / synonyms 0.7 |
| metric | name 1.0 / display_name 0.9 / synonyms 0.7 |
匹配算法 matchField:双向子串匹配(统一小写)——方向① query 包含字段值(中文整句命中关键),方向② 字段值包含 query 分词(英文原名命中);字段值按逗号切词逐项参与(synonyms 兼容);matched_fields 收集所有命中的字段名(排序去重)。
排序与截断:score 降序;同分按 type/name 稳定排序保证确定性;total = 命中总数(截断前的 len(items),search.go:249),超出 limit 时 items 截断为前 limit 条(search.go:250-252)。接口无 offset/page/page_size 参数,无法翻页取更多——limit 上限 50(search.go:219-226),前端渲染层分页只切分这 ≤50 条(见 4.7)。
对象级可见性(8-4 语义反转):searchObjects/searchProperties 对每个对象先做 objectVisible 判定——已注入 WithObjectAuthorizer 时调用 AuthorizeUser(ctx, uid, uname, "ontology:<name>:read"),不可见即跳过(fail-closed,不泄漏存在性),不再依赖 req.ObjectLevelVisible(search.go:20-22 文件头注释、:332/:424 内联注释、:56 ObjectLevelVisible 字段说明:「标志保留仅为兼容旧调用方(不再影响过滤行为)」)。仅当请求显式传 ObjectLevelVisible=true 且未注入授权回调(s.authorize == nil)时,payload 才带 object_visible: "needs_authorization" 标记(search.go:383-384/462-463);本产品 server 组装时总是注入 WithObjectAuthorizer(见 5.4),故该分支在本页不会命中。
可选向量增强:WithVectorSearch 注入时,向量命中的对象加入候选池并叠加 0.2 * 向量相似度,matched_fields 追加 vector;本产品默认不接向量后端。
6. 核心流程详解
6.1 主流程:执行一次语义检索
- 在 query 输入框输入查询语句(回车或点「检索」);
handleSearch校验 query 非空 → 组装{query, scopes, limit, filters?};POST /ontology/semantic-search→ 后端Search:空 query 直接报query 不能为空;- 后端按 scope 分发:
searchObjects/searchProperties/searchMetrics各拉候选,逐条做matchField匹配打分; - 未命中任何字段的对象/属性/指标被丢弃;命中项合并后 score 降序、limit 截断;
- 前端
scorePercent渲染进度条,展示 total 与 items 数量。
6.2 对象级可见性过滤时序
handleSemanticSearch把当前用户注入 ctx(WithSearchUser(currentUserID(c))),并附带 username 兜底(跨产品 JWT 时 user_id 不在本库,按用户名反查);searchObjects/searchProperties遍历每个对象:先passDataSourceFilter(若配了 data_source 过滤)→ 再objectVisible;- 已注入授权回调:
AuthorizeUser(uid, uname, "ontology:<name>:read")返回 false → 对象从结果中移除(8-4 语义反转后不再依赖object_level_visible,服务端默认强制过滤); - admin 角色经
AuthorizeUser内置通配自动通过;无用户(空 uid)按无权限处理(fail-closed)。
object_level_visible 请求字段仍存在于契约(search.go:62),但 search.go:56 明确其「保留仅为兼容旧调用方(不再影响过滤行为)」——服务端注入授权回调后默认强制对象级过滤。因此本页不暴露该开关(暴露会产生「可关闭过滤」的误导),前端请求体亦不携带。该字段属兼容保留/已语义反转,不是 UI 缺口(详见第 9 章)。6.3 分支流程:data_source 过滤
hasFilters 为 true 时请求体带 filters.data_source。后端 passDataSourceFilter:数字/数字字符串与对象 data_source_id 比较;字符串且非数字时经 DataSourceNameResolver 反查名称后 EqualFold 比较;未配置该 filter 恒通过。filter 传了但类型不支持时按不过滤处理(返回 false → 不命中)。
7. 权限与安全
- 认证:axios 拦截器附带 Bearer Token;401 统一清理并跳登录。
- 对象级可见性:检索对象/属性时按
ontology:<对象名>:read逐对象授权(AuthorizeUser内置 admin 通配)。越权对象不出现在结果中——不返回、不计数,避免存在性泄漏。 - 数据源过滤:
data_source过滤只影响命中集合,不能用来探测未授权数据源内容。 - 防注入:query 纯文本匹配,不拼接 SQL,无注入面。
8. 常见问题与排错
8.1 点「检索」提示「请输入查询语句」
- 现象:红条提示「请输入查询语句」,没有发起请求。
- 原因:
query.trim()为空(全空格或空输入)。这是前端校验,后端同样会以query 不能为空拒绝。 - 排查步骤:① 在输入框输入至少一个非空格字符;② 直接回车确认 keyup.enter 生效;③ 若输入了中文却仍报错,检查是否误输入了全角空格。
8.2 检索失败提示「语义检索失败:...」
- 现象:红条提示语义检索失败 + 后端 error 文案。
- 原因:请求被后端拒绝或超时。常见:后端未启动、token 过期(401 会先跳登录)、后端数据源/对象列表读取异常。
- 排查步骤:① 确认 18081 后端存活;② Network 面板看
POST /ontology/semantic-search状态码与响应{error};③ 若是 5xx,检查后端日志(常见于对象/指标元数据读取失败);④ 偶发超时(>30s)可缩小 scopes 或 limit 重试。
8.3 结果里「对象」比预期少或缺失
- 现象:搜某对象名,结果只有指标/属性或完全没有该对象。
- 原因:① 对象被对象级可见性过滤(当前用户无
ontology:<对象名>:read权限);② 对象处于deprecated状态(StatusDeprecated被跳过);③ query 与对象名称/描述字段双向子串均不命中。 - 排查步骤:① 确认当前登录用户对目标对象的读权限(用 admin 账号对比);② 检查对象状态是否为 deprecated;③ 用更短的关键词或对象 display_name 再搜;④ 打开某条结果的 payload 看
object_visible是否带needs_authorization标记。
8.4 score 显示 0% 但该条仍出现
- 现象:某条结果分数为 0% 或极低。
- 原因:
matched_fields命中但分数被round2保留两位后接近 0(如仅命中 description 权重 0.3,显示 30%);0% 通常意味着 score 为 0(后端可能叠加了向量分但该条无文本命中)。 - 排查步骤:① 展开该条看
matched_fields与 payload;② 若matched_fields含vector,说明是向量增强命中;③ 确认不是前端scorePercent对null的兜底(score == null显示 0%)。
8.5 limit 设置了 50 结果却只有几条
- 现象:limit 填 50,结果卡片显示「共 5 条命中」。
- 原因:
total是命中总数,可能本身就只有 5 条;或对象级可见性过滤掉了其余对象(total 只在过滤之后计数)。 - 排查步骤:① 看标题「共 N 条命中,展示 M 条」中 N 是否为 5;② 若 N 远大于 M 检查前端是否截断;③ 缩小 scopes 逐个域验证命中分布。
9. 已知缺陷与边界
| 项 | 说明 |
|---|---|
| object_level_visible 不暴露(设计如此,非缺口) | 该字段在后端已语义反转:search.go:56 注明「ObjectLevelVisible 标志保留仅为兼容旧调用方(不再影响过滤行为)」,服务端注入授权回调后默认强制对象级过滤(search.go:332/:424)。因此本页不提供该开关——暴露「可关闭对象级过滤」会产生误导;前端请求体不携带该字段亦不影响过滤语义 |
| 无向量后端 | 本产品默认不注入 WithVectorSearch,纯文本匹配,无语义向量增强 |
| 过滤后 total 口径 | total 为过滤+去重后的命中总数,越权对象不计入 |
| 中文匹配依赖子串 | 中文同义词需在 synonyms 里显式声明,否则整句命中依赖「query 包含字段值」方向 |
| 无服务端分页(已补渲染层分页) | 后端 Search 只认 limit(上限 50,search.go:219-226),无 offset/page/page_size,total 为截断前命中总数(:249),items 截断为前 limit 条(:250-252)。前端已补渲染层分页(10/20/50 条/页 + 上/下一页,见 4.7)——只切分这 ≤50 条已返回结果,不减少请求数据量、不能取更多命中;真正翻页需后端补 offset/分页契约(属后端契约缺口) |
| 大数据量性能 | object 域需遍历全部对象(properties 域还逐个拉 detail),对象很多时响应变慢 |
后端文件
action/products/foundry/semantic/search.go(SemanticSearchService 与打分/过滤逻辑)action/products/foundry/server/semantic_handlers.go(handleSemanticSearch)action/products/foundry/server/server.go(对象级可见性回调组装,第 553~584 行)
项目文档
action/wiki/upgrade-v5/dev-story/stage-1.md(数据域规格)action/wiki/changelog/2026-08-29-2-v5-stage1.md(Stage 1 交付说明)action/wiki/frontend-intro-v5/markdown/foundry/index.md(Foundry 页面清单)