31 Mar, 2026

1 commit


30 Mar, 2026

2 commits

  • tangwang
     
  • …nt.py)、[search/searcher.py](/data/saas-search/search/searcher.py)、[frontend/static/js/app.js](/data/saas-search/frontend/static/js/app.js)
    以及
    [tests/test_rerank_client.py](/data/saas-search/tests/test_rerank_client.py)。
    
    主要修复内容如下:
    - 精排现依据融合阶段得分进行排序,而非仅依据原始的 `fine_score`。
    - 最终重排不再依赖独立的 `fine_scores`
      数组(该数组在精排排序后可能产生同步偏差),而是直接读取命中结果附带的
    `_fine_score`。
    -
    精排与最终重排现均通过同一计算路径生成融合调试信息,该路径同时也决定实际排序结果,从而保证记录逻辑与生产逻辑保持一致。
    -
    调试信息载荷更加清晰:精排和最终重排阶段都会暴露融合输入/因子以及规范的
    `fusion_summary`,前端界面现在会渲染该摘要信息。
    
    主要问题:阶段逻辑重复且存在并行的数据通道:一个通道用于计算排序,另一个通道用于组装调试字段,还有第三个通道用于传递辅助数组。这造成了潜在的差异风险。本次重构通过将阶段得分作为唯一事实来源,并让调试/前端直接消费其输出而非事后重构,降低了该风险。
    
    验证结果:
    - `./.venv/bin/python -m pytest -q tests/test_rerank_client.py
      tests/test_search_rerank_window.py`
    - `./.venv/bin/python -m py_compile search/rerank_client.py
      search/searcher.py`
    
    结果:`22 passed`。
    
    当前的主流程:
    
    1. Query 解析
    2. ES 召回
    3. 粗排:只用 ES 内部文本/KNN 信号
    4. 款式 SKU 选择 + title suffix
    5. 精排:轻量 reranker + 文本/KNN 融合
    6. 最终 rerank:重 reranker + fine score + 文本/KNN 融合
    7. 分页、补全字段、格式化返回
    
    主控代码在 [searcher.py](/data/saas-search/search/searcher.py),打分与
    rerank 细节在
    [rerank_client.py](/data/saas-search/search/rerank_client.py),配置定义在
    [schema.py](/data/saas-search/config/schema.py) 和
    [config.yaml](/data/saas-search/config/config.yaml)。
    
    **先看入口怎么决定走哪条路**
    在 [searcher.py:348](/data/saas-search/search/searcher.py#L348)
    开始,`search()` 先读租户语言、开关、窗口大小。
    关键判断在 [searcher.py:364](/data/saas-search/search/searcher.py#L364)
    到 [searcher.py:372](/data/saas-search/search/searcher.py#L372):
    
    - `rerank_window` 现在是 80,见
      [config.yaml:256](/data/saas-search/config/config.yaml#L256)
    - `coarse_rank.input_window` 是 700,`output_window` 是 240,见
      [config.yaml:231](/data/saas-search/config/config.yaml#L231)
    - `fine_rank.input_window` 是 240,`output_window` 是 80,见
      [config.yaml:245](/data/saas-search/config/config.yaml#L245)
    
    所以如果请求满足 `from_ + size <= rerank_window`,就进入完整漏斗:
    - ES 实际取前 `700`
    - 粗排后留 `240`
    - 精排后留 `80`
    - 最终 rerank 也只处理这 `80`
    - 最后再做分页切片
    
    如果请求页超出 80,就不走后面的多阶段漏斗,直接按 ES 原逻辑返回。
    tangwang
     

27 Mar, 2026

2 commits


26 Mar, 2026

2 commits


24 Mar, 2026

5 commits

  • tangwang
     
  • 上面一版实现,性能上完全无法接受。因此进行了一轮策略简化
    
    style_sku_prepare_hits阶段耗时太长。请根据需求,思考优化的方法,给出性能优化的方案。
    1.
    _select_by_embedding,有缓存吗,option_value的值是有限的,之前已经算过的,就不用再算了。不仅仅是embedding相似的结果,整个option_value的匹配结果,是有包含、还是没包含,相似度多少,都不用重新计算。比如之前已经有一个sku的某个属性值叫做“卡其色”,已经算出来是否文本匹配了,那么不需要再去做文本匹配。如果已经算出来向量的相似度,那么不需要再去取向量以及计算相似度。
    2. 匹配可以适当的优化:
    匹配流程简化:
    1)找到第一个文本匹配的,如果有直接匹配成功。不需要考虑匹配多个的情况。
    2)如果全部都没有匹配,那么进行embedding筛选。
    
    匹配规则:
    option_name的匹配,直接看规范化后的option_name是不是意图维度的泛化词之一(比如颜色、color、colour),如果没有匹配的,现在应该是把所有维度都算上,这样匹配成本和比较成本太高了,去掉这些逻辑,这种情况不需要加后缀、不需要选择sku。
    ption_value的匹配。意图检测的时候,有匹配的query中的命中的词,这个词被包含在属性值中,那么就算匹配。属性值被包含在query(包括翻译文本)中,也算匹配。提高匹配的覆盖率。
    
    3.
    这一阶段得到sku选择的结果即可(选中的sku的id,也可以为空值表示没找到匹配成功的,这种情况不需要拼接title后缀给重排输入),但是不用着急做image_url的替换和sku的置顶。等最后填充的时候判断有选中sku的时候直接做替换和置顶即可。
    请你思考如何进行设计,提高性能的时候不带来复杂度的提升,可以适当的重构以降低修改后的代码行数。
    @search/sku_intent_selector.py @query/style_intent.py
    tangwang
     
  • tangwang
     
  • The backend now exposes a structured debug_info that is much closer to
    the real ranking pipeline:
    
    query_analysis now includes index_languages, query_tokens, query-vector
    summary, translation/enrichment plan, and translation debug.
    query_build now explains the ES recall plan: base-language clause,
    translated clauses, filters vs post-filters, KNN settings,
    function-score config, and related inputs.
    es_request distinguishes the logical DSL from the actual body sent to
    ES, including rerank prefetch _source.
    es_response now includes the initial ES ranking window stats used for
    score interpretation.
    rerank now includes execution state, templates, rendered rerank query
    text, window/top_n, service/meta, and the fusion formula.
    pagination now shows rerank-window fetch vs requested page plus
    page-fill details.
    For each result in debug_info.per_result, ranking debug is now much
    richer:
    
    initial rank and final rank
    raw ES score
    es_score_normalized = raw score / initial ES window max
    es_score_norm = min-max normalization over the initial ES window
    explicit normalization notes explaining that fusion does not directly
    consume an ES-normalized score
    rerank input details: doc template, title suffix, template field values,
    doc preview/length
    fusion breakdown: rerank_factor, text_factor, knn_factor, constants, raw
    inputs, final fused score
    text subcomponents: source/translation/weighted/primary/support/fallback
    evidence via matched_queries
    richer style-intent SKU debug, including selected SKU summary and intent
    texts
    tangwang
     
  • 2, 漏了一些重要的stage,比如「款式意图 SKU
    预筛选(StyleSkuSelector.prepare_hits)」,补上这个stage
    tangwang
     

20 Mar, 2026

3 commits

  • tangwang
     
  • tangwang
     
  • ResultFormatter.format_search_results() runs.
    
    What changed:
    
    For each final paginated SPU hit, the searcher now scans
    skus[].option1_value against the query text set built from the original
    query, normalized query, rewritten query, and translations.
    If no option1_value matches textually, it falls back to embedding
    similarity and picks the SKU with the highest inner product against the
    query embedding.
    The matched SKU is promoted to the front of the SPU’s skus list.
    The SPU-level image_url is replaced with that matched SKU’s image_src.
    I left api/result_formatter.py unchanged because it already preserves
    the SKU order and reads image_url from _source; updating the page hits
    in searcher makes the formatter return the desired result automatically.
    
    Verification:
    
    ReadLints on the edited files: no errors
    Passed targeted tests:
    pytest tests/test_search_rerank_window.py -k "translated_query or
    no_direct_option_match"
    tangwang
     

16 Mar, 2026

1 commit


13 Mar, 2026

3 commits


12 Mar, 2026

1 commit


11 Mar, 2026

2 commits

  • - 前端 JS 不再写死后端地址:默认 API_BASE_URL 为空串,所有搜索与 suggest 请求改为同源路径 (/search/*),仅在显式注入 window.API_BASE_URL 时才覆盖,避免 .env 中旧的 http://43.166.252.75:6002 等配置污染浏览器请求。
    - 在 scripts/frontend_server.py 上实现轻量级反向代理:拦截 /search/、/admin/、/indexer/ 的 GET/POST/OPTIONS 请求,服务端将请求转发到本机 6002 (BACKEND_PROXY_URL,默认 http://127.0.0.1:6002),并把响应原样返回前端。
    - 通过“浏览器 → web服务器:6003(认证) → GPU:6003(本项目前端) → GPU 本机:6002(后端)”这条链路,彻底绕开 web 服务器 6002 上单独的 Basic Auth,解决了外网访问时前端能打开但搜索请求被 web:6002 拦截的问题。
    - frontend_server 默认不再注入 window.API_BASE_URL,只有在设置 FRONTEND_INJECT_API_BASE_URL=1 且 API_BASE_URL 有值时才向 HTML 注入脚本,确保默认行为始终是同源调用,由 6003 统一代理后端。
    - 更新 frontend/index.html 中的静态 JS 版本号(tenant_facets_config.js 和 app.js),强制浏览器拉取最新脚本,避免旧版前端继续使用硬编码的后端地址。
    
    Made-with: Cursor
    tangwang
     
  • Made-with: Cursor
    tangwang
     

10 Mar, 2026

1 commit


06 Mar, 2026

2 commits


02 Mar, 2026

1 commit

  • - 新增 suggestion 模块(mapping/builder/service),支持按租户构建 `search_suggestions_tenant_{tenant_id}` 索引
    - 新增 `main.py build-suggestions` CLI 与 `scripts/build_suggestions.sh`,支持基于商品 title/qanchors 与近 365 天搜索日志的全量构建
    - 实现 `/search/suggestions` 接口(多语言 + 结果直达),并接入前端自动补全使用新的后端 API
    - 为 suggestion 增加 `README` / `RUNBOOK` / `TROUBLESHOOTING` 文档,更新搜索 API 对接指南与速查表
    - 补充 `tests/test_suggestions.py` 单元测试,覆盖语言解析和 SuggestionService 查询流程
    
    Made-with: Cursor
    tangwang
     

04 Feb, 2026

1 commit



24 Jan, 2026

1 commit


27 Dec, 2025

1 commit


25 Dec, 2025

1 commit


20 Dec, 2025

1 commit


16 Dec, 2025

2 commits



08 Dec, 2025

1 commit


04 Dec, 2025

1 commit


03 Dec, 2025

2 commits

  • {
      "facets": [
        {
          "field": "category1_name",
          "size": 15,
          "type": "terms"
        },
        "specifications.color",
        "specifications.size"
      ]
    }
    
    {
      "facets": [
        {"field": "category1_name", "size": 15, "type": "terms"},
        {"field": "specifications.color", "size": 10, "type": "terms"},
        {"field": "specifications.size", "size": 10, "type": "terms"}
      ]
    }
    
    之前是上面的接口形式,主要是考虑 属性的分面, 因为 款式都是有限的 不需要设定 "size": 10, "type": "terms" 这些参数。
    
    但是从接口设计层面,最好按下面这样,这样的话 specifications.color 和 category1_name 的组装格式 完全一样。前端不需要感知 属性分面 和 类别等其他字段分面的差异。
    tangwang
     
  • tangwang
     

02 Dec, 2025

1 commit

  • 后端请求模型变更(api/models.py)
    SearchRequest.sku_filter_dimension 从 Optional[str] 改为 Optional[List[str]]。
    语义:列表表示一个或多个“维度标签”,例如:
    单维度:["color"]、["option1"]
    多维度:["color", "size"]、["option1", "option2"]
    描述更新为:对 维度组合进行分组,每个组合只保留一个 SKU。
    结果格式化与去重逻辑(api/result_formatter.py)
    ResultFormatter.format_search_results(..., sku_filter_dimension: Optional[List[str]] = None),调用处已同步更新。
    单维度旧逻辑升级为多维度逻辑:
    新方法:_filter_skus_by_dimensions(skus, dimensions, option1_name, option2_name, option3_name, specifications)。
    维度解析规则(按顺序处理,并去重):
    若维度是 option1 / option2 / option3 → 对应 option1_value / option2_value / option3_value。
    否则,将维度字符串转小写后,分别与 option1_name / option2_name / option3_name 对比,相等则映射到对应的 option*_value。
    未能映射到任何字段的维度会被忽略。
    对每个 SKU:
    按解析出的字段列表(例如 ["option1_value", "option2_value"])取值,组成 key,如 ("red", "L");None 用空串 ""。
    按 key 分组,每个 key 只保留遇到的第一个 SKU。
    若列表为空或所有维度都无法解析,则 不做过滤,返回原始 skus。
    Searcher 参数类型同步(search/searcher.py)
    Searcher.search(...) 中 sku_filter_dimension 参数类型从 Optional[str] 改为 Optional[List[str]]。
    传给 ResultFormatter.format_search_results 时,直接传该列表。
    前端参数格式调整(frontend/static/js/app.js)
    输入框 #skuFilterDimension 依旧是一个文本框,但解析方式改为:
    函数 getSkuFilterDimension():
    读取文本,如:"color" 或 "color,size" 或 "option1, color"。
    用逗号 , 拆分,trim() 后过滤空串,返回 字符串数组,例如:
    "color" → ["color"]
    "color,size" → ["color", "size"]
    若最终数组为空,则返回 null。
    搜索请求体中仍使用字段名 sku_filter_dimension,但现在值是 string[] 或 null:
        body: JSON.stringify({      // ...      sku_filter_dimension: skuFilterDimension,  // 例如 ["color", "size"]      debug: state.debug    })
    文档更新(docs/搜索API对接指南.md)
    请求体示例中的类型由:
    "sku_filter_dimension": "string"
    改为:
    "sku_filter_dimension": ["string"]
    参数表中:
    从 string 改为 array[string],说明为“维度列表,按组合分组,每个组合保留一个 SKU”。
    功能说明章节“SKU筛选维度 (sku_filter_dimension)”已调整为 列表语义 + 组合去重,并补充了示例:
    单维度:
          {        "query": "芭比娃娃",        "sku_filter_dimension": ["color"]      }
    多维度组合:
          {        "query": "芭比娃娃",        "sku_filter_dimension": ["color", "size"]      }
    使用方式总结
    单维度去重(保持旧行为的等价写法)
    旧:"sku_filter_dimension": "color"
    新:"sku_filter_dimension": ["color"]
    多维度组合去重(你新提的需求)
    例如希望“每个 SPU 下,同一颜色+尺码组合只保留一个 SKU”:
        {      "query": "芭比娃娃",      "sku_filter_dimension": ["color", "size"]    }
    tangwang