diff --git a/docs/image-dress1.png b/docs/image-dress1.png deleted file mode 100644 index 7b786de..0000000 Binary files a/docs/image-dress1.png and /dev/null differ diff --git a/docs/image-dress2.png b/docs/image-dress2.png deleted file mode 100644 index deaf810..0000000 Binary files a/docs/image-dress2.png and /dev/null differ diff --git a/docs/image-shoes1.png b/docs/image-shoes1.png deleted file mode 100644 index c2b67ef..0000000 Binary files a/docs/image-shoes1.png and /dev/null differ diff --git a/docs/image-spu-specifications.png b/docs/image-spu-specifications.png new file mode 100644 index 0000000..7b786de Binary files /dev/null and b/docs/image-spu-specifications.png differ diff --git a/docs/issue-2026-03-21-ES能力提升.md b/docs/issue-2026-03-21-ES能力提升.md deleted file mode 100644 index 1250b29..0000000 --- a/docs/issue-2026-03-21-ES能力提升.md +++ /dev/null @@ -1,70 +0,0 @@ -ES 付费版本 or 定制开发(建议先看下付费版本价格) -ES定制开发: -RRF / retrievers - -Elastic 的订阅矩阵里明确列了这些相关能力:Retrievers: linear, rule, RRF, text similarity re-ranker,以及 Reciprocal Rank Fusion (RRF) for hybrid search。 - -这类能力最有价值的点是: -它们把混合检索从“自己拼 DSL 和手搓打分”变成了官方支持的多阶段检索框架。重排:text similarity re-ranker / Elastic Rerank. text_similarity_reranker 用 NLP 模型对 top-k 结果按语义相似度重新排序;它可以用内置的 Elastic Rerank,也可以接 Cohere、Vertex AI,或者你自己上传的 text similarity 模型。 - -{ - "retriever": { - "rrf": { - "retrievers": [ - { "standard": { "query": { ... } } }, - { "knn": { ... } } - ] - } - } -} - - -加reranker: -text_similarity_reranker 用 NLP 模型对 top-k 结果按语义相似度重新排序;它可以用内置的 Elastic Rerank,也可以接 Cohere、Vertex AI,或者你自己上传的 text similarity 模型。 - -{ - "retriever": { - "text_similarity_reranker": { - "retriever": { - "rrf": { ... } - }, - ... - } - } -} - -{ - "retriever": { - "text_similarity_reranker": { - "retriever": { - "rrf": { - "retrievers": [ - { - "standard": { - "query": { - "...": "..." - } - } - }, - { - "knn": { - "...": "..." - } - } - ], - "rank_window_size": 100, - "rank_constant": 20 - } - }, - "field": "your_rerank_text_field", - "inference_text": "白色 oversized T-shirt", - "inference_id": ".rerank-v1-elasticsearch", - "rank_window_size": 50 - } - }, - "size": 20 -} - - - - diff --git a/docs/issue-2026-03-21-意图判断-done03-24.md b/docs/issue-2026-03-21-意图判断-done03-24.md deleted file mode 100644 index 91e82f5..0000000 --- a/docs/issue-2026-03-21-意图判断-done03-24.md +++ /dev/null @@ -1,268 +0,0 @@ - - -增加款式意图识别模块。意图类型: 颜色,尺码(目前只需要支持这两种) - -一、 意图判断 -- 意图召回层: -每种意图,有一个召回词集合 -对query(包括原始query、各种翻译query 都做匹配) -- 以颜色意图为例: -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 -query匹配了其中任何一个词,都认为,具有颜色意图 -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 - -二、 意图使用: - 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 -流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter - 要改为: - 1. 有款式意图的时候,才做sku筛选 - 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选 - 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 - 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用: - 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url - 2. 用来做rerank doc的title补充,从而参与rerank - 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) - -- sku筛选的规则也要优化: -现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。 -改为: - 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 - 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 - 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 - 这个sku筛选也需要提取为一个独立的模块。 - -细节备注: -intent 考虑由 QueryParser 编排、具体实现拆成独立模块,主义好,现有的分词等基础设施的复用,缺失的英文分词可以补充。 -在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name - -先仔细理解需求,查看代码,深度思考应该如何设计,和当前的系统较好的融合,给出统一的设计,可以根据需要适当改造当前的实现,降低整个系统的复杂度,提高模块化程度,而不是打补丁。修改后的最终状态应该是要足够简单、清晰、无冗余和分叉,模块间低耦合。多步思考确认最佳施工方案之后才进行代码修改。 - -5. TODO: 搜索接口里,results[].skus 不是全量子 SKU:由 sku_filter_dimension 控制在应用层按维度分组折叠,每个「维度取值组合」只保留一条 SKU(组内第一条)。请求未传该字段时,Pydantic 默认是 ["option1"],等价于只按 option1_value 去重;服务端不会读取店铺主题的「主展示维」,需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3(或对应 option*_name)时,若仍用默认只按 option1(常见为颜色)折叠,同一颜色下多种款式只会出现一条代表 SKU,无法从返回的 skus 里拿到该颜色下的全部款式行。(若业务需要全量子款,需传包含对应维度的 sku_filter_dimension,或传 null/[] 跳过折叠——以当前 ResultFormatter 实现为准。) - - - - - -GPT5.4 主要思路: -我准备按这条线改,尽量把逻辑收束成两个新模块而不是继续堆在 Searcher 里: - -QueryParser 负责产出统一的“款式意图分析结果”,包含:命中的意图类型、标准化值、命中的 query 变体、分词结果、维度候选词。 -独立的 SKU 选择模块负责:维度解析、候选值抽取、三阶段匹配、选中 SKU 后的主图替换,以及生成 rerank 用的属性后缀。 -Searcher 只编排:解析 query -> 首次 ES 召回 -> 若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本 -> run_rerank -> 分页 -> page fill -> ResultFormatter。这样旧的页内 SKU 置顶逻辑可以下沉或删除,避免前后两个阶段重复做同一件事。我再补看一下你最近写的意图草稿文档,确认有没有现成词表示例或边界条件,避免我这边把词表格式和你预期做偏。 - - - - - -意图匹配的性能优化: -上面一版实现,性能上完全无法接受。因此进行了一轮策略简化 - -prompt -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 - - -修改结果: -核心变化在 `search/sku_intent_selector.py` 和 `search/searcher.py`。 - -现在的策略是: -- `option_name` 只做规范化后 alias 精确匹配;任何意图维度没 resolve 到字段,就直接不选 SKU、不加 suffix。 -- `option_value` 文本匹配改成顺序扫描 SKU,找到第一个命中的就直接返回,不再收集多个候选再比较。 -- 文本匹配规则支持两种: - - query 命中的意图词被属性值包含 - - 属性值被 query/translation 包含 -- 只有当所有 SKU 都没有文本命中时,才做 embedding 选择。 -- `prepare_hits()` 现在只产出决策和 `_style_rerank_suffix`,不再提前改 `_source`;真正的 `image_url` 替换和 SKU 置顶统一后移到 `apply_precomputed_decisions()`。 -- 加了请求级缓存,避免同一个 `option_value`/`selection_text` 在一次请求里重复做文本判断、取向量和算相似度。 -- 顺手删掉了旧的 generalized match / fallback_text 路径,代码比之前更短也更直。 - -另外补了回归测试,覆盖了: -- 翻译文本命中后正确选 SKU -- 多个文本命中时取第一个 -- `option_name` 不命中 alias 时不做 SKU 选择 -- 无文本命中时走 embedding fallback - -验证过: -- `pytest tests/test_search_rerank_window.py -q` 通过 -- 变更文件 lint 无报错 - - ------------------------------- - - - ---- - -## 1. 现状(与需求的差距) - -**流水线**(`search/searcher.py`)大致是: - -1. `QueryParser.parse` → `ParsedQuery`(含 `translations`、`query_tokens` 等) -2. 组 ES 查询;若在重排窗口内,第一次查询把 `_source` 裁成「重排模板所需字段」(`_resolve_rerank_source_filter`) -3. ES 搜索 → `run_rerank`(`rerank_client.build_docs_from_hits` 用 `{title}` 等拼 doc) -4. 按 `from/size` 切片 → **page fill** 用 `ids` 查询把当前页 `_source` 补全 -5. `_apply_sku_sorting_for_page_hits`(仅 **option1**,先子串包含命中第一个,否则全量 option1 embedding) -6. `ResultFormatter`(`sku_filter_dimension` 只做**展示层**按维度折叠 SKU,与置顶逻辑独立) - - -**与需求冲突但必须一起解决的一点**:page fill 会用 ES 拉回来的 `_source`**整份覆盖**当前 hit(约 841–842 行)。若在 rerank **之前**只改内存里的 `skus` 顺序/`image_url`,**不**在 fill 后再处理一次,最终响应会被覆盖掉。因此「rerank 前对所有 window 内 hit 做 SKU 决策」和「用户看到的最终列表」之间,必须有一条**明确的数据契约**(见下文 §4)。 - ---- - -## 2. 模块划分(建议:`intent` + `sku_intent` 两层) - -避免继续在 `Searcher` 里堆方法,建议新建小包,职责清晰、由 `Searcher` 编排。 - -| 模块 | 职责 | -|------|------| -| **`query/intent/`**(或 `search/intent/`,二选一以「离谁更近」为准;更推荐 **`query/intent`**,因为输入完全是 query 侧事实) | 加载词表、**意图召回**、多 query 变体 + 粗细分词、输出结构化 **`IntentProfile`** | -| **`search/sku_intent/`**(或 `intent/sku_selection.py`) | 根据 `IntentProfile` 解析 **option1/2/3** 哪一维、生成每 SKU 的**匹配文本**、三轮匹配规则、embedding 批处理、对 `_source` 做 **promote + image_url** | -| **`search/rerank_client.py`(薄扩展)** | 支持「每条 hit 的 doc 文本」:模板扩展或 **显式传入 per-hit 字符串列表**,避免把业务塞进 format 字符串 | - -**`IntentProfile`(概念模型)建议包含**: - -- `active_intents: Set[Literal["color","size"]]`(可扩展) -- 每种意图:`canonical_terms`(命中行的标准词)、`matched_surface_forms`(可选,用于 debug) -- **维度别名**:如 color → `{"color","颜色","colour",...}`(配置或独立小词表) -- 原始用于匹配的 token 集合:每个 query 变体 ×(细粒度 | 粗粒度),便于日志与单测 - -**词表**: - -- **意图召回表**:每行逗号分隔同义词,首词标准化;颜色、尺码各一份(路径放 `config/` 或 `resources/intent/` + `config.yaml` 指路径)。 -- **SKU 第二轮「泛化」表**(对 **option 取值** 做同义扩展):与意图召回表分开,避免语义混在一起。 - ---- - -## 3. 意图判断(与 `QueryParser` 的衔接) - -需求:对 **原始 query + 各类翻译** 都做匹配;**细粒度 + 粗粒度** 分词。 - -现状: - -- `ParsedQuery` 里 **`query_tokens` 只对 rewritten 后的 `query_text` 跑了一次 HanLP**(`query_parser.py` 269–274 行附近),**没有**对 `original_query`、各 `translations` 的 token 缓存。 -- 已有 **`simple_tokenize_query`**(粗粒度)在 `query_parser.py`。 - -**建议**: - -- 在 **`IntentDetector.detect(parsed_query, tokenizer_fn)`** 内统一生成「query 变体列表」:至少包含 `original_query`、`query_normalized`、`rewritten_query`、`translations` 的值(与当前 `_build_sku_query_texts` 思路一致,但升级为**结构化**)。 -- 细粒度:复用 `QueryParser._get_query_tokens`(需把该方法暴露为公开 API 或注入同一 HanLP callable),对每个变体字符串调用。 -- 粗粒度:对每个变体调用 `simple_tokenize_query`。 -- 匹配逻辑:**任意变体 × 任意粒度** 的 token 落在「标准化 → 同义词闭包」上即视为命中该意图(与你描述的行内同义一致)。 - -**可选优化**:在 `parse()` 里顺带产出 `intent_profile`,减少一次遍历;但为控制 `QueryParser` 体积,更稳妥的是 **parse 之后**单独调 `IntentDetector`,依赖清晰。 - ---- - -## 4. 流水线改造(与 page fill 的契约) - -目标顺序变为: - -`ES(window)`(有意图时 `_source` 含 `skus` + `option*_name`) -→ **对每个 hit:SKU 决策 + 生成 rerank 用后缀/全文** -→ `run_rerank`(doc = 标题 + 后缀) -→ 切片 -→ page fill -→ **最终响应前再应用一次 SKU 决策(或与 prefetch 结果合并)** -→ `ResultFormatter` - -**为何最后还要一次?** 因为 page fill 会覆盖 `_source`,rerank 前内存里的 `skus` 顺序不能当作最终真相。 - -**推荐契约(降低复杂度)**: - -1. **Rerank 前**:对 window 内每个 hit 计算 `SkuIntentDecision`(至少包含:`option_slot` 1/2/3、`candidate_sku_index` 或 `sku_id`、`rerank_suffix` 字符串)。可挂在 hit 的**非 ES 字段**上,例如 `hit["_intent_sku"] = {...}`(或只存 `rerank_doc_text` 全文)。 -2. **`run_rerank`**:`build_docs_from_hits` 若发现 hit 上已有 `rerank_doc_text`(或 `style_suffix` + 模板),则优先使用,否则走原模板。 -3. **Page fill 之后**:对**当前页** hit 再调用**同一** `SkuIntentSelector.apply(source, parsed_query, intent_profile)`(或根据 `_id` 合并 prefetch 决策)。这样最终 `image_url` / SKU 顺序与 rerank 一致,且不被 fill 冲掉。 - -若担心算两次 embedding:**第一次**在 window 全量上算 query 向量 + option 向量;第二次仅对当前页且可带缓存(按 `embed_key` 去重),一般量很小。 - -**不在重排窗口内**:没有「rerank 前全 window」这一步;可在 **ResultFormatter 前**对当前页 `es_hits` 用同一 `SkuIntentSelector`(仅当有意图时),与「有意图才做 SKU 筛选」一致。 - ---- - -## 5. `_resolve_rerank_source_filter` 与 ES 字段 - -需求:有意图时预取需包含 `skus`、`option1_name`、`option2_name`、`option3_name`。 - -建议签名扩展为: - -`_resolve_rerank_source_filter(doc_template, intent_profile: Optional[IntentProfile])` - -- 若 `intent_profile` 非空且含 color/size(或任意「款式意图」),在 `includes` 中**合并**上述字段(并与模板解析出的 `title` 等取并集)。 -- 注意与全局 `source_fields` 的 tri-state 语义(`_apply_source_filter`)是否冲突:若租户配置 `_source` 白名单且不含 `skus`,需定义优先级——**建议**:「款式意图所需字段」作为**最低保证**合并进本次请求的 fetch includes,或在文档中写明限制。 - ---- - -## 6. 多维度 option 与「未匹配维度名」 - -需求逻辑可落到纯函数: - -1. 对每个意图类型,有 **维度别名集合**(如 color)。 -2. 依次与 `option1_name`、`option2_name`、`option3_name`(字符串,注意多语言:与 indexer 一致,可能是纯英文或中文)做 **casefold / 规范化** 后匹配别名表。 -3. 命中则该 SKU 行的匹配字段为 `option{k}_value`;用于 embedding key 时继续用 `name:value` 形式(沿用现有 `_sku_option1_embedding_key` 思路,泛化为 `option_slot`)。 -4. **若三个 name 都不匹配意图维度**:用 `option1_value`、`option2_value`、`option3_value` **空格拼接**成一条「兜底描述字符串」,供: - - 与 query 的包含/泛化/embedding 比较; - - 作为 `rerank_suffix` 的一部分(若你希望无明确维度时仍加强 rerank)。 - -**多意图同时存在**(如同时颜色+尺码):需要在产品层定规则,例如: - -- 只对「主意图」排序(配置优先级 color > size),或 -- 要求两个维度都满足的 SKU 优先,否则退化为单意图。 - -实现上可在 `SkuIntentSelector` 输入 `List[IntentType]` 与策略枚举,避免写死 if-else 散落。 - ---- - -## 7. 三轮 SKU 匹配规则(独立模块内) - -从当前「第一个包含就返回」改为: - -1. **第一轮**:统计「option 匹配文本被 **整条 query 文本** **包含**」的 SKU(或对每个 query 变体分别计,再合并——建议与你现有 `_build_sku_query_texts` 对齐);**若恰好 1 个** → 选中。 -2. **第二轮**:若 0 个,对每个 SKU 的候选词走 **取值泛化表**(同义词行),再跑包含判断;仍统计「多个 / 零个」。 -3. **第三轮**: - - 若 **多个** 满足包含(第一轮或第二轮)→ 仅在这多个上算 embedding,取相似度最高; - - 若 **仍 0 个** → 对 **全部** SKU 算 embedding,取最高。 - -实现上保持 **批量 encode**(与当前 `option1_values_to_encode` 去重逻辑类似),只是把「embed_key」从固定 option1 改为按 slot 动态生成。 - ---- - -## 8. `sku_filter_dimension`(API)与意图的关系 - -- **`sku_filter_dimension`**:客户端指定「结果里 SKU 列表如何按维度折叠」,在 `ResultFormatter._filter_skus_by_dimensions` 中实现。 -- **意图 SKU 置顶**:服务端根据 query 推断维度与取值,改顺序与主图。 - -建议约定: - -- **置顶 / 换图**仅在意图开启时执行; -- **`sku_filter_dimension` 仍只影响返回 SKU 条数结构**;若与意图维度冲突(例如意图命中 color,客户端只按 size 折叠),应用**文档说明优先级**:常见做法是 **先意图置顶,再 filter**(或相反,需在 PRD 写清)。 - -避免在 `ResultFormatter` 里再猜意图;意图结论由上游传入或在 Formatter 前已完成 `_source` 调整。 - ---- - -## 9. 配置与观测 - -- `config.yaml`:`intent.enabled`、`intent.lexicon_paths`、`intent.dimension_aliases`(或按类型分块)。 -- `RequestContext` / `debug`:写入 `intent_profile`、`sku_intent_decision`、rerank 用的 doc 摘要,便于与 `docs/TODO-意图判断.md` 对齐。 - ---- - -## 10. 小结 - -- **核心架构**:**`IntentDetector`(query 侧)** + **`SkuIntentSelector`(search 侧)** + **`run_rerank` 的 per-hit doc 覆盖** + **`_resolve_rerank_source_filter` 条件 includes**。 -- **必须处理 page fill 覆盖 `_source`**:rerank 前决策与 **fill 后再 apply 一次**(或等价合并策略),否则会出现「重排用了带后缀的 doc、返回结果却是未置顶 SKU」的不一致。 -- **与现有系统融合点**:`ParsedQuery` 变体列表、HanLP + `simple_tokenize_query`、`TextEmbeddingEncoder`、`ResultFormatter` / `sku_filter_dimension` 的边界清晰,避免把意图逻辑复制到 `api/` 层。 - -若你后续希望把「多意图优先级」或「rerank 后缀格式」定成唯一产品规则,可以在实现前写进同一份 spec,模块接口会很好稳定下来。 \ No newline at end of file diff --git a/docs/issue-2026-03-26-ES文本搜索-补充多模态knn放入should-done-0327.md b/docs/issue-2026-03-26-ES文本搜索-补充多模态knn放入should-done-0327.md deleted file mode 100644 index 2e138fc..0000000 --- a/docs/issue-2026-03-26-ES文本搜索-补充多模态knn放入should-done-0327.md +++ /dev/null @@ -1,72 +0,0 @@ -目前knn跟query里面是并列的层级,如下: -{ - "size": 400, - "from": 0, - "query": { - "bool": { - "must": [... - ], - } - }, - "knn": { - "field": "title_embedding", - "query_vector": [...], - "k": 120, - "num_candidates": 400, - "boost": 2, - "_name": "knn_query" - }, -其中query的结构是这样的: -"query": { - "bool": { - "should": [ - { - "bool": { - "_name": "base_query", -\# 原始query - } - }, - { - "bool": { - "_name": "base_query_trans_zh", -\# 翻译query。有可能是base_query_trans_en,也有可能两者都有 - "boost": 0.75 - } - } - ], - "minimum_should_match": 1 - } - }, -我想把knn放到should里面,和base_query、base_query_trans_zh并列。 -另外,现在过滤是在knn里面单独加了一遍: - "knn": { - "field": "title_embedding", - "query_vector": [...], - "k": 120, - "num_candidates": 400, - "boost": 2, - "_name": "knn_query", - "filter": { - "range": { - "min_price": { - "gte": 100, - "lt": 200 - } - } - } - } -现在不需要了。因为knn在query的内层了。共用过滤。 - -另外: -我需要再增加一个knn。 -需要参考文本embedding获得的逻辑, -通过 -curl -X POST "http://localhost:6008/embed/clip_text?normalize=true&priority=1" \ - -H "Content-Type: application/json" \ - -d '["纯棉短袖", "street tee"]' -(用 POST /embed/clip_text 生成多模态文本向量。和文本embedding获取方法类似。注意思考代码如何精简,不要冗余。) -得到文本的多模态embedding。 -然后在这里补充一个多模态embedding,寻找图片相似的结果,对应的商品图片字段为image_embedding.vector。 -重排融合:之前有knn的配置bias和exponential。现在,文本和图片的embedding相似需要融合,融合方式是dis_max,因此需要配置: -1)各自的权重和tie_breaker -2)整个向量方面的权重(bias和exponential) \ No newline at end of file diff --git a/docs/issue-2026-03-27-keywords限定-done-0327.txt b/docs/issue-2026-03-27-keywords限定-done-0327.txt deleted file mode 100644 index a68186e..0000000 --- a/docs/issue-2026-03-27-keywords限定-done-0327.txt +++ /dev/null @@ -1,93 +0,0 @@ -@query/query_parser.py @scripts/es_debug_search.py -原始query、以及每一个翻译,都要有一个对应的keywords_query(token分词后,得到名词) -参考这段代码,获取每一个长度大于 1 的名词,然后用空格拼接起来,作为keywords_query -import hanlp -from typing import List, Tuple, Dict, Any - -class KeywordExtractor: - """ - 基于 HanLP 的名词关键词提取器 - """ - def __init__(self): - # 加载带位置信息的分词模型(细粒度) - self.tok = hanlp.load(hanlp.pretrained.tok.CTB9_TOK_ELECTRA_BASE_CRF) - self.tok.config.output_spans = True # 启用位置输出 - - # 加载词性标注模型 - self.pos_tag = hanlp.load(hanlp.pretrained.pos.CTB9_POS_ELECTRA_SMALL) - - def extract_keywords(self, query: str) -> str: - """ - 从查询中提取关键词(名词,长度 ≥ 2) - - Args: - query: 输入文本 - - Returns: - 拼接后的关键词字符串,非连续词之间自动插入空格 - """ - query = query.strip() - # 分词结果带位置:[[word, start, end], ...] - tok_result_with_position = self.tok(query) - tok_result = [x[0] for x in tok_result_with_position] - - # 词性标注 - pos_tag_result = list(zip(tok_result, self.pos_tag(tok_result))) - - # 需要忽略的词 - ignore_keywords = ['玩具'] - - keywords = [] - last_end_pos = 0 - - for (word, postag), (_, start_pos, end_pos) in zip(pos_tag_result, tok_result_with_position): - if len(word) >= 2 and postag.startswith('N'): - if word in ignore_keywords: - continue - # 如果当前词与上一个词在原文中不连续,插入空格 - if start_pos != last_end_pos and keywords: - keywords.append(" ") - keywords.append(word) - last_end_pos = end_pos - # 可选:打印调试信息 - # print(f'分词: {word} | 词性: {postag} | 起始: {start_pos} | 结束: {end_pos}') - - return "".join(keywords).strip() - - -最后,在组织检索表达式时,目前是每一个 query (base_query base_query_trans_en base_query_trans_zh 三种情况)。 会组成一个bool查询,以base_query为例: - "bool": { - "should": [ - { - "bool": { - "_name": "base_query", - "must": [ - { - "combined_fields": { -... - } - } - ], - "should": [ - { - "multi_match": { -... "type": "best_fields", -... - }, - { - "multi_match": { -... - "type": "phrase", -... - } - } - ] - } - }, - -base_query_trans_en base_query_trans_zh 也是同样 - -在这个布尔查询的must里面加一项:keywords,搜索的字段和combined_fields一样,命中比例要求50% - - -结合现有代码做出合理的设计,呈现简单清晰的数据接口,而不是打补丁 \ No newline at end of file diff --git a/docs/issue-2026-03-27-添加粗排精排-第2轮-done-0327.txt b/docs/issue-2026-03-27-添加粗排精排-第2轮-done-0327.txt deleted file mode 100644 index 5e8c57f..0000000 --- a/docs/issue-2026-03-27-添加粗排精排-第2轮-done-0327.txt +++ /dev/null @@ -1,38 +0,0 @@ -这是上一轮检索效果优化的需求: -参考 -searcher.py -rerank_client.py -schema.py -es_query_builder.py -config.yaml -相关性检索优化说明.md - -在ES返回到rerank期间增加一轮粗排+一轮精排。 -1. ES召回,600 -2. 粗排:600->240。配置文件增加粗排相关配置,包括输入条数(配置为700,ES拉取的条数改为粗排输入条数),然后增加粗排的融合公式配置,参考现有的reranker融合公式即可、只是去掉其中的重排模型项。 -3. 现在的sku选择、为reranker生成title后缀这一套逻辑,是放在粗排后,因为精排也是一个reranker模型(只不过是一个轻量级的,bge-reranker),需要用这个title后缀。 -4. 精排:240-80,使用bge-reranker,但是,因为reranker只能选一个backend,考虑如何重构。现在,精排也是一个独立的进程、独立提供端口,服务。但是,因为跟重排逻辑是一致的(即使有部分不一致也应该分离不一致的点进行配置化),所以共用代码,只是根据需要启动两个实例,避免代码冗余。 -5. 重排:80,也是用当前的重排代码,调用单独的实例(即现在使用的实例),返回后,经过融合公式,到分页,也参考现在的融合公式,但是,加入一项精排模型打分。 -测试时,使用跟我同样的环境./scripts/service_ctl.sh reranker backend 重启相关服务进行测试 - -你已经完成了一般修改,已把三段排序链路接上了,主改动在 search/searcher.py、search/rerank_client.py、config/schema.py、config/loader.py、config/services_config.py 和 config/config.yaml。 - -现在链路是: -ES 拉取 coarse_rank.input_window 条 -> 粗排按 text/knn 融合裁到 coarse_rank.output_window -> 再做 SKU 选择和 title suffix -> 精排调用轻量 reranker 裁到 fine_rank.output_window -> 最终重排调用现有 reranker,并在最终融合里加入 fine_score。同时把 reranker client/provider 改成了按 service_profile 选不同 service_url,这样 fine/final 可以共用同一套服务代码,只起不同实例。 - -你用了./.venv/bin/pytest -q tests/test_rerank_client.py tests/test_search_rerank_window.py 进行验证,结果是 18 passed。也按要求尝试了 ./scripts/service_ctl.sh restart reranker backend 做联调,但当前环境里只有 6007 的现有 reranker,配置里的 fine 端口 6009 还没对应实例,而且服务重启后进程状态不太稳定,没能完成完整线上链路验证。当前最关键的落地前置是把轻量 bge 实例补起来,再用同样脚本重启联调。 - -因此,需要回顾这个漏斗的代码,进行进一步的优化: -1. Fine Rank环节的信息都是N/A。fine 实例也需要在config.yaml中体现,并且需要被server_ctl.sh管理,参考现在是怎么管理reranker的。fine和reranker分别调用哪个模型(两者代码功能,但是只是启动不同的实例),请你深度思考后决定。这里是需要参考当前代码、根据实际需要(原来只有一个reranker、现在多了一个精排,也要调用一个重排模型,因此带来的必要的部分重构。 -2. Ranking Funnel、Fusion Factors、Signal Breakdown -这些是不是整合起来、按漏斗收集、整理信息、以及进行呈现比较好。 -ES 召回的环节,展示Matched Queries各项打分、ES的总分、norm后打分、排序位置,等等关键信息 -粗排:粗排融合公式的各项输入、重要中间结果和参数、最后得分,排序位置以及上升/下降了多少。等等关键信息。 -精排:同样例举关键的输入、中间过程、输出、排序和位置变化等。 -reranker:类似 - -因为涉及的环节较多,非常要注意的一个点是:不要每次修改都在原来的基础上,为实现目标而打补丁,应该观察一下所涉及的代码现在是怎么做的,务必注意如何适当的清理掉现有逻辑,该如何对其进行修改,来达到目的,以达到代码的精简,避免冗余、分叉。 -特别是1关于将现在的单独的reranker服务,在不增加代码复杂度的情况下,如何可以通过配置启动多个实例、各自可以配置选择的模型等相关参数、分别服务于精排和重排,是设计的重中之重。这里需要思考配置如何拆分,需要多读相关代码、思考最佳实践。 - - - diff --git a/docs/issue-2026-03-27-添加粗排精排-第3轮-done-0327.txt b/docs/issue-2026-03-27-添加粗排精排-第3轮-done-0327.txt deleted file mode 100644 index 46505c2..0000000 --- a/docs/issue-2026-03-27-添加粗排精排-第3轮-done-0327.txt +++ /dev/null @@ -1,98 +0,0 @@ -这是上一轮检索效果优化的需求: -参考 -searcher.py -rerank_client.py -schema.py -es_query_builder.py -config.yaml -相关性检索优化说明.md - -在ES返回到rerank期间增加一轮粗排+一轮精排。 -1. ES召回,600 -2. 粗排:600->240。配置文件增加粗排相关配置,包括输入条数(配置为700,ES拉取的条数改为粗排输入条数),然后增加粗排的融合公式配置,参考现有的reranker融合公式即可、只是去掉其中的重排模型项。 -3. 现在的sku选择、为reranker生成title后缀这一套逻辑,是放在粗排后,因为精排也是一个reranker模型(只不过是一个轻量级的,bge-reranker),需要用这个title后缀。 -4. 精排:240-80,使用bge-reranker,但是,因为reranker只能选一个backend,考虑如何重构。现在,精排也是一个独立的进程、独立提供端口,服务。但是,因为跟重排逻辑是一致的(即使有部分不一致也应该分离不一致的点进行配置化),所以共用代码,只是根据需要启动两个实例,避免代码冗余。 -5. 重排:80,也是用当前的重排代码,调用单独的实例(即现在使用的实例),返回后,经过融合公式,到分页,也参考现在的融合公式,但是,加入一项精排模型打分。 -测试时,使用跟我同样的环境./scripts/service_ctl.sh reranker backend 重启相关服务进行测试 - -你已经完成了一般修改,已把三段排序链路接上了,主改动在 search/searcher.py、search/rerank_client.py、config/schema.py、config/loader.py、config/services_config.py 和 config/config.yaml。 - -现在链路是: -ES 拉取 coarse_rank.input_window 条 -> 粗排按 text/knn 融合裁到 coarse_rank.output_window -> 再做 SKU 选择和 title suffix -> 精排调用轻量 reranker 裁到 fine_rank.output_window -> 最终重排调用现有 reranker,并在最终融合里加入 fine_score。同时把 reranker client/provider 改成了按 service_profile 选不同 service_url,这样 fine/final 可以共用同一套服务代码,只起不同实例。 - -并且,你对调试展示进行了重构。你已经把结果卡片和全局调试面板都改成按漏斗阶段取值和展示,在 app.js 里把 ES 召回、粗排、精排、最终 rerank 分开渲染了。 -现在每条结果的 debug 会按阶段展示: -ES 召回:rank、ES score、norm score、matched queries。 -粗排:rank/rank_change、coarse_score、text/knn 输入、text_source/text_translation/text_primary/text_support、text_knn/image_knn、factor。 -精排:rank/rank_change、fine_score、fine input。 -最终 rerank:rank/rank_change、rerank_score、text/knn score、各 factor、fused_score,以及完整 signals。 - -请你仔细阅读漏斗环节的这些代码,特别是关于打分、重排序、debug信息记录方面的。 - - -现在,请注意,需要优化的是: -1. Fine Rank环节似乎没有进行融合公式的计算、继而进行重排序,请修复。 -2.从软件工程的视角review代码: -因为增加了多重排序漏斗,数据的记录、传递,交互的接口,是否设计足够合理,存在哪些问题。 -请从软件工程的角度审视这些逻辑,是否有需要梳理、清理和重写的地方。 -3. Fine Rank和Final Rerank环节信息记录优化: -这两个环节都要体现融合公式的输入、关键因子、以及融合公式的得分。为了避免代码膨胀,Fine Rank和Final Rerank -都可以采用一个字符串记录这些关键信息,字符串内包括融合公式各项的名称和具体数值,以及最终结果。你也可以继续沿用当前的记录方式,需要你对比一下哪种代码量更少、更清晰简洁。 -也要仔细思考当前的代码,真实的计算和信息的记录,是否存在分离的情况,是否存在冗余和分叉。这种情况是不允许的,存在隐藏的风险,以后改了正式逻辑而没有改调试信息,将导致不一致。 -务必注意,当前已经有相关的信息记录逻辑,注意不要叠补丁,可以适当修改、或者清理重写,而不是新增,要使得代码更简洁和干净,并保证信息记录与真实逻辑一致。 - - -涉及代码较多,请耐心阅读,以上都是一些需要深度思考的任务,慢慢来,留足够多的时间来review和重新设计。 - - - - - - -因为增加了两个环节,多了很多变量。 -以这些为效果评估的测试集,调试参数。这次的调整范围是,融合公式中的各个 -falda negra oficina -red fitted tee -黒いミディ丈スカート -黑色中长半身裙 -чёрное летнее платье -修身牛仔裤 -date night dress -vacation outfit dress -minimalist top - -仔细思考这些漏斗中重要的信息如何呈现。对应的修改前端代码。 -注意包括整体漏斗信息的呈现,以及每条结构中独自的信息。 -我需要这些信息,辅助各环节融合公式的调参,根据我的需求,深度思考该如何设计,要呈现哪些信息,如何呈现。 -可以对现有的逻辑做适当的重构,重新整理。 - - - - -fine 实例也需要在config.yaml中体现,并且需要被server_ctl.sh管理,参考现在是怎么管理reranker的。fine和reranker分别调用哪个模型(两者代码功能,但是只是启动不同的实例),请你深度思考后决定。这里是需要参考当前代码、根据实际需要(原来只有一个reranker、现在多了一个精排,也要调用一个重排模型,因此带来的必要的部分重构。 - -1. Fine Rank环节的信息都是N/A,是没有配置吗。fine rank是使用bge-reranker,复用当前reranker模型的代码,但是需要单独起一个服务、单独加载一个模型。 -2. Ranking Funnel、Fusion Factors、Signal Breakdown -这些是不是整合起来、按漏斗收集、整理信息、以及进行呈现比较好。 -ES 召回的环节,展示Matched Queries各项打分、ES的总分、norm后打分、排序位置,等等关键信息 -粗排:粗排融合公式的各项输入、重要中间结果和参数、最后得分,排序位置以及上升/下降了多少。等等关键信息。 -精排:同样例举关键的输入、中间过程、输出、排序和位置变化等。 -reranker:类似 - -因为涉及的环节较多,非常要注意的一个点是:不要每次修改都在原来的基础上,为实现目标而打补丁,应该观察一下所涉及的代码现在是怎么做的,务必注意如何适当的清理掉现有逻辑,该如何对其进行修改,来达到目的,以达到代码的精简,避免冗余、分叉。 - - - - - -1. Fine Rank 这个环节没有体现融合公式的输入、关键因子、以及融合公式的得分。为了避免代码膨胀,Fine Rank和Final Rerank -都可以采用一个字符串记录这些关键信息,字符串内包括融合公式各项的名称和具体数值,以及最终结果。 - - - - - - - - - diff --git a/docs/issue-2026-03-27-添加粗排精排-第一轮-done-0327.txt b/docs/issue-2026-03-27-添加粗排精排-第一轮-done-0327.txt deleted file mode 100644 index 030d63c..0000000 --- a/docs/issue-2026-03-27-添加粗排精排-第一轮-done-0327.txt +++ /dev/null @@ -1,136 +0,0 @@ -我: -进行一轮检索效果优化: -参考 -[searcher.py](search/searcher.py) -[rerank_client.py](search/rerank_client.py) -[schema.py](config/schema.py) -[es_query_builder.py](search/es_query_builder.py) -[config.yaml](config/config.yaml) -[相关性检索优化说明.md](docs/相关性检索优化说明.md) - -在ES返回到rerank期间增加一轮粗排+一轮精排。 -1. ES召回,600 -2. 粗排:600->240。配置文件增加粗排相关配置,包括输入条数(配置为700,ES拉取的条数改为粗排输入条数),然后增加粗排的融合公式配置,参考现有的reranker融合公式即可、只是去掉其中的重排模型项。 -3. 现在的sku选择、为reranker生成title后缀这一套逻辑,是放在粗排后,因为精排也是一个reranker模型(只不过是一个轻量级的,bge-reranker),需要用这个title后缀。 -4. 精排:240-80,使用bge-reranker,但是,因为reranker只能选一个backend,考虑如何重构。现在,精排也是一个独立的进程、独立提供端口,服务。但是,因为跟重排逻辑是一致的(即使有部分不一致也应该分离不一致的点进行配置化),所以共用代码,只是根据需要启动两个实例,避免代码冗余。 -5. 重排:80,也是用当前的重排代码,调用单独的实例(即现在使用的实例),返回后,经过融合公式,到分页,也参考现在的融合公式,但是,加入一项精排模型打分。 -测试时,使用跟我同样的环境./scripts/service_ctl.sh reranker backend 重启相关服务进行测试 - - - - - -codex: -前端这轮我把调试展示重构成了“两层漏斗”: - -1. 整体漏斗 -在 [frontend/static/js/app.js](/data/saas-search/frontend/static/js/app.js) 里把原来散着的 `Query Analysis / Rerank / Stage Timings / ES Query` 重新组织成了: -- `Query Context` -- `Ranking Funnel` -- `Timing Breakdown` -- `Raw Payloads` - -其中 `Ranking Funnel` 现在按阶段卡片展示: -- `ES Recall` -- `Coarse Rank` -- `Fine Rank` -- `Final Rerank` -- `Page Return` - -每张卡都尽量只放调参最关键的信息:输入/输出条数、模板、service URL、top_n、fusion/meta。这样你看漏斗收缩、看每段服务表现、看融合参数会更直接。 - -2. 单条结果漏斗 -在结果卡片右侧的 debug 区,我改成了真正的单条漏斗卡片,按“分数结果”和“公式分解”分开看: -- 顶部先看 `Initial Rank / Final Rank / Rank Delta` -- 再看阶段分数:`ES / ES Norm / Coarse / Fine / Rerank / Fused` -- 再看融合因子:`coarse_text_factor / coarse_knn_factor / text_factor / knn_factor / fine_factor / rerank_factor` -- 再看信号拆解:`text_source / text_translation / text_primary / text_support / text_knn / image_knn` - -同时保留了可展开的: -- `Rerank Input` -- `Selected SKU` -- `Matched Queries` -- `Multilingual Fields` - -为了支撑前端,我顺手在 [search/searcher.py](/data/saas-search/search/searcher.py) 里补了单条 `coarse_score / coarse_text_factor / coarse_knn_factor` 到 `per_result`,这样单条卡片现在能把粗排也完整展示出来。 - -样式上在 [frontend/static/css/style.css](/data/saas-search/frontend/static/css/style.css) 加了阶段卡片、分数 pill、时延条形图和移动端适配。JS 语法我用 `node --check frontend/static/js/app.js` 过了,后端补充字段也用 `./.venv/bin/python -m py_compile search/searcher.py` 过了。 - -如果你愿意,我下一步可以继续把这个视图再往“调参工作台”方向推一层,比如再加: -- 漏斗阶段间的 drop rate -- 每条结果的 rank jump 高亮 -- 批量对比 top N 结果里某个因子分布 -- fusion 参数和结果联动的可视化摘要 - - - - - - - - -我: -1. Fine Rank环节的信息都是N/A,是没有配置吗。fine rank是使用bge-reranker,复用当前reranker模型的代码,但是需要单独起一个服务、单独加载一个模型。 -2. Ranking Funnel、Fusion Factors、Signal Breakdown -这些是不是整合起来、按漏斗收集、整理信息、以及进行呈现比较好。 -ES 召回的环节,展示Matched Queries各项打分、ES的总分、norm后打分、排序位置,等等关键信息 -粗排:粗排融合公式的各项输入、重要中间结果和参数、最后得分,排序位置以及上升/下降了多少。等等关键信息。 -精排:同样例举关键的输入、中间过程、输出、排序和位置变化等。 -reranker:类似 - -因为涉及的环节较多,非常要注意的一个点是:不要每次修改都在原来的基础上,为实现目标而打补丁,应该观察一下所涉及的代码现在是怎么做的,务必注意如何适当的清理掉现有逻辑,该如何对其进行修改,来达到目的,以达到代码的精简,避免冗余、分叉。 - - - - - - -codex: -`Fine Rank` 之前显示 `N/A`,根因不是前端单纯没写,而是后端原来只有“精排成功拿到分数”时才会填那块 debug;一旦 `fine` 服务没起、没配 `service_url`、调用失败,前端只能看到空值。现在我把这层补齐了: - -- 顶层 `debug_info.fine_rank` 现在会始终带 `status / service_profile / service_url / input_window / output_window / query_template / doc_template` -- 失败时会带 `error` -- 成功时再补 `docs / top_n / meta` -- 所以你现在能区分: - - `disabled` - - `pending` - - `ok` - - `failed_no_scores` - - `failed` - -也就是说,`fine rank` 本身仍然需要你按设计单独起一个 `bge-reranker` 实例,这一层代码已经按“独立服务 + 共用 reranker 代码”的方式接好了,但服务本身还是要单独启动。 - -这轮我也把调试展示做了结构化重构,不再继续在旧视图上打补丁。现在是按阶段组织信息: - -- 整体漏斗: - - `ES Recall` - - `Coarse Rank` - - `Fine Rank` - - `Final Rerank` - - `Page Return` -- 单条结果: - - 用统一的 `ranking_funnel` 结构来表达每个阶段的 - - `rank` - - `delta` - - `score` - - 关键中间量 - - 关键 factor - - 输入 payload - -具体改动在: -- [search/searcher.py](/data/saas-search/search/searcher.py) -- [frontend/static/js/app.js](/data/saas-search/frontend/static/js/app.js) -- [frontend/static/css/style.css](/data/saas-search/frontend/static/css/style.css) - -单条结果现在按 stage card 展示,而不是把 `Ranking Funnel / Fusion Factors / Signal Breakdown` 拆成三块平铺,这样你可以顺着 ES -> 粗排 -> 精排 -> rerank 看每段的输入、过程、输出和位次变化,更适合调融合公式。 - -检查我也跑过了: -- `node --check frontend/static/js/app.js` -- `./.venv/bin/python -m py_compile search/searcher.py` -- `./.venv/bin/pytest -q tests/test_search_rerank_window.py` - -都通过了。 - -下一步如果你要,我建议直接继续做两件事中的一个: -1. 把 `fine` 实例真正拉起来,然后我再把前端里 `status=failed/pending` 的场景跑成真实数据。 -2. 在当前漏斗视图上继续加“阶段 rank 分布 / delta 高亮 / topN 因子对比”,把它做成更像调参工作台。 \ No newline at end of file diff --git a/docs/issue-2026-03-28-添加粗排精排-第4轮-done-0328.txt b/docs/issue-2026-03-28-添加粗排精排-第4轮-done-0328.txt deleted file mode 100644 index c710ebf..0000000 --- a/docs/issue-2026-03-28-添加粗排精排-第4轮-done-0328.txt +++ /dev/null @@ -1,314 +0,0 @@ -这是上一轮检索质量优化的需求说明: - -参考文件: -`searcher.py` -`rerank_client.py` -`schema.py` -`es_query_builder.py` -`config.yaml` -`相关性检索优化说明.md` - -在ES返回结果与重排之间增加一个粗排阶段和一个精排阶段。 - -1. ES召回:600条 -2. 粗排阶段:600 -> 240条。 - 在配置文件中增加粗排相关配置,包括输入大小(设置为700,即从ES获取的数量应改为粗排的输入大小)。 - 然后增加粗排的融合公式配置。可参考现有的重排融合公式,但需要去掉重排模型那一项。 -3. 目前重排的SKU选择和标题后缀生成逻辑,应放在粗排之后,因为精排也是一个重排模型(轻量级模型,`bge-reranker`),它也需要这个标题后缀。 -4. 精排阶段:240 -> 80条。 - 使用`bge-reranker`,但由于目前重排只能选择一个后端,请考虑如何重构。 - 现在精排也应该是一个独立的进程和服务,拥有自己的端口。 - 但由于其逻辑与重排逻辑一致(即便存在差异,这些差异也应抽离并配置化),代码应该共享。按需启动两个实例即可,避免代码重复。 -5. 最终重排:80条。 - 仍使用当前重排代码,调用独立的实例(即当前在用的那个)。 - 返回后,应用融合公式,再进行分页。 - 这里也应参考当前的融合公式,但需增加一项:精排模型得分。 - -测试时,请使用与我相同的环境,并使用以下命令重启相关服务: -`./scripts/service_ctl.sh reranker backend` - -你已经完成了整体修改,并将三级排序流水线串联起来了。 -主要改动在: -`search/searcher.py` -`search/rerank_client.py` -`config/schema.py` -`config/loader.py` -`config/services_config.py` -以及 `config/config.yaml`。 - -现在的流程是: - -ES获取 `coarse_rank.input_window` 条 -> -粗排通过文本/KNN融合裁剪至 `coarse_rank.output_window` -> -然后进行SKU选择和标题后缀处理 -> -精排调用轻量重排,裁剪至 `fine_rank.output_window` -> -最终重排调用现有重排,最终融合时也加入了 `fine_score`。 - -同时,重排客户端/提供者已改为通过 `service_profile` 选择不同的 `service_url`,因此精排和最终重排可以共享同一套服务代码,仅以不同实例运行。 - -你还重构了调试展示。 -你修改了结果卡片和全局调试面板,使其按漏斗阶段读取并渲染数值,在 `app.js` 中,你现在分别渲染ES召回、粗排、精排和最终重排。 - -现在,每个结果的调试信息按阶段展示: - -* ES召回:`rank`、ES得分、归一化得分、匹配查询 -* 粗排:`rank` / `rank_change`、`coarse_score`、文本/KNN输入、`text_source` / `text_translation` / `text_primary` / `text_support`、`text_knn` / `image_knn`、`factor` -* 精排:`rank` / `rank_change`、`fine_score`、`fine input` -* 最终重排:`rank` / `rank_change`、`rerank_score`、文本/KNN得分、各因子、`fused_score` 以及完整信号 - -请仔细阅读这些漏斗阶段的代码,特别是涉及打分、重排和调试信息记录的部分。 - -现在,请注意需要优化的部分: - -1. 精排阶段似乎没有计算融合公式并据此重排。请修复此问题。 -2. 从软件工程的角度审视代码: - 既然引入了多级排序漏斗,数据记录、传递和交互接口的设计是否足够合理?存在哪些问题? - 请从软件工程角度审视这一逻辑,判断是否有需要重新组织、清理或重写的部分。 -3. 优化精排和最终重排阶段的信息记录: - 这两个阶段都应体现融合公式的输入、关键因子以及融合公式计算出的得分。 - 为避免代码臃肿,精排和最终重排都可以使用一个字符串来记录这些关键信息。该字符串可以包含融合公式中各项的名称和值,以及最终结果。 - 你也可以继续使用当前的记录方式;请对比哪种方式代码更少、更清晰简洁。 - 同时请仔细思考当前代码:实际的计算过程和记录的信息是否分离?是否存在冗余或分歧? - 这是不可取的,因为会引入潜在风险:如果后续修改了生产逻辑但未更新调试信息,就会导致不一致。 - -请特别注意:现在已经存在相关的信息记录逻辑。不要只是层层打补丁。 -你可以适当修改,或者清理重写,而不仅仅是增加代码。 -目标是让代码更简单、更干净,同时确保记录的信息始终与实际逻辑保持一致。 - -涉及代码较多,请耐心阅读。 -以上所有任务都需要深入思考。请慢慢来,为全面的审查和重新设计留出足够空间。 - - - - - - -**整体图** -这个 pipeline 现在可以理解成一条“先广召回,再逐层收窄、逐层加贵信号”的漏斗: - -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 原逻辑返回。 - -这点非常重要,因为它决定了“贵模型只服务头部结果”。 - -**Step 1:Query 解析阶段** -在 [searcher.py:432](/data/saas-search/search/searcher.py#L432) 到 [searcher.py:469](/data/saas-search/search/searcher.py#L469): -`query_parser.parse()` 做几件事: - -- 规范化 query -- 检测语言 -- 可能做 rewrite -- 生成文本向量 -- 如果有图搜,还会带图片向量 -- 生成翻译结果 -- 识别 style intent - -这一步的结果存在 `parsed_query` 里,后面 ES 查询、style SKU 选择、fine/final rerank 全都依赖它。 - -**Step 2:ES Query 构建** -ES DSL 在 [searcher.py:471](/data/saas-search/search/searcher.py#L471) 开始,通过 [es_query_builder.py:181](/data/saas-search/search/es_query_builder.py#L181) 的 `build_query()` 生成。 - -这里的核心结构是: -- 文本召回 clause -- 文本向量 KNN clause -- 图片向量 KNN clause -- 它们一起放进 `bool.should` -- 过滤条件放进 `filter` -- facet 的多选条件走 `post_filter` - -KNN 部分在 [es_query_builder.py:250](/data/saas-search/search/es_query_builder.py#L250) 之后: -- 文本向量 clause 名字固定叫 `knn_query` -- 图片向量 clause 名字固定叫 `image_knn_query` - -而文本召回那边,后续 fusion 代码约定会去读: -- 原始 query 的 named query:`base_query` -- 翻译 query 的 named query:`base_query_trans_*` - -也就是说,后面的粗排/精排/最终 rerank,并不是重新理解 ES score,而是从 `matched_queries` 里把这些命名子信号拆出来自己重算。 - -**Step 3:ES 召回** -在 [searcher.py:579](/data/saas-search/search/searcher.py#L579) 到 [searcher.py:627](/data/saas-search/search/searcher.py#L627)。 - -这里有个很关键的工程优化: -如果在 rerank window 内,第一次 ES 拉取时会把 `_source` 关掉,只取排序必需信号,见 [searcher.py:517](/data/saas-search/search/searcher.py#L517) 到 [searcher.py:523](/data/saas-search/search/searcher.py#L523)。 - -原因是: -- 粗排先只需要 `_score` 和 `matched_queries` -- 不需要一上来把 700 条完整商品详情都拉回来 -- 等粗排收窄后,再补 fine/final rerank 需要的字段 - -这是现在这条 pipeline 很核心的性能设计点。 - -**Step 4:粗排** -粗排入口在 [searcher.py:638](/data/saas-search/search/searcher.py#L638),真正的打分在 [rerank_client.py:348](/data/saas-search/search/rerank_client.py#L348) 的 `coarse_resort_hits()`。 - -粗排只看两类信号: -- `text_score` -- `knn_score` - -它们先都从统一 helper `_build_hit_signal_bundle()` 里拿,见 [rerank_client.py:246](/data/saas-search/search/rerank_client.py#L246)。 - -文本分怎么来,见 [rerank_client.py:200](/data/saas-search/search/rerank_client.py#L200): -- `source_score = matched_queries["base_query"]` -- `translation_score = max(base_query_trans_*)` -- `weighted_translation = 0.8 * translation_score` -- `primary_text = max(source, weighted_translation)` -- `support_text = 另一路` -- `text_score = primary_text + 0.25 * support_text` - -这就是一个 text dismax 思路: -原 query 是主路,翻译 query 是辅助路,但不是简单相加。 - -向量分怎么来,见 [rerank_client.py:156](/data/saas-search/search/rerank_client.py#L156): -- `text_knn_score` -- `image_knn_score` -- 分别乘自己的 weight -- 取强的一路做主路 -- 弱的一路按 `knn_tie_breaker` 做辅助 - -然后粗排融合公式在 [rerank_client.py:334](/data/saas-search/search/rerank_client.py#L334): -- `coarse_score = (text_score + text_bias)^text_exponent * (knn_score + knn_bias)^knn_exponent` - -配置定义在 [schema.py:124](/data/saas-search/config/schema.py#L124) 和 [config.yaml:231](/data/saas-search/config/config.yaml#L231)。 - -算完后: -- 写入 `hit["_coarse_score"]` -- 按 `_coarse_score` 排序 -- 留前 240,见 [searcher.py:645](/data/saas-search/search/searcher.py#L645) - -**Step 5:粗排后补字段 + SKU 选择** -粗排完以后,`searcher` 会按 doc template 反推 fine/final rerank 需要哪些 `_source` 字段,然后只补这些字段,见 [searcher.py:669](/data/saas-search/search/searcher.py#L669)。 - -之后才做 style SKU 选择,见 [searcher.py:696](/data/saas-search/search/searcher.py#L696)。 - -为什么放这里? -因为现在 fine rank 也是 reranker,它也要吃 title suffix。 -而 suffix 是 SKU 选择之后写到 hit 上的 `_style_rerank_suffix`。 -真正把 suffix 拼进 doc 文本的地方在 [rerank_client.py:65](/data/saas-search/search/rerank_client.py#L65) 到 [rerank_client.py:74](/data/saas-search/search/rerank_client.py#L74)。 - -所以顺序必须是: -- 先粗排 -- 再选 SKU -- 再用带 suffix 的 title 去跑 fine/final rerank - -**Step 6:精排** -入口在 [searcher.py:711](/data/saas-search/search/searcher.py#L711),实现是 [rerank_client.py:603](/data/saas-search/search/rerank_client.py#L603) 的 `run_lightweight_rerank()`。 - -它会做三件事: - -1. 用 `build_docs_from_hits()` 把每条商品变成 reranker 输入文本 -2. 用 `service_profile="fine"` 调轻量服务 -3. 不再只按 `fine_score` 排,而是按融合后的 `_fine_fused_score` 排 - -精排融合公式现在是: -- `fine_stage_score = fine_factor * text_factor * knn_factor * style_boost` - -具体公共计算在 [rerank_client.py:286](/data/saas-search/search/rerank_client.py#L286) 的 `_compute_multiplicative_fusion()`: -- `fine_factor = (fine_score + fine_bias)^fine_exponent` -- `text_factor = (text_score + text_bias)^text_exponent` -- `knn_factor = (knn_score + knn_bias)^knn_exponent` -- 如果命中了 selected SKU,再乘 style boost - -写回 hit 的字段见 [rerank_client.py:655](/data/saas-search/search/rerank_client.py#L655): -- `_fine_score` -- `_fine_fused_score` -- `_text_score` -- `_knn_score` - -排序逻辑在 [rerank_client.py:683](/data/saas-search/search/rerank_client.py#L683): -按 `_fine_fused_score` 降序排,然后留前 80,见 [searcher.py:727](/data/saas-search/search/searcher.py#L727)。 - -这就是你这次特别关心的点:现在 fine rank 已经不是“模型裸分排序”,而是“模型分 + ES 文本/KNN 信号融合后排序”。 - -**Step 7:最终 rerank** -入口在 [searcher.py:767](/data/saas-search/search/searcher.py#L767),实现是 [rerank_client.py:538](/data/saas-search/search/rerank_client.py#L538) 的 `run_rerank()`。 - -它和 fine rank 很像,但多了一个更重的模型分 `rerank_score`。 -最终公式是: - -- `final_score = rerank_factor * fine_factor * text_factor * knn_factor * style_boost` - -也就是: -- fine rank 产生的 `fine_score` 不会丢 -- 到最终 rerank 时,它会继续作为一个乘法项参与最终融合 - -这个逻辑在 [rerank_client.py:468](/data/saas-search/search/rerank_client.py#L468) 到 [rerank_client.py:476](/data/saas-search/search/rerank_client.py#L476)。 - -算完后写入: -- `_rerank_score` -- `_fused_score` - -然后按 `_fused_score` 排序,见 [rerank_client.py:531](/data/saas-search/search/rerank_client.py#L531)。 - -这里你可以把它理解成: -- fine rank 负责“轻量快速筛一遍,把 240 缩成 80” -- 最终 rerank 负责“用更贵模型做最终拍板” -- 但最终拍板时,不会忽略 fine rank 结果,而是把 fine score 当成一个先验信号保留进去 - -**Step 8:分页与字段补全** -多阶段排序只在头部窗口内完成。 -真正返回给用户前,在 [searcher.py:828](/data/saas-search/search/searcher.py#L828) 之后还会做两件事: - -- 先按 `from_:from_+size` 对最终 80 条切片 -- 再按用户原始 `_source` 需求补回页面真正要显示的字段,见 [searcher.py:859](/data/saas-search/search/searcher.py#L859) - -所以这条链路是“三次不同目的的数据访问”: - -- 第一次 ES:只要排序信号 -- 第二次按 id 回填:只要 fine/final rerank 需要字段 -- 第三次按页面 ids 回填:只要最终页面显示字段 - -这也是为什么它性能上比“一次全量拉 700 条完整文档”更合理。 - -**Step 9:结果格式化与 debug funnel** -最后在 [searcher.py:906](/data/saas-search/search/searcher.py#L906) 进入结果处理。 -这里会把每个商品的阶段信息组装成 `ranking_funnel`,见 [searcher.py:1068](/data/saas-search/search/searcher.py#L1068): - -- `es_recall` -- `coarse_rank` -- `fine_rank` -- `rerank` -- `final_page` - -其中: -- coarse stage 主要保留 text/translation/knn 的拆分信号 -- fine/rerank stage 现在都保留 `fusion_inputs`、`fusion_factors`、`fusion_summary` -- `fusion_summary` 来自真实计算过程本身,见 [rerank_client.py:265](/data/saas-search/search/rerank_client.py#L265) - -这点很重要,因为现在“实际排序逻辑”和“debug 展示逻辑”是同源的,不是两套各写一份。 - -**一句话总结这条 pipeline** -这条 pipeline 的本质是: - -- ES 负责便宜的大范围召回 -- 粗排负责只靠 ES 内置信号先做一次结构化筛选 -- style SKU 选择负责把商品文本改造成更适合 reranker 理解的输入 -- fine rank 负责用轻模型把候选进一步压缩 -- final rerank 负责用重模型做最终判定 -- 每一层都尽量复用前一层信号,而不是推翻重来 - -如果你愿意,我下一步可以继续按“一个具体 query 的真实流转样例”来讲,比如假设用户搜 `black dress`,我把它从 `parsed_query`、ES named queries、coarse/fine/final 的每个分数怎么出来,完整手推一遍。 \ No newline at end of file diff --git a/docs/issue-2026-03-29-索引修改-done-0330.md b/docs/issue-2026-03-29-索引修改-done-0330.md deleted file mode 100644 index f6e33a1..0000000 --- a/docs/issue-2026-03-29-索引修改-done-0330.md +++ /dev/null @@ -1,43 +0,0 @@ - - - - - -工程(金伟)配合修改: - - -一、tags字段改支持多语言: -spu表tags字段,跟title走一样的翻译逻辑,填入原始语言、zh、en。 - -检查以下字段,都跟title一样走翻译逻辑 -title -keywords -tags -brief -description -vendor -category_path -category_name_text - - -二、/indexer/enrich-content接口的修改 -1. 请求参数,把language去掉,因为我返回的内容直接对应索引结构,不用你做处理了,因此不需要指定语言,降低耦合。 -2. 返回 enriched_attributes enriched_tags qanchors三个字段,按原始内容填入。 -3. enriched_tags是本次新增的,注意区别于tags字段。tags字段来源于mysql spu表,enriched_tags是本接口返回的。 - - -三、specifications的value,需要翻译,也是需要填中英文: -{ - "specifications": [ - { - "sku_id": "sku-red-s", - "name": "color", - "value_keyword": "красный", - "value_text": { - "zh": "红色", - "en": "red" - } - } - ] -} - diff --git a/docs/issue-2026-03-30-query分析性能优化-done-0331.md b/docs/issue-2026-03-30-query分析性能优化-done-0331.md deleted file mode 100644 index 0a3f6f4..0000000 --- a/docs/issue-2026-03-30-query分析性能优化-done-0331.md +++ /dev/null @@ -1,264 +0,0 @@ - -总体的目的是: -1)要对原始query进行翻译(通常是en/zh的一种或者两种) -2)对原始query要有关键词提取(关键词提取可能依赖分词;中英文分词要不一样,各自寻求最优性能的方法。zh的可以保持不变,en的可以优化) -3)其他的一些任务可能依赖分词 -4)获取text embedding/clip embedding - - -英文关键词提取:走spacy进行关键词提取(即主干分析)。提取出query中的核心词,用于搜索时候的term求交,其余的词不参与求交、只用于权重计算。 - - -实现: - -# Query 模块说明 - -本目录实现搜索请求侧的**查询理解与解析**:在不做 Elasticsearch 语言计划拼装的前提下,产出可供检索层、重排层与调试界面消费的**结构化事实**(规范化文本、检测语言、可选翻译、文本与 CLIP 向量、分词与关键词、可选的样式意图与标题排除配置等)。下面按**当前实现**说明策略与数据流,便于与 `search/`、`context/`、`frontend/` 对照阅读。 - ---- - -## 包内文件与职责 - -| 文件 | 作用 | -|------|------| -| `query_parser.py` | 入口 `QueryParser`:编排规范化、改写、语言检测、异步翻译与向量、分词、关键词、意图与排除检测;定义 `ParsedQuery`。 | -| `tokenization.py` | 轻量分词、文本规范化、`TokenizedText` 与按请求复用的 `QueryTextAnalysisCache`(模型分词与语言提示、粗细分词策略)。 | -| `keyword_extractor.py` | `KeywordExtractor`:中文走 HanLP 分词 + 词性名词串;英文走 spaCy 核心词;`collect_keywords_queries` 汇总 `base` 与各翻译语种。 | -| `english_keyword_extractor.py` | `EnglishKeywordExtractor`:`en_core_web_sm` + 依存/名词块规则,产出短字符串供检索侧关键词子句使用。 | -| `language_detector.py` | 脚本优先 + Lingua 的通用语言检测(与 `QueryParser` 的英文 ASCII 快路径配合使用)。 | -| `query_rewriter.py` | 基于配置词典的查询改写与规范化。 | -| `style_intent.py` | 从配置加载样式意图词表,对查询变体做候选匹配,产出 `StyleIntentProfile`。 | -| `product_title_exclusion.py` | 从配置加载标题排除规则,对多路查询文本做触发词匹配,产出 `ProductTitleExclusionProfile`。 | - -公开符号见 `query/__init__.py`(`QueryParser`、`ParsedQuery`、`KEYWORDS_QUERY_BASE_KEY` 等)。 - ---- - -## 解析产物:`ParsedQuery` - -`ParsedQuery` 是单次 `parse()` 的权威结果容器,字段含义与下游约定如下。 - -- **`original_query` / `query_normalized` / `rewritten_query`**:分别为原始输入、规范化后、词典改写后的主查询文本;后续翻译、向量、默认分词与 `base` 关键词均以**改写后的 `rewritten_query`(在代码变量中常名为 `query_text`)**为基准。 -- **`detected_language`**:解析时认定的源语言代码;若检测为 `unknown` 或空,则回退到 `SearchConfig.query_config.default_language`。 -- **`translations`**:键为**目标语言代码**(如 `zh`、`en`),值为翻译服务返回的字符串;仅包含本次请求实际需要的目标语种(见下文翻译目标推导)。 -- **`query_vector` / `image_query_vector`**:分别为 BGE 类文本向量与 CLIP 文本向量(维度由各自编码服务决定);未生成或未在超时内完成则为 `None`。 -- **`query_tokens`**:对**改写后主查询**做分词后的字符串列表,供例如 KNN 参数按 token 数分支等逻辑使用;分词路径由 `QueryTextAnalysisCache` 决定(纯拉丁英文可走轻量分词,含汉字则走 HanLP)。 -- **`keywords_queries`**:与「主查询 + 各翻译变体」平行的**关键词子查询**映射:键 `base`(常量 `KEYWORDS_QUERY_BASE_KEY`)对应源语言侧关键词串,其它键与 `translations` 的语种键一致。空串或无法提取的条目**不会写入**字典。 -- **`style_intent_profile` / `product_title_exclusion_profile`**:可选的理解结果;是否生效完全由 `config.yaml` 中 `query_config` 的对应开关与词表/规则决定。 -- **`_text_analysis_cache`**:单次解析内的分词与语言提示缓存,**不参与序列化**,仅供同一次 `parse` 内各检测器复用,避免对同一文本重复调用 HanLP。 - -与重排相关的文本选择由独立函数 `rerank_query_text()` 完成:检测为 `zh` 或 `en` 时始终用原始查询;其它语言优先英译再中译,见 `query_parser.py` 中实现。 - ---- - -## `QueryParser.parse()` 的执行顺序与策略 - -解析主流程在 `QueryParser.parse()` 中实现。整体目标是:在**共享等待预算**下并行完成翻译与向量请求,同时尽量减少主线程上重复、昂贵的分词与 NLP 调用,并把结果写入可选的 `context`(请求上下文)供日志与 `debug_info` 使用。 - -### 1. 规范化与改写 - -- 使用 `QueryNormalizer` 得到 `query_normalized` 并可选写入 `context.store_intermediate_result('query_normalized', ...)`。 -- 若配置了改写词典,则用 `QueryRewriter` 可能更新 `query_text`;改写成功时记录 `rewritten_query` 与告警。 - -### 2. 语言检测:通用路径与英文 ASCII 快路径 - -- **快路径**:当「活跃语种集合」仅为 `en` 与 `zh` 的子集时(活跃集合取 `target_languages` 归一化结果,若为空则回退到 `query_config.supported_languages`),且当前查询为**纯 ASCII、含字母、不含汉字**,则**直接判定为 `en`**,不再调用 `LanguageDetector`(避免 Lingua 等开销)。逻辑见 `_detect_query_language()` 与 `_is_ascii_latin_query()`。 - -```303:317:query/query_parser.py - def _detect_query_language( - self, - query_text: str, - *, - target_languages: Optional[List[str]] = None, - ) -> str: - normalized_targets = self._normalize_language_codes(target_languages) - supported_languages = self._normalize_language_codes( - getattr(self.config.query_config, "supported_languages", None) - ) - active_languages = normalized_targets or supported_languages - if active_languages and set(active_languages).issubset({"en", "zh"}): - if self._is_ascii_latin_query(query_text): - return "en" - return self.language_detector.detect(query_text) -``` - -- **通用路径**:`LanguageDetector` 先按 Unicode 脚本返回明确语种(如汉字块即 `zh`),否则用 Lingua 在一大组语言中判别,见 `language_detector.py`。 - -检测最终结果写入 `context.store_intermediate_result('detected_language', ...)`(若提供 `context`)。 - -### 3. 按请求分词缓存与语言提示 - -每次 `parse` 会新建 `QueryTextAnalysisCache(tokenizer=self._tokenizer)`,并对**原始串、规范化串、改写后串**调用 `set_language_hint(..., detected_lang)`,使后续对同一文本的 `get_tokenizer_result` / `get_tokenized_text` 能按语言选择**是否调用 HanLP**。 - -### 4. HanLP 模型(与 `KeywordExtractor` 对齐) - -`QueryParser` 默认构建的 `self._tokenizer` 为 HanLP 预训练分词模型 **`FINE_ELECTRA_SMALL_ZH`**,并开启 `output_spans=True`,以便与关键词提取共用「带偏移的分词结果」。 - -```237:245:query/query_parser.py - def _build_tokenizer(self) -> Callable[[str], Any]: - """Build the tokenizer used by query parsing. No fallback path by design.""" - if hanlp is None: - raise RuntimeError("HanLP is required for QueryParser tokenization") - logger.info("Initializing HanLP tokenizer...") - tokenizer = hanlp.load(hanlp.pretrained.tok.FINE_ELECTRA_SMALL_ZH) - tokenizer.config.output_spans = True - logger.info("HanLP tokenizer initialized") - return tokenizer -``` - -`KeywordExtractor` 在未注入自定义 `tokenizer` 时同样加载 **`FINE_ELECTRA_SMALL_ZH`**,并额外加载 **`CTB9_POS_ELECTRA_SMALL`** 做词性标注;二者在「中文路径」上语义一致,便于复用 `tokenizer_result`。 - -### 5. 异步富集:翻译、文本向量、CLIP 文本向量 - -- 翻译目标:`translation_targets = normalized_targets` 中**去掉与检测源语言相同**的代码后的列表(例如源为 `en` 且索引语言为 `["en","zh"]` 时只翻 `zh`)。 -- 翻译模型名:由 `_pick_query_translation_model()` 根据「源语言是否在索引语言内」及 `zh↔en` 等分支从 `QueryConfig` 选取。 -- 当 `generate_vector` 为真且配置开启文本嵌入时,向线程池提交 `text_encoder.encode([query_text], ...)`;当配置了 `image_embedding_field` 时提交 `image_encoder.encode_clip_text(query_text, ...)`。 -- 线程池:`ThreadPoolExecutor`,`max_workers` 为 `min(任务数, 4)` 与至少 1。 -- **提交顺序**:先尽可能提交所有异步任务,再在主线程上做「与异步重叠」的轻量工作(见下一节),最后 `concurrent.futures.wait(..., timeout=budget_sec)`。超时未完成的任务会记 warning,并 `shutdown(wait=False)` 不阻塞关闭线程池。 - -等待预算(毫秒)来自 `QueryConfig`: - -- 源语言在索引语言内:`translation_embedding_wait_budget_ms_source_in_index` -- 否则:`translation_embedding_wait_budget_ms_source_not_in_index` - -完成每个 future 后打 `Async enrichment task finished` 日志(含 `elapsed_ms`,为从提交到完成的大致墙钟时间)。 - -### 6. 主查询分词与「base」关键词(与异步重叠) - -在异步任务已提交之后、`wait()` 之前,当前实现会: - -1. 通过 `text_analysis_cache.get_tokenizer_result(query_text)` 得到分词结果,再 `extract_token_strings` 得到 **`query_tokens`**; -2. 调用 `KeywordExtractor.extract_keywords(query_text, language_hint=detected_lang, tokenizer_result=...)` 得到 **`keywords_base_query`**(若失败则日志告警,base 关键词可能为空)。 - -这样主线程在等翻译/向量时,已并行完成源侧分词与源侧关键词的大部分工作。 - -### 7. 等待结束后的关键词汇总与检测器 - -`wait()` 返回后: - -- 若有翻译结果,写入 `context.store_intermediate_result("translations", translations)`,并对每条翻译 `text_analysis_cache.set_language_hint(result, lang)`,保证后续对该翻译串的分词/关键词走正确语言路径。 -- `collect_keywords_queries(...)` 合并 **`base`**(可传入已算好的 `base_keywords_query` 避免重复抽取)与各翻译语种的关键词,得到 **`keywords_queries`**;成功时 `context.store_intermediate_result("keywords_queries", keywords_queries)` 并打 `Keyword extraction completed` 日志。 -- 构造带 `_text_analysis_cache` 的 `ParsedQuery` 草稿,依次调用 `StyleIntentDetector.detect` 与 `ProductTitleExclusionDetector.detect`,再把完整 `ParsedQuery` 返回。 - -解析阶段会打聚合耗时日志 `Query parse stage timings`,字段含义为: - -- **`before_wait_ms`**:从解析开始计时点到进入 `wait()` 之前的主线程耗时(含规范化、改写、语言检测、提交异步任务、主查询分词、base 关键词等); -- **`async_wait_ms`**:`wait()` 阻塞时间; -- **`base_keywords_ms`**:base 关键词抽取耗时; -- **`keyword_tail_ms`**:`collect_keywords_queries` 及前后尾部逻辑中关键词相关部分的主要耗时; -- **`tail_sync_ms`**:`wait()` 之后整段同步尾巴(含关键词汇总、两检测器、写中间结果等)。 - ---- - -## 分词与 `QueryTextAnalysisCache` - -### `get_tokenizer_result`:何时走 HanLP,何时走轻量分词 - -- 若未配置模型 `tokenizer`,直接返回空列表路径的轻量结果(由上层避免依赖)。 -- 若根据**该文本的语言提示**与**是否含汉字**判断不需要模型:返回 `simple_tokenize_query` 的列表(字符串 token),**不调用 HanLP**。 -- 否则对该文本调用一次 `self.tokenizer(text)`(HanLP),结果按文本缓存,同一次 `parse` 内重复访问同一字符串不会重复推理。 - -核心判断在 `_should_use_model_tokenizer`:**语言提示为 `zh` 时,仅当文本含汉字才用模型**;非 `zh` 提示时,仅当文本含汉字才用模型。因此纯英文主查询在提示为 `en` 时走轻量分词;中文翻译串在 `set_language_hint(..., "zh")` 且含汉字时走 HanLP。 - -### `coarse_tokens` 与 `fine_tokens`:`TokenizedText` - -- **`fine_tokens`**:来自 `extract_token_strings(get_tokenizer_result(...))`,在中文路径上即 HanLP 分词后的词串(已按规范化键去重保序)。 -- **`coarse_tokens`**:由 `_build_coarse_tokens` 决定。若语言提示为 **`zh`**,或文本含汉字且已有 `tokenizer_tokens`,则 **粗粒度 token 与 fine 一致**(即采用模型分词粒度,而不用「整段 CJK 连成一项」的纯正则策略)。否则使用 `simple_tokenize_query`(适合拉丁词、数字、带连字符/撇号的英文词形)。 - -```92:103:query/tokenization.py -def _build_coarse_tokens( - text: str, - *, - language_hint: Optional[str], - tokenizer_tokens: Sequence[str], -) -> List[str]: - normalized_language = normalize_query_text(language_hint) - if normalized_language == "zh" or (contains_han_text(text) and tokenizer_tokens): - # Chinese coarse tokenization should follow the model tokenizer rather than a - # regex that collapses the whole sentence into one CJK span. - return list(_dedupe_preserve_order(tokenizer_tokens)) - return _dedupe_preserve_order(simple_tokenize_query(text)) -``` - -- **`candidates`**:在 fine、coarse、两类 n-gram 短语(上限由 `max_ngram` 控制)以及整句 `normalized_text` 上合并去重,供 `StyleIntentDetector`、`ProductTitleExclusionDetector` 等做子串/短语级匹配。 - -`tokenize_text()` 是对单次无缓存场景的薄封装:内部新建 `QueryTextAnalysisCache` 再 `get_tokenized_text`。 - ---- - -## 关键词提取:`KeywordExtractor` 与 `collect_keywords_queries` - -### 路由规则 - -`extract_keywords` 根据 `language_hint` 分支: - -- **`en`**:完全交给 `EnglishKeywordExtractor`(spaCy),**不使用** HanLP 分词结果做 POS 名词筛选(即使调用方传入 `tokenizer_result` 也会被忽略在该路径内)。 -- **`zh`**:使用 HanLP 分词结果(优先复用传入的 `tokenizer_result`),再对词序列跑 `CTB9_POS_ELECTRA_SMALL`,保留**长度 ≥ 2 且词性以 `N` 开头**的词;非连续名词之间插入空格拼接成一条字符串(与 ES 侧 `keywords_query` 的用法一致)。 -- **其它非空语言码**:当前实现返回空串,即**不为该语种生成关键词子句**(由调用方决定是否跳过)。 - -### `collect_keywords_queries` - -- 键 **`base`**:对应 `rewritten_query` 的关键词;若调用方已预先计算 `base_keywords_query` 则直接写入,避免重复抽取。 -- 其它键:与 `translations` 中每个非空语种一一对应,语言码归一化为小写。 -- 全程可传入 `text_analysis_cache`,以便 `get_tokenizer_result` 命中缓存并与检测器共享分词结果。 - -常量 `KEYWORDS_QUERY_BASE_KEY` 的值为字符串 **`"base"`**,与检索构建里读取的字段一致。 - ---- - -## 英文关键词:`EnglishKeywordExtractor` - -- 依赖 **spaCy** 模型 **`en_core_web_sm`**,加载时关闭 `ner`、`textcat` 以减轻开销;加载失败时记录 warning 并走基于 `simple_tokenize_query` 的回退策略。 -- 主路径用依存句法与名词块规则收集一小组「核心词」候选(如直接宾语名词、部分 ROOT 名词/专有名词、INTJ 结构下的宾语等),并处理价格/目的介词宾语降级、人口学名词(如 `women`)弱化、尺寸类 ROOT 与主语搭配等边界情况。 -- 使用 `_project_terms_to_query_tokens` 将 spaCy 词形映射回查询中的**表面分词**(例如复合词 `t-shirt`),避免在关键词串中出现被错误切断的片段。 - -最终返回**最多三个词**的空格连接字符串,用于检索侧第二层 `combined_fields` 的紧凑查询(见下节)。 - ---- - -## 与检索层的关系(消费方摘要) - -`ParsedQuery.keywords_queries` 由 `search/es_query_builder.py` 读取:在构建某一语言的 lexical 子句时,除主 `combined_fields`(完整 `query`)外,若存在非空的 `keywords_query` 且与主查询不同,会追加第二个 `combined_fields`,使用单独的 `minimum_should_match`(由 builder 的 `keywords_minimum_should_match` 配置)和较低 boost,从而在**不替代全文查询**的前提下加强核心词匹配。 - -`query_tokens` 在同文件中间接影响例如带文本向量时的 KNN 分支参数(按 token 数量选用长查询的 k / num_candidates 等)。具体字段与 boost 以 `ESQueryBuilder` 当前实现为准。 - ---- - -## 样式意图与标题排除(简要) - -- **`StyleIntentRegistry` / `StyleIntentDetector`**:从 `QueryConfig.style_intent_terms` 等加载意图定义;`detect` 时按中英变体取查询文本,经 `tokenize_text` 或缓存得到 `TokenizedText`,在 `candidates` 上与配置同义词表匹配,输出 `StyleIntentProfile`(含 `query_variants` 与命中意图列表)。 -- **`ProductTitleExclusionRegistry` / `ProductTitleExclusionDetector`**:从 `QueryConfig.product_title_exclusion_rules` 加载规则;对 `original_query`、`query_normalized`、`rewritten_query` 及所有 `translations` 去重后分词匹配触发词,输出 `ProductTitleExclusionProfile`。 - -二者均依赖 `tokenization` 与可选的 HanLP,启用与否由配置项控制。 - ---- - -## 可观测性与调试 - -当 `QueryParser.parse(..., context=...)` 传入请求上下文时,典型中间结果包括: - -- `query_normalized`、`rewritten_query`、`detected_language`、`query_tokens` -- `translation_{lang}`、`translations` -- `keywords_queries` -- `query_vector_shape`、`image_query_vector_shape` -- `style_intent_profile`、`product_title_exclusion_profile` - -搜索主流程在 `search/searcher.py` 中会把解析结果写入 `QueryAnalysisResult`(含 **`keywords_queries`**),并在 `debug=true` 时把 `query_analysis` 挂到响应的 `debug_info`;前端调试页在 `frontend/static/js/app.js` 中展示 **Translations** 与 **Keywords Queries** 等块,便于与翻译结果并列查看。 - ---- - -## 依赖与环境提示 - -- **HanLP**:分词与中文词性标注;模型名以本文与源码为准(`FINE_ELECTRA_SMALL_ZH` + `CTB9_POS_ELECTRA_SMALL`)。 -- **spaCy**:英文关键词路径需要可导入的 **`en_core_web_sm`**(若缺失则英文关键词退化为轻量规则)。 -- **Lingua**:通用语言检测(在英文 ASCII 快路径不适用时参与拉丁语系判别)。 - -运行与测试时请使用项目约定的虚拟环境(见仓库根目录 `CLAUDE.md` / `activate.sh`),避免系统 Python 缺少上述依赖。 - ---- - -## 扩展与测试 - -- 单元测试中与解析、分词、意图相关的用例分布在 `tests/test_query_parser_mixed_language.py`、`tests/test_tokenization.py`、`tests/test_style_intent.py`、`tests/test_product_title_exclusion.py` 等文件中;修改分词或关键词策略时应同步更新或新增测试,以保持与本文描述一致。 - -若新增语种或改写语言检测策略,应同步审视:`QueryParser._detect_query_language`、`QueryTextAnalysisCache._should_use_model_tokenizer`、`KeywordExtractor.extract_keywords` 中非 `zh`/`en` 分支,以及 ES 侧是否应为新语种生成 `keywords_query`。 diff --git a/docs/issue-2026-03-31-评估框架-done-0331.md b/docs/issue-2026-03-31-评估框架-done-0331.md deleted file mode 100644 index 257ce6a..0000000 --- a/docs/issue-2026-03-31-评估框架-done-0331.md +++ /dev/null @@ -1,151 +0,0 @@ - - -参考资料: - -1. 搜索接口: - -```bash -export BASE_URL="${BASE_URL:-http://localhost:6002}" -export TENANT_ID="${TENANT_ID:-163}" # 改成你的租户ID -``` -```bash -curl -sS "$BASE_URL/search/" \ - -H "Content-Type: application/json" \ - -H "X-Tenant-ID: $TENANT_ID" \ - -d '{ - "query": "芭比娃娃", - "size": 20, - "from": 0, - "language": "zh" - }' -``` - -response: -{ - "results": [ - { - "spu_id": "12345", - "title": "芭比时尚娃娃", - "image_url": "https://example.com/image.jpg", - "specifications":[], - "skus":[{"sku_id":" ... -... - -2. 重排服务: -curl -X POST "http://localhost:6007/rerank" \ - -H "Content-Type: application/json" \ - -d '{ - "query": "玩具 芭比", - "docs": ["12PCS 6 Types of Dolls with Bottles", "纯棉T恤 短袖"], - "top_n":386, - "normalize": true - }' - - -3. 基于指定字段查询:es_debug_search.py - - -主要任务: -1. 评估工具的建立: -注意判断结果好坏,要用统一的评估工具,不要对每个query设定关键词匹配的规则来判断是否符合要求,这样不可扩展,这种方式且容易有误判还是复杂,并且不好扩展到其他搜索词。 -因此要做一个搜索结果评估工具、多个结果对比的工具,供后面的标注集合构建工具调用。工具内部实现可以是调用大模型来判断,说清楚什么叫高相关、基本相关、不相关: - -prompt: -```bash -你是一个电商搜索结果相关性评估助手。请根据用户查询(query)和每个商品的信息,输出该商品的相关性等级。 - -## 相关性等级标准 -Exact 完全相关 — 完全匹配用户搜索需求。 -Partial 部分相关 — 主意图满足(同品类或相近用途,基本上符合搜索意图),但次要属性(如颜色、风格、尺码等)跟用户需求有偏差或无法确认。 -Irrelevant 不相关 — 品类或用途不符,主诉求未满足。 - -1. {title1} {option1_value1} {option2_value1} {option3_value1} -2. {title2} {option1_value2} {option2_value2}, {option3_value2} -... -50. {title50} {option1_value50} {option2_value50} {option3_value50} - -## 输出格式 -严格输出 {input_nums} 行,每行仅Exact / Partial / Irrelevant三者之一。按顺序对应上述 50 个商品。不要输出任何其他任何信息 -``` - - -2. 测试集(结果标注)建立: -@queries/queries.txt - -对其中每一个query: -1. 召回: -1)参考搜索接口 召回结果。搜索结果的top500,纳入召回池,打分全部标记为1 -2)调用重排模型,扫描全库(tenant_id=163),如果已经在召回池(打分已经是1了),则跳过,其余的全部过reranker模型接口调用。每80个doc做一次请求。注意重排模型打分一定要做缓存(本地文件缓存即可。query+title->rerank_score)。 -3)对reranker打分超过0.5的结果数大于1000条的query,则打印一行日志,跳过这个query,表示相关结果太多、容易被满足 - - -2. 对如上召回的内容,进行全排序,然后逐批进行llm评判标注(50个一批),每一批都记录exact比例和不相关比例,打印日志。 -直到连续三批不相关比例都大于92%。 -最少要跑15批,最多跑40批 - -3. 请你思考如何存储结果、并利于以后的对比、使用、展示。 - - - - -3. 评估工具页面: -请你设计一个搜索评估交互页面。端口6010。 -页面主题:上方是搜索框,如果发起搜索,那么下方给出本次结果的总体指标以及top100结果(允许翻页) - -总体指标: -| 指标 | 含义 | -|------|------| -| **P@5, P@10, P@20, P@50** | 前 K 个结果中「仅 3 相关」的精确率 | -| **P@5_2_3 ~ P@50_2_3** | 前 K 个结果中「2 和 3 都算相关」的精确率 | -| **MAP_3** | 仅 3 相关时的 Average Precision(单 query) | -| **MAP_2_3** | 2 和 3 都相关时的 Average Precision | - -结果列表: -按行列下来,每行左侧给每个结果找到标注值(三个等级。对结果也可以颜色标记),展示图片,title.en+title.en+首个sku的option1/2/3_value(分三行展示,这三行和左侧的图片并列) - - -评测页面最左侧: -queries默认是queries/queries.txt,填入左侧列表框,点击其中任何一个发起搜索。 - -4. 批量评估工具 - -给一个批量执行脚本, - -这里要新增一个批量评估的页面。点击批量评估的按钮,对所有搜索词依次发起搜索,最后汇总总体的评估指标,生成报告,报告名称带上时间标记和一些关键信息。并且记录当时的主搜索程序的config.yaml。 -你需要精心地设计如何切换两种模式,通过同一个端口承载这两种不同交互的内容。 -批量评估关注的是所有搜索词总体的评估指标。 -需要记录测试环境时间以及当时的配置文件,以及对应的结果。要保存历次的评估记录,并能查到每一次评估结果对应的配置文件有相关的指标 - -以上是我的总体设计,但有不周全的地方。你要站在更高的层次理解我的需求,你有足够的自由可以适当调整设计,基于你所了解的自动化搜索评估框架的最佳实践,做出更优秀的设计和更好的实现。 - - - - - - -1. 请仔细检验这个标注集的质量,如果质量不符合要求,那么你要优化工具,迭代直至标注集的结果质量足够高,可以以此为自动化工具来评估检索效果,对检索效果形成指导性意见。 -2. 在结果标注集的质量足够好,批量评估工具足够好用,并且经过你的试用,能判断出搜索质量好坏的情况下,开始真正的动手检索效果调优:基于这个50条query的结果标注集和批量评估工具,对融合公式进行调参。请你先精心地设计实验,设计几组参数,对几组参数分别修改config.yaml、重启(./restart.sh backend)、跑批量评估、收集结果。 -注意评估的过程中,如果发现工具不好用,发现日志不全,发现可以通过修改工具或者日志来提高效率,都可以先做这些,根据完善。 -注意你是代码的总负责人,你有任何权限来满足你进行检索效果调优的需要。你如果发现有其他可能带来更大提升的点,也可以进行实验,你甚至可以修改融合、重排漏斗的代码,来进行实验,以追求更好的结果指标。 -但是注意,因为收到性能和耗时的约束,不要调大reranker模型的输入条数、不要打开精排,耗时方面无法承受两轮reranker模型的调用。 - - - - - - - - - -@scripts/evaluation/README.md @scripts/evaluation/eval_framework/framework.py -@start_eval.sh.sh (29-35) -请以如下流程为准,进行改造: -如果重建的话,对每个query: -每个搜索结果应该会扫描全库, -1. 搜索结果的top500,纳入召回池,打分全部标记为1 -2. 调用重排模型,扫描全库(tenant_id=163),如果已经在召回池(打分已经是1了),则跳过,其余的全部过 -3. 对reranker打分超过0.5的大于1000条,则打印一行日志,跳过这个query,表示相关结果太多、容易被满足 - -对如上召回的内容,进行全排序,然后逐批进行llm评判标注(50个一批),每一批都记录exact比例和不相关比例,打印日志。 -直到连续三批不相关比例都大于92%。 -最少要跑15批,最多跑40批 diff --git a/docs/issue.md b/docs/issue.md deleted file mode 100644 index 2180025..0000000 --- a/docs/issue.md +++ /dev/null @@ -1,596 +0,0 @@ -项目 TODO 清单 - -2. 核心搜索功能优化 - -2.1 意图识别模块 - -- 增加款式意图识别模块 - -- 意图类型: 颜色,尺码(目前只需要支持这两种) - -- 意图召回层: -每种意图,有一个召回词集合 -对query(包括原始query、各种翻译query 都做匹配) - -- 以颜色意图为例: -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 -query匹配了其中任何一个词,都认为,具有颜色意图 -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 - -- 意图判断: - 暂时留空,直接返回true。目前没有模型,即只要召回了(词表匹配了),即认为有该维度款式需求。 - (以后考虑建设fasttext/bert系列多分类模型) - -- 意图使用: -我们第一阶段,使用 参与ES提权。 - -- 一、参与ES提权 - -- 二、参与reranker - -- 如果有: 先做sku筛选,然后把最优的拼接到名称中,参与reranker。 - - - -- 现在在reranker、分页之后、做填充的时候,已经有做sku的筛选。 -需要优化: -现在是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。改为 - 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 - 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 - 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 - 这个sku筛选也需要提取为一个独立的模块。 - -- 另外:现在是reranker、分页之后做sku筛选,要改为: - 1. 有款式意图的时候,才做sku筛选 - 2. sku筛选的时机,改为在reranker之前,对所有内容做sku筛选,然后 - 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、olors、、、、),遍历option1_name,option2_name,option3_name,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 - 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) - 5. TODO : 还有一个问题。 目前,sku只返回一个维度(店铺主维度。默认应该是option1,不是所有维度的sku信息都返回的。所以,如果有款式意图,但是主维度是颜色,那么拿不到全的款式sku) - -- 筛选SKU: 先只筛选第一个维度,但考虑到用户搜索词可能带了尺码,所以第二、三个维度也要考虑 - - 当前项目功能已经较多,但是有清晰的框架,请务必基于现有框架进行改造,不要进行补丁式的修改,避免代码逻辑分叉。 - 请一步一步来,先设计意图识别模块,仔细思考需求,意图识别模块需要提供哪些内容,用于返回数据接口的定义,深度思考,定义一个合理的接口后,再给出合理的模块设计。 - -2.3 向量检索与融合 - -- 把knn跟文本相关性的融合方式修改为 "rank": {"rrf": {} }需要licence,可以帮我修改源码支持吗? - - knn_boost: 2.0 - - { - "query": { ...全文检索... }, - "knn": { ...向量检索... }, - "rank": { - "rrf": {} - } - } - -- 融合打分(已完成,2026-03) - - 以下已经完成: - 1. fuse_scores_and_resort 已改为乘法融合,并通过 matched_queries 提取: - - base_query - - base_query_trans_* - - fallback_original_query_* - - knn_query - 2. 文本相关性大分不再依赖 phrase_query / keywords_query,这两类查询已清理。 - 3. 当前融合策略: - - text_score = primary(weighted_source, weighted_translation, weighted_fallback) + 0.25 * support - - fused_score = (rerank_score + 0.00001) * (text_score + 0.1) ** 0.35 * (knn_score + 0.6) ** 0.2 - 4. track_scores 与 include_named_queries_score 已接入,调试字段与评估方法已同步到: - - docs/相关性检索优化说明.md - - docs/搜索API对接指南-01-搜索接口.md(分册;原单文件已拆分) - - docs/Usage-Guide.md - - 未完成的: - (归一化、次序融合?还乘法公式?) - RRF:先把多路召回稳妥融合 - linear + minmax:让你能精调 knn 和文本的权重 - reranker:对前面召回出来的 top-k 再做“最后一刀” - -2.4 文本相关性优化 - -- 调研: -Princeton WordNet — 英文同义词底库 -Shopify Product Taxonomy — 电商品类标准 -Querqy — 电商搜索规则框架 -gensimpson/elasticsearch-synonyms — ES 同义词规则落地 - -- tags字段使用的优化: -现在是keyword,在搜索中,不太好使用(目前主要用于suggest)。 -可以考虑也拆分多语言,配合analyzer使用(和qanchors一样) - -- 是否需要: -当「源语言不在 index_languages」且「某些目标语言的翻译缺失」时,ES 里会额外加一层 用「原始 query 字符串」去撞缺失语种字段 - -- 检索相关性优化: -原始搜索词和翻译的词,都需要有对应的主干分析 -这个主干可以根据词性简单提取名词即可 -在搜索时,原始词和主干都成对地出现,原始词和trunk_keywords一起组成一个或查询。 -有一种方案是把原始词和主干词拼接起来。但是bm25要调tf系数。 - -2.5 图片相关性与向量字段调整 - -- "image_embedding": { - "type": "nested", - "properties": { - "vector": { - "type": "dense_vector", - "dims": 1024, - "index": true, - "similarity": "dot_product", - "element_type": "bfloat16" - }, - "url": { - "type": "text" - } - } -}, -去掉 image_embedding_512 -image_embedding改为,一个spu有多个sku向量,每个向量内部properties: -除了vector url还应该包括,该图片是对应哪些sku -"image_embedding": { - "type": "nested", - "properties": { - "vector": { - "type": "dense_vector", - "dims": 1024, - "index": true, - "similarity": "dot_product", - "element_type": "bfloat16" - }, - "url": { - "type": "text" - } - } -}, - -- 引入图片的相关性: -图片的向量最好做SKU维度,用 SPU 维度还是 SKU 维度? - 1. SKU维度(主款式,option1维度),如果用户搜索“蓝色 T恤”,这种图片相关性会比较有价值。 - 2. 我不考虑颜色的差异,其余的款式一般是大小之类的。这些图片,embedding细分到 SKU 维度,可能价值不大,性价比偏低 - -- 属性的筛选: -训练一个bert/transformer多分类模型,分类: 颜色、尺寸、材质 等等。但是要注意一些属性的值不规范、非常多,要考虑 是不是做规范化,如何规范化。 - -2.6 无结果重查与翻译缺失处理 - -- 无结果重查 -稀有语言,翻译可能超时(因为zh-en互译之外的翻译耗时更长) - - ---- - -3. 模型与推理服务优化 - -3.1 大模型API与本地部署 - -- 外部需求: - 1. 对推理能力要求很低、对耗时要求很高的大模型API(或者本地部署一个7b Q4量化的大模型),prompt大概30-50个token,首token响应要求500ms以内 - 2. ES支持reranker pipline? - -- 本地部署一个7b Q4量化的大模型 - -3.2 Embedding服务优化 - -- 先阅读文本embedding相关的代码: -@embeddings/README.md @embeddings/server.py @docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md @embeddings/text_encoder.py -目前有TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入限制,超限返回过载状态码。 - -- 文本embedding服务,要支持 priority 查询参数,priority > 0:不计入上述 inflight、不会因准入被拒绝(图片embedding不需要支持,因为只有离线需要用到图片embedding) -priority == 0(默认,适合做索引之类的离线任务):仍走原有 TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入;超限返回过载状态码。 -priority > 0(或者==1)(适合在线请求):不会因准入被拒绝,但是仍然需要占用inflight,这样保证在线请求不被限制,并且在线请求很多的时候可以拒绝掉离线的请求。 - -- 除了限制规则的修改,更进一步的,也需要保证这种请求是优先处理的(priority=1的相比=0的更优先被处理)。 -关于技术方案,有Worker + 双队列、PriorityMutex等等,除此之外,也请你思考合适的方案。 -成熟稳定、不带来复杂度、性能、稳定性方面的副作用,是最重要的。请先了解代码、需求,深度思考解决方案 - -- 向量的缓存 - -3.3 Reranker优化 - -- 多reranker: -改 reranker 服务,一次请求返回多路分 -服务启动时 加载多个 backend(或按请求懒加载),/rerank 响应扩展为例如 -scores: [...](兼容主后端)+ scores_by_backend: { "bge": [...], "qwen3_vllm": [...] }。 -搜索侧解析多路分,再融合或只透传 debug。 -优点:搜索侧仍只调一个 URL。缺点:单进程多大模型 显存压力很大; - -- 融合层要注意的一点 -fuse_scores_and_resort 目前只消费 一条 rerank_scores 序列,并写入 _rerank_score -多 backend 之后需要rerank_scores 都参与融合 - -- 必要性: -见 qwen3-reranker和bge-m3的严重badcase -不一定是要多reranker的方式,但是一定会需要解决方案。 - -- reranker 补充:nvidia/llama-nemotron-rerank-1b-v2 -https://huggingface.co/nvidia/llama-nemotron-rerank-1b-v2 -后端推理也建议使用vLLM -注意搜索相关资料,挖掘我的特斯拉 T4 GPU 的性能,充分挖掘性能 -你有充足的自由度进行实验 -encoder架构。 -比较新。 -性能更好。 -亚马逊 电商搜索数据集比qwen-reranker-4b更好。 -支持vLLM。 - -- Qwen3-Reranker-4B-GGUF -https://modelscope.cn/models/dengcao/Qwen3-Reranker-4B-GGUF/summary - 1. 要确定选择哪种量化方式 - 2. 确定提示词 - -- qwen3-embedding、qwen3-reranker (done) -选一个推理引擎,相比于我自己直接调 sentence-transformers,主要是多进程和负载均衡、连续批处理,比较有用 -当前结论:embedding 场景优先 TEI;vLLM 更偏向生成式与 rerank 场景。 - -- rerank 性能优化 - -3.4 翻译模型优化 - -- 翻译,增加facebook/nllb-200-distilled-600M -https://blog.csdn.net/qq_42746084/article/details/154947534 -https://huggingface.co/facebook/nllb-200-distilled-600M - -- 店铺的语言:英语能占到80%,所以专门增加一个en-zh的 -https://huggingface.co/Helsinki-NLP/opus-mt-zh-en -https://huggingface.co/Helsinki-NLP/opus-mt-en-zh - -- opus-mt-zh-en - - from transformers import AutoTokenizer, AutoModelForSeq2SeqLM - model_name = "./models/opus-mt-en-zh" - tokenizer = AutoTokenizer.from_pretrained(model_name) - model = AutoModelForSeq2SeqLM.from_pretrained(model_name) - data = 'test' - encoded = tokenizer([data], return_tensors="pt") - translation = model.generate(**encoded) - result = tokenizer.batch_decode(translation, skip_special_tokens=True)[0] - print(result) - -- nllb-200-distilled-600M性能优化 -已完成(2026-03) - - CTranslate2 迁移 + float16 转换 - - 扩展压测报告:perf_reports/20260318/translation_local_models_ct2/README.md - - T4 聚焦调优报告:perf_reports/20260318/translation_local_models_ct2_focus/README.md - - NLLB T4 商品标题专项报告:perf_reports/20260318/nllb_t4_product_names_ct2/README.md - - 当前结论: - - NLLB 在线默认推荐:ct2_inter_threads=4 + ct2_max_queued_batches=32 + ct2_batch_type=examples + ct2_decoding_length_mode=source(+8,min=32) - - opus-mt-zh-en 维持保守默认更稳 - - opus-mt-en-zh 如追求离线吞吐可继续做单独 profile - -- 请搜索nllb-200-distilled-600M这类seq2seq、transformer架构的模型,有哪些性能优化方案,提高线上翻译服务的吞吐量、降低耗时,搜索相关的在线推理服务方案,找到高性能的服务化方法 - -- 查看翻译的缓存情况 - -3.5 其他模型优化 - -- cnclip的性能优化 - - ---- - -4. 性能优化与超时配置 - -4.1 超时配置 - -- Query 分析阶段等待翻译/embedding 的硬超时 -配置文件位置:config/config.yaml -配置项:query_config.async_wait_timeout_ms: 80 -代码生效点:query/query_parser.py 使用该值换算成秒传给 wait(...) - -2. Embedding HTTP 调用超时(Text/Image) -不再使用任何环境变量覆盖(之前提到的 EMBEDDING_HTTP_TIMEOUT_SEC 已不采用) -配置文件位置:config/config.yaml -配置项:services.embedding.providers.http.timeout_sec(已在 YAML 里补了示例默认 60) -代码生效点: -embeddings/text_encoder.py:requests.post(..., timeout=self.timeout_sec) -embeddings/image_encoder.py:requests.post(..., timeout=self.timeout_sec) - -4.2 生成式服务优化(Partial Mode) - -- product_enrich : Partial Mode : done -https://help.aliyun.com/zh/model-studio/partial-mode?spm=a2c4g.11186623.help-menu-2400256.d_0_3_0_7.74a630119Ct6zR -需在messages 数组中将最后一条消息的 role 设置为 assistant,并在其 content 中提供前缀,在此消息中设置参数 "partial": true。messages格式如下: -[ - { - "role": "user", - "content": "请补全这个斐波那契函数,勿添加其它内容" - }, - { - "role": "assistant", - "content": "def calculate_fibonacci(n):\n if n <= 1:\n return n\n else:\n", - "partial": true - } -] -模型会以前缀内容为起点开始生成。 -支持 非思考模式。 - - ---- - -5. Elasticsearch相关 - -- es需要licence的两个功能,如果费用低,开通下licence,或者改es源码定制开发下,支持 rank.rrf,reranker - - { - "query": { ...全文检索... }, - "knn": { ...向量检索... }, - "rank": { - "rrf": {} - } - } - - ---- - -1. 配置体系重构 -Referring to @docs/config-system-review-and-redesign.md , most of the modifications have been completed. Could you conduct a review to check what else needs improvement in the configuration documentation system? Are there any outstanding issues? - -一、仍然存在大量通过环境变量获取配置的地方 -_SERVICE_KIND = (os.getenv("EMBEDDING_SERVICE_KIND", "all") or "all").strip().lower() -if _SERVICE_KIND not in {"all", "text", "image"}: - raise RuntimeError( - f"Invalid EMBEDDING_SERVICE_KIND={_SERVICE_KIND!r}; expected all, text, or image" - ) -_TEXT_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_TEXT_MODEL", "true").lower() in ("1", "true", "yes") -_IMAGE_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_IMAGE_MODEL", "true").lower() in ("1", "true", "yes") -open_text_model = _TEXT_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "text"} -open_image_model = _IMAGE_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "image"} - -_text_encode_lock = threading.Lock() -_image_encode_lock = threading.Lock() - -_TEXT_MICROBATCH_WINDOW_SEC = max( - 0.0, float(os.getenv("TEXT_MICROBATCH_WINDOW_MS", "4")) / 1000.0 -) -_TEXT_REQUEST_TIMEOUT_SEC = max( - 1.0, float(os.getenv("TEXT_REQUEST_TIMEOUT_SEC", "30")) -) -_TEXT_MAX_INFLIGHT = max(1, int(os.getenv("TEXT_MAX_INFLIGHT", "32"))) -_IMAGE_MAX_INFLIGHT = max(1, int(os.getenv("IMAGE_MAX_INFLIGHT", "1"))) -_OVERLOAD_STATUS_CODE = int(os.getenv("EMBEDDING_OVERLOAD_STATUS_CODE", "503")) -_LOG_PREVIEW_COUNT = max(1, int(os.getenv("EMBEDDING_LOG_PREVIEW_COUNT", "3"))) -_LOG_TEXT_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_TEXT_PREVIEW_CHARS", "120"))) -_LOG_IMAGE_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_IMAGE_PREVIEW_CHARS", "180"))) -_VECTOR_PREVIEW_DIMS = max(1, int(os.getenv("EMBEDDING_VECTOR_PREVIEW_DIMS", "6"))) -_CACHE_PREFIX = str(REDIS_CONFIG.get("embedding_cache_prefix", "embedding")).strip() or "embedding" - - - - - -还有这些写死的地址 @embedding/config.py - -self.TEI_BASE_URL = str(text_backend.get("base_url") or "http://127.0.0.1:8080") -self.TEI_TIMEOUT_SEC = int(text_backend.get("timeout_sec", 60)) - -self.USE_CLIP_AS_SERVICE = services.image_backend == "clip_as_service" -self.CLIP_AS_SERVICE_SERVER = str(image_backend.get("server") or "grpc://127.0.0.1:51000") - - - - -看起来似乎并没有完全遵循这些原则? -4. 重新设计的设计原则 -重新设计应遵循以下规则。 - -4.1 单一逻辑配置系统 -可以有多个文件,但不能有多个职责重叠的加载器。 -必须有一个加载器管道,能够生成一个类型化的 AppConfig 对象。 - -4.2 配置文件负责声明,解析代码负责解释,环境变量负责运行时注入 -职责应明确如下: -配置文件 -声明非敏感的目标行为和可部署的非敏感设置 -解析逻辑 -加载、合并、验证、规范化并暴露类型化的配置 -绝不发明隐藏的业务行为 -环境变量 -承载密钥和少量运行时/进程相关的值 -不随意地重新定义业务行为 - -4.3 整个系统采用单一的优先级规则 -除非明确豁免,否则每个配置类别都应遵循相同的合并模型。 - -4.4 业务行为不得有静默的隐式后备 -在启动时,如果必需的配置缺失或无效,应快速失败。 -不要静默地回退到诸如硬编码语言列表之类的遗留行为。 - -4.5 有效配置必须可观测 -每个服务都应能够展示: -配置版本或哈希值 -加载的源文件 -环境名称 -经过清理的有效配置 - -5. 推荐的目标设计 - -5.1 边界模型 -使用三个清晰的层级。 -层级 1:代码仓库管理的静态配置 -目的: -搜索行为 -租户行为 -提供商/后端注册表 -非敏感的服务拓扑默认值 -功能开关 -示例: -字段权重 -查询策略 -重排序融合参数 -租户语言方案 -翻译能力注册表 -嵌入后端选择默认值 - -层级 2:特定环境的层叠配置 -目的: -按环境区分的非敏感差异 -按环境区分的服务端点 -按环境区分的资源大小默认值 -开发/测试/生产环境的运维差异 -示例: -本地嵌入 URL 与生产环境嵌入 URL -开发环境重排序后端与生产环境重排序后端 -本地开发环境中较低的并发度 - -层级 3:环境变量 -目的: -密钥 -绑定主机/端口 -外部基础设施凭证 -容器编排器的最后一步注入 -示例: -ES_HOST, ES_USERNAME, ES_PASSWORD -DB_HOST, DB_USERNAME, DB_PASSWORD -REDIS_HOST, REDIS_PASSWORD -DASHSCOPE_API_KEY, DEEPL_AUTH_KEY -API_HOST, API_PORT, INDEXER_PORT, TRANSLATION_PORT -规则: -环境变量不应成为选择业务行为(如翻译模型、嵌入后端或租户语言策略)的常规途径 -如果允许对非敏感字段进行环境变量覆盖,则必须将其明确列出并记录为运维覆盖手段,而非隐藏的约定 - -5.2 统一的优先级 -推荐的优先级顺序: -代码中的模式(schema)默认值 -config/base.yaml -config/environments/.yaml -来自 config/tenants/ 的租户层叠配置 -针对明确允许的运行时键的环境变量 -仅适用于当前进程的命令行标志 -重要规则: -只能有一个模块实现此合并逻辑 -任何业务模块不得直接调用 os.getenv() 来获取配置 - - - -二、cursor(composer2)帮我审查的 - -## 配置文档系统审查 - -这是对配置文档系统的简要审查,对照 [`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 和当前代码库进行。 - -### 实现已匹配的部分 - -代码库已朝着重新设计的目标迈进了很大一步: - -- 单一类型化根节点:`AppConfig` 位于 [`config/schema.py`](config/schema.py),通过 [`config/loader.py`](config/loader.py) 和 [`get_app_config()`](config/loader.py) 加载。 -- 精简的 `services_config`:[`config/services_config.py`](config/services_config.py) 委托给 `get_app_config()`(解决了服务层的发现 A)。 -- 租户目录:[`config/tenant_config_loader.py`](config/tenant_config_loader.py) 使用 `get_app_config().tenants`。 -- 重写资产路径:`config/dictionaries/query_rewrite.dict` 已存在(解决了发现 E 中的文件名不匹配问题)。 -- 可观测性:[`GET /admin/config`](api/routes/admin.py) 返回经过脱敏处理的有效配置树;[`GET /admin/config/meta`](api/routes/admin.py) 暴露环境信息、`config_hash`、`loaded_files`、`deprecated_keys`(涵盖了 §5.10 的意图;文档中提议的是 `/admin/config/effective`,但实际实现位于 `/admin/config`)。 - -因此,"单一加载器 + 有效配置可见性" 的故事在代码中已基本实现;文档尚未完全跟上。 - ---- - -## 文档问题(影响最大) - -### 1. 管理 API 文档中关于 `/admin/config` 的描述错误 - -[`docs/搜索API对接指南-06-管理接口(Admin).md`](docs/搜索API对接指南-06-管理接口(Admin).md)(原单文件 `搜索API对接指南.md` 已拆分为分册)仍将 `/admin/config` 描述为按租户的 JSON(包含 `tenant_id`、`es_index_name`、`supported_languages` 等字段)。实际实现返回的是 `AppConfig.sanitized_dict()`(完整的应用配置,敏感信息已脱敏),而不是租户摘要字段。 - -这些指南中还缺少: `GET /admin/config/meta`。 - -健康检查: 拆分指南中的示例包含了 [`HealthResponse`](api/models.py) 中不存在的字段(只有 `status` 和 `elasticsearch`)。 - -对于任何仅根据文档进行 API 集成的人来说,这是最明显的"未解决问题"。 - -### 2. 面向开发者的指南仍将 `services_config` 作为"配置解析器"的核心 - -[`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §5.2 仍说搜索配置由 `ConfigLoader` 加载,服务由 `config/services_config` "解析"。§6.2 仍将 `config/services_config.py` 列为主要的"解析入口"。[`docs/QUICKSTART.md`](docs/QUICKSTART.md) §3.1 仍说"配置解析:`config/services_config.py`"。 - -文档中准确的说法应该是:规范入口是 `config/loader.py` + `get_app_config()`;[`config/config_loader.py`](config/config_loader.py) 中的 `ConfigLoader` 包装了统一加载器;`services_config` 是现有调用点的兼容性外观。 - -### 3. 重新设计文档本身不是"活的"状态文档 - -[`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 读起来仍是纯粹的问题陈述 + 目标,没有简短的**"已实现 vs 剩余"**部分。这很容易让人假设什么都没做,或者重复工作。添加一个小附录(或一页 `config/README.md` —— 见下文)可以解决这个问题。 - -### 4. 缺少 `config/README.md`(§5.3 中推荐) - -仍然没有专门的 `config/README.md` 来描述:加载器入口点、高级优先级、字典存放位置、指向 `/admin/config` + `/admin/config/meta` 的链接,以及重新设计文档的链接。这是重新设计中明确的交付物,可以锚定"文档系统"。 - -### 5. 轻微的文档整洁问题 - -- [`docs/QUICKSTART.md`](docs/QUICKSTART.md) §1.9 环境变量项目后的行有一个多余字符:`---·`(可能是打字错误)。 -- [`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §10 文档索引没有列出 `config-system-review-and-redesign.md` 或未来的 `config/README.md`。 - ---- - -## 重新设计目标与当前代码之间的差距(文档不应声称"已完成") - -这些影响文档的诚实度: - -| 主题 | 状态 | -|--------|--------| -| `config dump` CLI(§5.10) | `main.py` 中不存在;运维人员依赖 HTTP 或临时脚本。 | -| 隐藏的 `["en", "zh"]` 回退(阶段 3 / 发现 D) | 仍在 [`indexer/document_transformer.py`](indexer/document_transformer.py)、[`suggestion/builder.py`](suggestion/builder.py) 等中使用。 | -| 加载器外的 `os.getenv`(规则 1–2) | 仍在例如 [`embeddings/server.py`](embeddings/server.py)、[`reranker/server.py`](reranker/server.py)、[`api/app.py`](api/app.py) 中使用 —— 文档声称"仅加载器"将是夸大其词。 | -| 拆分 `base.yaml` / `environments/` / `tenants/*.yaml`(阶段 5) | 未采用;仍是单一的 [`config/config.yaml`](config/config.yaml)。 | -| 遗留租户标志(阶段 6 / 发现 H) | [`indexer/README.md`](indexer/README.md) 仍描述上游 MySQL 的 `translate_to_en` / `translate_to_zh`(这可能作为上游模式文档保留;需与 Python `tenant_config` 模型区分开来)。 | - ---- - -## 推荐的后续步骤(仅文档,按优先级排序) - -1. 修复管理 API 文档(合并指南 + `-06-` 拆分):`/admin/config` 的响应格式,添加 `/admin/config/meta`,使健康检查示例与 [`HealthResponse`](api/models.py) 一致。 -2. 更新 DEVELOPER_GUIDE §5–§6 和 QUICKSTART §1.9 / §3.1,将 `get_app_config()` / `loader.py` 描述为主要入口,将 `services_config` 描述为适配器。 -3. 添加 `config/README.md`(简短的操作 + 开发者入口)。 -4. 在 `config-system-review-and-redesign.md` 中添加带日期的实现状态表(已交付 vs 推迟的内容),使审查文档不与现实矛盾。 -5. DEVELOPER_GUIDE §9 检查清单:将"配置来自 `services_config`"替换为允许 `get_app_config()` 或精简适配器的语言,与 §6 保持一致。 - -如果需要,我可以在后续处理中为项目 1–3 和重新设计文档中的简短状态块应用补丁。 - -其他云API -1 -1)提供两个rerank云API_KEY给我:(优先级:高) -AWS Bedrock / Azure 两家云有提供的Cohere Rerank 3.5/4模型API,开通APIKEY -google云 Vertex AI Ranking API - -已经调研: -阿里云在美国地区没有提供任意reranker API -AWS Bedrock / Azure 两家云有提供Cohere Rerank 3.5 -google云Vertex AI Ranking API性能更好 - -以上两个APIKEY给我,我来测试性能和效果。 - - -2)寻找美国地区reranker API最佳实践(优先级:高) -效果要求:qwen3-reranker-4b(或者同等能力。可对比huggingface公开的评测指标)的API -性能要求:在我们的服务器上,一个请求内排序400条结果、耗时低于300ms -测试评估:基于电商领域商品搜索场景评估效果(我可以提供数据) -据我了解的Cohere Rerank可能达不到这个性能要求,可能可以考虑拆分为4个请求、每个100条,做到300ms以内可能可以。 -参考Cohere Rerank 3.5 benchmark: -https://docs.oracle.com/en-us/iaas/Content/generative-ai/benchmark-cohere-rerank-3-5.htm - - -3)提供谷歌翻译API的apikey (优先级:低) -给我apikey,我看下耗时,希望耗时P95低于80ms满足在线请求使用 -在线翻译的问题已经基本解决,这一块需求不是特别大。 - -2 -混用 大模型 使用:hunyuan-turbos-latest -混元 OpenAI 兼容接口相关调用示例:https://cloud.tencent.com/document/product/1729/111007 - -腾讯云 混元大模型 API_KEY:sk-mN2PiW2gp57B3ykxGs4QhvYxhPzXRZ2bcR5kPqadjboGYwiz - -hunyuan翻译:使用模型 hunyuan-translation -https://cloud.tencent.com/document/product/1729/113395#4.-.E7.A4.BA.E4.BE.8B - -谷歌翻译 基础版:https://docs.cloud.google.com/translate/docs/reference/rest/v2/translate - -阿里云 百炼模型 现在使用的apikey是国内的。 -各地域的 Base URL 和对应的 API Key 是绑定的。 - -现在使用了美国的服务器,使用了美国的地址,需要在 美国地域控制台页面(https://modelstudio.console.aliyun.com/us-east-1 )中创建或获取API_KEY: - -登录 百炼美国地域控制台:https://modelstudio.console.aliyun.com/us-east-1?spm=5176.2020520104.0.0.6b383a98WjpXff -在 API Key 管理 中创建或复制一个适用于美国地域的 Key - -搜索效果反馈: -做完一些短期优化后,需要做一些case驱动的优化。 -给到100条测试用例,每个搜索词,要记录请求ID、以及 希望排序靠前但是没有靠前的(比如希望出现在第一页但是没出现在第一页的)、以及未召回的商品ID(希望出现在前几页但是没翻到的) -6. 其他任务 - -- suggest 索引,现在是全量脚本,要交给金伟 \ No newline at end of file diff --git a/docs/issue.txt b/docs/issue.txt deleted file mode 100644 index 4fe7fb0..0000000 --- a/docs/issue.txt +++ /dev/null @@ -1,640 +0,0 @@ - - - -本地部署一个7b Q4量化的大模型 -es需要licence的两个功能,如果费用低,开通下licence,或者改es源码定制开发下,支持 rank.rrf,reranker - - - -把knn跟文本相关性的融合方式修改为 "rank": {"rrf": {} }需要licence,可以帮我修改源码支持吗? - - knn_boost: 2.0 - - -{ - "query": { ...全文检索... }, - "knn": { ...向量检索... }, - "rank": { - "rrf": {} - } -} - - -"image_embedding": { - "type": "nested", - "properties": { - "vector": { - "type": "dense_vector", - "dims": 1024, - "index": true, - "similarity": "dot_product", - "element_type": "bfloat16" - }, - "url": { - "type": "text" - } - } -}, -去掉 image_embedding_512 -image_embedding改为,一个spu有多个sku向量,每个向量内部properties: -除了vector url还应该包括,该图片是对应哪些sku -"image_embedding": { - "type": "nested", - "properties": { - "vector": { - "type": "dense_vector", - "dims": 1024, - "index": true, - "similarity": "dot_product", - "element_type": "bfloat16" - }, - "url": { - "type": "text" - } - } -}, - - -外部需求: -1. 对推理能力要求很低、对耗时要求很高的大模型API(或者本地部署一个7b Q4量化的大模型),prompt大概30-50个token,首token响应要求500ms以内 -2. ES支持reranker pipline? - - - - - - -增加款式意图识别模块 - -意图类型: 颜色,尺码(目前只需要支持这两种) - -意图召回层: -每种意图,有一个召回词集合 -对query(包括原始query、各种翻译query 都做匹配) - -以颜色意图为例: -有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 -query匹配了其中任何一个词,都认为,具有颜色意图 -匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 - -意图判断: 暂时留空,直接返回true。目前没有模型,即只要召回了(词表匹配了),即认为有该维度款式需求。 -(以后考虑建设fasttext/bert系列多分类模型) - - -意图使用: - -我们第一阶段,使用 参与ES提权。 - -一、参与ES提权 - - -二、参与reranker - - -如果有: 先做sku筛选,然后把最优的拼接到名称中,参与reranker。 - - -现在在reranker、分页之后、做填充的时候,已经有做sku的筛选。 -需要优化: -现在是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。改为 -1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 -2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 -3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 -这个sku筛选也需要提取为一个独立的模块。 - - -另外:现在是reranker、分页之后做sku筛选,要改为: -1. 有款式意图的时候,才做sku筛选 -2. sku筛选的时机,改为在reranker之前,对所有内容做sku筛选,然后 -3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、olors、、、、),遍历option1_name,option2_name,option3_name,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 -4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) -5. TODO : 还有一个问题。 目前,sku只返回一个维度(店铺主维度。默认应该是option1,不是所有维度的sku信息都返回的。所以,如果有款式意图,但是主维度是颜色,那么拿不到全的款式sku) - - -当前项目功能已经较多,但是有清晰的框架,请务必基于现有框架进行改造,不要进行补丁式的修改,避免代码逻辑分叉。 -请一步一步来,先设计意图识别模块,仔细思考需求,意图识别模块需要提供哪些内容,用于返回数据接口的定义,深度思考,定义一个合理的接口后,再给出合理的模块设计。 - - - - -文本相关性: -调研: -Princeton WordNet — 英文同义词底库 -Shopify Product Taxonomy — 电商品类标准 -Querqy — 电商搜索规则框架 -gensimpson/elasticsearch-synonyms — ES 同义词规则落地 - - -tags字段使用的优化: -现在是keyword,在搜索中,不太好使用(目前主要用于suggest)。 -可以考虑也拆分多语言,配合analyzer使用(和qanchors一样) - - - - -是否需要: -当「源语言不在 index_languages」且「某些目标语言的翻译缺失」时,ES 里会额外加一层 用「原始 query 字符串」去撞缺失语种字段 - - - -先阅读文本embedding相关的代码: -@embeddings/README.md @embeddings/server.py @docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md @embeddings/text_encoder.py -目前有TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入限制,超限返回过载状态码。 - -文本embedding服务,要支持 priority 查询参数,priority > 0:不计入上述 inflight、不会因准入被拒绝(图片embedding不需要支持,因为只有离线需要用到图片embedding) -priority == 0(默认,适合做索引之类的离线任务):仍走原有 TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入;超限返回过载状态码。 -priority > 0(或者==1)(适合在线请求):不会因准入被拒绝,但是仍然需要占用inflight,这样保证在线请求不被限制,并且在线请求很多的时候可以拒绝掉离线的请求。 - -除了限制规则的修改,更进一步的,也需要保证这种请求是优先处理的(priority=1的相比=0的更优先被处理)。 -关于技术方案,有Worker + 双队列、PriorityMutex等等,除此之外,也请你思考合适的方案。 -成熟稳定、不带来复杂度、性能、稳定性方面的副作用,是最重要的。请先了解代码、需求,深度思考解决方案 - - - -配置体系的重构。 - -Referring to @docs/config-system-review-and-redesign.md , most of the modifications have been completed. Could you conduct a review to check what else needs improvement in the configuration documentation system? Are there any outstanding issues? - -一、仍然存在大量通过环境变量获取配置的地方 -_SERVICE_KIND = (os.getenv("EMBEDDING_SERVICE_KIND", "all") or "all").strip().lower() -if _SERVICE_KIND not in {"all", "text", "image"}: - raise RuntimeError( - f"Invalid EMBEDDING_SERVICE_KIND={_SERVICE_KIND!r}; expected all, text, or image" - ) -_TEXT_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_TEXT_MODEL", "true").lower() in ("1", "true", "yes") -_IMAGE_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_IMAGE_MODEL", "true").lower() in ("1", "true", "yes") -open_text_model = _TEXT_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "text"} -open_image_model = _IMAGE_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "image"} - -_text_encode_lock = threading.Lock() -_image_encode_lock = threading.Lock() - -_TEXT_MICROBATCH_WINDOW_SEC = max( - 0.0, float(os.getenv("TEXT_MICROBATCH_WINDOW_MS", "4")) / 1000.0 -) -_TEXT_REQUEST_TIMEOUT_SEC = max( - 1.0, float(os.getenv("TEXT_REQUEST_TIMEOUT_SEC", "30")) -) -_TEXT_MAX_INFLIGHT = max(1, int(os.getenv("TEXT_MAX_INFLIGHT", "32"))) -_IMAGE_MAX_INFLIGHT = max(1, int(os.getenv("IMAGE_MAX_INFLIGHT", "1"))) -_OVERLOAD_STATUS_CODE = int(os.getenv("EMBEDDING_OVERLOAD_STATUS_CODE", "503")) -_LOG_PREVIEW_COUNT = max(1, int(os.getenv("EMBEDDING_LOG_PREVIEW_COUNT", "3"))) -_LOG_TEXT_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_TEXT_PREVIEW_CHARS", "120"))) -_LOG_IMAGE_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_IMAGE_PREVIEW_CHARS", "180"))) -_VECTOR_PREVIEW_DIMS = max(1, int(os.getenv("EMBEDDING_VECTOR_PREVIEW_DIMS", "6"))) -_CACHE_PREFIX = str(REDIS_CONFIG.get("embedding_cache_prefix", "embedding")).strip() or "embedding" - - - - - -还有这些写死的地址 @embedding/config.py - -self.TEI_BASE_URL = str(text_backend.get("base_url") or "http://127.0.0.1:8080") -self.TEI_TIMEOUT_SEC = int(text_backend.get("timeout_sec", 60)) - -self.USE_CLIP_AS_SERVICE = services.image_backend == "clip_as_service" -self.CLIP_AS_SERVICE_SERVER = str(image_backend.get("server") or "grpc://127.0.0.1:51000") - - - - -看起来似乎并没有完全遵循这些原则? -4. 重新设计的设计原则 -重新设计应遵循以下规则。 - -4.1 单一逻辑配置系统 -可以有多个文件,但不能有多个职责重叠的加载器。 -必须有一个加载器管道,能够生成一个类型化的 AppConfig 对象。 - -4.2 配置文件负责声明,解析代码负责解释,环境变量负责运行时注入 -职责应明确如下: -配置文件 -声明非敏感的目标行为和可部署的非敏感设置 -解析逻辑 -加载、合并、验证、规范化并暴露类型化的配置 -绝不发明隐藏的业务行为 -环境变量 -承载密钥和少量运行时/进程相关的值 -不随意地重新定义业务行为 - -4.3 整个系统采用单一的优先级规则 -除非明确豁免,否则每个配置类别都应遵循相同的合并模型。 - -4.4 业务行为不得有静默的隐式后备 -在启动时,如果必需的配置缺失或无效,应快速失败。 -不要静默地回退到诸如硬编码语言列表之类的遗留行为。 - -4.5 有效配置必须可观测 -每个服务都应能够展示: -配置版本或哈希值 -加载的源文件 -环境名称 -经过清理的有效配置 - -5. 推荐的目标设计 - -5.1 边界模型 -使用三个清晰的层级。 -层级 1:代码仓库管理的静态配置 -目的: -搜索行为 -租户行为 -提供商/后端注册表 -非敏感的服务拓扑默认值 -功能开关 -示例: -字段权重 -查询策略 -重排序融合参数 -租户语言方案 -翻译能力注册表 -嵌入后端选择默认值 - -层级 2:特定环境的层叠配置 -目的: -按环境区分的非敏感差异 -按环境区分的服务端点 -按环境区分的资源大小默认值 -开发/测试/生产环境的运维差异 -示例: -本地嵌入 URL 与生产环境嵌入 URL -开发环境重排序后端与生产环境重排序后端 -本地开发环境中较低的并发度 - -层级 3:环境变量 -目的: -密钥 -绑定主机/端口 -外部基础设施凭证 -容器编排器的最后一步注入 -示例: -ES_HOST, ES_USERNAME, ES_PASSWORD -DB_HOST, DB_USERNAME, DB_PASSWORD -REDIS_HOST, REDIS_PASSWORD -DASHSCOPE_API_KEY, DEEPL_AUTH_KEY -API_HOST, API_PORT, INDEXER_PORT, TRANSLATION_PORT -规则: -环境变量不应成为选择业务行为(如翻译模型、嵌入后端或租户语言策略)的常规途径 -如果允许对非敏感字段进行环境变量覆盖,则必须将其明确列出并记录为运维覆盖手段,而非隐藏的约定 - -5.2 统一的优先级 -推荐的优先级顺序: -代码中的模式(schema)默认值 -config/base.yaml -config/environments/.yaml -来自 config/tenants/ 的租户层叠配置 -针对明确允许的运行时键的环境变量 -仅适用于当前进程的命令行标志 -重要规则: -只能有一个模块实现此合并逻辑 -任何业务模块不得直接调用 os.getenv() 来获取配置 - - - -二、cursor(composer2)帮我审查的 - -## 配置文档系统审查 - -这是对**配置文档系统**的简要审查,对照 [`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 和当前代码库进行。 - -### 实现已匹配的部分 - -代码库已朝着重新设计的目标迈进了很大一步: - -- **单一类型化根节点**:`AppConfig` 位于 [`config/schema.py`](config/schema.py),通过 [`config/loader.py`](config/loader.py) 和 [`get_app_config()`](config/loader.py) 加载。 -- **精简的 `services_config`**:[`config/services_config.py`](config/services_config.py) 委托给 `get_app_config()`(解决了服务层的发现 A)。 -- **租户目录**:[`config/tenant_config_loader.py`](config/tenant_config_loader.py) 使用 `get_app_config().tenants`。 -- **重写资产路径**:`config/dictionaries/query_rewrite.dict` 已存在(解决了发现 E 中的文件名不匹配问题)。 -- **可观测性**:[`GET /admin/config`](api/routes/admin.py) 返回**经过脱敏处理的有效配置树**;[`GET /admin/config/meta`](api/routes/admin.py) 暴露环境信息、`config_hash`、`loaded_files`、`deprecated_keys`(涵盖了 §5.10 的意图;文档中提议的是 `/admin/config/effective`,但实际实现位于 `/admin/config`)。 - -因此,**"单一加载器 + 有效配置可见性"** 的故事在代码中已基本实现;文档尚未完全跟上。 - ---- - -## 文档问题(影响最大) - -### 1. 管理 API 文档中关于 `/admin/config` 的描述错误 - -[`docs/搜索API对接指南-06-管理接口(Admin).md`](docs/搜索API对接指南-06-管理接口(Admin).md)(原单文件 `搜索API对接指南.md` 已拆分为分册)仍将 `/admin/config` 描述为**按租户**的 JSON(包含 `tenant_id`、`es_index_name`、`supported_languages` 等字段)。实际实现返回的是 **`AppConfig.sanitized_dict()`**(完整的应用配置,敏感信息已脱敏),而不是租户摘要字段。 - -**这些指南中还缺少:** `GET /admin/config/meta`。 - -**健康检查:** 拆分指南中的示例包含了 [`HealthResponse`](api/models.py) 中不存在的字段(只有 `status` 和 `elasticsearch`)。 - -对于任何仅根据文档进行 API 集成的人来说,这是最明显的"未解决问题"。 - -### 2. 面向开发者的指南仍将 `services_config` 作为"配置解析器"的核心 - -[`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §5.2 仍说搜索配置由 **`ConfigLoader`** 加载,服务由 **`config/services_config`** "解析"。§6.2 仍将 **`config/services_config.py`** 列为主要的"解析入口"。[`docs/QUICKSTART.md`](docs/QUICKSTART.md) §3.1 仍说"配置解析:`config/services_config.py`"。 - -文档中准确的说法应该是:**规范入口是 `config/loader.py` + `get_app_config()`**;[`config/config_loader.py`](config/config_loader.py) 中的 `ConfigLoader` 包装了统一加载器;`services_config` 是现有调用点的**兼容性外观**。 - -### 3. 重新设计文档本身不是"活的"状态文档 - -[`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 读起来仍是**纯粹的问题陈述 + 目标**,没有简短的**"已实现 vs 剩余"**部分。这很容易让人假设什么都没做,或者重复工作。添加一个小附录(或一页 `config/README.md` —— 见下文)可以解决这个问题。 - -### 4. 缺少 `config/README.md`(§5.3 中推荐) - -仍然没有专门的 **`config/README.md`** 来描述:加载器入口点、高级优先级、字典存放位置、指向 `/admin/config` + `/admin/config/meta` 的链接,以及重新设计文档的链接。这是重新设计中明确的交付物,可以锚定"文档系统"。 - -### 5. 轻微的文档整洁问题 - -- [`docs/QUICKSTART.md`](docs/QUICKSTART.md) §1.9 环境变量项目后的行有一个多余字符:`---·`(可能是打字错误)。 -- [`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §10 **文档索引**没有列出 `config-system-review-and-redesign.md` 或未来的 `config/README.md`。 - ---- - -## 重新设计目标与当前代码之间的差距(文档不应声称"已完成") - -这些影响文档的诚实度: - -| 主题 | 状态 | -|--------|--------| -| **`config dump` CLI**(§5.10) | `main.py` 中不存在;运维人员依赖 HTTP 或临时脚本。 | -| **隐藏的 `["en", "zh"]` 回退**(阶段 3 / 发现 D) | 仍在 [`indexer/document_transformer.py`](indexer/document_transformer.py)、[`suggestion/builder.py`](suggestion/builder.py) 等中使用。 | -| **加载器外的 `os.getenv`**(规则 1–2) | 仍在例如 [`embeddings/server.py`](embeddings/server.py)、[`reranker/server.py`](reranker/server.py)、[`api/app.py`](api/app.py) 中使用 —— 文档声称"仅加载器"将是夸大其词。 | -| **拆分 `base.yaml` / `environments/` / `tenants/*.yaml`**(阶段 5) | 未采用;仍是单一的 [`config/config.yaml`](config/config.yaml)。 | -| **遗留租户标志**(阶段 6 / 发现 H) | [`indexer/README.md`](indexer/README.md) 仍描述上游 MySQL 的 `translate_to_en` / `translate_to_zh`(这可能作为**上游模式**文档保留;需与 Python `tenant_config` 模型区分开来)。 | - ---- - -## 推荐的后续步骤(仅文档,按优先级排序) - -1. **修复管理 API 文档**(合并指南 + `-06-` 拆分):`/admin/config` 的响应格式,添加 `/admin/config/meta`,使健康检查示例与 [`HealthResponse`](api/models.py) 一致。 -2. **更新 DEVELOPER_GUIDE §5–§6 和 QUICKSTART §1.9 / §3.1**,将 **`get_app_config()` / `loader.py`** 描述为主要入口,将 **`services_config`** 描述为适配器。 -3. **添加 `config/README.md`**(简短的操作 + 开发者入口)。 -4. **在 `config-system-review-and-redesign.md` 中添加**带日期的**实现状态表**(已交付 vs 推迟的内容),使审查文档不与现实矛盾。 -5. **DEVELOPER_GUIDE §9 检查清单**:将"配置来自 `services_config`"替换为允许 `get_app_config()` 或精简适配器的语言,与 §6 保持一致。 - -如果需要,我可以在后续处理中为项目 1–3 和重新设计文档中的简短状态块应用补丁。 - - - - -筛选SKU: 先只筛选第一个维度,但考虑到用户搜索词可能带了尺码,所以第二、三个维度也要考虑 - - -引入图片的相关性: -图片的向量最好做SKU维度,用 SPU 维度还是 SKU 维度? -1. SKU维度(主款式,option1维度),如果用户搜索“蓝色 T恤”,这种图片相关性会比较有价值。 -2. 我不考虑颜色的差异,其余的款式一般是大小之类的。这些图片,项链细粉到 SKU 维度,可能价值不大,性价比偏低 - - - -属性的筛选: -训练一个bert/transformer多分类模型,分类: 颜色、尺寸、材质 等等。但是要注意一些属性的值不规范、非常多,要考虑 是不是做规范化,如何规范化。 - - - - -无结果重查 -稀有语言,翻译可能超时(因为zh-en互译之外的翻译耗时更长) - - - - - - -检索相关性优化: -原始搜索词和翻译的词,都需要有对应的主干分析 -这个主干可以根据词性简单提取名词即可 -在搜索时,原始词和主干都成对地出现,原始词和trunk_keywords一起组成一个或查询。 -有一种方案是把原始词和主干词拼接起来。但是bm25要调tf系数。 - - - - -nllb-200-distilled-600M性能优化 -已完成(2026-03) -- CTranslate2 迁移 + float16 转换 -- 扩展压测报告:`perf_reports/20260318/translation_local_models_ct2/README.md` -- T4 聚焦调优报告:`perf_reports/20260318/translation_local_models_ct2_focus/README.md` -- NLLB T4 商品标题专项报告:`perf_reports/20260318/nllb_t4_product_names_ct2/README.md` -- 当前结论: - - NLLB 在线默认推荐:`ct2_inter_threads=4 + ct2_max_queued_batches=32 + ct2_batch_type=examples + ct2_decoding_length_mode=source(+8,min=32)` - - `opus-mt-zh-en` 维持保守默认更稳 - - `opus-mt-en-zh` 如追求离线吞吐可继续做单独 profile - -请搜索nllb-200-distilled-600M这类seq2seq、transformer架构的模型,有哪些性能优化方案,提高线上翻译服务的吞吐量、降低耗时,搜索相关的在线推理服务方案,找到高性能的服务化方法 - -cnclip的性能优化 - -rerank 性能优化 - - -超时 -Query 分析阶段等待翻译/embedding 的硬超时 -配置文件位置:config/config.yaml -配置项:query_config.async_wait_timeout_ms: 80 -代码生效点:query/query_parser.py 使用该值换算成秒传给 wait(...) -2) Embedding HTTP 调用超时(Text/Image) -不再使用任何环境变量覆盖(之前提到的 EMBEDDING_HTTP_TIMEOUT_SEC 已不采用) -配置文件位置:config/config.yaml -配置项:services.embedding.providers.http.timeout_sec(已在 YAML 里补了示例默认 60) -代码生效点: -embeddings/text_encoder.py:requests.post(..., timeout=self.timeout_sec) -embeddings/image_encoder.py:requests.post(..., timeout=self.timeout_sec) - - - - - - - - - - - -多reranker: - -改 reranker 服务,一次请求返回多路分 -服务启动时 加载多个 backend(或按请求懒加载),/rerank 响应扩展为例如 -scores: [...](兼容主后端)+ scores_by_backend: { "bge": [...], "qwen3_vllm": [...] }。 -搜索侧解析多路分,再融合或只透传 debug。 -优点:搜索侧仍只调一个 URL。缺点:单进程多大模型 显存压力很大; - -融合层要注意的一点 -fuse_scores_and_resort 目前只消费 一条 rerank_scores 序列,并写入 _rerank_score -多 backend 之后需要rerank_scores 都参与融合 - -必要性: -见 qwen3-reranker和bge-m3的严重badcase -不一定是要多reranker的方式,但是一定会需要解决方案。 - - - - -product_enrich : Partial Mode : done -https://help.aliyun.com/zh/model-studio/partial-mode?spm=a2c4g.11186623.help-menu-2400256.d_0_3_0_7.74a630119Ct6zR -需在messages 数组中将最后一条消息的 role 设置为 assistant,并在其 content 中提供前缀,在此消息中设置参数 "partial": true。messages格式如下: -[ - { - "role": "user", - "content": "请补全这个斐波那契函数,勿添加其它内容" - }, - { - "role": "assistant", - "content": "def calculate_fibonacci(n):\n if n <= 1:\n return n\n else:\n", - "partial": true - } -] -模型会以前缀内容为起点开始生成。 -支持 非思考模式。 - - - - - -融合打分(已完成,2026-03) - -以下已经完成: -1. `fuse_scores_and_resort` 已改为乘法融合,并通过 `matched_queries` 提取: - - `base_query` - - `base_query_trans_*` - - `fallback_original_query_*` - - `knn_query` -2. 文本相关性大分不再依赖 `phrase_query` / `keywords_query`,这两类查询已清理。 -3. 当前融合策略: - - `text_score = primary(weighted_source, weighted_translation, weighted_fallback) + 0.25 * support` - - `fused_score = (rerank_score + 0.00001) * (text_score + 0.1) ** 0.35 * (knn_score + 0.6) ** 0.2` -4. `track_scores` 与 `include_named_queries_score` 已接入,调试字段与评估方法已同步到: - - `docs/相关性检索优化说明.md` - - `docs/搜索API对接指南-01-搜索接口.md`(分册;原单文件已拆分) - - `docs/Usage-Guide.md` - -未完成的: -(归一化、次序融合?还乘法公式?) -RRF:先把多路召回稳妥融合 -linear + minmax:让你能精调 knn 和文本的权重 -reranker:对前面召回出来的 top-k 再做“最后一刀” - - - - -suggest 索引,现在是全量脚本,要交给金伟 - - - -翻译,增加facebook/nllb-200-distilled-600M -https://blog.csdn.net/qq_42746084/article/details/154947534 -https://huggingface.co/facebook/nllb-200-distilled-600M - - -店铺的语言:英语能占到80%,所以专门增加一个en-zh的 -https://huggingface.co/Helsinki-NLP/opus-mt-zh-en -https://huggingface.co/Helsinki-NLP/opus-mt-en-zh - - -opus-mt-zh-en - -from transformers import AutoTokenizer, AutoModelForSeq2SeqLM -model_name = "./models/opus-mt-en-zh" -tokenizer = AutoTokenizer.from_pretrained(model_name) -model = AutoModelForSeq2SeqLM.from_pretrained(model_name) -data = 'test' -encoded = tokenizer([data], return_tensors="pt") -translation = model.generate(**encoded) -result = tokenizer.batch_decode(translation, skip_special_tokens=True)[0] -print(result) - - - - - -Qwen3-Reranker-4B-GGUF -https://modelscope.cn/models/dengcao/Qwen3-Reranker-4B-GGUF/summary -1. 要确定选择哪种量化方式 -2. 确定提示词 - - - -reranker 补充:nvidia/llama-nemotron-rerank-1b-v2 -https://huggingface.co/nvidia/llama-nemotron-rerank-1b-v2 -后端推理也建议使用vLLM -注意搜索相关资料,挖掘我的特斯拉 T4 GPU 的性能,充分挖掘性能 -你有充足的自由度进行实验 -encoder架构。 -比较新。 -性能更好。 -亚马逊 电商搜索数据集比qwen-reranker-4b更好。 -支持vLLM。 - - - - - -查看翻译的缓存情况 - -向量的缓存 - - - - - - - - -AI - 生产 - MySQL -HOST:10.200.16.14 / localhost -端口:3316 -用户名:root -密码:qY8tgodLoA&KT#yQ - -AI - 生产 - Redis -HOST:10.200.16.14 / localhost -端口:6479 -密码:dxEkegEZ@C5SXWKv - - -远程登录方式: -# redis -redis-cli -h 43.166.252.75 -p 6479 - -# mysql 3个用户,都可以远程登录 -mysql -uroot -p'qY8tgodLoA&KT#yQ' -CREATE USER 'saas'@'%' IDENTIFIED BY '6dlpco6dVGuqzt^l'; -CREATE USER 'sa'@'%' IDENTIFIED BY 'C#HU!GPps7ck8tsM'; - - - -ES: -HOST:10.200.16.14 / localhost -端口:9200 -访问示例: -用户名密码:saas:4hOaLaf41y2VuI8y - - -安装 nvidia-container-toolkit (done) -https://mirrors.aliyun.com/github/releases/NVIDIA/nvidia-container-toolkit/ -https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html - - -qwen3-embedding、qwen3-reranker (done) -选一个推理引擎,相比于我自己直接调 sentence-transformers,主要是多进程和负载均衡、连续批处理,比较有用 -当前结论:embedding 场景优先 TEI;vLLM 更偏向生成式与 rerank 场景。 - - - -混用 大模型 使用:hunyuan-turbos-latest -混元 OpenAI 兼容接口相关调用示例:https://cloud.tencent.com/document/product/1729/111007 - - -腾讯云 混元大模型 API_KEY:sk-mN2PiW2gp57B3ykxGs4QhvYxhPzXRZ2bcR5kPqadjboGYwiz - -hunyuan翻译:使用模型 hunyuan-translation -https://cloud.tencent.com/document/product/1729/113395#4.-.E7.A4.BA.E4.BE.8B - - -谷歌翻译 基础版:https://docs.cloud.google.com/translate/docs/reference/rest/v2/translate - - - - -阿里云 百炼模型 现在使用的apikey是国内的。 -各地域的 Base URL 和对应的 API Key 是绑定的。 - -现在使用了美国的服务器,使用了美国的地址,需要在 美国地域控制台页面(https://modelstudio.console.aliyun.com/us-east-1 )中创建或获取API_KEY: - -登录 百炼美国地域控制台:https://modelstudio.console.aliyun.com/us-east-1?spm=5176.2020520104.0.0.6b383a98WjpXff -在 API Key 管理 中创建或复制一个适用于美国地域的 Key diff --git a/docs/issues/issue-2026-03-21-ES能力提升.md b/docs/issues/issue-2026-03-21-ES能力提升.md new file mode 100644 index 0000000..1250b29 --- /dev/null +++ b/docs/issues/issue-2026-03-21-ES能力提升.md @@ -0,0 +1,70 @@ +ES 付费版本 or 定制开发(建议先看下付费版本价格) +ES定制开发: +RRF / retrievers + +Elastic 的订阅矩阵里明确列了这些相关能力:Retrievers: linear, rule, RRF, text similarity re-ranker,以及 Reciprocal Rank Fusion (RRF) for hybrid search。 + +这类能力最有价值的点是: +它们把混合检索从“自己拼 DSL 和手搓打分”变成了官方支持的多阶段检索框架。重排:text similarity re-ranker / Elastic Rerank. text_similarity_reranker 用 NLP 模型对 top-k 结果按语义相似度重新排序;它可以用内置的 Elastic Rerank,也可以接 Cohere、Vertex AI,或者你自己上传的 text similarity 模型。 + +{ + "retriever": { + "rrf": { + "retrievers": [ + { "standard": { "query": { ... } } }, + { "knn": { ... } } + ] + } + } +} + + +加reranker: +text_similarity_reranker 用 NLP 模型对 top-k 结果按语义相似度重新排序;它可以用内置的 Elastic Rerank,也可以接 Cohere、Vertex AI,或者你自己上传的 text similarity 模型。 + +{ + "retriever": { + "text_similarity_reranker": { + "retriever": { + "rrf": { ... } + }, + ... + } + } +} + +{ + "retriever": { + "text_similarity_reranker": { + "retriever": { + "rrf": { + "retrievers": [ + { + "standard": { + "query": { + "...": "..." + } + } + }, + { + "knn": { + "...": "..." + } + } + ], + "rank_window_size": 100, + "rank_constant": 20 + } + }, + "field": "your_rerank_text_field", + "inference_text": "白色 oversized T-shirt", + "inference_id": ".rerank-v1-elasticsearch", + "rank_window_size": 50 + } + }, + "size": 20 +} + + + + diff --git a/docs/issues/issue-2026-03-21-意图判断-done03-24.md b/docs/issues/issue-2026-03-21-意图判断-done03-24.md new file mode 100644 index 0000000..91e82f5 --- /dev/null +++ b/docs/issues/issue-2026-03-21-意图判断-done03-24.md @@ -0,0 +1,268 @@ + + +增加款式意图识别模块。意图类型: 颜色,尺码(目前只需要支持这两种) + +一、 意图判断 +- 意图召回层: +每种意图,有一个召回词集合 +对query(包括原始query、各种翻译query 都做匹配) +- 以颜色意图为例: +有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 +query匹配了其中任何一个词,都认为,具有颜色意图 +匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 + +二、 意图使用: + 当前 SKU 置顶逻辑在「分页 + 详情回填」之后 +流程是:run_rerank → 按 from/size 切片 → page fill → _apply_sku_sorting_for_page_hits → ResultFormatter + 要改为: + 1. 有款式意图的时候,才做sku筛选 + 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选 + 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、colors...),遍历spu的option1_name,option2_name,option3_name字段,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 + 1. 比如匹配到option2_name,那么取每一个sku的option2_values。如果没匹配到任何一个,那么把三个属性值都用空格拼接起来。这个值要记录下来。有两个作用: + 1. 用来跟query匹配,看哪个更query相关性更高,以此进行最优sku筛选,把选出来的sku置顶,并替换spu的image_url + 2. 用来做rerank doc的title补充,从而参与rerank + 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) + +- sku筛选的规则也要优化: +现在的逻辑是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。没有匹配的再用embedding相似度。 +改为: + 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 + 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 + 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 + 这个sku筛选也需要提取为一个独立的模块。 + +细节备注: +intent 考虑由 QueryParser 编排、具体实现拆成独立模块,主义好,现有的分词等基础设施的复用,缺失的英文分词可以补充。 +在重排窗口内,第一次 ES 查询会把 _source 裁成「重排模板需要的字段」,默认只有 title 等,不包含 skus / option*_name。因此,有意图的时候,需要给这一次的_source加上 skus / option*_name + +先仔细理解需求,查看代码,深度思考应该如何设计,和当前的系统较好的融合,给出统一的设计,可以根据需要适当改造当前的实现,降低整个系统的复杂度,提高模块化程度,而不是打补丁。修改后的最终状态应该是要足够简单、清晰、无冗余和分叉,模块间低耦合。多步思考确认最佳施工方案之后才进行代码修改。 + +5. TODO: 搜索接口里,results[].skus 不是全量子 SKU:由 sku_filter_dimension 控制在应用层按维度分组折叠,每个「维度取值组合」只保留一条 SKU(组内第一条)。请求未传该字段时,Pydantic 默认是 ["option1"],等价于只按 option1_value 去重;服务端不会读取店铺主题的「主展示维」,需调用方与装修配置对齐并传入正确维度。因此当用户有款式等更细粒度意图、而款式落在 option2/option3(或对应 option*_name)时,若仍用默认只按 option1(常见为颜色)折叠,同一颜色下多种款式只会出现一条代表 SKU,无法从返回的 skus 里拿到该颜色下的全部款式行。(若业务需要全量子款,需传包含对应维度的 sku_filter_dimension,或传 null/[] 跳过折叠——以当前 ResultFormatter 实现为准。) + + + + + +GPT5.4 主要思路: +我准备按这条线改,尽量把逻辑收束成两个新模块而不是继续堆在 Searcher 里: + +QueryParser 负责产出统一的“款式意图分析结果”,包含:命中的意图类型、标准化值、命中的 query 变体、分词结果、维度候选词。 +独立的 SKU 选择模块负责:维度解析、候选值抽取、三阶段匹配、选中 SKU 后的主图替换,以及生成 rerank 用的属性后缀。 +Searcher 只编排:解析 query -> 首次 ES 召回 -> 若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本 -> run_rerank -> 分页 -> page fill -> ResultFormatter。这样旧的页内 SKU 置顶逻辑可以下沉或删除,避免前后两个阶段重复做同一件事。我再补看一下你最近写的意图草稿文档,确认有没有现成词表示例或边界条件,避免我这边把词表格式和你预期做偏。 + + + + + +意图匹配的性能优化: +上面一版实现,性能上完全无法接受。因此进行了一轮策略简化 + +prompt +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 + + +修改结果: +核心变化在 `search/sku_intent_selector.py` 和 `search/searcher.py`。 + +现在的策略是: +- `option_name` 只做规范化后 alias 精确匹配;任何意图维度没 resolve 到字段,就直接不选 SKU、不加 suffix。 +- `option_value` 文本匹配改成顺序扫描 SKU,找到第一个命中的就直接返回,不再收集多个候选再比较。 +- 文本匹配规则支持两种: + - query 命中的意图词被属性值包含 + - 属性值被 query/translation 包含 +- 只有当所有 SKU 都没有文本命中时,才做 embedding 选择。 +- `prepare_hits()` 现在只产出决策和 `_style_rerank_suffix`,不再提前改 `_source`;真正的 `image_url` 替换和 SKU 置顶统一后移到 `apply_precomputed_decisions()`。 +- 加了请求级缓存,避免同一个 `option_value`/`selection_text` 在一次请求里重复做文本判断、取向量和算相似度。 +- 顺手删掉了旧的 generalized match / fallback_text 路径,代码比之前更短也更直。 + +另外补了回归测试,覆盖了: +- 翻译文本命中后正确选 SKU +- 多个文本命中时取第一个 +- `option_name` 不命中 alias 时不做 SKU 选择 +- 无文本命中时走 embedding fallback + +验证过: +- `pytest tests/test_search_rerank_window.py -q` 通过 +- 变更文件 lint 无报错 + + +------------------------------ + + + +--- + +## 1. 现状(与需求的差距) + +**流水线**(`search/searcher.py`)大致是: + +1. `QueryParser.parse` → `ParsedQuery`(含 `translations`、`query_tokens` 等) +2. 组 ES 查询;若在重排窗口内,第一次查询把 `_source` 裁成「重排模板所需字段」(`_resolve_rerank_source_filter`) +3. ES 搜索 → `run_rerank`(`rerank_client.build_docs_from_hits` 用 `{title}` 等拼 doc) +4. 按 `from/size` 切片 → **page fill** 用 `ids` 查询把当前页 `_source` 补全 +5. `_apply_sku_sorting_for_page_hits`(仅 **option1**,先子串包含命中第一个,否则全量 option1 embedding) +6. `ResultFormatter`(`sku_filter_dimension` 只做**展示层**按维度折叠 SKU,与置顶逻辑独立) + + +**与需求冲突但必须一起解决的一点**:page fill 会用 ES 拉回来的 `_source`**整份覆盖**当前 hit(约 841–842 行)。若在 rerank **之前**只改内存里的 `skus` 顺序/`image_url`,**不**在 fill 后再处理一次,最终响应会被覆盖掉。因此「rerank 前对所有 window 内 hit 做 SKU 决策」和「用户看到的最终列表」之间,必须有一条**明确的数据契约**(见下文 §4)。 + +--- + +## 2. 模块划分(建议:`intent` + `sku_intent` 两层) + +避免继续在 `Searcher` 里堆方法,建议新建小包,职责清晰、由 `Searcher` 编排。 + +| 模块 | 职责 | +|------|------| +| **`query/intent/`**(或 `search/intent/`,二选一以「离谁更近」为准;更推荐 **`query/intent`**,因为输入完全是 query 侧事实) | 加载词表、**意图召回**、多 query 变体 + 粗细分词、输出结构化 **`IntentProfile`** | +| **`search/sku_intent/`**(或 `intent/sku_selection.py`) | 根据 `IntentProfile` 解析 **option1/2/3** 哪一维、生成每 SKU 的**匹配文本**、三轮匹配规则、embedding 批处理、对 `_source` 做 **promote + image_url** | +| **`search/rerank_client.py`(薄扩展)** | 支持「每条 hit 的 doc 文本」:模板扩展或 **显式传入 per-hit 字符串列表**,避免把业务塞进 format 字符串 | + +**`IntentProfile`(概念模型)建议包含**: + +- `active_intents: Set[Literal["color","size"]]`(可扩展) +- 每种意图:`canonical_terms`(命中行的标准词)、`matched_surface_forms`(可选,用于 debug) +- **维度别名**:如 color → `{"color","颜色","colour",...}`(配置或独立小词表) +- 原始用于匹配的 token 集合:每个 query 变体 ×(细粒度 | 粗粒度),便于日志与单测 + +**词表**: + +- **意图召回表**:每行逗号分隔同义词,首词标准化;颜色、尺码各一份(路径放 `config/` 或 `resources/intent/` + `config.yaml` 指路径)。 +- **SKU 第二轮「泛化」表**(对 **option 取值** 做同义扩展):与意图召回表分开,避免语义混在一起。 + +--- + +## 3. 意图判断(与 `QueryParser` 的衔接) + +需求:对 **原始 query + 各类翻译** 都做匹配;**细粒度 + 粗粒度** 分词。 + +现状: + +- `ParsedQuery` 里 **`query_tokens` 只对 rewritten 后的 `query_text` 跑了一次 HanLP**(`query_parser.py` 269–274 行附近),**没有**对 `original_query`、各 `translations` 的 token 缓存。 +- 已有 **`simple_tokenize_query`**(粗粒度)在 `query_parser.py`。 + +**建议**: + +- 在 **`IntentDetector.detect(parsed_query, tokenizer_fn)`** 内统一生成「query 变体列表」:至少包含 `original_query`、`query_normalized`、`rewritten_query`、`translations` 的值(与当前 `_build_sku_query_texts` 思路一致,但升级为**结构化**)。 +- 细粒度:复用 `QueryParser._get_query_tokens`(需把该方法暴露为公开 API 或注入同一 HanLP callable),对每个变体字符串调用。 +- 粗粒度:对每个变体调用 `simple_tokenize_query`。 +- 匹配逻辑:**任意变体 × 任意粒度** 的 token 落在「标准化 → 同义词闭包」上即视为命中该意图(与你描述的行内同义一致)。 + +**可选优化**:在 `parse()` 里顺带产出 `intent_profile`,减少一次遍历;但为控制 `QueryParser` 体积,更稳妥的是 **parse 之后**单独调 `IntentDetector`,依赖清晰。 + +--- + +## 4. 流水线改造(与 page fill 的契约) + +目标顺序变为: + +`ES(window)`(有意图时 `_source` 含 `skus` + `option*_name`) +→ **对每个 hit:SKU 决策 + 生成 rerank 用后缀/全文** +→ `run_rerank`(doc = 标题 + 后缀) +→ 切片 +→ page fill +→ **最终响应前再应用一次 SKU 决策(或与 prefetch 结果合并)** +→ `ResultFormatter` + +**为何最后还要一次?** 因为 page fill 会覆盖 `_source`,rerank 前内存里的 `skus` 顺序不能当作最终真相。 + +**推荐契约(降低复杂度)**: + +1. **Rerank 前**:对 window 内每个 hit 计算 `SkuIntentDecision`(至少包含:`option_slot` 1/2/3、`candidate_sku_index` 或 `sku_id`、`rerank_suffix` 字符串)。可挂在 hit 的**非 ES 字段**上,例如 `hit["_intent_sku"] = {...}`(或只存 `rerank_doc_text` 全文)。 +2. **`run_rerank`**:`build_docs_from_hits` 若发现 hit 上已有 `rerank_doc_text`(或 `style_suffix` + 模板),则优先使用,否则走原模板。 +3. **Page fill 之后**:对**当前页** hit 再调用**同一** `SkuIntentSelector.apply(source, parsed_query, intent_profile)`(或根据 `_id` 合并 prefetch 决策)。这样最终 `image_url` / SKU 顺序与 rerank 一致,且不被 fill 冲掉。 + +若担心算两次 embedding:**第一次**在 window 全量上算 query 向量 + option 向量;第二次仅对当前页且可带缓存(按 `embed_key` 去重),一般量很小。 + +**不在重排窗口内**:没有「rerank 前全 window」这一步;可在 **ResultFormatter 前**对当前页 `es_hits` 用同一 `SkuIntentSelector`(仅当有意图时),与「有意图才做 SKU 筛选」一致。 + +--- + +## 5. `_resolve_rerank_source_filter` 与 ES 字段 + +需求:有意图时预取需包含 `skus`、`option1_name`、`option2_name`、`option3_name`。 + +建议签名扩展为: + +`_resolve_rerank_source_filter(doc_template, intent_profile: Optional[IntentProfile])` + +- 若 `intent_profile` 非空且含 color/size(或任意「款式意图」),在 `includes` 中**合并**上述字段(并与模板解析出的 `title` 等取并集)。 +- 注意与全局 `source_fields` 的 tri-state 语义(`_apply_source_filter`)是否冲突:若租户配置 `_source` 白名单且不含 `skus`,需定义优先级——**建议**:「款式意图所需字段」作为**最低保证**合并进本次请求的 fetch includes,或在文档中写明限制。 + +--- + +## 6. 多维度 option 与「未匹配维度名」 + +需求逻辑可落到纯函数: + +1. 对每个意图类型,有 **维度别名集合**(如 color)。 +2. 依次与 `option1_name`、`option2_name`、`option3_name`(字符串,注意多语言:与 indexer 一致,可能是纯英文或中文)做 **casefold / 规范化** 后匹配别名表。 +3. 命中则该 SKU 行的匹配字段为 `option{k}_value`;用于 embedding key 时继续用 `name:value` 形式(沿用现有 `_sku_option1_embedding_key` 思路,泛化为 `option_slot`)。 +4. **若三个 name 都不匹配意图维度**:用 `option1_value`、`option2_value`、`option3_value` **空格拼接**成一条「兜底描述字符串」,供: + - 与 query 的包含/泛化/embedding 比较; + - 作为 `rerank_suffix` 的一部分(若你希望无明确维度时仍加强 rerank)。 + +**多意图同时存在**(如同时颜色+尺码):需要在产品层定规则,例如: + +- 只对「主意图」排序(配置优先级 color > size),或 +- 要求两个维度都满足的 SKU 优先,否则退化为单意图。 + +实现上可在 `SkuIntentSelector` 输入 `List[IntentType]` 与策略枚举,避免写死 if-else 散落。 + +--- + +## 7. 三轮 SKU 匹配规则(独立模块内) + +从当前「第一个包含就返回」改为: + +1. **第一轮**:统计「option 匹配文本被 **整条 query 文本** **包含**」的 SKU(或对每个 query 变体分别计,再合并——建议与你现有 `_build_sku_query_texts` 对齐);**若恰好 1 个** → 选中。 +2. **第二轮**:若 0 个,对每个 SKU 的候选词走 **取值泛化表**(同义词行),再跑包含判断;仍统计「多个 / 零个」。 +3. **第三轮**: + - 若 **多个** 满足包含(第一轮或第二轮)→ 仅在这多个上算 embedding,取相似度最高; + - 若 **仍 0 个** → 对 **全部** SKU 算 embedding,取最高。 + +实现上保持 **批量 encode**(与当前 `option1_values_to_encode` 去重逻辑类似),只是把「embed_key」从固定 option1 改为按 slot 动态生成。 + +--- + +## 8. `sku_filter_dimension`(API)与意图的关系 + +- **`sku_filter_dimension`**:客户端指定「结果里 SKU 列表如何按维度折叠」,在 `ResultFormatter._filter_skus_by_dimensions` 中实现。 +- **意图 SKU 置顶**:服务端根据 query 推断维度与取值,改顺序与主图。 + +建议约定: + +- **置顶 / 换图**仅在意图开启时执行; +- **`sku_filter_dimension` 仍只影响返回 SKU 条数结构**;若与意图维度冲突(例如意图命中 color,客户端只按 size 折叠),应用**文档说明优先级**:常见做法是 **先意图置顶,再 filter**(或相反,需在 PRD 写清)。 + +避免在 `ResultFormatter` 里再猜意图;意图结论由上游传入或在 Formatter 前已完成 `_source` 调整。 + +--- + +## 9. 配置与观测 + +- `config.yaml`:`intent.enabled`、`intent.lexicon_paths`、`intent.dimension_aliases`(或按类型分块)。 +- `RequestContext` / `debug`:写入 `intent_profile`、`sku_intent_decision`、rerank 用的 doc 摘要,便于与 `docs/TODO-意图判断.md` 对齐。 + +--- + +## 10. 小结 + +- **核心架构**:**`IntentDetector`(query 侧)** + **`SkuIntentSelector`(search 侧)** + **`run_rerank` 的 per-hit doc 覆盖** + **`_resolve_rerank_source_filter` 条件 includes**。 +- **必须处理 page fill 覆盖 `_source`**:rerank 前决策与 **fill 后再 apply 一次**(或等价合并策略),否则会出现「重排用了带后缀的 doc、返回结果却是未置顶 SKU」的不一致。 +- **与现有系统融合点**:`ParsedQuery` 变体列表、HanLP + `simple_tokenize_query`、`TextEmbeddingEncoder`、`ResultFormatter` / `sku_filter_dimension` 的边界清晰,避免把意图逻辑复制到 `api/` 层。 + +若你后续希望把「多意图优先级」或「rerank 后缀格式」定成唯一产品规则,可以在实现前写进同一份 spec,模块接口会很好稳定下来。 \ No newline at end of file diff --git a/docs/issues/issue-2026-03-26-ES文本搜索-补充多模态knn放入should-done-0327.md b/docs/issues/issue-2026-03-26-ES文本搜索-补充多模态knn放入should-done-0327.md new file mode 100644 index 0000000..2e138fc --- /dev/null +++ b/docs/issues/issue-2026-03-26-ES文本搜索-补充多模态knn放入should-done-0327.md @@ -0,0 +1,72 @@ +目前knn跟query里面是并列的层级,如下: +{ + "size": 400, + "from": 0, + "query": { + "bool": { + "must": [... + ], + } + }, + "knn": { + "field": "title_embedding", + "query_vector": [...], + "k": 120, + "num_candidates": 400, + "boost": 2, + "_name": "knn_query" + }, +其中query的结构是这样的: +"query": { + "bool": { + "should": [ + { + "bool": { + "_name": "base_query", +\# 原始query + } + }, + { + "bool": { + "_name": "base_query_trans_zh", +\# 翻译query。有可能是base_query_trans_en,也有可能两者都有 + "boost": 0.75 + } + } + ], + "minimum_should_match": 1 + } + }, +我想把knn放到should里面,和base_query、base_query_trans_zh并列。 +另外,现在过滤是在knn里面单独加了一遍: + "knn": { + "field": "title_embedding", + "query_vector": [...], + "k": 120, + "num_candidates": 400, + "boost": 2, + "_name": "knn_query", + "filter": { + "range": { + "min_price": { + "gte": 100, + "lt": 200 + } + } + } + } +现在不需要了。因为knn在query的内层了。共用过滤。 + +另外: +我需要再增加一个knn。 +需要参考文本embedding获得的逻辑, +通过 +curl -X POST "http://localhost:6008/embed/clip_text?normalize=true&priority=1" \ + -H "Content-Type: application/json" \ + -d '["纯棉短袖", "street tee"]' +(用 POST /embed/clip_text 生成多模态文本向量。和文本embedding获取方法类似。注意思考代码如何精简,不要冗余。) +得到文本的多模态embedding。 +然后在这里补充一个多模态embedding,寻找图片相似的结果,对应的商品图片字段为image_embedding.vector。 +重排融合:之前有knn的配置bias和exponential。现在,文本和图片的embedding相似需要融合,融合方式是dis_max,因此需要配置: +1)各自的权重和tie_breaker +2)整个向量方面的权重(bias和exponential) \ No newline at end of file diff --git a/docs/issues/issue-2026-03-27-keywords限定-done-0327.txt b/docs/issues/issue-2026-03-27-keywords限定-done-0327.txt new file mode 100644 index 0000000..a68186e --- /dev/null +++ b/docs/issues/issue-2026-03-27-keywords限定-done-0327.txt @@ -0,0 +1,93 @@ +@query/query_parser.py @scripts/es_debug_search.py +原始query、以及每一个翻译,都要有一个对应的keywords_query(token分词后,得到名词) +参考这段代码,获取每一个长度大于 1 的名词,然后用空格拼接起来,作为keywords_query +import hanlp +from typing import List, Tuple, Dict, Any + +class KeywordExtractor: + """ + 基于 HanLP 的名词关键词提取器 + """ + def __init__(self): + # 加载带位置信息的分词模型(细粒度) + self.tok = hanlp.load(hanlp.pretrained.tok.CTB9_TOK_ELECTRA_BASE_CRF) + self.tok.config.output_spans = True # 启用位置输出 + + # 加载词性标注模型 + self.pos_tag = hanlp.load(hanlp.pretrained.pos.CTB9_POS_ELECTRA_SMALL) + + def extract_keywords(self, query: str) -> str: + """ + 从查询中提取关键词(名词,长度 ≥ 2) + + Args: + query: 输入文本 + + Returns: + 拼接后的关键词字符串,非连续词之间自动插入空格 + """ + query = query.strip() + # 分词结果带位置:[[word, start, end], ...] + tok_result_with_position = self.tok(query) + tok_result = [x[0] for x in tok_result_with_position] + + # 词性标注 + pos_tag_result = list(zip(tok_result, self.pos_tag(tok_result))) + + # 需要忽略的词 + ignore_keywords = ['玩具'] + + keywords = [] + last_end_pos = 0 + + for (word, postag), (_, start_pos, end_pos) in zip(pos_tag_result, tok_result_with_position): + if len(word) >= 2 and postag.startswith('N'): + if word in ignore_keywords: + continue + # 如果当前词与上一个词在原文中不连续,插入空格 + if start_pos != last_end_pos and keywords: + keywords.append(" ") + keywords.append(word) + last_end_pos = end_pos + # 可选:打印调试信息 + # print(f'分词: {word} | 词性: {postag} | 起始: {start_pos} | 结束: {end_pos}') + + return "".join(keywords).strip() + + +最后,在组织检索表达式时,目前是每一个 query (base_query base_query_trans_en base_query_trans_zh 三种情况)。 会组成一个bool查询,以base_query为例: + "bool": { + "should": [ + { + "bool": { + "_name": "base_query", + "must": [ + { + "combined_fields": { +... + } + } + ], + "should": [ + { + "multi_match": { +... "type": "best_fields", +... + }, + { + "multi_match": { +... + "type": "phrase", +... + } + } + ] + } + }, + +base_query_trans_en base_query_trans_zh 也是同样 + +在这个布尔查询的must里面加一项:keywords,搜索的字段和combined_fields一样,命中比例要求50% + + +结合现有代码做出合理的设计,呈现简单清晰的数据接口,而不是打补丁 \ No newline at end of file diff --git a/docs/issues/issue-2026-03-27-添加粗排精排-第2轮-done-0327.txt b/docs/issues/issue-2026-03-27-添加粗排精排-第2轮-done-0327.txt new file mode 100644 index 0000000..5e8c57f --- /dev/null +++ b/docs/issues/issue-2026-03-27-添加粗排精排-第2轮-done-0327.txt @@ -0,0 +1,38 @@ +这是上一轮检索效果优化的需求: +参考 +searcher.py +rerank_client.py +schema.py +es_query_builder.py +config.yaml +相关性检索优化说明.md + +在ES返回到rerank期间增加一轮粗排+一轮精排。 +1. ES召回,600 +2. 粗排:600->240。配置文件增加粗排相关配置,包括输入条数(配置为700,ES拉取的条数改为粗排输入条数),然后增加粗排的融合公式配置,参考现有的reranker融合公式即可、只是去掉其中的重排模型项。 +3. 现在的sku选择、为reranker生成title后缀这一套逻辑,是放在粗排后,因为精排也是一个reranker模型(只不过是一个轻量级的,bge-reranker),需要用这个title后缀。 +4. 精排:240-80,使用bge-reranker,但是,因为reranker只能选一个backend,考虑如何重构。现在,精排也是一个独立的进程、独立提供端口,服务。但是,因为跟重排逻辑是一致的(即使有部分不一致也应该分离不一致的点进行配置化),所以共用代码,只是根据需要启动两个实例,避免代码冗余。 +5. 重排:80,也是用当前的重排代码,调用单独的实例(即现在使用的实例),返回后,经过融合公式,到分页,也参考现在的融合公式,但是,加入一项精排模型打分。 +测试时,使用跟我同样的环境./scripts/service_ctl.sh reranker backend 重启相关服务进行测试 + +你已经完成了一般修改,已把三段排序链路接上了,主改动在 search/searcher.py、search/rerank_client.py、config/schema.py、config/loader.py、config/services_config.py 和 config/config.yaml。 + +现在链路是: +ES 拉取 coarse_rank.input_window 条 -> 粗排按 text/knn 融合裁到 coarse_rank.output_window -> 再做 SKU 选择和 title suffix -> 精排调用轻量 reranker 裁到 fine_rank.output_window -> 最终重排调用现有 reranker,并在最终融合里加入 fine_score。同时把 reranker client/provider 改成了按 service_profile 选不同 service_url,这样 fine/final 可以共用同一套服务代码,只起不同实例。 + +你用了./.venv/bin/pytest -q tests/test_rerank_client.py tests/test_search_rerank_window.py 进行验证,结果是 18 passed。也按要求尝试了 ./scripts/service_ctl.sh restart reranker backend 做联调,但当前环境里只有 6007 的现有 reranker,配置里的 fine 端口 6009 还没对应实例,而且服务重启后进程状态不太稳定,没能完成完整线上链路验证。当前最关键的落地前置是把轻量 bge 实例补起来,再用同样脚本重启联调。 + +因此,需要回顾这个漏斗的代码,进行进一步的优化: +1. Fine Rank环节的信息都是N/A。fine 实例也需要在config.yaml中体现,并且需要被server_ctl.sh管理,参考现在是怎么管理reranker的。fine和reranker分别调用哪个模型(两者代码功能,但是只是启动不同的实例),请你深度思考后决定。这里是需要参考当前代码、根据实际需要(原来只有一个reranker、现在多了一个精排,也要调用一个重排模型,因此带来的必要的部分重构。 +2. Ranking Funnel、Fusion Factors、Signal Breakdown +这些是不是整合起来、按漏斗收集、整理信息、以及进行呈现比较好。 +ES 召回的环节,展示Matched Queries各项打分、ES的总分、norm后打分、排序位置,等等关键信息 +粗排:粗排融合公式的各项输入、重要中间结果和参数、最后得分,排序位置以及上升/下降了多少。等等关键信息。 +精排:同样例举关键的输入、中间过程、输出、排序和位置变化等。 +reranker:类似 + +因为涉及的环节较多,非常要注意的一个点是:不要每次修改都在原来的基础上,为实现目标而打补丁,应该观察一下所涉及的代码现在是怎么做的,务必注意如何适当的清理掉现有逻辑,该如何对其进行修改,来达到目的,以达到代码的精简,避免冗余、分叉。 +特别是1关于将现在的单独的reranker服务,在不增加代码复杂度的情况下,如何可以通过配置启动多个实例、各自可以配置选择的模型等相关参数、分别服务于精排和重排,是设计的重中之重。这里需要思考配置如何拆分,需要多读相关代码、思考最佳实践。 + + + diff --git a/docs/issues/issue-2026-03-27-添加粗排精排-第3轮-done-0327.txt b/docs/issues/issue-2026-03-27-添加粗排精排-第3轮-done-0327.txt new file mode 100644 index 0000000..46505c2 --- /dev/null +++ b/docs/issues/issue-2026-03-27-添加粗排精排-第3轮-done-0327.txt @@ -0,0 +1,98 @@ +这是上一轮检索效果优化的需求: +参考 +searcher.py +rerank_client.py +schema.py +es_query_builder.py +config.yaml +相关性检索优化说明.md + +在ES返回到rerank期间增加一轮粗排+一轮精排。 +1. ES召回,600 +2. 粗排:600->240。配置文件增加粗排相关配置,包括输入条数(配置为700,ES拉取的条数改为粗排输入条数),然后增加粗排的融合公式配置,参考现有的reranker融合公式即可、只是去掉其中的重排模型项。 +3. 现在的sku选择、为reranker生成title后缀这一套逻辑,是放在粗排后,因为精排也是一个reranker模型(只不过是一个轻量级的,bge-reranker),需要用这个title后缀。 +4. 精排:240-80,使用bge-reranker,但是,因为reranker只能选一个backend,考虑如何重构。现在,精排也是一个独立的进程、独立提供端口,服务。但是,因为跟重排逻辑是一致的(即使有部分不一致也应该分离不一致的点进行配置化),所以共用代码,只是根据需要启动两个实例,避免代码冗余。 +5. 重排:80,也是用当前的重排代码,调用单独的实例(即现在使用的实例),返回后,经过融合公式,到分页,也参考现在的融合公式,但是,加入一项精排模型打分。 +测试时,使用跟我同样的环境./scripts/service_ctl.sh reranker backend 重启相关服务进行测试 + +你已经完成了一般修改,已把三段排序链路接上了,主改动在 search/searcher.py、search/rerank_client.py、config/schema.py、config/loader.py、config/services_config.py 和 config/config.yaml。 + +现在链路是: +ES 拉取 coarse_rank.input_window 条 -> 粗排按 text/knn 融合裁到 coarse_rank.output_window -> 再做 SKU 选择和 title suffix -> 精排调用轻量 reranker 裁到 fine_rank.output_window -> 最终重排调用现有 reranker,并在最终融合里加入 fine_score。同时把 reranker client/provider 改成了按 service_profile 选不同 service_url,这样 fine/final 可以共用同一套服务代码,只起不同实例。 + +并且,你对调试展示进行了重构。你已经把结果卡片和全局调试面板都改成按漏斗阶段取值和展示,在 app.js 里把 ES 召回、粗排、精排、最终 rerank 分开渲染了。 +现在每条结果的 debug 会按阶段展示: +ES 召回:rank、ES score、norm score、matched queries。 +粗排:rank/rank_change、coarse_score、text/knn 输入、text_source/text_translation/text_primary/text_support、text_knn/image_knn、factor。 +精排:rank/rank_change、fine_score、fine input。 +最终 rerank:rank/rank_change、rerank_score、text/knn score、各 factor、fused_score,以及完整 signals。 + +请你仔细阅读漏斗环节的这些代码,特别是关于打分、重排序、debug信息记录方面的。 + + +现在,请注意,需要优化的是: +1. Fine Rank环节似乎没有进行融合公式的计算、继而进行重排序,请修复。 +2.从软件工程的视角review代码: +因为增加了多重排序漏斗,数据的记录、传递,交互的接口,是否设计足够合理,存在哪些问题。 +请从软件工程的角度审视这些逻辑,是否有需要梳理、清理和重写的地方。 +3. Fine Rank和Final Rerank环节信息记录优化: +这两个环节都要体现融合公式的输入、关键因子、以及融合公式的得分。为了避免代码膨胀,Fine Rank和Final Rerank +都可以采用一个字符串记录这些关键信息,字符串内包括融合公式各项的名称和具体数值,以及最终结果。你也可以继续沿用当前的记录方式,需要你对比一下哪种代码量更少、更清晰简洁。 +也要仔细思考当前的代码,真实的计算和信息的记录,是否存在分离的情况,是否存在冗余和分叉。这种情况是不允许的,存在隐藏的风险,以后改了正式逻辑而没有改调试信息,将导致不一致。 +务必注意,当前已经有相关的信息记录逻辑,注意不要叠补丁,可以适当修改、或者清理重写,而不是新增,要使得代码更简洁和干净,并保证信息记录与真实逻辑一致。 + + +涉及代码较多,请耐心阅读,以上都是一些需要深度思考的任务,慢慢来,留足够多的时间来review和重新设计。 + + + + + + +因为增加了两个环节,多了很多变量。 +以这些为效果评估的测试集,调试参数。这次的调整范围是,融合公式中的各个 +falda negra oficina +red fitted tee +黒いミディ丈スカート +黑色中长半身裙 +чёрное летнее платье +修身牛仔裤 +date night dress +vacation outfit dress +minimalist top + +仔细思考这些漏斗中重要的信息如何呈现。对应的修改前端代码。 +注意包括整体漏斗信息的呈现,以及每条结构中独自的信息。 +我需要这些信息,辅助各环节融合公式的调参,根据我的需求,深度思考该如何设计,要呈现哪些信息,如何呈现。 +可以对现有的逻辑做适当的重构,重新整理。 + + + + +fine 实例也需要在config.yaml中体现,并且需要被server_ctl.sh管理,参考现在是怎么管理reranker的。fine和reranker分别调用哪个模型(两者代码功能,但是只是启动不同的实例),请你深度思考后决定。这里是需要参考当前代码、根据实际需要(原来只有一个reranker、现在多了一个精排,也要调用一个重排模型,因此带来的必要的部分重构。 + +1. Fine Rank环节的信息都是N/A,是没有配置吗。fine rank是使用bge-reranker,复用当前reranker模型的代码,但是需要单独起一个服务、单独加载一个模型。 +2. Ranking Funnel、Fusion Factors、Signal Breakdown +这些是不是整合起来、按漏斗收集、整理信息、以及进行呈现比较好。 +ES 召回的环节,展示Matched Queries各项打分、ES的总分、norm后打分、排序位置,等等关键信息 +粗排:粗排融合公式的各项输入、重要中间结果和参数、最后得分,排序位置以及上升/下降了多少。等等关键信息。 +精排:同样例举关键的输入、中间过程、输出、排序和位置变化等。 +reranker:类似 + +因为涉及的环节较多,非常要注意的一个点是:不要每次修改都在原来的基础上,为实现目标而打补丁,应该观察一下所涉及的代码现在是怎么做的,务必注意如何适当的清理掉现有逻辑,该如何对其进行修改,来达到目的,以达到代码的精简,避免冗余、分叉。 + + + + + +1. Fine Rank 这个环节没有体现融合公式的输入、关键因子、以及融合公式的得分。为了避免代码膨胀,Fine Rank和Final Rerank +都可以采用一个字符串记录这些关键信息,字符串内包括融合公式各项的名称和具体数值,以及最终结果。 + + + + + + + + + diff --git a/docs/issues/issue-2026-03-27-添加粗排精排-第一轮-done-0327.txt b/docs/issues/issue-2026-03-27-添加粗排精排-第一轮-done-0327.txt new file mode 100644 index 0000000..030d63c --- /dev/null +++ b/docs/issues/issue-2026-03-27-添加粗排精排-第一轮-done-0327.txt @@ -0,0 +1,136 @@ +我: +进行一轮检索效果优化: +参考 +[searcher.py](search/searcher.py) +[rerank_client.py](search/rerank_client.py) +[schema.py](config/schema.py) +[es_query_builder.py](search/es_query_builder.py) +[config.yaml](config/config.yaml) +[相关性检索优化说明.md](docs/相关性检索优化说明.md) + +在ES返回到rerank期间增加一轮粗排+一轮精排。 +1. ES召回,600 +2. 粗排:600->240。配置文件增加粗排相关配置,包括输入条数(配置为700,ES拉取的条数改为粗排输入条数),然后增加粗排的融合公式配置,参考现有的reranker融合公式即可、只是去掉其中的重排模型项。 +3. 现在的sku选择、为reranker生成title后缀这一套逻辑,是放在粗排后,因为精排也是一个reranker模型(只不过是一个轻量级的,bge-reranker),需要用这个title后缀。 +4. 精排:240-80,使用bge-reranker,但是,因为reranker只能选一个backend,考虑如何重构。现在,精排也是一个独立的进程、独立提供端口,服务。但是,因为跟重排逻辑是一致的(即使有部分不一致也应该分离不一致的点进行配置化),所以共用代码,只是根据需要启动两个实例,避免代码冗余。 +5. 重排:80,也是用当前的重排代码,调用单独的实例(即现在使用的实例),返回后,经过融合公式,到分页,也参考现在的融合公式,但是,加入一项精排模型打分。 +测试时,使用跟我同样的环境./scripts/service_ctl.sh reranker backend 重启相关服务进行测试 + + + + + +codex: +前端这轮我把调试展示重构成了“两层漏斗”: + +1. 整体漏斗 +在 [frontend/static/js/app.js](/data/saas-search/frontend/static/js/app.js) 里把原来散着的 `Query Analysis / Rerank / Stage Timings / ES Query` 重新组织成了: +- `Query Context` +- `Ranking Funnel` +- `Timing Breakdown` +- `Raw Payloads` + +其中 `Ranking Funnel` 现在按阶段卡片展示: +- `ES Recall` +- `Coarse Rank` +- `Fine Rank` +- `Final Rerank` +- `Page Return` + +每张卡都尽量只放调参最关键的信息:输入/输出条数、模板、service URL、top_n、fusion/meta。这样你看漏斗收缩、看每段服务表现、看融合参数会更直接。 + +2. 单条结果漏斗 +在结果卡片右侧的 debug 区,我改成了真正的单条漏斗卡片,按“分数结果”和“公式分解”分开看: +- 顶部先看 `Initial Rank / Final Rank / Rank Delta` +- 再看阶段分数:`ES / ES Norm / Coarse / Fine / Rerank / Fused` +- 再看融合因子:`coarse_text_factor / coarse_knn_factor / text_factor / knn_factor / fine_factor / rerank_factor` +- 再看信号拆解:`text_source / text_translation / text_primary / text_support / text_knn / image_knn` + +同时保留了可展开的: +- `Rerank Input` +- `Selected SKU` +- `Matched Queries` +- `Multilingual Fields` + +为了支撑前端,我顺手在 [search/searcher.py](/data/saas-search/search/searcher.py) 里补了单条 `coarse_score / coarse_text_factor / coarse_knn_factor` 到 `per_result`,这样单条卡片现在能把粗排也完整展示出来。 + +样式上在 [frontend/static/css/style.css](/data/saas-search/frontend/static/css/style.css) 加了阶段卡片、分数 pill、时延条形图和移动端适配。JS 语法我用 `node --check frontend/static/js/app.js` 过了,后端补充字段也用 `./.venv/bin/python -m py_compile search/searcher.py` 过了。 + +如果你愿意,我下一步可以继续把这个视图再往“调参工作台”方向推一层,比如再加: +- 漏斗阶段间的 drop rate +- 每条结果的 rank jump 高亮 +- 批量对比 top N 结果里某个因子分布 +- fusion 参数和结果联动的可视化摘要 + + + + + + + + +我: +1. Fine Rank环节的信息都是N/A,是没有配置吗。fine rank是使用bge-reranker,复用当前reranker模型的代码,但是需要单独起一个服务、单独加载一个模型。 +2. Ranking Funnel、Fusion Factors、Signal Breakdown +这些是不是整合起来、按漏斗收集、整理信息、以及进行呈现比较好。 +ES 召回的环节,展示Matched Queries各项打分、ES的总分、norm后打分、排序位置,等等关键信息 +粗排:粗排融合公式的各项输入、重要中间结果和参数、最后得分,排序位置以及上升/下降了多少。等等关键信息。 +精排:同样例举关键的输入、中间过程、输出、排序和位置变化等。 +reranker:类似 + +因为涉及的环节较多,非常要注意的一个点是:不要每次修改都在原来的基础上,为实现目标而打补丁,应该观察一下所涉及的代码现在是怎么做的,务必注意如何适当的清理掉现有逻辑,该如何对其进行修改,来达到目的,以达到代码的精简,避免冗余、分叉。 + + + + + + +codex: +`Fine Rank` 之前显示 `N/A`,根因不是前端单纯没写,而是后端原来只有“精排成功拿到分数”时才会填那块 debug;一旦 `fine` 服务没起、没配 `service_url`、调用失败,前端只能看到空值。现在我把这层补齐了: + +- 顶层 `debug_info.fine_rank` 现在会始终带 `status / service_profile / service_url / input_window / output_window / query_template / doc_template` +- 失败时会带 `error` +- 成功时再补 `docs / top_n / meta` +- 所以你现在能区分: + - `disabled` + - `pending` + - `ok` + - `failed_no_scores` + - `failed` + +也就是说,`fine rank` 本身仍然需要你按设计单独起一个 `bge-reranker` 实例,这一层代码已经按“独立服务 + 共用 reranker 代码”的方式接好了,但服务本身还是要单独启动。 + +这轮我也把调试展示做了结构化重构,不再继续在旧视图上打补丁。现在是按阶段组织信息: + +- 整体漏斗: + - `ES Recall` + - `Coarse Rank` + - `Fine Rank` + - `Final Rerank` + - `Page Return` +- 单条结果: + - 用统一的 `ranking_funnel` 结构来表达每个阶段的 + - `rank` + - `delta` + - `score` + - 关键中间量 + - 关键 factor + - 输入 payload + +具体改动在: +- [search/searcher.py](/data/saas-search/search/searcher.py) +- [frontend/static/js/app.js](/data/saas-search/frontend/static/js/app.js) +- [frontend/static/css/style.css](/data/saas-search/frontend/static/css/style.css) + +单条结果现在按 stage card 展示,而不是把 `Ranking Funnel / Fusion Factors / Signal Breakdown` 拆成三块平铺,这样你可以顺着 ES -> 粗排 -> 精排 -> rerank 看每段的输入、过程、输出和位次变化,更适合调融合公式。 + +检查我也跑过了: +- `node --check frontend/static/js/app.js` +- `./.venv/bin/python -m py_compile search/searcher.py` +- `./.venv/bin/pytest -q tests/test_search_rerank_window.py` + +都通过了。 + +下一步如果你要,我建议直接继续做两件事中的一个: +1. 把 `fine` 实例真正拉起来,然后我再把前端里 `status=failed/pending` 的场景跑成真实数据。 +2. 在当前漏斗视图上继续加“阶段 rank 分布 / delta 高亮 / topN 因子对比”,把它做成更像调参工作台。 \ No newline at end of file diff --git a/docs/issues/issue-2026-03-28-添加粗排精排-第4轮-done-0328.txt b/docs/issues/issue-2026-03-28-添加粗排精排-第4轮-done-0328.txt new file mode 100644 index 0000000..c710ebf --- /dev/null +++ b/docs/issues/issue-2026-03-28-添加粗排精排-第4轮-done-0328.txt @@ -0,0 +1,314 @@ +这是上一轮检索质量优化的需求说明: + +参考文件: +`searcher.py` +`rerank_client.py` +`schema.py` +`es_query_builder.py` +`config.yaml` +`相关性检索优化说明.md` + +在ES返回结果与重排之间增加一个粗排阶段和一个精排阶段。 + +1. ES召回:600条 +2. 粗排阶段:600 -> 240条。 + 在配置文件中增加粗排相关配置,包括输入大小(设置为700,即从ES获取的数量应改为粗排的输入大小)。 + 然后增加粗排的融合公式配置。可参考现有的重排融合公式,但需要去掉重排模型那一项。 +3. 目前重排的SKU选择和标题后缀生成逻辑,应放在粗排之后,因为精排也是一个重排模型(轻量级模型,`bge-reranker`),它也需要这个标题后缀。 +4. 精排阶段:240 -> 80条。 + 使用`bge-reranker`,但由于目前重排只能选择一个后端,请考虑如何重构。 + 现在精排也应该是一个独立的进程和服务,拥有自己的端口。 + 但由于其逻辑与重排逻辑一致(即便存在差异,这些差异也应抽离并配置化),代码应该共享。按需启动两个实例即可,避免代码重复。 +5. 最终重排:80条。 + 仍使用当前重排代码,调用独立的实例(即当前在用的那个)。 + 返回后,应用融合公式,再进行分页。 + 这里也应参考当前的融合公式,但需增加一项:精排模型得分。 + +测试时,请使用与我相同的环境,并使用以下命令重启相关服务: +`./scripts/service_ctl.sh reranker backend` + +你已经完成了整体修改,并将三级排序流水线串联起来了。 +主要改动在: +`search/searcher.py` +`search/rerank_client.py` +`config/schema.py` +`config/loader.py` +`config/services_config.py` +以及 `config/config.yaml`。 + +现在的流程是: + +ES获取 `coarse_rank.input_window` 条 -> +粗排通过文本/KNN融合裁剪至 `coarse_rank.output_window` -> +然后进行SKU选择和标题后缀处理 -> +精排调用轻量重排,裁剪至 `fine_rank.output_window` -> +最终重排调用现有重排,最终融合时也加入了 `fine_score`。 + +同时,重排客户端/提供者已改为通过 `service_profile` 选择不同的 `service_url`,因此精排和最终重排可以共享同一套服务代码,仅以不同实例运行。 + +你还重构了调试展示。 +你修改了结果卡片和全局调试面板,使其按漏斗阶段读取并渲染数值,在 `app.js` 中,你现在分别渲染ES召回、粗排、精排和最终重排。 + +现在,每个结果的调试信息按阶段展示: + +* ES召回:`rank`、ES得分、归一化得分、匹配查询 +* 粗排:`rank` / `rank_change`、`coarse_score`、文本/KNN输入、`text_source` / `text_translation` / `text_primary` / `text_support`、`text_knn` / `image_knn`、`factor` +* 精排:`rank` / `rank_change`、`fine_score`、`fine input` +* 最终重排:`rank` / `rank_change`、`rerank_score`、文本/KNN得分、各因子、`fused_score` 以及完整信号 + +请仔细阅读这些漏斗阶段的代码,特别是涉及打分、重排和调试信息记录的部分。 + +现在,请注意需要优化的部分: + +1. 精排阶段似乎没有计算融合公式并据此重排。请修复此问题。 +2. 从软件工程的角度审视代码: + 既然引入了多级排序漏斗,数据记录、传递和交互接口的设计是否足够合理?存在哪些问题? + 请从软件工程角度审视这一逻辑,判断是否有需要重新组织、清理或重写的部分。 +3. 优化精排和最终重排阶段的信息记录: + 这两个阶段都应体现融合公式的输入、关键因子以及融合公式计算出的得分。 + 为避免代码臃肿,精排和最终重排都可以使用一个字符串来记录这些关键信息。该字符串可以包含融合公式中各项的名称和值,以及最终结果。 + 你也可以继续使用当前的记录方式;请对比哪种方式代码更少、更清晰简洁。 + 同时请仔细思考当前代码:实际的计算过程和记录的信息是否分离?是否存在冗余或分歧? + 这是不可取的,因为会引入潜在风险:如果后续修改了生产逻辑但未更新调试信息,就会导致不一致。 + +请特别注意:现在已经存在相关的信息记录逻辑。不要只是层层打补丁。 +你可以适当修改,或者清理重写,而不仅仅是增加代码。 +目标是让代码更简单、更干净,同时确保记录的信息始终与实际逻辑保持一致。 + +涉及代码较多,请耐心阅读。 +以上所有任务都需要深入思考。请慢慢来,为全面的审查和重新设计留出足够空间。 + + + + + + +**整体图** +这个 pipeline 现在可以理解成一条“先广召回,再逐层收窄、逐层加贵信号”的漏斗: + +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 原逻辑返回。 + +这点非常重要,因为它决定了“贵模型只服务头部结果”。 + +**Step 1:Query 解析阶段** +在 [searcher.py:432](/data/saas-search/search/searcher.py#L432) 到 [searcher.py:469](/data/saas-search/search/searcher.py#L469): +`query_parser.parse()` 做几件事: + +- 规范化 query +- 检测语言 +- 可能做 rewrite +- 生成文本向量 +- 如果有图搜,还会带图片向量 +- 生成翻译结果 +- 识别 style intent + +这一步的结果存在 `parsed_query` 里,后面 ES 查询、style SKU 选择、fine/final rerank 全都依赖它。 + +**Step 2:ES Query 构建** +ES DSL 在 [searcher.py:471](/data/saas-search/search/searcher.py#L471) 开始,通过 [es_query_builder.py:181](/data/saas-search/search/es_query_builder.py#L181) 的 `build_query()` 生成。 + +这里的核心结构是: +- 文本召回 clause +- 文本向量 KNN clause +- 图片向量 KNN clause +- 它们一起放进 `bool.should` +- 过滤条件放进 `filter` +- facet 的多选条件走 `post_filter` + +KNN 部分在 [es_query_builder.py:250](/data/saas-search/search/es_query_builder.py#L250) 之后: +- 文本向量 clause 名字固定叫 `knn_query` +- 图片向量 clause 名字固定叫 `image_knn_query` + +而文本召回那边,后续 fusion 代码约定会去读: +- 原始 query 的 named query:`base_query` +- 翻译 query 的 named query:`base_query_trans_*` + +也就是说,后面的粗排/精排/最终 rerank,并不是重新理解 ES score,而是从 `matched_queries` 里把这些命名子信号拆出来自己重算。 + +**Step 3:ES 召回** +在 [searcher.py:579](/data/saas-search/search/searcher.py#L579) 到 [searcher.py:627](/data/saas-search/search/searcher.py#L627)。 + +这里有个很关键的工程优化: +如果在 rerank window 内,第一次 ES 拉取时会把 `_source` 关掉,只取排序必需信号,见 [searcher.py:517](/data/saas-search/search/searcher.py#L517) 到 [searcher.py:523](/data/saas-search/search/searcher.py#L523)。 + +原因是: +- 粗排先只需要 `_score` 和 `matched_queries` +- 不需要一上来把 700 条完整商品详情都拉回来 +- 等粗排收窄后,再补 fine/final rerank 需要的字段 + +这是现在这条 pipeline 很核心的性能设计点。 + +**Step 4:粗排** +粗排入口在 [searcher.py:638](/data/saas-search/search/searcher.py#L638),真正的打分在 [rerank_client.py:348](/data/saas-search/search/rerank_client.py#L348) 的 `coarse_resort_hits()`。 + +粗排只看两类信号: +- `text_score` +- `knn_score` + +它们先都从统一 helper `_build_hit_signal_bundle()` 里拿,见 [rerank_client.py:246](/data/saas-search/search/rerank_client.py#L246)。 + +文本分怎么来,见 [rerank_client.py:200](/data/saas-search/search/rerank_client.py#L200): +- `source_score = matched_queries["base_query"]` +- `translation_score = max(base_query_trans_*)` +- `weighted_translation = 0.8 * translation_score` +- `primary_text = max(source, weighted_translation)` +- `support_text = 另一路` +- `text_score = primary_text + 0.25 * support_text` + +这就是一个 text dismax 思路: +原 query 是主路,翻译 query 是辅助路,但不是简单相加。 + +向量分怎么来,见 [rerank_client.py:156](/data/saas-search/search/rerank_client.py#L156): +- `text_knn_score` +- `image_knn_score` +- 分别乘自己的 weight +- 取强的一路做主路 +- 弱的一路按 `knn_tie_breaker` 做辅助 + +然后粗排融合公式在 [rerank_client.py:334](/data/saas-search/search/rerank_client.py#L334): +- `coarse_score = (text_score + text_bias)^text_exponent * (knn_score + knn_bias)^knn_exponent` + +配置定义在 [schema.py:124](/data/saas-search/config/schema.py#L124) 和 [config.yaml:231](/data/saas-search/config/config.yaml#L231)。 + +算完后: +- 写入 `hit["_coarse_score"]` +- 按 `_coarse_score` 排序 +- 留前 240,见 [searcher.py:645](/data/saas-search/search/searcher.py#L645) + +**Step 5:粗排后补字段 + SKU 选择** +粗排完以后,`searcher` 会按 doc template 反推 fine/final rerank 需要哪些 `_source` 字段,然后只补这些字段,见 [searcher.py:669](/data/saas-search/search/searcher.py#L669)。 + +之后才做 style SKU 选择,见 [searcher.py:696](/data/saas-search/search/searcher.py#L696)。 + +为什么放这里? +因为现在 fine rank 也是 reranker,它也要吃 title suffix。 +而 suffix 是 SKU 选择之后写到 hit 上的 `_style_rerank_suffix`。 +真正把 suffix 拼进 doc 文本的地方在 [rerank_client.py:65](/data/saas-search/search/rerank_client.py#L65) 到 [rerank_client.py:74](/data/saas-search/search/rerank_client.py#L74)。 + +所以顺序必须是: +- 先粗排 +- 再选 SKU +- 再用带 suffix 的 title 去跑 fine/final rerank + +**Step 6:精排** +入口在 [searcher.py:711](/data/saas-search/search/searcher.py#L711),实现是 [rerank_client.py:603](/data/saas-search/search/rerank_client.py#L603) 的 `run_lightweight_rerank()`。 + +它会做三件事: + +1. 用 `build_docs_from_hits()` 把每条商品变成 reranker 输入文本 +2. 用 `service_profile="fine"` 调轻量服务 +3. 不再只按 `fine_score` 排,而是按融合后的 `_fine_fused_score` 排 + +精排融合公式现在是: +- `fine_stage_score = fine_factor * text_factor * knn_factor * style_boost` + +具体公共计算在 [rerank_client.py:286](/data/saas-search/search/rerank_client.py#L286) 的 `_compute_multiplicative_fusion()`: +- `fine_factor = (fine_score + fine_bias)^fine_exponent` +- `text_factor = (text_score + text_bias)^text_exponent` +- `knn_factor = (knn_score + knn_bias)^knn_exponent` +- 如果命中了 selected SKU,再乘 style boost + +写回 hit 的字段见 [rerank_client.py:655](/data/saas-search/search/rerank_client.py#L655): +- `_fine_score` +- `_fine_fused_score` +- `_text_score` +- `_knn_score` + +排序逻辑在 [rerank_client.py:683](/data/saas-search/search/rerank_client.py#L683): +按 `_fine_fused_score` 降序排,然后留前 80,见 [searcher.py:727](/data/saas-search/search/searcher.py#L727)。 + +这就是你这次特别关心的点:现在 fine rank 已经不是“模型裸分排序”,而是“模型分 + ES 文本/KNN 信号融合后排序”。 + +**Step 7:最终 rerank** +入口在 [searcher.py:767](/data/saas-search/search/searcher.py#L767),实现是 [rerank_client.py:538](/data/saas-search/search/rerank_client.py#L538) 的 `run_rerank()`。 + +它和 fine rank 很像,但多了一个更重的模型分 `rerank_score`。 +最终公式是: + +- `final_score = rerank_factor * fine_factor * text_factor * knn_factor * style_boost` + +也就是: +- fine rank 产生的 `fine_score` 不会丢 +- 到最终 rerank 时,它会继续作为一个乘法项参与最终融合 + +这个逻辑在 [rerank_client.py:468](/data/saas-search/search/rerank_client.py#L468) 到 [rerank_client.py:476](/data/saas-search/search/rerank_client.py#L476)。 + +算完后写入: +- `_rerank_score` +- `_fused_score` + +然后按 `_fused_score` 排序,见 [rerank_client.py:531](/data/saas-search/search/rerank_client.py#L531)。 + +这里你可以把它理解成: +- fine rank 负责“轻量快速筛一遍,把 240 缩成 80” +- 最终 rerank 负责“用更贵模型做最终拍板” +- 但最终拍板时,不会忽略 fine rank 结果,而是把 fine score 当成一个先验信号保留进去 + +**Step 8:分页与字段补全** +多阶段排序只在头部窗口内完成。 +真正返回给用户前,在 [searcher.py:828](/data/saas-search/search/searcher.py#L828) 之后还会做两件事: + +- 先按 `from_:from_+size` 对最终 80 条切片 +- 再按用户原始 `_source` 需求补回页面真正要显示的字段,见 [searcher.py:859](/data/saas-search/search/searcher.py#L859) + +所以这条链路是“三次不同目的的数据访问”: + +- 第一次 ES:只要排序信号 +- 第二次按 id 回填:只要 fine/final rerank 需要字段 +- 第三次按页面 ids 回填:只要最终页面显示字段 + +这也是为什么它性能上比“一次全量拉 700 条完整文档”更合理。 + +**Step 9:结果格式化与 debug funnel** +最后在 [searcher.py:906](/data/saas-search/search/searcher.py#L906) 进入结果处理。 +这里会把每个商品的阶段信息组装成 `ranking_funnel`,见 [searcher.py:1068](/data/saas-search/search/searcher.py#L1068): + +- `es_recall` +- `coarse_rank` +- `fine_rank` +- `rerank` +- `final_page` + +其中: +- coarse stage 主要保留 text/translation/knn 的拆分信号 +- fine/rerank stage 现在都保留 `fusion_inputs`、`fusion_factors`、`fusion_summary` +- `fusion_summary` 来自真实计算过程本身,见 [rerank_client.py:265](/data/saas-search/search/rerank_client.py#L265) + +这点很重要,因为现在“实际排序逻辑”和“debug 展示逻辑”是同源的,不是两套各写一份。 + +**一句话总结这条 pipeline** +这条 pipeline 的本质是: + +- ES 负责便宜的大范围召回 +- 粗排负责只靠 ES 内置信号先做一次结构化筛选 +- style SKU 选择负责把商品文本改造成更适合 reranker 理解的输入 +- fine rank 负责用轻模型把候选进一步压缩 +- final rerank 负责用重模型做最终判定 +- 每一层都尽量复用前一层信号,而不是推翻重来 + +如果你愿意,我下一步可以继续按“一个具体 query 的真实流转样例”来讲,比如假设用户搜 `black dress`,我把它从 `parsed_query`、ES named queries、coarse/fine/final 的每个分数怎么出来,完整手推一遍。 \ No newline at end of file diff --git a/docs/issues/issue-2026-03-29-索引修改-done-0330.md b/docs/issues/issue-2026-03-29-索引修改-done-0330.md new file mode 100644 index 0000000..f6e33a1 --- /dev/null +++ b/docs/issues/issue-2026-03-29-索引修改-done-0330.md @@ -0,0 +1,43 @@ + + + + + +工程(金伟)配合修改: + + +一、tags字段改支持多语言: +spu表tags字段,跟title走一样的翻译逻辑,填入原始语言、zh、en。 + +检查以下字段,都跟title一样走翻译逻辑 +title +keywords +tags +brief +description +vendor +category_path +category_name_text + + +二、/indexer/enrich-content接口的修改 +1. 请求参数,把language去掉,因为我返回的内容直接对应索引结构,不用你做处理了,因此不需要指定语言,降低耦合。 +2. 返回 enriched_attributes enriched_tags qanchors三个字段,按原始内容填入。 +3. enriched_tags是本次新增的,注意区别于tags字段。tags字段来源于mysql spu表,enriched_tags是本接口返回的。 + + +三、specifications的value,需要翻译,也是需要填中英文: +{ + "specifications": [ + { + "sku_id": "sku-red-s", + "name": "color", + "value_keyword": "красный", + "value_text": { + "zh": "红色", + "en": "red" + } + } + ] +} + diff --git a/docs/issues/issue-2026-03-30-query分析性能优化-done-0331.md b/docs/issues/issue-2026-03-30-query分析性能优化-done-0331.md new file mode 100644 index 0000000..0a3f6f4 --- /dev/null +++ b/docs/issues/issue-2026-03-30-query分析性能优化-done-0331.md @@ -0,0 +1,264 @@ + +总体的目的是: +1)要对原始query进行翻译(通常是en/zh的一种或者两种) +2)对原始query要有关键词提取(关键词提取可能依赖分词;中英文分词要不一样,各自寻求最优性能的方法。zh的可以保持不变,en的可以优化) +3)其他的一些任务可能依赖分词 +4)获取text embedding/clip embedding + + +英文关键词提取:走spacy进行关键词提取(即主干分析)。提取出query中的核心词,用于搜索时候的term求交,其余的词不参与求交、只用于权重计算。 + + +实现: + +# Query 模块说明 + +本目录实现搜索请求侧的**查询理解与解析**:在不做 Elasticsearch 语言计划拼装的前提下,产出可供检索层、重排层与调试界面消费的**结构化事实**(规范化文本、检测语言、可选翻译、文本与 CLIP 向量、分词与关键词、可选的样式意图与标题排除配置等)。下面按**当前实现**说明策略与数据流,便于与 `search/`、`context/`、`frontend/` 对照阅读。 + +--- + +## 包内文件与职责 + +| 文件 | 作用 | +|------|------| +| `query_parser.py` | 入口 `QueryParser`:编排规范化、改写、语言检测、异步翻译与向量、分词、关键词、意图与排除检测;定义 `ParsedQuery`。 | +| `tokenization.py` | 轻量分词、文本规范化、`TokenizedText` 与按请求复用的 `QueryTextAnalysisCache`(模型分词与语言提示、粗细分词策略)。 | +| `keyword_extractor.py` | `KeywordExtractor`:中文走 HanLP 分词 + 词性名词串;英文走 spaCy 核心词;`collect_keywords_queries` 汇总 `base` 与各翻译语种。 | +| `english_keyword_extractor.py` | `EnglishKeywordExtractor`:`en_core_web_sm` + 依存/名词块规则,产出短字符串供检索侧关键词子句使用。 | +| `language_detector.py` | 脚本优先 + Lingua 的通用语言检测(与 `QueryParser` 的英文 ASCII 快路径配合使用)。 | +| `query_rewriter.py` | 基于配置词典的查询改写与规范化。 | +| `style_intent.py` | 从配置加载样式意图词表,对查询变体做候选匹配,产出 `StyleIntentProfile`。 | +| `product_title_exclusion.py` | 从配置加载标题排除规则,对多路查询文本做触发词匹配,产出 `ProductTitleExclusionProfile`。 | + +公开符号见 `query/__init__.py`(`QueryParser`、`ParsedQuery`、`KEYWORDS_QUERY_BASE_KEY` 等)。 + +--- + +## 解析产物:`ParsedQuery` + +`ParsedQuery` 是单次 `parse()` 的权威结果容器,字段含义与下游约定如下。 + +- **`original_query` / `query_normalized` / `rewritten_query`**:分别为原始输入、规范化后、词典改写后的主查询文本;后续翻译、向量、默认分词与 `base` 关键词均以**改写后的 `rewritten_query`(在代码变量中常名为 `query_text`)**为基准。 +- **`detected_language`**:解析时认定的源语言代码;若检测为 `unknown` 或空,则回退到 `SearchConfig.query_config.default_language`。 +- **`translations`**:键为**目标语言代码**(如 `zh`、`en`),值为翻译服务返回的字符串;仅包含本次请求实际需要的目标语种(见下文翻译目标推导)。 +- **`query_vector` / `image_query_vector`**:分别为 BGE 类文本向量与 CLIP 文本向量(维度由各自编码服务决定);未生成或未在超时内完成则为 `None`。 +- **`query_tokens`**:对**改写后主查询**做分词后的字符串列表,供例如 KNN 参数按 token 数分支等逻辑使用;分词路径由 `QueryTextAnalysisCache` 决定(纯拉丁英文可走轻量分词,含汉字则走 HanLP)。 +- **`keywords_queries`**:与「主查询 + 各翻译变体」平行的**关键词子查询**映射:键 `base`(常量 `KEYWORDS_QUERY_BASE_KEY`)对应源语言侧关键词串,其它键与 `translations` 的语种键一致。空串或无法提取的条目**不会写入**字典。 +- **`style_intent_profile` / `product_title_exclusion_profile`**:可选的理解结果;是否生效完全由 `config.yaml` 中 `query_config` 的对应开关与词表/规则决定。 +- **`_text_analysis_cache`**:单次解析内的分词与语言提示缓存,**不参与序列化**,仅供同一次 `parse` 内各检测器复用,避免对同一文本重复调用 HanLP。 + +与重排相关的文本选择由独立函数 `rerank_query_text()` 完成:检测为 `zh` 或 `en` 时始终用原始查询;其它语言优先英译再中译,见 `query_parser.py` 中实现。 + +--- + +## `QueryParser.parse()` 的执行顺序与策略 + +解析主流程在 `QueryParser.parse()` 中实现。整体目标是:在**共享等待预算**下并行完成翻译与向量请求,同时尽量减少主线程上重复、昂贵的分词与 NLP 调用,并把结果写入可选的 `context`(请求上下文)供日志与 `debug_info` 使用。 + +### 1. 规范化与改写 + +- 使用 `QueryNormalizer` 得到 `query_normalized` 并可选写入 `context.store_intermediate_result('query_normalized', ...)`。 +- 若配置了改写词典,则用 `QueryRewriter` 可能更新 `query_text`;改写成功时记录 `rewritten_query` 与告警。 + +### 2. 语言检测:通用路径与英文 ASCII 快路径 + +- **快路径**:当「活跃语种集合」仅为 `en` 与 `zh` 的子集时(活跃集合取 `target_languages` 归一化结果,若为空则回退到 `query_config.supported_languages`),且当前查询为**纯 ASCII、含字母、不含汉字**,则**直接判定为 `en`**,不再调用 `LanguageDetector`(避免 Lingua 等开销)。逻辑见 `_detect_query_language()` 与 `_is_ascii_latin_query()`。 + +```303:317:query/query_parser.py + def _detect_query_language( + self, + query_text: str, + *, + target_languages: Optional[List[str]] = None, + ) -> str: + normalized_targets = self._normalize_language_codes(target_languages) + supported_languages = self._normalize_language_codes( + getattr(self.config.query_config, "supported_languages", None) + ) + active_languages = normalized_targets or supported_languages + if active_languages and set(active_languages).issubset({"en", "zh"}): + if self._is_ascii_latin_query(query_text): + return "en" + return self.language_detector.detect(query_text) +``` + +- **通用路径**:`LanguageDetector` 先按 Unicode 脚本返回明确语种(如汉字块即 `zh`),否则用 Lingua 在一大组语言中判别,见 `language_detector.py`。 + +检测最终结果写入 `context.store_intermediate_result('detected_language', ...)`(若提供 `context`)。 + +### 3. 按请求分词缓存与语言提示 + +每次 `parse` 会新建 `QueryTextAnalysisCache(tokenizer=self._tokenizer)`,并对**原始串、规范化串、改写后串**调用 `set_language_hint(..., detected_lang)`,使后续对同一文本的 `get_tokenizer_result` / `get_tokenized_text` 能按语言选择**是否调用 HanLP**。 + +### 4. HanLP 模型(与 `KeywordExtractor` 对齐) + +`QueryParser` 默认构建的 `self._tokenizer` 为 HanLP 预训练分词模型 **`FINE_ELECTRA_SMALL_ZH`**,并开启 `output_spans=True`,以便与关键词提取共用「带偏移的分词结果」。 + +```237:245:query/query_parser.py + def _build_tokenizer(self) -> Callable[[str], Any]: + """Build the tokenizer used by query parsing. No fallback path by design.""" + if hanlp is None: + raise RuntimeError("HanLP is required for QueryParser tokenization") + logger.info("Initializing HanLP tokenizer...") + tokenizer = hanlp.load(hanlp.pretrained.tok.FINE_ELECTRA_SMALL_ZH) + tokenizer.config.output_spans = True + logger.info("HanLP tokenizer initialized") + return tokenizer +``` + +`KeywordExtractor` 在未注入自定义 `tokenizer` 时同样加载 **`FINE_ELECTRA_SMALL_ZH`**,并额外加载 **`CTB9_POS_ELECTRA_SMALL`** 做词性标注;二者在「中文路径」上语义一致,便于复用 `tokenizer_result`。 + +### 5. 异步富集:翻译、文本向量、CLIP 文本向量 + +- 翻译目标:`translation_targets = normalized_targets` 中**去掉与检测源语言相同**的代码后的列表(例如源为 `en` 且索引语言为 `["en","zh"]` 时只翻 `zh`)。 +- 翻译模型名:由 `_pick_query_translation_model()` 根据「源语言是否在索引语言内」及 `zh↔en` 等分支从 `QueryConfig` 选取。 +- 当 `generate_vector` 为真且配置开启文本嵌入时,向线程池提交 `text_encoder.encode([query_text], ...)`;当配置了 `image_embedding_field` 时提交 `image_encoder.encode_clip_text(query_text, ...)`。 +- 线程池:`ThreadPoolExecutor`,`max_workers` 为 `min(任务数, 4)` 与至少 1。 +- **提交顺序**:先尽可能提交所有异步任务,再在主线程上做「与异步重叠」的轻量工作(见下一节),最后 `concurrent.futures.wait(..., timeout=budget_sec)`。超时未完成的任务会记 warning,并 `shutdown(wait=False)` 不阻塞关闭线程池。 + +等待预算(毫秒)来自 `QueryConfig`: + +- 源语言在索引语言内:`translation_embedding_wait_budget_ms_source_in_index` +- 否则:`translation_embedding_wait_budget_ms_source_not_in_index` + +完成每个 future 后打 `Async enrichment task finished` 日志(含 `elapsed_ms`,为从提交到完成的大致墙钟时间)。 + +### 6. 主查询分词与「base」关键词(与异步重叠) + +在异步任务已提交之后、`wait()` 之前,当前实现会: + +1. 通过 `text_analysis_cache.get_tokenizer_result(query_text)` 得到分词结果,再 `extract_token_strings` 得到 **`query_tokens`**; +2. 调用 `KeywordExtractor.extract_keywords(query_text, language_hint=detected_lang, tokenizer_result=...)` 得到 **`keywords_base_query`**(若失败则日志告警,base 关键词可能为空)。 + +这样主线程在等翻译/向量时,已并行完成源侧分词与源侧关键词的大部分工作。 + +### 7. 等待结束后的关键词汇总与检测器 + +`wait()` 返回后: + +- 若有翻译结果,写入 `context.store_intermediate_result("translations", translations)`,并对每条翻译 `text_analysis_cache.set_language_hint(result, lang)`,保证后续对该翻译串的分词/关键词走正确语言路径。 +- `collect_keywords_queries(...)` 合并 **`base`**(可传入已算好的 `base_keywords_query` 避免重复抽取)与各翻译语种的关键词,得到 **`keywords_queries`**;成功时 `context.store_intermediate_result("keywords_queries", keywords_queries)` 并打 `Keyword extraction completed` 日志。 +- 构造带 `_text_analysis_cache` 的 `ParsedQuery` 草稿,依次调用 `StyleIntentDetector.detect` 与 `ProductTitleExclusionDetector.detect`,再把完整 `ParsedQuery` 返回。 + +解析阶段会打聚合耗时日志 `Query parse stage timings`,字段含义为: + +- **`before_wait_ms`**:从解析开始计时点到进入 `wait()` 之前的主线程耗时(含规范化、改写、语言检测、提交异步任务、主查询分词、base 关键词等); +- **`async_wait_ms`**:`wait()` 阻塞时间; +- **`base_keywords_ms`**:base 关键词抽取耗时; +- **`keyword_tail_ms`**:`collect_keywords_queries` 及前后尾部逻辑中关键词相关部分的主要耗时; +- **`tail_sync_ms`**:`wait()` 之后整段同步尾巴(含关键词汇总、两检测器、写中间结果等)。 + +--- + +## 分词与 `QueryTextAnalysisCache` + +### `get_tokenizer_result`:何时走 HanLP,何时走轻量分词 + +- 若未配置模型 `tokenizer`,直接返回空列表路径的轻量结果(由上层避免依赖)。 +- 若根据**该文本的语言提示**与**是否含汉字**判断不需要模型:返回 `simple_tokenize_query` 的列表(字符串 token),**不调用 HanLP**。 +- 否则对该文本调用一次 `self.tokenizer(text)`(HanLP),结果按文本缓存,同一次 `parse` 内重复访问同一字符串不会重复推理。 + +核心判断在 `_should_use_model_tokenizer`:**语言提示为 `zh` 时,仅当文本含汉字才用模型**;非 `zh` 提示时,仅当文本含汉字才用模型。因此纯英文主查询在提示为 `en` 时走轻量分词;中文翻译串在 `set_language_hint(..., "zh")` 且含汉字时走 HanLP。 + +### `coarse_tokens` 与 `fine_tokens`:`TokenizedText` + +- **`fine_tokens`**:来自 `extract_token_strings(get_tokenizer_result(...))`,在中文路径上即 HanLP 分词后的词串(已按规范化键去重保序)。 +- **`coarse_tokens`**:由 `_build_coarse_tokens` 决定。若语言提示为 **`zh`**,或文本含汉字且已有 `tokenizer_tokens`,则 **粗粒度 token 与 fine 一致**(即采用模型分词粒度,而不用「整段 CJK 连成一项」的纯正则策略)。否则使用 `simple_tokenize_query`(适合拉丁词、数字、带连字符/撇号的英文词形)。 + +```92:103:query/tokenization.py +def _build_coarse_tokens( + text: str, + *, + language_hint: Optional[str], + tokenizer_tokens: Sequence[str], +) -> List[str]: + normalized_language = normalize_query_text(language_hint) + if normalized_language == "zh" or (contains_han_text(text) and tokenizer_tokens): + # Chinese coarse tokenization should follow the model tokenizer rather than a + # regex that collapses the whole sentence into one CJK span. + return list(_dedupe_preserve_order(tokenizer_tokens)) + return _dedupe_preserve_order(simple_tokenize_query(text)) +``` + +- **`candidates`**:在 fine、coarse、两类 n-gram 短语(上限由 `max_ngram` 控制)以及整句 `normalized_text` 上合并去重,供 `StyleIntentDetector`、`ProductTitleExclusionDetector` 等做子串/短语级匹配。 + +`tokenize_text()` 是对单次无缓存场景的薄封装:内部新建 `QueryTextAnalysisCache` 再 `get_tokenized_text`。 + +--- + +## 关键词提取:`KeywordExtractor` 与 `collect_keywords_queries` + +### 路由规则 + +`extract_keywords` 根据 `language_hint` 分支: + +- **`en`**:完全交给 `EnglishKeywordExtractor`(spaCy),**不使用** HanLP 分词结果做 POS 名词筛选(即使调用方传入 `tokenizer_result` 也会被忽略在该路径内)。 +- **`zh`**:使用 HanLP 分词结果(优先复用传入的 `tokenizer_result`),再对词序列跑 `CTB9_POS_ELECTRA_SMALL`,保留**长度 ≥ 2 且词性以 `N` 开头**的词;非连续名词之间插入空格拼接成一条字符串(与 ES 侧 `keywords_query` 的用法一致)。 +- **其它非空语言码**:当前实现返回空串,即**不为该语种生成关键词子句**(由调用方决定是否跳过)。 + +### `collect_keywords_queries` + +- 键 **`base`**:对应 `rewritten_query` 的关键词;若调用方已预先计算 `base_keywords_query` 则直接写入,避免重复抽取。 +- 其它键:与 `translations` 中每个非空语种一一对应,语言码归一化为小写。 +- 全程可传入 `text_analysis_cache`,以便 `get_tokenizer_result` 命中缓存并与检测器共享分词结果。 + +常量 `KEYWORDS_QUERY_BASE_KEY` 的值为字符串 **`"base"`**,与检索构建里读取的字段一致。 + +--- + +## 英文关键词:`EnglishKeywordExtractor` + +- 依赖 **spaCy** 模型 **`en_core_web_sm`**,加载时关闭 `ner`、`textcat` 以减轻开销;加载失败时记录 warning 并走基于 `simple_tokenize_query` 的回退策略。 +- 主路径用依存句法与名词块规则收集一小组「核心词」候选(如直接宾语名词、部分 ROOT 名词/专有名词、INTJ 结构下的宾语等),并处理价格/目的介词宾语降级、人口学名词(如 `women`)弱化、尺寸类 ROOT 与主语搭配等边界情况。 +- 使用 `_project_terms_to_query_tokens` 将 spaCy 词形映射回查询中的**表面分词**(例如复合词 `t-shirt`),避免在关键词串中出现被错误切断的片段。 + +最终返回**最多三个词**的空格连接字符串,用于检索侧第二层 `combined_fields` 的紧凑查询(见下节)。 + +--- + +## 与检索层的关系(消费方摘要) + +`ParsedQuery.keywords_queries` 由 `search/es_query_builder.py` 读取:在构建某一语言的 lexical 子句时,除主 `combined_fields`(完整 `query`)外,若存在非空的 `keywords_query` 且与主查询不同,会追加第二个 `combined_fields`,使用单独的 `minimum_should_match`(由 builder 的 `keywords_minimum_should_match` 配置)和较低 boost,从而在**不替代全文查询**的前提下加强核心词匹配。 + +`query_tokens` 在同文件中间接影响例如带文本向量时的 KNN 分支参数(按 token 数量选用长查询的 k / num_candidates 等)。具体字段与 boost 以 `ESQueryBuilder` 当前实现为准。 + +--- + +## 样式意图与标题排除(简要) + +- **`StyleIntentRegistry` / `StyleIntentDetector`**:从 `QueryConfig.style_intent_terms` 等加载意图定义;`detect` 时按中英变体取查询文本,经 `tokenize_text` 或缓存得到 `TokenizedText`,在 `candidates` 上与配置同义词表匹配,输出 `StyleIntentProfile`(含 `query_variants` 与命中意图列表)。 +- **`ProductTitleExclusionRegistry` / `ProductTitleExclusionDetector`**:从 `QueryConfig.product_title_exclusion_rules` 加载规则;对 `original_query`、`query_normalized`、`rewritten_query` 及所有 `translations` 去重后分词匹配触发词,输出 `ProductTitleExclusionProfile`。 + +二者均依赖 `tokenization` 与可选的 HanLP,启用与否由配置项控制。 + +--- + +## 可观测性与调试 + +当 `QueryParser.parse(..., context=...)` 传入请求上下文时,典型中间结果包括: + +- `query_normalized`、`rewritten_query`、`detected_language`、`query_tokens` +- `translation_{lang}`、`translations` +- `keywords_queries` +- `query_vector_shape`、`image_query_vector_shape` +- `style_intent_profile`、`product_title_exclusion_profile` + +搜索主流程在 `search/searcher.py` 中会把解析结果写入 `QueryAnalysisResult`(含 **`keywords_queries`**),并在 `debug=true` 时把 `query_analysis` 挂到响应的 `debug_info`;前端调试页在 `frontend/static/js/app.js` 中展示 **Translations** 与 **Keywords Queries** 等块,便于与翻译结果并列查看。 + +--- + +## 依赖与环境提示 + +- **HanLP**:分词与中文词性标注;模型名以本文与源码为准(`FINE_ELECTRA_SMALL_ZH` + `CTB9_POS_ELECTRA_SMALL`)。 +- **spaCy**:英文关键词路径需要可导入的 **`en_core_web_sm`**(若缺失则英文关键词退化为轻量规则)。 +- **Lingua**:通用语言检测(在英文 ASCII 快路径不适用时参与拉丁语系判别)。 + +运行与测试时请使用项目约定的虚拟环境(见仓库根目录 `CLAUDE.md` / `activate.sh`),避免系统 Python 缺少上述依赖。 + +--- + +## 扩展与测试 + +- 单元测试中与解析、分词、意图相关的用例分布在 `tests/test_query_parser_mixed_language.py`、`tests/test_tokenization.py`、`tests/test_style_intent.py`、`tests/test_product_title_exclusion.py` 等文件中;修改分词或关键词策略时应同步更新或新增测试,以保持与本文描述一致。 + +若新增语种或改写语言检测策略,应同步审视:`QueryParser._detect_query_language`、`QueryTextAnalysisCache._should_use_model_tokenizer`、`KeywordExtractor.extract_keywords` 中非 `zh`/`en` 分支,以及 ES 侧是否应为新语种生成 `keywords_query`。 diff --git a/docs/issues/issue-2026-03-31-评估框架-done-0331.md b/docs/issues/issue-2026-03-31-评估框架-done-0331.md new file mode 100644 index 0000000..257ce6a --- /dev/null +++ b/docs/issues/issue-2026-03-31-评估框架-done-0331.md @@ -0,0 +1,151 @@ + + +参考资料: + +1. 搜索接口: + +```bash +export BASE_URL="${BASE_URL:-http://localhost:6002}" +export TENANT_ID="${TENANT_ID:-163}" # 改成你的租户ID +``` +```bash +curl -sS "$BASE_URL/search/" \ + -H "Content-Type: application/json" \ + -H "X-Tenant-ID: $TENANT_ID" \ + -d '{ + "query": "芭比娃娃", + "size": 20, + "from": 0, + "language": "zh" + }' +``` + +response: +{ + "results": [ + { + "spu_id": "12345", + "title": "芭比时尚娃娃", + "image_url": "https://example.com/image.jpg", + "specifications":[], + "skus":[{"sku_id":" ... +... + +2. 重排服务: +curl -X POST "http://localhost:6007/rerank" \ + -H "Content-Type: application/json" \ + -d '{ + "query": "玩具 芭比", + "docs": ["12PCS 6 Types of Dolls with Bottles", "纯棉T恤 短袖"], + "top_n":386, + "normalize": true + }' + + +3. 基于指定字段查询:es_debug_search.py + + +主要任务: +1. 评估工具的建立: +注意判断结果好坏,要用统一的评估工具,不要对每个query设定关键词匹配的规则来判断是否符合要求,这样不可扩展,这种方式且容易有误判还是复杂,并且不好扩展到其他搜索词。 +因此要做一个搜索结果评估工具、多个结果对比的工具,供后面的标注集合构建工具调用。工具内部实现可以是调用大模型来判断,说清楚什么叫高相关、基本相关、不相关: + +prompt: +```bash +你是一个电商搜索结果相关性评估助手。请根据用户查询(query)和每个商品的信息,输出该商品的相关性等级。 + +## 相关性等级标准 +Exact 完全相关 — 完全匹配用户搜索需求。 +Partial 部分相关 — 主意图满足(同品类或相近用途,基本上符合搜索意图),但次要属性(如颜色、风格、尺码等)跟用户需求有偏差或无法确认。 +Irrelevant 不相关 — 品类或用途不符,主诉求未满足。 + +1. {title1} {option1_value1} {option2_value1} {option3_value1} +2. {title2} {option1_value2} {option2_value2}, {option3_value2} +... +50. {title50} {option1_value50} {option2_value50} {option3_value50} + +## 输出格式 +严格输出 {input_nums} 行,每行仅Exact / Partial / Irrelevant三者之一。按顺序对应上述 50 个商品。不要输出任何其他任何信息 +``` + + +2. 测试集(结果标注)建立: +@queries/queries.txt + +对其中每一个query: +1. 召回: +1)参考搜索接口 召回结果。搜索结果的top500,纳入召回池,打分全部标记为1 +2)调用重排模型,扫描全库(tenant_id=163),如果已经在召回池(打分已经是1了),则跳过,其余的全部过reranker模型接口调用。每80个doc做一次请求。注意重排模型打分一定要做缓存(本地文件缓存即可。query+title->rerank_score)。 +3)对reranker打分超过0.5的结果数大于1000条的query,则打印一行日志,跳过这个query,表示相关结果太多、容易被满足 + + +2. 对如上召回的内容,进行全排序,然后逐批进行llm评判标注(50个一批),每一批都记录exact比例和不相关比例,打印日志。 +直到连续三批不相关比例都大于92%。 +最少要跑15批,最多跑40批 + +3. 请你思考如何存储结果、并利于以后的对比、使用、展示。 + + + + +3. 评估工具页面: +请你设计一个搜索评估交互页面。端口6010。 +页面主题:上方是搜索框,如果发起搜索,那么下方给出本次结果的总体指标以及top100结果(允许翻页) + +总体指标: +| 指标 | 含义 | +|------|------| +| **P@5, P@10, P@20, P@50** | 前 K 个结果中「仅 3 相关」的精确率 | +| **P@5_2_3 ~ P@50_2_3** | 前 K 个结果中「2 和 3 都算相关」的精确率 | +| **MAP_3** | 仅 3 相关时的 Average Precision(单 query) | +| **MAP_2_3** | 2 和 3 都相关时的 Average Precision | + +结果列表: +按行列下来,每行左侧给每个结果找到标注值(三个等级。对结果也可以颜色标记),展示图片,title.en+title.en+首个sku的option1/2/3_value(分三行展示,这三行和左侧的图片并列) + + +评测页面最左侧: +queries默认是queries/queries.txt,填入左侧列表框,点击其中任何一个发起搜索。 + +4. 批量评估工具 + +给一个批量执行脚本, + +这里要新增一个批量评估的页面。点击批量评估的按钮,对所有搜索词依次发起搜索,最后汇总总体的评估指标,生成报告,报告名称带上时间标记和一些关键信息。并且记录当时的主搜索程序的config.yaml。 +你需要精心地设计如何切换两种模式,通过同一个端口承载这两种不同交互的内容。 +批量评估关注的是所有搜索词总体的评估指标。 +需要记录测试环境时间以及当时的配置文件,以及对应的结果。要保存历次的评估记录,并能查到每一次评估结果对应的配置文件有相关的指标 + +以上是我的总体设计,但有不周全的地方。你要站在更高的层次理解我的需求,你有足够的自由可以适当调整设计,基于你所了解的自动化搜索评估框架的最佳实践,做出更优秀的设计和更好的实现。 + + + + + + +1. 请仔细检验这个标注集的质量,如果质量不符合要求,那么你要优化工具,迭代直至标注集的结果质量足够高,可以以此为自动化工具来评估检索效果,对检索效果形成指导性意见。 +2. 在结果标注集的质量足够好,批量评估工具足够好用,并且经过你的试用,能判断出搜索质量好坏的情况下,开始真正的动手检索效果调优:基于这个50条query的结果标注集和批量评估工具,对融合公式进行调参。请你先精心地设计实验,设计几组参数,对几组参数分别修改config.yaml、重启(./restart.sh backend)、跑批量评估、收集结果。 +注意评估的过程中,如果发现工具不好用,发现日志不全,发现可以通过修改工具或者日志来提高效率,都可以先做这些,根据完善。 +注意你是代码的总负责人,你有任何权限来满足你进行检索效果调优的需要。你如果发现有其他可能带来更大提升的点,也可以进行实验,你甚至可以修改融合、重排漏斗的代码,来进行实验,以追求更好的结果指标。 +但是注意,因为收到性能和耗时的约束,不要调大reranker模型的输入条数、不要打开精排,耗时方面无法承受两轮reranker模型的调用。 + + + + + + + + + +@scripts/evaluation/README.md @scripts/evaluation/eval_framework/framework.py +@start_eval.sh.sh (29-35) +请以如下流程为准,进行改造: +如果重建的话,对每个query: +每个搜索结果应该会扫描全库, +1. 搜索结果的top500,纳入召回池,打分全部标记为1 +2. 调用重排模型,扫描全库(tenant_id=163),如果已经在召回池(打分已经是1了),则跳过,其余的全部过 +3. 对reranker打分超过0.5的大于1000条,则打印一行日志,跳过这个query,表示相关结果太多、容易被满足 + +对如上召回的内容,进行全排序,然后逐批进行llm评判标注(50个一批),每一批都记录exact比例和不相关比例,打印日志。 +直到连续三批不相关比例都大于92%。 +最少要跑15批,最多跑40批 diff --git a/docs/issues/issue-2026-04-01-评估框架-四级label-done-0402.md b/docs/issues/issue-2026-04-01-评估框架-四级label-done-0402.md new file mode 100644 index 0000000..2b84494 --- /dev/null +++ b/docs/issues/issue-2026-04-01-评估框架-四级label-done-0402.md @@ -0,0 +1,335 @@ +_QUERY_INTENT_ANALYSIS_TEMPLATE_EN = """You are an intent analysis expert for a fashion e-commerce search system. + +Given a user's search query, analyze the shopping intent behind the query in the context of fashion and apparel e-commerce, and summarize the user's core search need in one concise sentence. +Also provide the Chinese translation and English translation of the query. + +Requirements: +- Keep the intent analysis concise and easy to understand, using 1 to 3 short sentences. +- Stay grounded in the original query and summarize the user's likely shopping intent without adding unnecessary context. +- When the query is vague or ambiguous, take a conservative approach and keep the analysis close to the original wording. +- Chinese translation: if the original query is already in Chinese, keep it unchanged. +- English translation: if the original query is already in English, keep it unchanged. +- Do not output anything other than the required three-line format. + +Output format (strictly exactly three lines): +Intent: concise analysis of the user's search intent +Query中文翻译: Chinese translation of the query +Query English translation: English translation of the query + +Now analyze the following query: + +Query: {query} +""" + +_QUERY_INTENT_ANALYSIS_RESULT_TEMPLATE_ZH = """ +你是一个服装品类电商搜索意图分析专家。 + +给定用户输入的搜索词,请在服装品类电商场景下,分析该搜索词背后的购物意图,并用一句话简要描述用户的核心搜索需求。 +同时,提供该搜索词的中文翻译和英文翻译。 + +要求: +- 意图分析应简洁易懂,用 1 到 3 句短句概括用户的搜索意图。 +- 结合 query 本身,尽量贴近用户原始搜索需求进行总结,不添加不必要的背景、延伸或臆测。 +- 如果 query 不够明确或有歧义,应保守处理,尽量保持与原词表达一致。 +- 中文翻译:如果原始 query 本身就是中文,则按原样输出。 +- 英文翻译:如果原始 query 本身就是英文,则按原样输出。 +- 除指定格式外,不要输出任何额外说明。 + +输出格式(严格按三行输出): +Intent: 对用户搜索意图的简洁分析 +Query中文翻译: query 的中文翻译 +Query English translation: query 的英文翻译 + +现在请分析以下搜索词: + +Query: {query} +""" + +_CLASSIFY_TEMPLATE_EN = """You are a relevance judgment assistant for a fashion e-commerce search system. +Given a user query and the information for each product, assign a relevance label to each product. + +Your goal is to judge relevance from the perspective of e-commerce search ranking. +The key question is whether the user would view the product as the intended item, or as an acceptable substitute. + +## Relevance Labels + +### Exact Match +The product satisfies the user’s core shopping intent: the core product type matches, and all explicitly stated key attributes in the query are supported by the product information, with no obvious conflict. + +Typical use cases: +- The query contains only a product type, and the product is exactly that type. +- The query contains “product type + attributes”, and the product matches both the type and all explicitly stated attributes. + +### High Relevant +The product satisfies the user’s main intent: the core product type matches, but some explicitly requested attributes are missing from the product information, cannot be confirmed, or show minor / non-critical deviations. The product is still a good substitute for the user’s core need. + +Use “High Relevant” in the following cases: +- The core product type matches, but some requested attributes are missing, not mentioned, or cannot be verified. +- The core product type matches, but attributes such as color, material, style, fit, or length have minor deviations, as long as the deviation does not materially undermine the user’s main shopping intent. +- The product is not the user’s ideal target, but in an e-commerce shopping context, it would still be considered an acceptable and strong substitute. + +Typical examples: +- Query: “red slim-fit T-shirt” + Product: “women’s T-shirt” + → Color and fit cannot be confirmed. +- Query: “red slim-fit T-shirt” + Product: “blue slim-fit T-shirt” + → Product type and fit match, but the color is different. + +Detailed case: +- Query: “cotton long-sleeve shirt” +- Product: “J.VER Men's Linen Shirt Casual Button Down Long Sleeve Solid Plain Collar Summer Beach Shirt with Pocket” + +Analysis: +- Material mismatch: the query explicitly requires “cotton”, while the product is “linen”, so it cannot be labeled as “Exact Match”. +- However, the core category still matches: both are long-sleeve shirts. +- In e-commerce search, users may still click this item because the style and wearing scenario are similar. +- Therefore, it should be labeled as “High Relevant”: not the exact target, but a good substitute. + +Detailed case: +- Query: “black mid-length skirt” +- Product: “New spring autumn loose slimming full long floral skirt pleated skirt” + +Analysis: +- Category match: the product is a skirt, so the category matches. +- Color mismatch: the product description does not indicate black and explicitly mentions “floral”, which is substantially different from plain black. +- Length deviation: the user asks for “mid-length”, while the product title emphasizes “long skirt”, which is somewhat longer. +- However, the core category “skirt” still matches, and style features such as “slimming” and “full skirt” may still fit some preferences of users searching for a mid-length skirt. Also, “long” versus “mid-length” is a deviation, but not a severe contradiction. +- Therefore, this should be labeled as “High Relevant”: the core type matches, but there are several non-fatal attribute deviations. + +### Low Relevant +The product has a noticeable gap from the user’s core target, but still shares some similarity with the query in style, scenario, function, or broader category. A small portion of users may still view it as a barely acceptable substitute. It is not the intended item, but still has some relevance. + +Use “Low Relevant” in the following cases: +- The core product type does not match, but the two types are still very close in style, wearing scenario, or function, so there is still some substitutability. +- The core product type matches, but the product differs from the user’s ideal target on multiple attributes; it still has some relevance, but is no longer a strong substitute. +- An important query requirement is clearly violated, but the product still retains a limited reason to be clicked. + +Typical cases: +- Query: “black mid-length skirt” + Product: “New high-waisted V-neck mid-length dress elegant printed black sexy dress” + → The core product type differs (“skirt” vs “dress”), but both belong to closely related apparel types and share a similar mid-length style, so it is “Low Relevant”. + +- Query: “jeans” + Product: “casual pants” + → The core product type is different, but both belong to the broader pants category, and the style / wearing scenario may still be close enough to be a weak substitute. + +### Irrelevant +The product does not satisfy the user’s main shopping intent, and the likelihood of user engagement is very low. + +Typical situations: +- The core product type does not match the query and is not a close substitute in style, scenario, or function. +- The product belongs to a roughly related broader category, but not to an interchangeable subtype explicitly requested in the query, and the style or usage scenario differs significantly. +- The core product type matches, but the product clearly violates an explicit and important requirement in the query, with little or no acceptable substitutability. + +Typical examples: + +1. **Different core product type with no substitutability** +- Query: "pants", Product: "shoes" +- Query: "boots", Product: "sneakers" + +2. **Close core product type but clear conflict on key attributes** +- Query: "slim-fit pants", Product: "loose wide-leg pants" +- Query: "sleeveless dress", Product: "long-sleeve dress" +- Query: "loose-fitting hoodie", Product: "slim-fit T-shirt" + +3. **Same broad category but significant difference in style or usage scenario** +- Query: "jeans", Product: "sweatpants / dress pants" + +## Judgment Principles + +1. **Product type is the highest-priority factor.** + If the query explicitly specifies a concrete product type, the result must match that product type in order to be labeled as “Exact Match” or “High Relevant”. + Different product types should usually be labeled as “Low Relevant” or “Irrelevant”. + + - **Low Relevant**: use only when the two product types are very close in style, scenario, or function, and the user may still treat one as a barely acceptable substitute for the other. + - **Irrelevant**: all other product type mismatch cases. + +2. **Similar or related product types are usually not directly interchangeable when the query is explicit, but their closeness should determine whether the label is “Low Relevant” or “Irrelevant”.** + For example: + - **May be Low Relevant due to strong similarity in style / scenario**: dress vs skirt, long skirt vs mid-length skirt, jeans vs casual pants, sneakers vs skate shoes. + - **Should be Irrelevant due to substantial difference in style / scenario**: pants vs shoes, T-shirt vs hat, boots vs sneakers, jeans vs suit pants, backpack vs handbag. + +3. **Once the core product type matches, evaluate attributes.** + - All explicit attributes match → **Exact Match** + - Some attributes are missing, not mentioned, cannot be verified, or show only minor deviations → **High Relevant** + - There are multiple attribute deviations, or an important attribute is clearly violated, but the product still retains some substitutability → **Low Relevant** + - There is a clear and important hard conflict, and substitutability is extremely low → **Irrelevant** + +4. **Strictly distinguish among “not mentioned / cannot confirm”, “minor deviation”, and “explicit contradiction”.** + - If an attribute is not mentioned or cannot be verified, prefer **High Relevant**. + - If an attribute shows a minor deviation, such as different color, different material, or slightly different length, it should usually be labeled **High Relevant**. + - If an attribute is explicitly opposite to the query requirement, such as sleeveless vs long-sleeve or slim-fit vs loose wide-leg, decide between **Low Relevant** and **Irrelevant** based on the severity of the conflict and practical substitutability. + - If the conflict directly breaks the user’s main shopping goal, it should usually be labeled **Irrelevant**. + +5. **Substitutability should be judged from real shopping intent, not just surface-level textual similarity.** + The question is whether the user would realistically accept the product in a shopping scenario. + - Good substitute → **High Relevant** + - Barely acceptable substitute → **Low Relevant** + - Hardly substitutable at all → **Irrelevant** + +6. **When product information is insufficient, do not treat “cannot confirm” as “conflict”.** + If a product does not mention an attribute, that does not mean the attribute is definitely violated. + Therefore: + - If the attribute is not mentioned or cannot be confirmed, prefer **High Relevant**; + - Only treat it as a conflict when the product information clearly shows the opposite of the query requirement. + +Query: {query} +{intent_suffix} + +Products: +{lines} + +## Output Format +Output exactly {n} lines. +Each line must be exactly one of the following: +Exact Match +High Relevant +Low Relevant +Irrelevant + +Now, assign a relevance label to each result with respect to the query "{query}". +The output lines must correspond to the products above in the same order. +Do not output anything else. +""" + +_CLASSIFY_TEMPLATE_ZH = """你是一个服饰电商搜索系统中的相关性判断助手。 +给定用户查询词以及每个商品的信息,请为每个商品分配一个相关性标签。 + +你的目标是从电商搜索排序的角度,判断商品是否满足用户的购物意图。 +判断时应优先考虑“用户是否会把该商品视为目标商品,或可接受的替代品”。 + +## 相关性标签 + +### 完全相关 +商品满足用户的核心购物意图:核心商品类型匹配,且查询中所有明确提及的关键属性均有商品信息支持。 + +典型适用场景: +- 查询仅包含商品类型,商品即为该类型。 +- 查询包含“商品类型 + 属性”,商品在类型及所有明确属性上均符合。 + +### 基本相关 +商品满足用户的主要意图:核心商品类型匹配,但查询中明确提出的部分要求未在商品信息中体现、无法确认,或存在轻微偏差 / 非关键偏差。该商品仍是满足用户核心需求的良好替代品。 + +在以下情况使用“基本相关”: +- 核心商品类型匹配,但部分属性缺失、未提及或无法确认。 +- 核心商品类型匹配,但颜色、材质、风格、版型、长度等属性存在轻微偏差,只要这种偏差不会明显破坏用户的主要购买意图。 +- 商品不是用户最理想的目标,但在电商购物场景下仍可能被视为可接受、且较优的替代品。 + +典型情况: +- 查询:“红色修身T恤”,商品:“女士T恤” + → 颜色、版型无法确认。 +- 查询:“红色修身T恤”,商品:“蓝色修身T恤” + → 商品类型和版型匹配,但颜色不同。 + +详细案例: +- 查询:“棉质长袖衬衫” +- 商品:“J.VER男式亚麻衬衫休闲纽扣长袖衬衫纯色平领夏季沙滩衬衫带口袋” + +分析: +- 材质不符:Query 明确指定“棉质”,而商品为“亚麻”,因此不能判为“完全相关”。 +- 但核心品类仍然匹配:两者都是“长袖衬衫”。 +- 在电商搜索中,用户仍可能因为款式、穿着场景相近而点击该商品。 +- 因此应判为“基本相关”,即“非精确目标,但属于良好替代品”。 + +详细案例: +- 查询:“黑色中长半身裙” +- 商品:“春秋季新款宽松显瘦大摆长裙碎花半身裙褶皱设计裙” + +分析: +- 品类匹配:商品是“半身裙”,品类符合。 +- 颜色不匹配:商品描述未提及黑色,且明确包含“碎花”,与纯黑差异较大。 +- 长度存在偏差:用户要求“中长”,而商品标题强调“长裙”,长度偏长。 +- 但核心品类“半身裙”匹配,“显瘦”“大摆”等风格特征仍可能符合部分搜索“中长半身裙”用户的潜在偏好;同时“长裙”和“中长”虽有偏差,但不构成严重对立。 +- 因此应判为“基本相关”:核心品类匹配,但存在若干非致命属性偏差。 + +### 弱相关 +商品与用户的核心目标存在明显差距,但仍与查询在风格、场景、功能或大类上具有一定相似性,可能被少量用户视为勉强可接受的替代品。属于“非目标,但仍有一定关联”。 + +在以下情况使用“弱相关”: +- 核心商品类型不一致,但两者在风格、穿着场景或功能上非常接近,仍具有一定替代性。 +- 核心商品类型匹配,但在多个属性上与用户理想目标差距较大,虽仍有一定关联性,但已不是高质量替代品。 +- 查询要求中的某个重要属性被明显违背,但商品仍保留少量被点击的理由。 + +典型情况: +- 查询:“黑色中长半身裙”,商品:“新款高腰V领中长款连衣裙 优雅印花黑色性感连衣裙” + → 核心商品类型“半身裙”与“连衣裙”不同,但两者同属裙装,且款式上均为“中长款”,在穿搭场景上接近,因此属于“弱相关”。 + +- 查询:“牛仔裤”,商品:“休闲裤” + → 核心商品类型不同,但同属裤装大类,风格和穿着场景可能接近,可作为较弱替代品。 + +### 不相关 +商品未满足用户的主要购物意图,用户点击动机极低。 + +主要表现为以下情形之一: +- 核心商品类型与查询不匹配,且不属于风格 / 场景 / 功能接近的可替代品。 +- 商品虽属于大致相关的大类,但与查询明确指定的具体子类不可互换,且风格或场景差异大。 +- 核心商品类型匹配,但商品明显违背了查询中一个明确且重要的要求,且几乎不具备可接受的替代性。 + +典型情况: + +**1. 核心品类不同,且无替代性** +- 查询:“裤子”,商品:“鞋子” +- 查询:“靴子”,商品:“运动鞋” + +**2. 核心品类相近,但关键属性明显冲突** +- 查询:“紧身裤”,商品:“阔腿裤” +- 查询:“无袖连衣裙”,商品:“长袖连衣裙” +- 查询:“宽松卫衣”,商品:“修身T恤” + +**3. 核心品类同属大类,但风格、场景差异巨大** +- 查询:“牛仔裤”,商品:“运动裤 / 西裤” + +## 判断原则 + +1. **商品类型是最高优先级因素。** + 如果查询明确指定了具体商品类型,那么结果必须匹配该商品类型,才可能判为“完全相关”或“基本相关”。 + 不同商品类型通常应判为“弱相关”或“不相关”。 + + - **弱相关**:仅当两种商品类型在风格、场景、功能上非常接近,用户有一定概率将其视为勉强可接受的替代品时使用。 + - **不相关**:其他所有商品类型不匹配的情况。 + +2. **相似或相关的商品类型,在查询明确时通常不可直接互换,但要根据接近程度区分“弱相关”与“不相关”。** + 例如: + - **风格 / 场景高度接近,可判为弱相关**:连衣裙 vs 半身裙、长裙 vs 中长裙、牛仔裤 vs 休闲裤、运动鞋 vs 板鞋。 + - **风格 / 场景差异大,应判为不相关**:裤子 vs 鞋子、T恤 vs 帽子、靴子 vs 运动鞋、牛仔裤 vs 西装裤、双肩包 vs 手提包。 + +3. **当核心商品类型匹配后,再评估属性。** + - 所有明确属性都匹配 → **完全相关** + - 部分属性缺失、未提及、无法确认,或存在轻微偏差 → **基本相关** + - 存在多个属性偏差,或某个重要属性被明显违背,但商品仍保留一定替代性 → **弱相关** + - 存在明确且重要的强冲突,且替代性极低 → **不相关** + +4. **要严格区分“未提及 / 无法确认”“轻微偏差”“明确冲突”。** + - 如果某属性没有提及,或无法验证,优先判为“基本相关”。 + - 如果某属性存在轻微偏差,如颜色不同、材质不同、长度略有差异,通常判为“基本相关”。 + - 如果某属性与查询要求明确相反,如无袖 vs 长袖、修身 vs 宽松阔腿,则要根据冲突严重性与替代性,在“弱相关”与“不相关”之间判断。 + - 若该冲突会直接破坏用户的主要购买目标,通常判为“不相关”。 + +5. **“是否可替代”应从真实电商购物意图出发判断。** + 不是只看字面相似,而要看用户在购物场景下是否可能接受该商品。 + - 良好替代品 → **基本相关** + - 勉强替代品 → **弱相关** + - 几乎不可替代 → **不相关** + +6. **若商品信息不足,不要把“无法确认”误判为“冲突”。** + 商品未写明某属性,不等于该属性一定不符合。 + 因此: + - 未提及 / 无法确认,优先按“基本相关”处理; + - 只有当商品信息明确显示与查询要求相反时,才视为属性冲突。 + +查询:{query} +{intent_suffix} + +商品: +{lines} + +## 输出格式 +严格输出 {n} 行,每行只能是以下四者之一: +完全相关 +基本相关 +弱相关 +不相关 + +现在请根据 query“{query}”,为每个结果标注相关性标签。输出行必须与上方商品顺序一一对应,不要输出任何其他内容。 +""" \ No newline at end of file diff --git a/docs/issues/issue-2026-04-02-LTR日志准备&FM拟合效果初探-done-0402.md b/docs/issues/issue-2026-04-02-LTR日志准备&FM拟合效果初探-done-0402.md new file mode 100644 index 0000000..e1ce682 --- /dev/null +++ b/docs/issues/issue-2026-04-02-LTR日志准备&FM拟合效果初探-done-0402.md @@ -0,0 +1,153 @@ +一、主指标改为: +NDCG@20 + NDCG@50 + ERR@10 + Strong_Precision@10 + Strong_Precision@20 + Useful_Precision@50 + Avg_Grade@10 + Gain_Recall@20 + +二、完善日志,阅读代码 理解rank漏斗,梳理重要信号,为LTR做准备 + +三、然后进行调参(最新的一版报告batch_20260402T054003Z_00b6a8aa3d.md是基线): +只针对重排融合公式(config.yaml): + fusion: + es_bias: 10.0 + es_exponent: 0.05 + rerank_bias: 0.1 + rerank_exponent: 1.15 + fine_bias: 0.1 + fine_exponent: 1.0 + text_bias: 0.1 + text_exponent: 0.25 + text_translation_weight: 0.8 + knn_text_weight: 1.0 + knn_image_weight: 2.0 + knn_tie_breaker: 0.3 + knn_bias: 0.6 + knn_exponent: 0.4 + +主要考虑方向有: +各项bias调整(调大) +提高knn_image_weight +提高knn_exponent +提高es_exponent和降低text_exponent + +四、选择几条关键query,仔细观察日志,基于你的理解,进行优化。包括但不限于文本、图片knn召回的条数的调参。 +tassel maxi skirt +minimalist top +sock boots(有很多高相关的未召回) +图案连身衣 + +过程中要站在更高的视角理解rank体系,遇到不合理的地方可以重新设计和优化,积累的经验和工作流程要沉淀为文档。 + + + + + +排序日志补强了rerank_client.py 和 searcher.py, 现在会输出 retrieval_plan、ltr_summary、以及 doc/stage 级 ltr_features,方便直接看 funnel,也方便后续做 LTR。参考_build_ltr_feature_block、_summarize_ltr_features等函数。做一版离线LTR拟合的工作(离线脚本验证)。 +思路是查看和解析特征日志./logs/backend_verbose.log 中正好有且只有评估框架最新跑的一轮特征日志,仔细思考rank体系、查看数据、提炼线索,做一版特征工程,label为4个级别,参考 [framework.py](scripts/evaluation/eval_framework/framework.py) ,损失使用RankNet Pairwise Loss(不需要deltandcg权重,先简单点,大体思路是一个query内任意两个label不同的组pair形成一个有效pair做二分类样本,请你找到简易的RankNet Pairwise Loss实现)。拟合函数考虑FM(要做一些简单的特征工程比如对每个数值型特征都做多种pow log等非线性映射发散出多个特征) + + + + + +需求: +[检索调参与LTR工作流.md](docs/检索调参与LTR工作流.md) +The ranking logs have enhanced `rerank_client.py` and `searcher.py`, and now output `retrieval_plan`, `ltr_summary`, and doc/stage-level `ltr_features`, making it easy to view the funnel and facilitate subsequent LTR. Refer to functions such as `_build_ltr_feature_block` and `_summarize_ltr_features`. Perform an offline LTR fitting task (offline script validation). + +The idea is to inspect and parse the feature logs in `./logs/backend_verbose.log`, which contains exactly and only the latest round of feature logs from the evaluation framework. Carefully consider the ranking system, examine the data, extract insights, and perform feature engineering. The labels have 4 levels. Refer to `[framework.py](scripts/evaluation/eval_framework/framework.py)`. Use the RankNet Pairwise Loss (no need for deltaNDCG weight; keep it simple first. The general idea is to form a valid pair for each pair of items with different labels within a query as a binary classification sample. Find a simple implementation of RankNet Pairwise Loss). Consider FM as the fitting function (do some simple feature engineering, such as applying various nonlinear transformations like pow, log, etc., to each numerical feature to expand into multiple features). + + + +回答: +Implemented an offline LTR fitter in [offline_ltr_fit.py](/data/saas-search/scripts/evaluation/offline_ltr_fit.py) that: + +- parses `logs/backend_verbose.log` `search_response` events +- joins per-query `spu_id` rows with 4-level labels from `artifacts/search_evaluation/search_eval.sqlite3` +- builds engineered features from `ltr_features`, stage scores, ranks, and nonlinear transforms like `log1p`, `sqrt`, square, inverse, plus a few ratio/cross features +- trains an FM with RankNet-style pairwise logistic loss +- runs query-group 5-fold offline validation +- saves the fitted model and predictions as artifacts + +I validated it end-to-end with: +```bash +./.venv/bin/python scripts/evaluation/offline_ltr_fit.py +``` + +Latest run artifacts are in [summary.json](/data/saas-search/artifacts/search_evaluation/ltr_runs/offline_ltr_20260402T074937Z/summary.json) and [fm_ranknet_model.json](/data/saas-search/artifacts/search_evaluation/ltr_runs/offline_ltr_20260402T074937Z/fm_ranknet_model.json). + +Key result: cross-validated `Primary_Metric_Score` improved from `0.641844` for current fused ranking to `0.654043` for the FM model. On the full-data refit, the model reaches `0.752009`, which is optimistic in-sample but shows the signal is learnable. The training set used `54` queries, `5400` docs, and `154592` valid label-different pairs. + + + + +需求: +1. The essence of FM is the weights of second-order interactions. Please output the visualized feature names (including interaction features) and their corresponding weights, sorted by importance (you can decide how to measure the importance of weights; don't make it too complicated. If no better method, simply use the absolute value of the weight). +2. It is necessary to split 10 results as a test set to measure generalization performance, so as to determine whether overfitting has occurred. + + +回答: +Updated [offline_ltr_fit.py](/data/saas-search/scripts/evaluation/offline_ltr_fit.py) to do both: + +- export FM feature importance +- reserve a dedicated 10-query holdout test set for generalization + +I interpreted “10 results as a test set” as `10 queries`, because ranking generalization should be measured at the query-group level, not by splitting docs from the same query across train/test. + +Artifacts from the latest run are under: +- [summary.json](/data/saas-search/artifacts/search_evaluation/ltr_runs/offline_ltr_20260402T080116Z/summary.json) +- [feature_importance_linear.csv](/data/saas-search/artifacts/search_evaluation/ltr_runs/offline_ltr_20260402T080116Z/feature_importance_linear.csv) +- [feature_importance_interactions.csv](/data/saas-search/artifacts/search_evaluation/ltr_runs/offline_ltr_20260402T080116Z/feature_importance_interactions.csv) +- [fm_ranknet_model.json](/data/saas-search/artifacts/search_evaluation/ltr_runs/offline_ltr_20260402T080116Z/fm_ranknet_model.json) + +Key results: +- Cross-validation `Primary_Metric_Score`: `0.654043` +- Current fused baseline: `0.641844` +- 10-query holdout FM: `0.53056` +- 10-query holdout current fused baseline: `0.5674` + +So the FM looks promising in CV, but the 10-query holdout is worse than the current baseline, which is a real overfitting/generalization warning. + +Top linear features by absolute weight: +- `text_knn_score__square` → `0.9936` +- `knn_primary_score__square` → `0.9529` +- `has_translation_match` → `-0.9309` +- `knn_score__square` → `0.9224` +- `text_support_score__square` → `0.8481` + +Top interaction features by absolute weight: +- `text_score_fallback_to_es * initial_rank__log` → `0.7099` +- `text_support_score__log1p * initial_rank__log` → `0.6814` +- `text_knn_score__square * initial_rank__log` → `-0.6419` +- `has_text_knn * initial_rank__log` → `0.5879` +- `translation_share * source_share` → `0.5509` + +The 10 holdout queries were: +- `date night dress` +- `skiing trip insulated base layer` +- `streetwear t-shirt` +- `swim dress` +- `tall slim fit trousers` +- `tassel maxi skirt` +- `чёрное летнее платье` +- `فستان أسود متوسط الطول` +- `图案连身衣` +- `宽松开襟羊毛衫` + +Next useful step is pruning the feature space and reducing the heavy dependence on rank-derived interactions like `initial_rank__log`, then rerunning the same holdout test. + + + +已把 LTR 相关内容系统性补充进 [检索调参与LTR工作流.md](/data/saas-search/docs/检索调参与LTR工作流.md)。 + +把文档扩成了更适合正式立项的版本,重点补了这些层面: + +- LTR 项目的数据闭环:日志特征、离线标签、样本构造、训练、验证、上线前判断 +- 当前日志结构如何支撑 LTR:`retrieval_plan`、`ltr_features`、`ranking_funnel` 分别提供什么信息 +- 样本定义:为什么基础单元是 `query-doc`,训练时再转成 pair +- label 获取与边界:`search_eval.sqlite3`、4 档标签语义、当前标签体系的适用范围和风险 +- 特征工程思路:原始特征、非线性变换、比例特征、交叉前的人工组合特征 +- 模型与 loss 选择:为什么第一版用 FM + RankNet Pairwise Loss +- 当前离线实验结果解读:CV 提升、10-query holdout 掉分、这意味着什么 +- FM 权重如何解释:一阶权重、二阶交互权重、如何用 importance 指导后续特征裁剪 +- 正式开展 LTR 项目的建议路线:数据资产固化、feature ablation、模型升级、上线节奏 + +如果你愿意,我下一步可以继续把这份文档再往“可执行方案”推进一步,补一节: +- `训练样本落盘格式设计` +- `线上/离线特征一致性约束` +- `LTR 服务接入 searcher/rerank 的上线方案` diff --git a/docs/issues/issue.md b/docs/issues/issue.md new file mode 100644 index 0000000..2180025 --- /dev/null +++ b/docs/issues/issue.md @@ -0,0 +1,596 @@ +项目 TODO 清单 + +2. 核心搜索功能优化 + +2.1 意图识别模块 + +- 增加款式意图识别模块 + +- 意图类型: 颜色,尺码(目前只需要支持这两种) + +- 意图召回层: +每种意图,有一个召回词集合 +对query(包括原始query、各种翻译query 都做匹配) + +- 以颜色意图为例: +有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 +query匹配了其中任何一个词,都认为,具有颜色意图 +匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 + +- 意图判断: + 暂时留空,直接返回true。目前没有模型,即只要召回了(词表匹配了),即认为有该维度款式需求。 + (以后考虑建设fasttext/bert系列多分类模型) + +- 意图使用: +我们第一阶段,使用 参与ES提权。 + +- 一、参与ES提权 + +- 二、参与reranker + +- 如果有: 先做sku筛选,然后把最优的拼接到名称中,参与reranker。 + + + +- 现在在reranker、分页之后、做填充的时候,已经有做sku的筛选。 +需要优化: +现在是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。改为 + 1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 + 2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 + 3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 + 这个sku筛选也需要提取为一个独立的模块。 + +- 另外:现在是reranker、分页之后做sku筛选,要改为: + 1. 有款式意图的时候,才做sku筛选 + 2. sku筛选的时机,改为在reranker之前,对所有内容做sku筛选,然后 + 3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、olors、、、、),遍历option1_name,option2_name,option3_name,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 + 4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) + 5. TODO : 还有一个问题。 目前,sku只返回一个维度(店铺主维度。默认应该是option1,不是所有维度的sku信息都返回的。所以,如果有款式意图,但是主维度是颜色,那么拿不到全的款式sku) + +- 筛选SKU: 先只筛选第一个维度,但考虑到用户搜索词可能带了尺码,所以第二、三个维度也要考虑 + + 当前项目功能已经较多,但是有清晰的框架,请务必基于现有框架进行改造,不要进行补丁式的修改,避免代码逻辑分叉。 + 请一步一步来,先设计意图识别模块,仔细思考需求,意图识别模块需要提供哪些内容,用于返回数据接口的定义,深度思考,定义一个合理的接口后,再给出合理的模块设计。 + +2.3 向量检索与融合 + +- 把knn跟文本相关性的融合方式修改为 "rank": {"rrf": {} }需要licence,可以帮我修改源码支持吗? + + knn_boost: 2.0 + + { + "query": { ...全文检索... }, + "knn": { ...向量检索... }, + "rank": { + "rrf": {} + } + } + +- 融合打分(已完成,2026-03) + + 以下已经完成: + 1. fuse_scores_and_resort 已改为乘法融合,并通过 matched_queries 提取: + - base_query + - base_query_trans_* + - fallback_original_query_* + - knn_query + 2. 文本相关性大分不再依赖 phrase_query / keywords_query,这两类查询已清理。 + 3. 当前融合策略: + - text_score = primary(weighted_source, weighted_translation, weighted_fallback) + 0.25 * support + - fused_score = (rerank_score + 0.00001) * (text_score + 0.1) ** 0.35 * (knn_score + 0.6) ** 0.2 + 4. track_scores 与 include_named_queries_score 已接入,调试字段与评估方法已同步到: + - docs/相关性检索优化说明.md + - docs/搜索API对接指南-01-搜索接口.md(分册;原单文件已拆分) + - docs/Usage-Guide.md + + 未完成的: + (归一化、次序融合?还乘法公式?) + RRF:先把多路召回稳妥融合 + linear + minmax:让你能精调 knn 和文本的权重 + reranker:对前面召回出来的 top-k 再做“最后一刀” + +2.4 文本相关性优化 + +- 调研: +Princeton WordNet — 英文同义词底库 +Shopify Product Taxonomy — 电商品类标准 +Querqy — 电商搜索规则框架 +gensimpson/elasticsearch-synonyms — ES 同义词规则落地 + +- tags字段使用的优化: +现在是keyword,在搜索中,不太好使用(目前主要用于suggest)。 +可以考虑也拆分多语言,配合analyzer使用(和qanchors一样) + +- 是否需要: +当「源语言不在 index_languages」且「某些目标语言的翻译缺失」时,ES 里会额外加一层 用「原始 query 字符串」去撞缺失语种字段 + +- 检索相关性优化: +原始搜索词和翻译的词,都需要有对应的主干分析 +这个主干可以根据词性简单提取名词即可 +在搜索时,原始词和主干都成对地出现,原始词和trunk_keywords一起组成一个或查询。 +有一种方案是把原始词和主干词拼接起来。但是bm25要调tf系数。 + +2.5 图片相关性与向量字段调整 + +- "image_embedding": { + "type": "nested", + "properties": { + "vector": { + "type": "dense_vector", + "dims": 1024, + "index": true, + "similarity": "dot_product", + "element_type": "bfloat16" + }, + "url": { + "type": "text" + } + } +}, +去掉 image_embedding_512 +image_embedding改为,一个spu有多个sku向量,每个向量内部properties: +除了vector url还应该包括,该图片是对应哪些sku +"image_embedding": { + "type": "nested", + "properties": { + "vector": { + "type": "dense_vector", + "dims": 1024, + "index": true, + "similarity": "dot_product", + "element_type": "bfloat16" + }, + "url": { + "type": "text" + } + } +}, + +- 引入图片的相关性: +图片的向量最好做SKU维度,用 SPU 维度还是 SKU 维度? + 1. SKU维度(主款式,option1维度),如果用户搜索“蓝色 T恤”,这种图片相关性会比较有价值。 + 2. 我不考虑颜色的差异,其余的款式一般是大小之类的。这些图片,embedding细分到 SKU 维度,可能价值不大,性价比偏低 + +- 属性的筛选: +训练一个bert/transformer多分类模型,分类: 颜色、尺寸、材质 等等。但是要注意一些属性的值不规范、非常多,要考虑 是不是做规范化,如何规范化。 + +2.6 无结果重查与翻译缺失处理 + +- 无结果重查 +稀有语言,翻译可能超时(因为zh-en互译之外的翻译耗时更长) + + +--- + +3. 模型与推理服务优化 + +3.1 大模型API与本地部署 + +- 外部需求: + 1. 对推理能力要求很低、对耗时要求很高的大模型API(或者本地部署一个7b Q4量化的大模型),prompt大概30-50个token,首token响应要求500ms以内 + 2. ES支持reranker pipline? + +- 本地部署一个7b Q4量化的大模型 + +3.2 Embedding服务优化 + +- 先阅读文本embedding相关的代码: +@embeddings/README.md @embeddings/server.py @docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md @embeddings/text_encoder.py +目前有TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入限制,超限返回过载状态码。 + +- 文本embedding服务,要支持 priority 查询参数,priority > 0:不计入上述 inflight、不会因准入被拒绝(图片embedding不需要支持,因为只有离线需要用到图片embedding) +priority == 0(默认,适合做索引之类的离线任务):仍走原有 TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入;超限返回过载状态码。 +priority > 0(或者==1)(适合在线请求):不会因准入被拒绝,但是仍然需要占用inflight,这样保证在线请求不被限制,并且在线请求很多的时候可以拒绝掉离线的请求。 + +- 除了限制规则的修改,更进一步的,也需要保证这种请求是优先处理的(priority=1的相比=0的更优先被处理)。 +关于技术方案,有Worker + 双队列、PriorityMutex等等,除此之外,也请你思考合适的方案。 +成熟稳定、不带来复杂度、性能、稳定性方面的副作用,是最重要的。请先了解代码、需求,深度思考解决方案 + +- 向量的缓存 + +3.3 Reranker优化 + +- 多reranker: +改 reranker 服务,一次请求返回多路分 +服务启动时 加载多个 backend(或按请求懒加载),/rerank 响应扩展为例如 +scores: [...](兼容主后端)+ scores_by_backend: { "bge": [...], "qwen3_vllm": [...] }。 +搜索侧解析多路分,再融合或只透传 debug。 +优点:搜索侧仍只调一个 URL。缺点:单进程多大模型 显存压力很大; + +- 融合层要注意的一点 +fuse_scores_and_resort 目前只消费 一条 rerank_scores 序列,并写入 _rerank_score +多 backend 之后需要rerank_scores 都参与融合 + +- 必要性: +见 qwen3-reranker和bge-m3的严重badcase +不一定是要多reranker的方式,但是一定会需要解决方案。 + +- reranker 补充:nvidia/llama-nemotron-rerank-1b-v2 +https://huggingface.co/nvidia/llama-nemotron-rerank-1b-v2 +后端推理也建议使用vLLM +注意搜索相关资料,挖掘我的特斯拉 T4 GPU 的性能,充分挖掘性能 +你有充足的自由度进行实验 +encoder架构。 +比较新。 +性能更好。 +亚马逊 电商搜索数据集比qwen-reranker-4b更好。 +支持vLLM。 + +- Qwen3-Reranker-4B-GGUF +https://modelscope.cn/models/dengcao/Qwen3-Reranker-4B-GGUF/summary + 1. 要确定选择哪种量化方式 + 2. 确定提示词 + +- qwen3-embedding、qwen3-reranker (done) +选一个推理引擎,相比于我自己直接调 sentence-transformers,主要是多进程和负载均衡、连续批处理,比较有用 +当前结论:embedding 场景优先 TEI;vLLM 更偏向生成式与 rerank 场景。 + +- rerank 性能优化 + +3.4 翻译模型优化 + +- 翻译,增加facebook/nllb-200-distilled-600M +https://blog.csdn.net/qq_42746084/article/details/154947534 +https://huggingface.co/facebook/nllb-200-distilled-600M + +- 店铺的语言:英语能占到80%,所以专门增加一个en-zh的 +https://huggingface.co/Helsinki-NLP/opus-mt-zh-en +https://huggingface.co/Helsinki-NLP/opus-mt-en-zh + +- opus-mt-zh-en + + from transformers import AutoTokenizer, AutoModelForSeq2SeqLM + model_name = "./models/opus-mt-en-zh" + tokenizer = AutoTokenizer.from_pretrained(model_name) + model = AutoModelForSeq2SeqLM.from_pretrained(model_name) + data = 'test' + encoded = tokenizer([data], return_tensors="pt") + translation = model.generate(**encoded) + result = tokenizer.batch_decode(translation, skip_special_tokens=True)[0] + print(result) + +- nllb-200-distilled-600M性能优化 +已完成(2026-03) + - CTranslate2 迁移 + float16 转换 + - 扩展压测报告:perf_reports/20260318/translation_local_models_ct2/README.md + - T4 聚焦调优报告:perf_reports/20260318/translation_local_models_ct2_focus/README.md + - NLLB T4 商品标题专项报告:perf_reports/20260318/nllb_t4_product_names_ct2/README.md + - 当前结论: + - NLLB 在线默认推荐:ct2_inter_threads=4 + ct2_max_queued_batches=32 + ct2_batch_type=examples + ct2_decoding_length_mode=source(+8,min=32) + - opus-mt-zh-en 维持保守默认更稳 + - opus-mt-en-zh 如追求离线吞吐可继续做单独 profile + +- 请搜索nllb-200-distilled-600M这类seq2seq、transformer架构的模型,有哪些性能优化方案,提高线上翻译服务的吞吐量、降低耗时,搜索相关的在线推理服务方案,找到高性能的服务化方法 + +- 查看翻译的缓存情况 + +3.5 其他模型优化 + +- cnclip的性能优化 + + +--- + +4. 性能优化与超时配置 + +4.1 超时配置 + +- Query 分析阶段等待翻译/embedding 的硬超时 +配置文件位置:config/config.yaml +配置项:query_config.async_wait_timeout_ms: 80 +代码生效点:query/query_parser.py 使用该值换算成秒传给 wait(...) + +2. Embedding HTTP 调用超时(Text/Image) +不再使用任何环境变量覆盖(之前提到的 EMBEDDING_HTTP_TIMEOUT_SEC 已不采用) +配置文件位置:config/config.yaml +配置项:services.embedding.providers.http.timeout_sec(已在 YAML 里补了示例默认 60) +代码生效点: +embeddings/text_encoder.py:requests.post(..., timeout=self.timeout_sec) +embeddings/image_encoder.py:requests.post(..., timeout=self.timeout_sec) + +4.2 生成式服务优化(Partial Mode) + +- product_enrich : Partial Mode : done +https://help.aliyun.com/zh/model-studio/partial-mode?spm=a2c4g.11186623.help-menu-2400256.d_0_3_0_7.74a630119Ct6zR +需在messages 数组中将最后一条消息的 role 设置为 assistant,并在其 content 中提供前缀,在此消息中设置参数 "partial": true。messages格式如下: +[ + { + "role": "user", + "content": "请补全这个斐波那契函数,勿添加其它内容" + }, + { + "role": "assistant", + "content": "def calculate_fibonacci(n):\n if n <= 1:\n return n\n else:\n", + "partial": true + } +] +模型会以前缀内容为起点开始生成。 +支持 非思考模式。 + + +--- + +5. Elasticsearch相关 + +- es需要licence的两个功能,如果费用低,开通下licence,或者改es源码定制开发下,支持 rank.rrf,reranker + + { + "query": { ...全文检索... }, + "knn": { ...向量检索... }, + "rank": { + "rrf": {} + } + } + + +--- + +1. 配置体系重构 +Referring to @docs/config-system-review-and-redesign.md , most of the modifications have been completed. Could you conduct a review to check what else needs improvement in the configuration documentation system? Are there any outstanding issues? + +一、仍然存在大量通过环境变量获取配置的地方 +_SERVICE_KIND = (os.getenv("EMBEDDING_SERVICE_KIND", "all") or "all").strip().lower() +if _SERVICE_KIND not in {"all", "text", "image"}: + raise RuntimeError( + f"Invalid EMBEDDING_SERVICE_KIND={_SERVICE_KIND!r}; expected all, text, or image" + ) +_TEXT_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_TEXT_MODEL", "true").lower() in ("1", "true", "yes") +_IMAGE_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_IMAGE_MODEL", "true").lower() in ("1", "true", "yes") +open_text_model = _TEXT_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "text"} +open_image_model = _IMAGE_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "image"} + +_text_encode_lock = threading.Lock() +_image_encode_lock = threading.Lock() + +_TEXT_MICROBATCH_WINDOW_SEC = max( + 0.0, float(os.getenv("TEXT_MICROBATCH_WINDOW_MS", "4")) / 1000.0 +) +_TEXT_REQUEST_TIMEOUT_SEC = max( + 1.0, float(os.getenv("TEXT_REQUEST_TIMEOUT_SEC", "30")) +) +_TEXT_MAX_INFLIGHT = max(1, int(os.getenv("TEXT_MAX_INFLIGHT", "32"))) +_IMAGE_MAX_INFLIGHT = max(1, int(os.getenv("IMAGE_MAX_INFLIGHT", "1"))) +_OVERLOAD_STATUS_CODE = int(os.getenv("EMBEDDING_OVERLOAD_STATUS_CODE", "503")) +_LOG_PREVIEW_COUNT = max(1, int(os.getenv("EMBEDDING_LOG_PREVIEW_COUNT", "3"))) +_LOG_TEXT_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_TEXT_PREVIEW_CHARS", "120"))) +_LOG_IMAGE_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_IMAGE_PREVIEW_CHARS", "180"))) +_VECTOR_PREVIEW_DIMS = max(1, int(os.getenv("EMBEDDING_VECTOR_PREVIEW_DIMS", "6"))) +_CACHE_PREFIX = str(REDIS_CONFIG.get("embedding_cache_prefix", "embedding")).strip() or "embedding" + + + + + +还有这些写死的地址 @embedding/config.py + +self.TEI_BASE_URL = str(text_backend.get("base_url") or "http://127.0.0.1:8080") +self.TEI_TIMEOUT_SEC = int(text_backend.get("timeout_sec", 60)) + +self.USE_CLIP_AS_SERVICE = services.image_backend == "clip_as_service" +self.CLIP_AS_SERVICE_SERVER = str(image_backend.get("server") or "grpc://127.0.0.1:51000") + + + + +看起来似乎并没有完全遵循这些原则? +4. 重新设计的设计原则 +重新设计应遵循以下规则。 + +4.1 单一逻辑配置系统 +可以有多个文件,但不能有多个职责重叠的加载器。 +必须有一个加载器管道,能够生成一个类型化的 AppConfig 对象。 + +4.2 配置文件负责声明,解析代码负责解释,环境变量负责运行时注入 +职责应明确如下: +配置文件 +声明非敏感的目标行为和可部署的非敏感设置 +解析逻辑 +加载、合并、验证、规范化并暴露类型化的配置 +绝不发明隐藏的业务行为 +环境变量 +承载密钥和少量运行时/进程相关的值 +不随意地重新定义业务行为 + +4.3 整个系统采用单一的优先级规则 +除非明确豁免,否则每个配置类别都应遵循相同的合并模型。 + +4.4 业务行为不得有静默的隐式后备 +在启动时,如果必需的配置缺失或无效,应快速失败。 +不要静默地回退到诸如硬编码语言列表之类的遗留行为。 + +4.5 有效配置必须可观测 +每个服务都应能够展示: +配置版本或哈希值 +加载的源文件 +环境名称 +经过清理的有效配置 + +5. 推荐的目标设计 + +5.1 边界模型 +使用三个清晰的层级。 +层级 1:代码仓库管理的静态配置 +目的: +搜索行为 +租户行为 +提供商/后端注册表 +非敏感的服务拓扑默认值 +功能开关 +示例: +字段权重 +查询策略 +重排序融合参数 +租户语言方案 +翻译能力注册表 +嵌入后端选择默认值 + +层级 2:特定环境的层叠配置 +目的: +按环境区分的非敏感差异 +按环境区分的服务端点 +按环境区分的资源大小默认值 +开发/测试/生产环境的运维差异 +示例: +本地嵌入 URL 与生产环境嵌入 URL +开发环境重排序后端与生产环境重排序后端 +本地开发环境中较低的并发度 + +层级 3:环境变量 +目的: +密钥 +绑定主机/端口 +外部基础设施凭证 +容器编排器的最后一步注入 +示例: +ES_HOST, ES_USERNAME, ES_PASSWORD +DB_HOST, DB_USERNAME, DB_PASSWORD +REDIS_HOST, REDIS_PASSWORD +DASHSCOPE_API_KEY, DEEPL_AUTH_KEY +API_HOST, API_PORT, INDEXER_PORT, TRANSLATION_PORT +规则: +环境变量不应成为选择业务行为(如翻译模型、嵌入后端或租户语言策略)的常规途径 +如果允许对非敏感字段进行环境变量覆盖,则必须将其明确列出并记录为运维覆盖手段,而非隐藏的约定 + +5.2 统一的优先级 +推荐的优先级顺序: +代码中的模式(schema)默认值 +config/base.yaml +config/environments/.yaml +来自 config/tenants/ 的租户层叠配置 +针对明确允许的运行时键的环境变量 +仅适用于当前进程的命令行标志 +重要规则: +只能有一个模块实现此合并逻辑 +任何业务模块不得直接调用 os.getenv() 来获取配置 + + + +二、cursor(composer2)帮我审查的 + +## 配置文档系统审查 + +这是对配置文档系统的简要审查,对照 [`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 和当前代码库进行。 + +### 实现已匹配的部分 + +代码库已朝着重新设计的目标迈进了很大一步: + +- 单一类型化根节点:`AppConfig` 位于 [`config/schema.py`](config/schema.py),通过 [`config/loader.py`](config/loader.py) 和 [`get_app_config()`](config/loader.py) 加载。 +- 精简的 `services_config`:[`config/services_config.py`](config/services_config.py) 委托给 `get_app_config()`(解决了服务层的发现 A)。 +- 租户目录:[`config/tenant_config_loader.py`](config/tenant_config_loader.py) 使用 `get_app_config().tenants`。 +- 重写资产路径:`config/dictionaries/query_rewrite.dict` 已存在(解决了发现 E 中的文件名不匹配问题)。 +- 可观测性:[`GET /admin/config`](api/routes/admin.py) 返回经过脱敏处理的有效配置树;[`GET /admin/config/meta`](api/routes/admin.py) 暴露环境信息、`config_hash`、`loaded_files`、`deprecated_keys`(涵盖了 §5.10 的意图;文档中提议的是 `/admin/config/effective`,但实际实现位于 `/admin/config`)。 + +因此,"单一加载器 + 有效配置可见性" 的故事在代码中已基本实现;文档尚未完全跟上。 + +--- + +## 文档问题(影响最大) + +### 1. 管理 API 文档中关于 `/admin/config` 的描述错误 + +[`docs/搜索API对接指南-06-管理接口(Admin).md`](docs/搜索API对接指南-06-管理接口(Admin).md)(原单文件 `搜索API对接指南.md` 已拆分为分册)仍将 `/admin/config` 描述为按租户的 JSON(包含 `tenant_id`、`es_index_name`、`supported_languages` 等字段)。实际实现返回的是 `AppConfig.sanitized_dict()`(完整的应用配置,敏感信息已脱敏),而不是租户摘要字段。 + +这些指南中还缺少: `GET /admin/config/meta`。 + +健康检查: 拆分指南中的示例包含了 [`HealthResponse`](api/models.py) 中不存在的字段(只有 `status` 和 `elasticsearch`)。 + +对于任何仅根据文档进行 API 集成的人来说,这是最明显的"未解决问题"。 + +### 2. 面向开发者的指南仍将 `services_config` 作为"配置解析器"的核心 + +[`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §5.2 仍说搜索配置由 `ConfigLoader` 加载,服务由 `config/services_config` "解析"。§6.2 仍将 `config/services_config.py` 列为主要的"解析入口"。[`docs/QUICKSTART.md`](docs/QUICKSTART.md) §3.1 仍说"配置解析:`config/services_config.py`"。 + +文档中准确的说法应该是:规范入口是 `config/loader.py` + `get_app_config()`;[`config/config_loader.py`](config/config_loader.py) 中的 `ConfigLoader` 包装了统一加载器;`services_config` 是现有调用点的兼容性外观。 + +### 3. 重新设计文档本身不是"活的"状态文档 + +[`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 读起来仍是纯粹的问题陈述 + 目标,没有简短的**"已实现 vs 剩余"**部分。这很容易让人假设什么都没做,或者重复工作。添加一个小附录(或一页 `config/README.md` —— 见下文)可以解决这个问题。 + +### 4. 缺少 `config/README.md`(§5.3 中推荐) + +仍然没有专门的 `config/README.md` 来描述:加载器入口点、高级优先级、字典存放位置、指向 `/admin/config` + `/admin/config/meta` 的链接,以及重新设计文档的链接。这是重新设计中明确的交付物,可以锚定"文档系统"。 + +### 5. 轻微的文档整洁问题 + +- [`docs/QUICKSTART.md`](docs/QUICKSTART.md) §1.9 环境变量项目后的行有一个多余字符:`---·`(可能是打字错误)。 +- [`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §10 文档索引没有列出 `config-system-review-and-redesign.md` 或未来的 `config/README.md`。 + +--- + +## 重新设计目标与当前代码之间的差距(文档不应声称"已完成") + +这些影响文档的诚实度: + +| 主题 | 状态 | +|--------|--------| +| `config dump` CLI(§5.10) | `main.py` 中不存在;运维人员依赖 HTTP 或临时脚本。 | +| 隐藏的 `["en", "zh"]` 回退(阶段 3 / 发现 D) | 仍在 [`indexer/document_transformer.py`](indexer/document_transformer.py)、[`suggestion/builder.py`](suggestion/builder.py) 等中使用。 | +| 加载器外的 `os.getenv`(规则 1–2) | 仍在例如 [`embeddings/server.py`](embeddings/server.py)、[`reranker/server.py`](reranker/server.py)、[`api/app.py`](api/app.py) 中使用 —— 文档声称"仅加载器"将是夸大其词。 | +| 拆分 `base.yaml` / `environments/` / `tenants/*.yaml`(阶段 5) | 未采用;仍是单一的 [`config/config.yaml`](config/config.yaml)。 | +| 遗留租户标志(阶段 6 / 发现 H) | [`indexer/README.md`](indexer/README.md) 仍描述上游 MySQL 的 `translate_to_en` / `translate_to_zh`(这可能作为上游模式文档保留;需与 Python `tenant_config` 模型区分开来)。 | + +--- + +## 推荐的后续步骤(仅文档,按优先级排序) + +1. 修复管理 API 文档(合并指南 + `-06-` 拆分):`/admin/config` 的响应格式,添加 `/admin/config/meta`,使健康检查示例与 [`HealthResponse`](api/models.py) 一致。 +2. 更新 DEVELOPER_GUIDE §5–§6 和 QUICKSTART §1.9 / §3.1,将 `get_app_config()` / `loader.py` 描述为主要入口,将 `services_config` 描述为适配器。 +3. 添加 `config/README.md`(简短的操作 + 开发者入口)。 +4. 在 `config-system-review-and-redesign.md` 中添加带日期的实现状态表(已交付 vs 推迟的内容),使审查文档不与现实矛盾。 +5. DEVELOPER_GUIDE §9 检查清单:将"配置来自 `services_config`"替换为允许 `get_app_config()` 或精简适配器的语言,与 §6 保持一致。 + +如果需要,我可以在后续处理中为项目 1–3 和重新设计文档中的简短状态块应用补丁。 + +其他云API +1 +1)提供两个rerank云API_KEY给我:(优先级:高) +AWS Bedrock / Azure 两家云有提供的Cohere Rerank 3.5/4模型API,开通APIKEY +google云 Vertex AI Ranking API + +已经调研: +阿里云在美国地区没有提供任意reranker API +AWS Bedrock / Azure 两家云有提供Cohere Rerank 3.5 +google云Vertex AI Ranking API性能更好 + +以上两个APIKEY给我,我来测试性能和效果。 + + +2)寻找美国地区reranker API最佳实践(优先级:高) +效果要求:qwen3-reranker-4b(或者同等能力。可对比huggingface公开的评测指标)的API +性能要求:在我们的服务器上,一个请求内排序400条结果、耗时低于300ms +测试评估:基于电商领域商品搜索场景评估效果(我可以提供数据) +据我了解的Cohere Rerank可能达不到这个性能要求,可能可以考虑拆分为4个请求、每个100条,做到300ms以内可能可以。 +参考Cohere Rerank 3.5 benchmark: +https://docs.oracle.com/en-us/iaas/Content/generative-ai/benchmark-cohere-rerank-3-5.htm + + +3)提供谷歌翻译API的apikey (优先级:低) +给我apikey,我看下耗时,希望耗时P95低于80ms满足在线请求使用 +在线翻译的问题已经基本解决,这一块需求不是特别大。 + +2 +混用 大模型 使用:hunyuan-turbos-latest +混元 OpenAI 兼容接口相关调用示例:https://cloud.tencent.com/document/product/1729/111007 + +腾讯云 混元大模型 API_KEY:sk-mN2PiW2gp57B3ykxGs4QhvYxhPzXRZ2bcR5kPqadjboGYwiz + +hunyuan翻译:使用模型 hunyuan-translation +https://cloud.tencent.com/document/product/1729/113395#4.-.E7.A4.BA.E4.BE.8B + +谷歌翻译 基础版:https://docs.cloud.google.com/translate/docs/reference/rest/v2/translate + +阿里云 百炼模型 现在使用的apikey是国内的。 +各地域的 Base URL 和对应的 API Key 是绑定的。 + +现在使用了美国的服务器,使用了美国的地址,需要在 美国地域控制台页面(https://modelstudio.console.aliyun.com/us-east-1 )中创建或获取API_KEY: + +登录 百炼美国地域控制台:https://modelstudio.console.aliyun.com/us-east-1?spm=5176.2020520104.0.0.6b383a98WjpXff +在 API Key 管理 中创建或复制一个适用于美国地域的 Key + +搜索效果反馈: +做完一些短期优化后,需要做一些case驱动的优化。 +给到100条测试用例,每个搜索词,要记录请求ID、以及 希望排序靠前但是没有靠前的(比如希望出现在第一页但是没出现在第一页的)、以及未召回的商品ID(希望出现在前几页但是没翻到的) +6. 其他任务 + +- suggest 索引,现在是全量脚本,要交给金伟 \ No newline at end of file diff --git a/docs/issues/issue.txt b/docs/issues/issue.txt new file mode 100644 index 0000000..4fe7fb0 --- /dev/null +++ b/docs/issues/issue.txt @@ -0,0 +1,640 @@ + + + +本地部署一个7b Q4量化的大模型 +es需要licence的两个功能,如果费用低,开通下licence,或者改es源码定制开发下,支持 rank.rrf,reranker + + + +把knn跟文本相关性的融合方式修改为 "rank": {"rrf": {} }需要licence,可以帮我修改源码支持吗? + + knn_boost: 2.0 + + +{ + "query": { ...全文检索... }, + "knn": { ...向量检索... }, + "rank": { + "rrf": {} + } +} + + +"image_embedding": { + "type": "nested", + "properties": { + "vector": { + "type": "dense_vector", + "dims": 1024, + "index": true, + "similarity": "dot_product", + "element_type": "bfloat16" + }, + "url": { + "type": "text" + } + } +}, +去掉 image_embedding_512 +image_embedding改为,一个spu有多个sku向量,每个向量内部properties: +除了vector url还应该包括,该图片是对应哪些sku +"image_embedding": { + "type": "nested", + "properties": { + "vector": { + "type": "dense_vector", + "dims": 1024, + "index": true, + "similarity": "dot_product", + "element_type": "bfloat16" + }, + "url": { + "type": "text" + } + } +}, + + +外部需求: +1. 对推理能力要求很低、对耗时要求很高的大模型API(或者本地部署一个7b Q4量化的大模型),prompt大概30-50个token,首token响应要求500ms以内 +2. ES支持reranker pipline? + + + + + + +增加款式意图识别模块 + +意图类型: 颜色,尺码(目前只需要支持这两种) + +意图召回层: +每种意图,有一个召回词集合 +对query(包括原始query、各种翻译query 都做匹配) + +以颜色意图为例: +有一个词表,每一行 都逗号分割,互为同义词,行内第一个为标准化词 +query匹配了其中任何一个词,都认为,具有颜色意图 +匹配规则: 用细粒度、粗粒度分词,看是否有在词表中的。原始query分词、和每种翻译的分词,都要用。 + +意图判断: 暂时留空,直接返回true。目前没有模型,即只要召回了(词表匹配了),即认为有该维度款式需求。 +(以后考虑建设fasttext/bert系列多分类模型) + + +意图使用: + +我们第一阶段,使用 参与ES提权。 + +一、参与ES提权 + + +二、参与reranker + + +如果有: 先做sku筛选,然后把最优的拼接到名称中,参与reranker。 + + +现在在reranker、分页之后、做填充的时候,已经有做sku的筛选。 +需要优化: +现在是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。改为 +1. 第一轮:遍历完,如果有且仅有一个被query包含,那么认为匹配。 +2. 第二轮:如果有多个符合(被query包含),跳到3。如果没有,对每个词都走泛化词表进行匹配。 +3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的 +这个sku筛选也需要提取为一个独立的模块。 + + +另外:现在是reranker、分页之后做sku筛选,要改为: +1. 有款式意图的时候,才做sku筛选 +2. sku筛选的时机,改为在reranker之前,对所有内容做sku筛选,然后 +3. 从仅 option1 扩展到多个维度,识别的意图,包含意图的维度名(color)和维度名的泛化词list(color、颜色、colour、olors、、、、),遍历option1_name,option2_name,option3_name,看哪个能匹配上意图的维度名list,哪个匹配上了,则在这个维度筛选。 +4. Rerank doc (有款式意图的时候)要带上属性后缀,拼接到title后面。在调用 run_rerank 前,对每条 hit 生成「用于重排的 doc 文本」(标题 + 可选后缀) +5. TODO : 还有一个问题。 目前,sku只返回一个维度(店铺主维度。默认应该是option1,不是所有维度的sku信息都返回的。所以,如果有款式意图,但是主维度是颜色,那么拿不到全的款式sku) + + +当前项目功能已经较多,但是有清晰的框架,请务必基于现有框架进行改造,不要进行补丁式的修改,避免代码逻辑分叉。 +请一步一步来,先设计意图识别模块,仔细思考需求,意图识别模块需要提供哪些内容,用于返回数据接口的定义,深度思考,定义一个合理的接口后,再给出合理的模块设计。 + + + + +文本相关性: +调研: +Princeton WordNet — 英文同义词底库 +Shopify Product Taxonomy — 电商品类标准 +Querqy — 电商搜索规则框架 +gensimpson/elasticsearch-synonyms — ES 同义词规则落地 + + +tags字段使用的优化: +现在是keyword,在搜索中,不太好使用(目前主要用于suggest)。 +可以考虑也拆分多语言,配合analyzer使用(和qanchors一样) + + + + +是否需要: +当「源语言不在 index_languages」且「某些目标语言的翻译缺失」时,ES 里会额外加一层 用「原始 query 字符串」去撞缺失语种字段 + + + +先阅读文本embedding相关的代码: +@embeddings/README.md @embeddings/server.py @docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md @embeddings/text_encoder.py +目前有TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入限制,超限返回过载状态码。 + +文本embedding服务,要支持 priority 查询参数,priority > 0:不计入上述 inflight、不会因准入被拒绝(图片embedding不需要支持,因为只有离线需要用到图片embedding) +priority == 0(默认,适合做索引之类的离线任务):仍走原有 TEXT_MAX_INFLIGHT / IMAGE_MAX_INFLIGHT 准入;超限返回过载状态码。 +priority > 0(或者==1)(适合在线请求):不会因准入被拒绝,但是仍然需要占用inflight,这样保证在线请求不被限制,并且在线请求很多的时候可以拒绝掉离线的请求。 + +除了限制规则的修改,更进一步的,也需要保证这种请求是优先处理的(priority=1的相比=0的更优先被处理)。 +关于技术方案,有Worker + 双队列、PriorityMutex等等,除此之外,也请你思考合适的方案。 +成熟稳定、不带来复杂度、性能、稳定性方面的副作用,是最重要的。请先了解代码、需求,深度思考解决方案 + + + +配置体系的重构。 + +Referring to @docs/config-system-review-and-redesign.md , most of the modifications have been completed. Could you conduct a review to check what else needs improvement in the configuration documentation system? Are there any outstanding issues? + +一、仍然存在大量通过环境变量获取配置的地方 +_SERVICE_KIND = (os.getenv("EMBEDDING_SERVICE_KIND", "all") or "all").strip().lower() +if _SERVICE_KIND not in {"all", "text", "image"}: + raise RuntimeError( + f"Invalid EMBEDDING_SERVICE_KIND={_SERVICE_KIND!r}; expected all, text, or image" + ) +_TEXT_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_TEXT_MODEL", "true").lower() in ("1", "true", "yes") +_IMAGE_ENABLED_BY_ENV = os.getenv("EMBEDDING_ENABLE_IMAGE_MODEL", "true").lower() in ("1", "true", "yes") +open_text_model = _TEXT_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "text"} +open_image_model = _IMAGE_ENABLED_BY_ENV and _SERVICE_KIND in {"all", "image"} + +_text_encode_lock = threading.Lock() +_image_encode_lock = threading.Lock() + +_TEXT_MICROBATCH_WINDOW_SEC = max( + 0.0, float(os.getenv("TEXT_MICROBATCH_WINDOW_MS", "4")) / 1000.0 +) +_TEXT_REQUEST_TIMEOUT_SEC = max( + 1.0, float(os.getenv("TEXT_REQUEST_TIMEOUT_SEC", "30")) +) +_TEXT_MAX_INFLIGHT = max(1, int(os.getenv("TEXT_MAX_INFLIGHT", "32"))) +_IMAGE_MAX_INFLIGHT = max(1, int(os.getenv("IMAGE_MAX_INFLIGHT", "1"))) +_OVERLOAD_STATUS_CODE = int(os.getenv("EMBEDDING_OVERLOAD_STATUS_CODE", "503")) +_LOG_PREVIEW_COUNT = max(1, int(os.getenv("EMBEDDING_LOG_PREVIEW_COUNT", "3"))) +_LOG_TEXT_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_TEXT_PREVIEW_CHARS", "120"))) +_LOG_IMAGE_PREVIEW_CHARS = max(32, int(os.getenv("EMBEDDING_LOG_IMAGE_PREVIEW_CHARS", "180"))) +_VECTOR_PREVIEW_DIMS = max(1, int(os.getenv("EMBEDDING_VECTOR_PREVIEW_DIMS", "6"))) +_CACHE_PREFIX = str(REDIS_CONFIG.get("embedding_cache_prefix", "embedding")).strip() or "embedding" + + + + + +还有这些写死的地址 @embedding/config.py + +self.TEI_BASE_URL = str(text_backend.get("base_url") or "http://127.0.0.1:8080") +self.TEI_TIMEOUT_SEC = int(text_backend.get("timeout_sec", 60)) + +self.USE_CLIP_AS_SERVICE = services.image_backend == "clip_as_service" +self.CLIP_AS_SERVICE_SERVER = str(image_backend.get("server") or "grpc://127.0.0.1:51000") + + + + +看起来似乎并没有完全遵循这些原则? +4. 重新设计的设计原则 +重新设计应遵循以下规则。 + +4.1 单一逻辑配置系统 +可以有多个文件,但不能有多个职责重叠的加载器。 +必须有一个加载器管道,能够生成一个类型化的 AppConfig 对象。 + +4.2 配置文件负责声明,解析代码负责解释,环境变量负责运行时注入 +职责应明确如下: +配置文件 +声明非敏感的目标行为和可部署的非敏感设置 +解析逻辑 +加载、合并、验证、规范化并暴露类型化的配置 +绝不发明隐藏的业务行为 +环境变量 +承载密钥和少量运行时/进程相关的值 +不随意地重新定义业务行为 + +4.3 整个系统采用单一的优先级规则 +除非明确豁免,否则每个配置类别都应遵循相同的合并模型。 + +4.4 业务行为不得有静默的隐式后备 +在启动时,如果必需的配置缺失或无效,应快速失败。 +不要静默地回退到诸如硬编码语言列表之类的遗留行为。 + +4.5 有效配置必须可观测 +每个服务都应能够展示: +配置版本或哈希值 +加载的源文件 +环境名称 +经过清理的有效配置 + +5. 推荐的目标设计 + +5.1 边界模型 +使用三个清晰的层级。 +层级 1:代码仓库管理的静态配置 +目的: +搜索行为 +租户行为 +提供商/后端注册表 +非敏感的服务拓扑默认值 +功能开关 +示例: +字段权重 +查询策略 +重排序融合参数 +租户语言方案 +翻译能力注册表 +嵌入后端选择默认值 + +层级 2:特定环境的层叠配置 +目的: +按环境区分的非敏感差异 +按环境区分的服务端点 +按环境区分的资源大小默认值 +开发/测试/生产环境的运维差异 +示例: +本地嵌入 URL 与生产环境嵌入 URL +开发环境重排序后端与生产环境重排序后端 +本地开发环境中较低的并发度 + +层级 3:环境变量 +目的: +密钥 +绑定主机/端口 +外部基础设施凭证 +容器编排器的最后一步注入 +示例: +ES_HOST, ES_USERNAME, ES_PASSWORD +DB_HOST, DB_USERNAME, DB_PASSWORD +REDIS_HOST, REDIS_PASSWORD +DASHSCOPE_API_KEY, DEEPL_AUTH_KEY +API_HOST, API_PORT, INDEXER_PORT, TRANSLATION_PORT +规则: +环境变量不应成为选择业务行为(如翻译模型、嵌入后端或租户语言策略)的常规途径 +如果允许对非敏感字段进行环境变量覆盖,则必须将其明确列出并记录为运维覆盖手段,而非隐藏的约定 + +5.2 统一的优先级 +推荐的优先级顺序: +代码中的模式(schema)默认值 +config/base.yaml +config/environments/.yaml +来自 config/tenants/ 的租户层叠配置 +针对明确允许的运行时键的环境变量 +仅适用于当前进程的命令行标志 +重要规则: +只能有一个模块实现此合并逻辑 +任何业务模块不得直接调用 os.getenv() 来获取配置 + + + +二、cursor(composer2)帮我审查的 + +## 配置文档系统审查 + +这是对**配置文档系统**的简要审查,对照 [`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 和当前代码库进行。 + +### 实现已匹配的部分 + +代码库已朝着重新设计的目标迈进了很大一步: + +- **单一类型化根节点**:`AppConfig` 位于 [`config/schema.py`](config/schema.py),通过 [`config/loader.py`](config/loader.py) 和 [`get_app_config()`](config/loader.py) 加载。 +- **精简的 `services_config`**:[`config/services_config.py`](config/services_config.py) 委托给 `get_app_config()`(解决了服务层的发现 A)。 +- **租户目录**:[`config/tenant_config_loader.py`](config/tenant_config_loader.py) 使用 `get_app_config().tenants`。 +- **重写资产路径**:`config/dictionaries/query_rewrite.dict` 已存在(解决了发现 E 中的文件名不匹配问题)。 +- **可观测性**:[`GET /admin/config`](api/routes/admin.py) 返回**经过脱敏处理的有效配置树**;[`GET /admin/config/meta`](api/routes/admin.py) 暴露环境信息、`config_hash`、`loaded_files`、`deprecated_keys`(涵盖了 §5.10 的意图;文档中提议的是 `/admin/config/effective`,但实际实现位于 `/admin/config`)。 + +因此,**"单一加载器 + 有效配置可见性"** 的故事在代码中已基本实现;文档尚未完全跟上。 + +--- + +## 文档问题(影响最大) + +### 1. 管理 API 文档中关于 `/admin/config` 的描述错误 + +[`docs/搜索API对接指南-06-管理接口(Admin).md`](docs/搜索API对接指南-06-管理接口(Admin).md)(原单文件 `搜索API对接指南.md` 已拆分为分册)仍将 `/admin/config` 描述为**按租户**的 JSON(包含 `tenant_id`、`es_index_name`、`supported_languages` 等字段)。实际实现返回的是 **`AppConfig.sanitized_dict()`**(完整的应用配置,敏感信息已脱敏),而不是租户摘要字段。 + +**这些指南中还缺少:** `GET /admin/config/meta`。 + +**健康检查:** 拆分指南中的示例包含了 [`HealthResponse`](api/models.py) 中不存在的字段(只有 `status` 和 `elasticsearch`)。 + +对于任何仅根据文档进行 API 集成的人来说,这是最明显的"未解决问题"。 + +### 2. 面向开发者的指南仍将 `services_config` 作为"配置解析器"的核心 + +[`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §5.2 仍说搜索配置由 **`ConfigLoader`** 加载,服务由 **`config/services_config`** "解析"。§6.2 仍将 **`config/services_config.py`** 列为主要的"解析入口"。[`docs/QUICKSTART.md`](docs/QUICKSTART.md) §3.1 仍说"配置解析:`config/services_config.py`"。 + +文档中准确的说法应该是:**规范入口是 `config/loader.py` + `get_app_config()`**;[`config/config_loader.py`](config/config_loader.py) 中的 `ConfigLoader` 包装了统一加载器;`services_config` 是现有调用点的**兼容性外观**。 + +### 3. 重新设计文档本身不是"活的"状态文档 + +[`docs/config-system-review-and-redesign.md`](docs/config-system-review-and-redesign.md) 读起来仍是**纯粹的问题陈述 + 目标**,没有简短的**"已实现 vs 剩余"**部分。这很容易让人假设什么都没做,或者重复工作。添加一个小附录(或一页 `config/README.md` —— 见下文)可以解决这个问题。 + +### 4. 缺少 `config/README.md`(§5.3 中推荐) + +仍然没有专门的 **`config/README.md`** 来描述:加载器入口点、高级优先级、字典存放位置、指向 `/admin/config` + `/admin/config/meta` 的链接,以及重新设计文档的链接。这是重新设计中明确的交付物,可以锚定"文档系统"。 + +### 5. 轻微的文档整洁问题 + +- [`docs/QUICKSTART.md`](docs/QUICKSTART.md) §1.9 环境变量项目后的行有一个多余字符:`---·`(可能是打字错误)。 +- [`docs/DEVELOPER_GUIDE.md`](docs/DEVELOPER_GUIDE.md) §10 **文档索引**没有列出 `config-system-review-and-redesign.md` 或未来的 `config/README.md`。 + +--- + +## 重新设计目标与当前代码之间的差距(文档不应声称"已完成") + +这些影响文档的诚实度: + +| 主题 | 状态 | +|--------|--------| +| **`config dump` CLI**(§5.10) | `main.py` 中不存在;运维人员依赖 HTTP 或临时脚本。 | +| **隐藏的 `["en", "zh"]` 回退**(阶段 3 / 发现 D) | 仍在 [`indexer/document_transformer.py`](indexer/document_transformer.py)、[`suggestion/builder.py`](suggestion/builder.py) 等中使用。 | +| **加载器外的 `os.getenv`**(规则 1–2) | 仍在例如 [`embeddings/server.py`](embeddings/server.py)、[`reranker/server.py`](reranker/server.py)、[`api/app.py`](api/app.py) 中使用 —— 文档声称"仅加载器"将是夸大其词。 | +| **拆分 `base.yaml` / `environments/` / `tenants/*.yaml`**(阶段 5) | 未采用;仍是单一的 [`config/config.yaml`](config/config.yaml)。 | +| **遗留租户标志**(阶段 6 / 发现 H) | [`indexer/README.md`](indexer/README.md) 仍描述上游 MySQL 的 `translate_to_en` / `translate_to_zh`(这可能作为**上游模式**文档保留;需与 Python `tenant_config` 模型区分开来)。 | + +--- + +## 推荐的后续步骤(仅文档,按优先级排序) + +1. **修复管理 API 文档**(合并指南 + `-06-` 拆分):`/admin/config` 的响应格式,添加 `/admin/config/meta`,使健康检查示例与 [`HealthResponse`](api/models.py) 一致。 +2. **更新 DEVELOPER_GUIDE §5–§6 和 QUICKSTART §1.9 / §3.1**,将 **`get_app_config()` / `loader.py`** 描述为主要入口,将 **`services_config`** 描述为适配器。 +3. **添加 `config/README.md`**(简短的操作 + 开发者入口)。 +4. **在 `config-system-review-and-redesign.md` 中添加**带日期的**实现状态表**(已交付 vs 推迟的内容),使审查文档不与现实矛盾。 +5. **DEVELOPER_GUIDE §9 检查清单**:将"配置来自 `services_config`"替换为允许 `get_app_config()` 或精简适配器的语言,与 §6 保持一致。 + +如果需要,我可以在后续处理中为项目 1–3 和重新设计文档中的简短状态块应用补丁。 + + + + +筛选SKU: 先只筛选第一个维度,但考虑到用户搜索词可能带了尺码,所以第二、三个维度也要考虑 + + +引入图片的相关性: +图片的向量最好做SKU维度,用 SPU 维度还是 SKU 维度? +1. SKU维度(主款式,option1维度),如果用户搜索“蓝色 T恤”,这种图片相关性会比较有价值。 +2. 我不考虑颜色的差异,其余的款式一般是大小之类的。这些图片,项链细粉到 SKU 维度,可能价值不大,性价比偏低 + + + +属性的筛选: +训练一个bert/transformer多分类模型,分类: 颜色、尺寸、材质 等等。但是要注意一些属性的值不规范、非常多,要考虑 是不是做规范化,如何规范化。 + + + + +无结果重查 +稀有语言,翻译可能超时(因为zh-en互译之外的翻译耗时更长) + + + + + + +检索相关性优化: +原始搜索词和翻译的词,都需要有对应的主干分析 +这个主干可以根据词性简单提取名词即可 +在搜索时,原始词和主干都成对地出现,原始词和trunk_keywords一起组成一个或查询。 +有一种方案是把原始词和主干词拼接起来。但是bm25要调tf系数。 + + + + +nllb-200-distilled-600M性能优化 +已完成(2026-03) +- CTranslate2 迁移 + float16 转换 +- 扩展压测报告:`perf_reports/20260318/translation_local_models_ct2/README.md` +- T4 聚焦调优报告:`perf_reports/20260318/translation_local_models_ct2_focus/README.md` +- NLLB T4 商品标题专项报告:`perf_reports/20260318/nllb_t4_product_names_ct2/README.md` +- 当前结论: + - NLLB 在线默认推荐:`ct2_inter_threads=4 + ct2_max_queued_batches=32 + ct2_batch_type=examples + ct2_decoding_length_mode=source(+8,min=32)` + - `opus-mt-zh-en` 维持保守默认更稳 + - `opus-mt-en-zh` 如追求离线吞吐可继续做单独 profile + +请搜索nllb-200-distilled-600M这类seq2seq、transformer架构的模型,有哪些性能优化方案,提高线上翻译服务的吞吐量、降低耗时,搜索相关的在线推理服务方案,找到高性能的服务化方法 + +cnclip的性能优化 + +rerank 性能优化 + + +超时 +Query 分析阶段等待翻译/embedding 的硬超时 +配置文件位置:config/config.yaml +配置项:query_config.async_wait_timeout_ms: 80 +代码生效点:query/query_parser.py 使用该值换算成秒传给 wait(...) +2) Embedding HTTP 调用超时(Text/Image) +不再使用任何环境变量覆盖(之前提到的 EMBEDDING_HTTP_TIMEOUT_SEC 已不采用) +配置文件位置:config/config.yaml +配置项:services.embedding.providers.http.timeout_sec(已在 YAML 里补了示例默认 60) +代码生效点: +embeddings/text_encoder.py:requests.post(..., timeout=self.timeout_sec) +embeddings/image_encoder.py:requests.post(..., timeout=self.timeout_sec) + + + + + + + + + + + +多reranker: + +改 reranker 服务,一次请求返回多路分 +服务启动时 加载多个 backend(或按请求懒加载),/rerank 响应扩展为例如 +scores: [...](兼容主后端)+ scores_by_backend: { "bge": [...], "qwen3_vllm": [...] }。 +搜索侧解析多路分,再融合或只透传 debug。 +优点:搜索侧仍只调一个 URL。缺点:单进程多大模型 显存压力很大; + +融合层要注意的一点 +fuse_scores_and_resort 目前只消费 一条 rerank_scores 序列,并写入 _rerank_score +多 backend 之后需要rerank_scores 都参与融合 + +必要性: +见 qwen3-reranker和bge-m3的严重badcase +不一定是要多reranker的方式,但是一定会需要解决方案。 + + + + +product_enrich : Partial Mode : done +https://help.aliyun.com/zh/model-studio/partial-mode?spm=a2c4g.11186623.help-menu-2400256.d_0_3_0_7.74a630119Ct6zR +需在messages 数组中将最后一条消息的 role 设置为 assistant,并在其 content 中提供前缀,在此消息中设置参数 "partial": true。messages格式如下: +[ + { + "role": "user", + "content": "请补全这个斐波那契函数,勿添加其它内容" + }, + { + "role": "assistant", + "content": "def calculate_fibonacci(n):\n if n <= 1:\n return n\n else:\n", + "partial": true + } +] +模型会以前缀内容为起点开始生成。 +支持 非思考模式。 + + + + + +融合打分(已完成,2026-03) + +以下已经完成: +1. `fuse_scores_and_resort` 已改为乘法融合,并通过 `matched_queries` 提取: + - `base_query` + - `base_query_trans_*` + - `fallback_original_query_*` + - `knn_query` +2. 文本相关性大分不再依赖 `phrase_query` / `keywords_query`,这两类查询已清理。 +3. 当前融合策略: + - `text_score = primary(weighted_source, weighted_translation, weighted_fallback) + 0.25 * support` + - `fused_score = (rerank_score + 0.00001) * (text_score + 0.1) ** 0.35 * (knn_score + 0.6) ** 0.2` +4. `track_scores` 与 `include_named_queries_score` 已接入,调试字段与评估方法已同步到: + - `docs/相关性检索优化说明.md` + - `docs/搜索API对接指南-01-搜索接口.md`(分册;原单文件已拆分) + - `docs/Usage-Guide.md` + +未完成的: +(归一化、次序融合?还乘法公式?) +RRF:先把多路召回稳妥融合 +linear + minmax:让你能精调 knn 和文本的权重 +reranker:对前面召回出来的 top-k 再做“最后一刀” + + + + +suggest 索引,现在是全量脚本,要交给金伟 + + + +翻译,增加facebook/nllb-200-distilled-600M +https://blog.csdn.net/qq_42746084/article/details/154947534 +https://huggingface.co/facebook/nllb-200-distilled-600M + + +店铺的语言:英语能占到80%,所以专门增加一个en-zh的 +https://huggingface.co/Helsinki-NLP/opus-mt-zh-en +https://huggingface.co/Helsinki-NLP/opus-mt-en-zh + + +opus-mt-zh-en + +from transformers import AutoTokenizer, AutoModelForSeq2SeqLM +model_name = "./models/opus-mt-en-zh" +tokenizer = AutoTokenizer.from_pretrained(model_name) +model = AutoModelForSeq2SeqLM.from_pretrained(model_name) +data = 'test' +encoded = tokenizer([data], return_tensors="pt") +translation = model.generate(**encoded) +result = tokenizer.batch_decode(translation, skip_special_tokens=True)[0] +print(result) + + + + + +Qwen3-Reranker-4B-GGUF +https://modelscope.cn/models/dengcao/Qwen3-Reranker-4B-GGUF/summary +1. 要确定选择哪种量化方式 +2. 确定提示词 + + + +reranker 补充:nvidia/llama-nemotron-rerank-1b-v2 +https://huggingface.co/nvidia/llama-nemotron-rerank-1b-v2 +后端推理也建议使用vLLM +注意搜索相关资料,挖掘我的特斯拉 T4 GPU 的性能,充分挖掘性能 +你有充足的自由度进行实验 +encoder架构。 +比较新。 +性能更好。 +亚马逊 电商搜索数据集比qwen-reranker-4b更好。 +支持vLLM。 + + + + + +查看翻译的缓存情况 + +向量的缓存 + + + + + + + + +AI - 生产 - MySQL +HOST:10.200.16.14 / localhost +端口:3316 +用户名:root +密码:qY8tgodLoA&KT#yQ + +AI - 生产 - Redis +HOST:10.200.16.14 / localhost +端口:6479 +密码:dxEkegEZ@C5SXWKv + + +远程登录方式: +# redis +redis-cli -h 43.166.252.75 -p 6479 + +# mysql 3个用户,都可以远程登录 +mysql -uroot -p'qY8tgodLoA&KT#yQ' +CREATE USER 'saas'@'%' IDENTIFIED BY '6dlpco6dVGuqzt^l'; +CREATE USER 'sa'@'%' IDENTIFIED BY 'C#HU!GPps7ck8tsM'; + + + +ES: +HOST:10.200.16.14 / localhost +端口:9200 +访问示例: +用户名密码:saas:4hOaLaf41y2VuI8y + + +安装 nvidia-container-toolkit (done) +https://mirrors.aliyun.com/github/releases/NVIDIA/nvidia-container-toolkit/ +https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html + + +qwen3-embedding、qwen3-reranker (done) +选一个推理引擎,相比于我自己直接调 sentence-transformers,主要是多进程和负载均衡、连续批处理,比较有用 +当前结论:embedding 场景优先 TEI;vLLM 更偏向生成式与 rerank 场景。 + + + +混用 大模型 使用:hunyuan-turbos-latest +混元 OpenAI 兼容接口相关调用示例:https://cloud.tencent.com/document/product/1729/111007 + + +腾讯云 混元大模型 API_KEY:sk-mN2PiW2gp57B3ykxGs4QhvYxhPzXRZ2bcR5kPqadjboGYwiz + +hunyuan翻译:使用模型 hunyuan-translation +https://cloud.tencent.com/document/product/1729/113395#4.-.E7.A4.BA.E4.BE.8B + + +谷歌翻译 基础版:https://docs.cloud.google.com/translate/docs/reference/rest/v2/translate + + + + +阿里云 百炼模型 现在使用的apikey是国内的。 +各地域的 Base URL 和对应的 API Key 是绑定的。 + +现在使用了美国的服务器,使用了美国的地址,需要在 美国地域控制台页面(https://modelstudio.console.aliyun.com/us-east-1 )中创建或获取API_KEY: + +登录 百炼美国地域控制台:https://modelstudio.console.aliyun.com/us-east-1?spm=5176.2020520104.0.0.6b383a98WjpXff +在 API Key 管理 中创建或复制一个适用于美国地域的 Key -- libgit2 0.21.2