Commit dfb45131db8e478b2d5dda563bc558e222131e42
1 parent
e81cbdf5
issues文档整理
Showing
5 changed files
with
605 additions
and
0 deletions
Show diff stats
| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | +需求: | |
| 2 | +目前160条结果(rerank_window: 160)会进入重排,重排中 文本和图片向量的相关性,都会作为融合公式的因子之一(粗排和reranker都有): | |
| 3 | +knn_score | |
| 4 | +text_knn | |
| 5 | +image_knn | |
| 6 | +text_factor | |
| 7 | +knn_factor | |
| 8 | +但是文本向量召回和图片向量召回,是使用 KNN 索引召回的方式,并不是所有结果都有这两个得分,这两项得分都有为0的。 | |
| 9 | +为了解决这个问题,有一个方法是对最终能进入重排的 160 条,看其中还有哪些分别缺失文本和图片向量召回的得分,再通过某种方式让 ES 去算,或者从 ES 把向量拉回来,自己算,或者在召回的时候请求 ES 的时候,就通过某种设定,确保前面的若干条都带有这两个分数,不知道有哪些方法,我感觉这些方法都不太好,请你思考一下 | |
| 10 | + | |
| 11 | +考虑的一个方案: | |
| 12 | +想在“第一次 ES 搜索”里,只对 topN 补向量精算,考虑 rescore 或 retriever.rescorer的方案(官方明确支持多段 rescore/支持 score_mode: multiply,甚至示例里就有 function_score/script_score 放进 rescore 的写法。) | |
| 13 | +这意味着你完全可以: | |
| 14 | +初检仍然用现在的 lexical + text knn + image knn 召回候选 | |
| 15 | +对 window_size=160 做 rescore | |
| 16 | +用 exact script_score 给 top160 补 text/image vector 分 | |
| 17 | +顺手把你现在本地 coarse 融合迁回 ES | |
| 18 | + | |
| 19 | +export ES_AUTH="saas:4hOaLaf41y2VuI8y" | |
| 20 | +export ES="http://127.0.0.1:9200" | |
| 21 | +"index":"search_products_tenant_163" | |
| 22 | + | |
| 23 | +有个细节暴露出来了:dotProduct() 这类向量函数在 script_score 评分上下文能用,但在 script_fields 取字段上下文里不认。所以如果我们要把 exact 分顺手回传给 rerank,用 script_fields 的话得自己写数组循环,不能直接调向量内建函数。 | |
| 24 | + | |
| 25 | +重排打分公式需要的base_query base_query_trans_zh knn_query image_knn_query还能不能拿到?请你考虑,尽量想想如何得到这些打分,如果实在拿不到去想替代的办法比如简化打分公式。 | ... | ... |
docs/issues/issue-2026-04-16-bayes寻参-数据集扩增-clothing_top771.md
0 → 100644
| ... | ... | @@ -0,0 +1,70 @@ |
| 1 | +Prompt - 1 | |
| 2 | + | |
| 3 | +参考当前的评估框架 | |
| 4 | +@scripts/evaluation/README.md @scripts/evaluation/eval_framework/framework.py | |
| 5 | +@start_eval.sh.sh | |
| 6 | +当前,是基于54个评测样本(queries.txt),建立了自动化评估的系统,便于发现策略在这个评估集上的效果。 | |
| 7 | + | |
| 8 | +我需要扩大评估样本,使用all_keywords.txt.top1w.shuf.top1k.clothing_filtered(771条)为query集合,走标注流程,从而新建一个标注集。 | |
| 9 | +那么以后eval-web服务,现在的Batch Evaluation按钮,应该支持多个评估集合,左侧的History,也有对应多个评估集合的评估结果,请你考虑如何支持、如何设计。请进行统一的设计,不要补丁式的支持。 | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | +Response - 1 | |
| 14 | + | |
| 15 | +设计已经整理并写回到 [issue-2026-04-16-bayes寻参-TODO.md](/data/saas-search/docs/issues/issue-2026-04-16-bayes寻参-TODO.md:24)。这次我没有做补丁式方案,而是把它统一成“多 dataset registry 模式”:`dataset_id` 成为一等主键,`eval-web` 的 Queries / Batch Evaluation / History / 调参脚本都围绕当前 dataset 工作,历史产物按 dataset 隔离,并且每次 batch/build 都固化一份 dataset snapshot,避免以后 query 文件变更导致历史失真。 | |
| 16 | + | |
| 17 | +这份设计里最关键的取舍是:评估集和标签缓存分层。`relevance_labels` / `rerank_scores` 继续按 `(tenant_id, query, spu_id)` 共享复用,不按 dataset 重复标注;真正带 `dataset_id` 的是 `build_runs`、`batch_runs`、artifact 目录、Web API 和前端上下文。文档里也补了配置结构、API 设计、UI 交互、迁移方案,以及调参链路如何接入 `--dataset-id`。另外我顺手核对了当前文件规模:`queries.txt` 现在仓库里是 53 条非空 query,`all_keywords.txt.top1w.shuf.top1k.clothing_filtered` 是 771 条。 | |
| 18 | + | |
| 19 | +如果你要我继续落地实现,我建议下一步先做 3 件事:dataset registry + artifact path helper、`batch_runs/build_runs` 存储升级、`/api/datasets` 与 History 按 dataset 过滤。此次没有跑测试,因为目前是文档设计更新,没有改功能代码。 | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | +prompt - 2 | |
| 26 | + | |
| 27 | +请推进、完成这些事项、并且把llm标注跑起来 | |
| 28 | + | |
| 29 | +response - 2 | |
| 30 | + | |
| 31 | +多评估集这套已经落地到代码里了,核心改动在 [config/config.yaml](/data/saas-search/config/config.yaml:48)、[config/schema.py](/data/saas-search/config/schema.py:410)、[config/loader.py](/data/saas-search/config/loader.py:304) 和新加的 [datasets.py](/data/saas-search/scripts/evaluation/eval_framework/datasets.py:1)。现在 `search_evaluation.datasets` 里正式注册了 `core_queries` 和 `clothing_top771`,CLI / store / framework / Web API / 前端都改成以 `dataset_id` 为主轴了,关键实现分别在 [cli.py](/data/saas-search/scripts/evaluation/eval_framework/cli.py:1)、[store.py](/data/saas-search/scripts/evaluation/eval_framework/store.py:1)、[framework.py](/data/saas-search/scripts/evaluation/eval_framework/framework.py:1)、[web_app.py](/data/saas-search/scripts/evaluation/eval_framework/web_app.py:1)、[index.html](/data/saas-search/scripts/evaluation/eval_framework/static/index.html:1)、[eval_web.js](/data/saas-search/scripts/evaluation/eval_framework/static/eval_web.js:1)。调参入口也已经带上 `--dataset-id`,在 [tune_fusion.py](/data/saas-search/scripts/evaluation/tune_fusion.py:1) 和几个启动脚本里都打通了;文档同步写到了 [scripts/evaluation/README.md](/data/saas-search/scripts/evaluation/README.md:1) 和 [issue-2026-04-16-bayes寻参-TODO.md](/data/saas-search/docs/issues/issue-2026-04-16-bayes寻参-TODO.md:35)。 | |
| 32 | + | |
| 33 | +验证这边我跑过一轮编译检查,另外新增了 [tests/test_search_evaluation_datasets.py](/data/saas-search/tests/test_search_evaluation_datasets.py:1),`./.venv/bin/pytest -q tests/test_search_evaluation_datasets.py` 结果是 `2 passed`。`eval-web` 代码已经按新模型重启过一次并健康通过,但在当前机器资源占用下它后来没有稳定驻留,我没把这个问题硬掩过去;如果你要立刻看 UI,我建议等这轮大标注稳定后再单独 `./scripts/service_ctl.sh restart eval-web` 复位。 | |
| 34 | + | |
| 35 | +LLM 标注已经跑起来了,目标 dataset 是 `clothing_top771`。我先手动拉起了 reranker 并确认 `http://127.0.0.1:6007/health` 返回 `{"status":"ok"...}`,然后启动了 rebuild build。当前进度已经进入第 1 个 query `白色oversized T-shirt` 的 LLM 批量标注阶段,最新日志大约推进到 `llm_batch=24/40`。你现在可以直接看 [logs/eval.log](/data/saas-search/logs/eval.log:1) 跟主进度,看 [logs/verbose/eval_verbose.log](/data/saas-search/logs/verbose/eval_verbose.log:1) 跟完整 LLM I/O。唯一要注意的是:这轮 build 依赖我手动拉起的 reranker,因为当前 `search.rerank.enabled=false`,`service_ctl` 会跳过它。 | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | +prompt - 3 | |
| 42 | + | |
| 43 | +“LLM 标注已经跑起来了,目标 dataset 是 clothing_top771。” | |
| 44 | +好像进程在第 48 条 build上停了 | |
| 45 | +"query 雪纺衬衫 —— 正好是 query 文件里 第 47 行。 | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | +response - 3 | |
| 50 | + | |
| 51 | +当前不是卡死,已经越过你说的第 48 条。`clothing_top771` 的 `query_builds` 现在是 **54**,正在跑第 **55/771**(`barbie backpack`),第 47/48(`雪纺衬衫` / `convertible zip-off hiking pants`)都已被识别为已完成并跳过续跑。 | |
| 52 | + | |
| 53 | +我把这次流程做成了可持续的“统一续跑”能力,避免再靠临时脚本: | |
| 54 | +- 在 [scripts/evaluation/eval_framework/cli.py](/data/saas-search/scripts/evaluation/eval_framework/cli.py) 新增 `build --resume-missing --continue-on-error --max-retries-per-query --retry-backoff-sec`,并修正默认参数逻辑(有 `dataset_id` 时不再强塞 legacy `queries_file`)。 | |
| 55 | +- 在 [scripts/evaluation/start_eval.sh](/data/saas-search/scripts/evaluation/start_eval.sh) 新增 `batch-rebuild-resume` 入口,统一用 dataset 模式,`REPO_EVAL_QUERIES` 仅作可选覆盖。 | |
| 56 | +- 在 [scripts/start_eval_web.sh](/data/saas-search/scripts/start_eval_web.sh) 做同样的 dataset/queries 统一化。 | |
| 57 | +- 在 [scripts/evaluation/README.md](/data/saas-search/scripts/evaluation/README.md) 补了中断续跑说明和新命令。 | |
| 58 | +- 已验证 `eval-web` 多数据集接口正常(`/api/datasets`、`/api/history?dataset_id=...` 均返回 `core_queries` 与 `clothing_top771` 分域结果)。 | |
| 59 | + | |
| 60 | +当前在线进程: | |
| 61 | +- LLM 标注:`PID 2062901`(`build ... --dataset-id clothing_top771 --resume-missing ...`) | |
| 62 | +- reranker:`PID 2065235`(6007,`/health` 返回 `ok`) | |
| 63 | + | |
| 64 | +如果你要自己盯进度,直接看: | |
| 65 | +```bash | |
| 66 | +tail -f logs/eval.log | |
| 67 | +ls -1 artifacts/search_evaluation/datasets/clothing_top771/query_builds | wc -l | |
| 68 | +curl -sS http://127.0.0.1:6007/health | |
| 69 | +``` | |
| 70 | + | ... | ... |
docs/issues/issue-2026-04-16-bayes寻参-数据集扩增.md deleted
docs/issues/issue-2026-04-16-bayes寻参-TODO.md renamed to docs/issues/issue-2026-04-16-数据集扩增&bayes寻参-TODO.md
docs/issues/issue-2026-04-20-款式意图下SKU选取增强(option,taxonomy,图像)-0420done.md
0 → 100644
| ... | ... | @@ -0,0 +1,510 @@ |
| 1 | +# 1-prompt | |
| 2 | + | |
| 3 | +现在索引是spu粒度的,sku是作为spu的nested属性。召回和计算exact_image_knn_query都会计算图片的相似度,image_embedding字段是一个list,跟sku是一一对应的。 | |
| 4 | +我希望,是因为哪个image_embedding被召回的、或者重排的时候,是取的哪个元素的exact_image_knn_query得分(image_embedding这个list内最高的得分),我希望将这个sku前置。 | |
| 5 | + | |
| 6 | +之前在重排之前有这个流程: | |
| 7 | + 1. 有款式意图的时候,才做sku筛选 | |
| 8 | + 2. sku筛选的时机,改为在reranker之前,对所有内容(rerank输入的所有spus)做sku筛选 | |
| 9 | +若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本 | |
| 10 | +Searcher 只编排:解析 query -> 首次 ES 召回 -> 若有款式意图则对 rerank 窗口内全部 hits 做 SKU 预筛选并补充 rerank doc 文本 -> run_rerank -> 分页 | |
| 11 | +但是,现在是关闭重排的,所以,这个流程可能没了?请你检查。 | |
| 12 | + | |
| 13 | +我的总的意图是,如果有款式方面的意图,需要做spu内部的sku的排序, | |
| 14 | +这两点也需要检查: | |
| 15 | +1. 之前的流程,不知道是否仍然有效(因为现在关闭了reranker) | |
| 16 | +2. 现在补充了image_embedding的召回和exact_image_knn_query得分,那么应该能知道哪个sku是最匹配的,希望把这个信号利用上, | |
| 17 | +1)召回的时候,是哪个图片向量相似度最高 | |
| 18 | +2)重排的时候是用哪个算的相似度?这个信号如何利用 | |
| 19 | + "rescore_query": { | |
| 20 | + "bool": { | |
| 21 | + "should": [ | |
| 22 | + { | |
| 23 | + "nested": { | |
| 24 | + "path": "image_embedding", | |
| 25 | + "_name": "exact_image_knn_query", | |
| 26 | + "score_mode": "max", | |
| 27 | + "query": { | |
| 28 | + "script_score": { | |
| 29 | + "query": { | |
| 30 | + "exists": { | |
| 31 | + "field": "image_embedding.vector" | |
| 32 | + } | |
| 33 | + }, | |
| 34 | + "script": { | |
| 35 | + "source": "((dotProduct(params.query_vector, 'image_embedding.vector') + 1.0) / 2.0) * params.boost", | |
| 36 | + "params": { | |
| 37 | + | |
| 38 | +最终组织结果的时候,sku的排序,要把这个信号利用进来,要按照图片的相似度排序。 | |
| 39 | +是直接用图片的信号,把图片得分最高的url,用于sku的筛选(选取对应url的sku。因为skus和image_embedding是两个字段,skus里面有image_src,可以用于url匹配,选取对应url的sku作为图片得分最高的sku)还是跟以前的策略融合在一起,还是,请思考 | |
| 40 | + | |
| 41 | + | |
| 42 | +可以以query=白色T恤进行实验 | |
| 43 | + | |
| 44 | +# 1-response | |
| 45 | +我仔细读了当前 `search/searcher.py`、`search/sku_intent_selector.py`、`search/es_query_builder.py`、`mappings/search_products.json` 和 `indexer/document_transformer.py`,先回答你的两个核对问题,再给出如何把图片信号融入的设计。 | |
| 46 | + | |
| 47 | +--- | |
| 48 | + | |
| 49 | +## 1. 关闭 rerank 之后,原来的款式意图 SKU 流程是否还有效? | |
| 50 | + | |
| 51 | +**仍然有效。** 关键点: | |
| 52 | + | |
| 53 | +- `in_rank_window = (from_ + size) <= rerank_window`(`searcher.py:529`)只依赖**配置里的 `rerank_window`**,跟请求/配置里的 `do_rerank` 没关系。 | |
| 54 | +- 进入 `in_rank_window` 分支后,款式意图处理是**无条件**执行的: | |
| 55 | + - `searcher.py:1070-1081`:在 coarse 完成、ranking 窗口 source fill 之后,调用 `_apply_style_intent_to_hits` 算出 `style_intent_decisions`(带 `_style_rerank_suffix`)。 | |
| 56 | + - `searcher.py:1083-1154`:fine 和 rerank 阶段现在都通过 `_run_optional_stage` 包装,`enabled=False` 时走 passthrough,**不会丢**前面挂在 hit 上的信号。 | |
| 57 | + - `searcher.py:1205-1209`:page fill 覆盖 `_source` 之后,再调一次 `apply_precomputed_decisions`,把 SKU 置顶 + `image_url` 替换最终写回。 | |
| 58 | +- 非 rank-window 分支(`searcher.py:1225-1231`)走 `_apply_style_intent_to_hits` 并在 `result_processing` 前 `apply_precomputed_decisions`(1254-1259)。 | |
| 59 | + | |
| 60 | +所以"先 prepare、后 apply"的契约没断。唯一的"废功"是 `_style_rerank_suffix`——关掉重排之后它挂在 hit 上但没人读;代价很小、一旦你重新打开 rerank 又立刻生效,建议保留。 | |
| 61 | + | |
| 62 | +--- | |
| 63 | + | |
| 64 | +## 2. 当前 `image_embedding` / `exact_image_knn_query` 的信号完全**没**喂到 SKU 选择里 | |
| 65 | + | |
| 66 | +我去看了索引 & 查询两端: | |
| 67 | + | |
| 68 | +- `mappings/search_products.json:1054` 的 `image_embedding` 是 nested,每个元素 `{vector, url}`;索引时(`indexer/document_transformer.py:490-536`)把 SPU 主图 + SKU 图片 URL 合起来去重后逐个编码,**`url` 就是 `skus[].image_src` 里能对得上的那串**。 | |
| 69 | +- `es_query_builder.py:193-281` 里 `build_image_knn_clause` 和 `build_exact_image_knn_rescore_clause` 都是 nested + `score_mode: max`,但**都没带 `inner_hits`**。所以 ES 只告诉我们"nested 里最高分是多少",不告诉我们是哪条 url 命中的。 | |
| 70 | +- `StyleSkuSelector` 当前完全靠"意图词 × `optionN_value` 文本匹配"选 SKU(`sku_intent_selector.py:252-284`),没有任何 image 通路。 | |
| 71 | + | |
| 72 | +也就是说:**"哪个 image_embedding 被召回 / 被 rescore 命中"这个你已经花代价算出来的信号,下游根本拿不到**。 | |
| 73 | + | |
| 74 | +--- | |
| 75 | + | |
| 76 | +## 3. 融合设计(建议方案) | |
| 77 | + | |
| 78 | +我倾向**融合**而不是二选一,理由是两类信号各有盲区: | |
| 79 | + | |
| 80 | +| 信号 | 擅长 | 盲区 | | |
| 81 | +|---|---|---| | |
| 82 | +| 文本款式意图(现有) | "白色"能命中 option `color=white` | 店家把颜色塞在 title 或图里、option 是数字码时失效 | | |
| 83 | +| 图片 KNN 最佳 url(新增) | 视觉接近 query(文本走 CN-CLIP 后也和图片空间对齐) | 可能选出"卖相最好"的主图,和明确的颜色诉求冲突 | | |
| 84 | + | |
| 85 | +### 契约上的改动最小化:依旧 `SkuSelectionDecision` + `apply_precomputed_decisions` | |
| 86 | + | |
| 87 | +只是在 prepare 阶段多一个候选源、decision 里多几个字段做 debug 和融合依据。 | |
| 88 | + | |
| 89 | +### 具体改动点 | |
| 90 | + | |
| 91 | +**a. `es_query_builder.py`:给两个 nested image 查询都加 `inner_hits`** | |
| 92 | + | |
| 93 | +- `build_exact_image_knn_rescore_clause`:`{"inner_hits": {"name": "exact_image_knn_hits", "size": 1, "_source": ["url"]}}` | |
| 94 | +- `build_image_knn_clause`:`{"inner_hits": {"name": "image_knn_hits", "size": 1, "_source": ["url"]}}` | |
| 95 | + | |
| 96 | +两个都加的原因:rescore 是 `in_rank_window` 才有;不在窗口内时还能吃到初召回的 inner_hits。代价:每个 hit 多返回一个 `{url, _score}`,很轻。 | |
| 97 | + | |
| 98 | +**b. `sku_intent_selector.py`:扩展 decision,新增 image-pick 通路** | |
| 99 | + | |
| 100 | +```python | |
| 101 | +@dataclass(frozen=True) | |
| 102 | +class SkuSelectionDecision: | |
| 103 | + ... # 保留现有字段 | |
| 104 | + image_pick_sku_id: Optional[str] = None | |
| 105 | + image_pick_url: Optional[str] = None | |
| 106 | + image_pick_score: Optional[float] = None | |
| 107 | + final_source: str = "text" # "text" | "image" | "none" | |
| 108 | +``` | |
| 109 | + | |
| 110 | +- 新增 `_pick_sku_by_image(hit, source)`:读 `hit["inner_hits"]["exact_image_knn_hits"]`(缺省回退到 `image_knn_hits`),取 top url,在 `skus[]` 里按 `image_src` 归一化后等值匹配(注意处理 `//` 协议无关前缀、大小写、querystring 这些脏数据,按 indexer 那边的写入规则来)。 | |
| 111 | +- `_select_for_source` 的新决策树: | |
| 112 | + 1. `text_pick` = 现在的文本匹配结果。 | |
| 113 | + 2. `image_pick` = 上述 helper 的结果。 | |
| 114 | + 3. 有款式意图: | |
| 115 | + - `text_pick` 非空 → 选 `text_pick`,`final_source="text"`;把 `image_pick` 存进 decision 里仅用于 debug/验证(两者一致时在 debug 打标,不一致时也不翻盘——query 显式指名了颜色/尺码,视觉信号不该覆盖)。 | |
| 116 | + - `text_pick` 为空但 `image_pick` 非空 → 选 `image_pick`,`final_source="image"`。 | |
| 117 | + 4. 无款式意图: | |
| 118 | + - 若 `image_pick` 的分数 ≥ 阈值(配置项,例如 `query_config.image_sku_selection_min_score`),选 `image_pick`。否则不做 SKU 选择(保持 SPU 原顺序)。 | |
| 119 | +- `_apply_decision_to_source` 无需大改,仍然是"把选中 SKU 顶到前面 + 替换 `image_url` 为该 SKU 的 `image_src`";只是 `selected_sku_id` 的来源更丰富了。 | |
| 120 | + | |
| 121 | +**c. `searcher.py`:把 image-pick 路径也纳入 "prepare → apply"** | |
| 122 | + | |
| 123 | +- 把"`_has_style_intent` 才 prepare"改成"`_has_style_intent` 或 `has_image_inner_hits` 就 prepare"。命名上把 `_apply_style_intent_to_hits` 改成 `_apply_sku_selection_to_hits` 更准确(可做可不做,看你对改动面范围的容忍度)。 | |
| 124 | +- `_resolve_rerank_source_filter` 已经在有款式意图时补了 `skus`/`option*_name`;image-only 路径还需要 `skus`(至少 `sku_id`、`image_src`),所以触发条件要扩到"只要可能选 SKU"。 | |
| 125 | +- inner_hits 会带在 top-level `hit` 上,不会被 page fill 的 `_source` 覆盖影响,但 page fill 用的是 `ids` 查询,**本来就不会有 inner_hits**。这正好符合现有契约:**prepare 时把 image_pick 落到 `SkuSelectionDecision` 里**,apply 阶段不再依赖 inner_hits,就不会丢。 | |
| 126 | +- `context.store_intermediate_result("style_intent_profile", ...)` 附近把 image-pick 的诊断也写进 debug。 | |
| 127 | + | |
| 128 | +**d. 配置** | |
| 129 | + | |
| 130 | +```yaml | |
| 131 | +query_config: | |
| 132 | + image_sku_selection_enabled: true # 总开关 | |
| 133 | + image_sku_selection_min_score: 0.55 # 无款式意图时的保护阈值 | |
| 134 | + image_sku_selection_when_text_missing: true # 有意图但文本未命中时回退到图片 | |
| 135 | +``` | |
| 136 | + | |
| 137 | +### 我为什么不建议"用图片分直接替换原策略" | |
| 138 | + | |
| 139 | +两个理由: | |
| 140 | +1. 你现在的文本策略对明确的词面命中是**最可信**的信号;query 说"白色",结果给了视觉更漂亮但色号不同的 SKU,这是产品上的倒退。 | |
| 141 | +2. 图片 KNN 的 max score 在 nested score_mode=max 下是"最相似那张图的分",但 CN-CLIP 的文本→图像相似度阈值校准很敏感,不同品类/拍摄风格分布差异大。拿来**做候选**稳,拿来**做唯一决定**不稳。 | |
| 142 | + | |
| 143 | +--- | |
| 144 | + | |
| 145 | +## 4. `query=白色T恤` 的实验 | |
| 146 | + | |
| 147 | +改完之后可以这样验证: | |
| 148 | + | |
| 149 | +```bash | |
| 150 | +source activate.sh | |
| 151 | +python main.py search "白色T恤" --tenant-id 1 --size 10 --debug \ | |
| 152 | + | jq '.debug_info.per_result[] | {spu_id, style_intent_sku, first_sku: .skus[0]?}' | |
| 153 | +``` | |
| 154 | + | |
| 155 | +预期关注点: | |
| 156 | +- **有 color option 的 SPU**:`style_intent_sku.final_source == "text"`,并且 `image_pick_sku_id` 多数情况下与 text 选的一致(一致率是个好指标)。 | |
| 157 | +- **没有 color option、但多图的 SPU**:`final_source == "image"`,`skus[0].image_src` 的图肉眼是白色。 | |
| 158 | +- **两者冲突**:debug 里应能看到 `selected_sku_id != image_pick_sku_id` 的案例,确认当前默认"文本优先"没有被图片翻盘。 | |
| 159 | +- 分别跑 `--debug` 对比改前/改后同一 query 的 `results[].image_url` 和 `skus[0].image_src`,观察白色主图替换的命中率提升。 | |
| 160 | + | |
| 161 | +如果想更系统,可以扩 `scripts/evaluation/` 里已有的回归 case,加一个"图片颜色是否匹配意图"的人工抽检指标。 | |
| 162 | + | |
| 163 | +--- | |
| 164 | + | |
| 165 | +## 5. 两个我想先和你确认的产品决策 | |
| 166 | + | |
| 167 | +1. **无款式意图时**,是否允许图片信号单独触发 SKU 置顶?(我倾向:允许,但带阈值保护,默认开。) | |
| 168 | +2. **文本命中 vs 图片命中冲突**时,是硬性文本优先,还是做分数融合(先各自 min-max 归一再取加权 max)?我倾向硬性文本优先——实现简单、行为可解释;融合方案留作后续 A/B。 | |
| 169 | + | |
| 170 | +告诉我你的取舍,我就按上面的改动清单一次性改掉(`es_query_builder.py` 两个 inner_hits、`sku_intent_selector.py` 扩字段 + 新通路、`searcher.py` 触发条件扩容 + 命名收敛、配置项、配套单测),并跑 `白色T恤` 的实际 debug 对照给你看。 | |
| 171 | + | |
| 172 | + | |
| 173 | +# 2-prompt | |
| 174 | +1. 无款式意图时,允许图片信号单独触发 SKU 置顶 | |
| 175 | + | |
| 176 | +2. 文本命中,硬性文本优先,当匹配失败的时候("selected_sku_id": null)使用图片最相近的 | |
| 177 | + | |
| 178 | +3. es中有这个字段enriched_taxonomy_attributes: | |
| 179 | +"enriched_taxonomy_attributes": [ | |
| 180 | +{ | |
| 181 | +"name": "Color", | |
| 182 | +文本匹配的时候,考虑更全面一点: | |
| 183 | +1)把这个字段考虑进去,enriched_taxonomy_attributes内Color对应的value匹配的时候 | |
| 184 | +我在 source_fields:加了enriched_taxonomy_attributes之后好像还是没返回这个字段,你检查下是不是能拿到,可以考虑对query=卡其色裙子 首条结果验证下 | |
| 185 | + | |
| 186 | +4. 还是颜色意图文本匹配的问题 query=卡其色裙子的首条结果: | |
| 187 | + "selected_sku_id": null, | |
| 188 | +"specifications": [ | |
| 189 | +{ | |
| 190 | +"value_text": {...}, | |
| 191 | +"name": "颜色", | |
| 192 | +"sku_id": "447652", | |
| 193 | +"value_keyword": "卡其色v领收腰长裙【常规款】" | |
| 194 | +}, | |
| 195 | + | |
| 196 | +意图检测是成功的: | |
| 197 | +intent_detection: | |
| 198 | +active: yes | |
| 199 | +intents: | |
| 200 | +intent_type: color | |
| 201 | +dimension_aliases: color, colors, colour, colours, 颜色, 色, 色系 | |
| 202 | +matched_term: 卡其色 | |
| 203 | +canonical_value: beige | |
| 204 | + | |
| 205 | +但是 "selected_sku_id": null, 颜色没有匹配上,value_keyword 包含了“卡其色”,没有匹配成功,因此也应该没有享受到提权 | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | +# 2-response | |
| 211 | +好,那就直接在当前 agent 模式下按计划施工。施工顺序按"先不破坏现有行为、再扩新通路": | |
| 212 | + | |
| 213 | +**Phase 1**(修复 + 显字段,低风险) | |
| 214 | +1. `api/models.py` 扩 `SpuResult` 两个富化字段 | |
| 215 | +2. `api/result_formatter.py` 透传 | |
| 216 | +3. `sku_intent_selector.py`:扩匹配词集合(zh+en+attr)+ 收敛为子串匹配;加 `enriched_taxonomy_attributes` 作为 SPU 级确认 | |
| 217 | + | |
| 218 | +**Phase 2**(image 回退通路) | |
| 219 | +4. `es_query_builder.py` 两个 nested image 查询加 `inner_hits` | |
| 220 | +5. `sku_intent_selector.py` 扩 `SkuSelectionDecision` + `_pick_sku_by_image` + text-first/image-fallback 决策树 | |
| 221 | +6. `searcher.py` 触发条件扩为"款式意图 OR image inner_hits",`_resolve_rerank_source_filter` 无意图时也带 `skus` 字段 | |
| 222 | +7. `config.yaml` 加 `image_sku_selection.*` | |
| 223 | + | |
| 224 | +**Phase 3**(调试 + 回归) | |
| 225 | +8. debug payload 扩 text_pick/image_pick/final_source | |
| 226 | +9. 单测补四个关键场景 | |
| 227 | +10. 跑 `卡其色裙子` / `白色T恤` 对照 | |
| 228 | + | |
| 229 | +开始吗?两件事需要你拍板后我再动: | |
| 230 | + | |
| 231 | +1. **`enriched_taxonomy_attributes` 的语义级别**:你要的是"作为另一个 SKU-级 value 匹配源",还是我上面提议的"仅作 SPU 级确认信号,不直接决定 SKU"?我倾向后者——因为这个字段是 SPU 级的,不能单独指定某个 SKU。前者会带来"SKU A 其实不是卡其色、却被 SPU 级信号顶上去"的错案。 | |
| 232 | + | |
| 233 | +2. **image-pick 的分数阈值**:无意图时建议给个阈值(`min_score_no_intent`),有意图 fallback 时不加阈值。你接受这个不对称吗?接受的话我用 `0.55` 起步,上线后调。 | |
| 234 | + | |
| 235 | +你确认这两点,我就一把改完。 | |
| 236 | + | |
| 237 | +# 3-prompt | |
| 238 | +请综合考虑、全部完成,enriched_taxonomy_attributes的Color对应到值是和option1_name/option1_values/option2/option3一样对待的,就是当前使用的属性值匹配,多一个字段来源,即,即使对于颜色意图,同时把me对应的option value和enriched_taxonomy_attributes的Color对应到值一起判断,要考虑统一设计、精炼代码。image-pick设计和代码保持精炼,遵循统一的最优的设计,尽量不要fallback。 | |
| 239 | + | |
| 240 | +# 3-response | |
| 241 | +27/27 通过。 | |
| 242 | + | |
| 243 | +## 问题 2:是不是所有"选中"都享受 1.2x 提权? | |
| 244 | + | |
| 245 | +**是的,目前无差别地都享受,这确实是个设计缺口。** 机制如下: | |
| 246 | + | |
| 247 | +```490:490:search/rerank_client.py | |
| 248 | + return bool(str(hit.get("_style_rerank_suffix") or "").strip()) | |
| 249 | +``` | |
| 250 | + | |
| 251 | +```641:642:search/rerank_client.py | |
| 252 | + sku_selected = _has_selected_sku(hit) | |
| 253 | + style_boost = style_intent_selected_sku_boost if sku_selected else 1.0 | |
| 254 | +``` | |
| 255 | + | |
| 256 | +只要 `prepare_hits` 给 hit 写了 `_style_rerank_suffix`(只要 `selected_text` 非空),下游 `run_rerank` / `run_lightweight_rerank` 就把 `style_boost=1.2` 乘进 fused score。 | |
| 257 | +当前 `_style_rerank_suffix` 的写入条件是 `decision.rerank_suffix` 非空 —— 等价于 `final_source ∈ {"text", "image"}`。也就是说: | |
| 258 | + | |
| 259 | +| 选中来源 (`final_source`) | 是否吃 1.2x 款式提权(现状) | | |
| 260 | +|---|---| | |
| 261 | +| `text`(自有值命中意图词) | ✅ | | |
| 262 | +| `image`(仅图像相似度置顶) | ✅ ← 语义上不该等价 | | |
| 263 | +| `none` | ❌ | | |
| 264 | + | |
| 265 | +把"图像置顶"和"文本命中"按同一强度做 SPU 级提权,存在两个隐患: | |
| 266 | +1. **置信度不同**:文本命中是显式属性匹配(强信号),图像 pick 是基于视觉相近(弱信号、还可能是 SPU 主图而非 SKU 图);给同样 1.2x 会让"图像近似 → SPU 排名拔高"发生在没有真正属性命中的情况下。 | |
| 267 | +2. **无意图纯图像触发时更夸张**:query 无任何款式意图,但只要 image_knn 的 top url 能对上某个 SKU,整条 SPU 就吃到 1.2x,等于让向量相似度二次乘进融合。 | |
| 268 | + | |
| 269 | +建议的区分策略(**只改 rerank_client**,不改决策侧): | |
| 270 | + | |
| 271 | +- 文本命中(`final_source="text"`)→ 保留 1.2x(可配置) | |
| 272 | +- 图像命中(`final_source="image"`)→ 1.0x(不加提权,仅做 SKU 置顶 + 改封面图) | |
| 273 | + | |
| 274 | +最简实现:把"是否命中"从"后缀是否非空"改为"后缀非空 AND 是文本来源"。具体做法: | |
| 275 | + | |
| 276 | +1. `prepare_hits` 只在 `final_source == "text"` 时写 `_style_rerank_suffix`;`image` 时不写,但仍把决策放进 `decisions`(`apply_precomputed_decisions` 照常置顶 SKU + 换封面图)。 | |
| 277 | +2. 对应的 rerank doc 后缀也就不含图像源的颜色/尺码值(避免把一个"靠图选的 SKU"当作文本证据喂给 reranker)。 | |
| 278 | + | |
| 279 | +要我现在就把这条改了吗?或者你更希望 image 源也吃提权但用一个更小的系数(比如 1.05),这样保留视觉加分但不等同于文本命中。 | |
| 280 | + | |
| 281 | +# 4-prompt | |
| 282 | +两个问题,请修复: | |
| 283 | +1. option匹配,应该要能成功匹配才对: | |
| 284 | +{ | |
| 285 | +"sku_id": "441679", | |
| 286 | +"price": 13, | |
| 287 | +"compare_at_price": 0, | |
| 288 | +"sku_code": "", | |
| 289 | +"stock": 100, | |
| 290 | +"weight": 0, | |
| 291 | +"weight_unit": "kg", | |
| 292 | +"option1_value": "卡其色(无内衬)", | |
| 293 | + | |
| 294 | +2. 即使option没有匹配、而是依靠enriched_taxonomy_attributes 匹配的,那么rerank_suffix selected_text 也要翻译真实的匹配项,final_source也最好 enriched_taxonomy_attributes 和option有所区分 | |
| 295 | +Selected SKU | |
| 296 | +{ | |
| 297 | + "selected_sku_id": "441670", | |
| 298 | + "rerank_suffix": "白色(无内衬)", | |
| 299 | + "selected_text": "白色(无内衬)", | |
| 300 | + "final_source": "text", | |
| 301 | + | |
| 302 | +# 4-response | |
| 303 | +## 改动摘要 | |
| 304 | + | |
| 305 | +### 问题 1(option 匹配) | |
| 306 | +我上一条消息里修的 "SKU 自有值优先于 taxonomy" 就解决了这个问题。用你给的样本跑: | |
| 307 | + | |
| 308 | +``` | |
| 309 | +query=卡其色裙子 | |
| 310 | +skus=[441670:白色(无内衬), 441679:卡其色(无内衬)] | |
| 311 | +→ selected_sku_id: 441679 | |
| 312 | + selected_text: 卡其色(无内衬) | |
| 313 | + final_source: option | |
| 314 | +``` | |
| 315 | + | |
| 316 | +全角括号之所以能命中是两个机制叠加: | |
| 317 | +- 全角 `(` 不在 `[\u4e00-\u9fff]` 范围内,分词器按 `["卡其色", "无内衬"]` 切分; | |
| 318 | +- 纯中文词 `卡其色` 落入 "pure-Han 子串回退" 分支,即使分词切分不一致也能匹配。 | |
| 319 | + | |
| 320 | +加了一个贴着真实数据命名的回归 case `test_zh_color_intent_matches_noisy_option_value_with_fullwidth_parens` 锁死。 | |
| 321 | + | |
| 322 | +### 问题 2(区分 taxonomy / option,`selected_text` 反映真实匹配项) | |
| 323 | + | |
| 324 | +**决策结构扩展**: | |
| 325 | + | |
| 326 | +```python | |
| 327 | +SkuSelectionDecision: | |
| 328 | + final_source: "option" | "taxonomy" | "image" | "none" | |
| 329 | + matched_sources: Dict[intent_type, "option"|"taxonomy"] # 逐意图细分 | |
| 330 | + selected_text / rerank_suffix: 真正命中的原始文本(option 源为 sku.optionN_value;taxonomy 源为 enriched_taxonomy_attributes 对应 value) | |
| 331 | +``` | |
| 332 | + | |
| 333 | +**匹配逻辑**(`_find_text_matched_skus`):per-intent 走两条互斥路径,保留权威原则: | |
| 334 | +- SKU 在该维度有自有值 → 只用自有值判断 → 命中时 `source=option` | |
| 335 | +- SKU 该维度值为空(槽未解析 或 optionN 值空)→ 用 SPU taxonomy 判断 → 命中时 `source=taxonomy` | |
| 336 | + | |
| 337 | +**整体来源聚合**:任一意图靠 taxonomy 命中 → 整体 `final_source="taxonomy"`;全部由 option 命中 → `final_source="option"`。这保留了"最弱文本证据"的含义,下游(比如 rerank boost)可以据此做差异化。 | |
| 338 | + | |
| 339 | +**真实匹配项回填示例**: | |
| 340 | +- `option1_name=颜色, option1_value=卡其色(无内衬)`:`selected_text="卡其色(无内衬)"`,`final_source=option` | |
| 341 | +- `option1_name=Style, enriched_taxonomy_attributes=[{Color, {zh:"卡其色"}}]`:`selected_text="卡其色"`(来自 taxonomy 原文),`final_source=taxonomy` | |
| 342 | + | |
| 343 | +### 与 1.2x 款式提权的衔接(回答你上一条的后半问) | |
| 344 | + | |
| 345 | +当前 `rerank_client` 判断 "是否选中" 的依据仍是 `_style_rerank_suffix` 是否非空——也就是说 `option` / `taxonomy` / `image` 三种来源目前仍一视同仁都吃 1.2x。**现在有了 `final_source` 作为可区分的信号,你想怎么分级,我马上就能改**。常见选择: | |
| 346 | + | |
| 347 | +- `option`:1.2x(强信号) | |
| 348 | +- `taxonomy`:1.1x(SPU 级,弱一档) | |
| 349 | +- `image`:1.0x(不加提权,仅做 SKU 置顶 + 换封面) | |
| 350 | + | |
| 351 | +要不要我把这三档都拉到 `config.yaml` 里做成可配?或者先把 image 档位降到 1.0x、其它保持不变? | |
| 352 | + | |
| 353 | +# 5-prompt | |
| 354 | +q=藏青百褶半身裙 | |
| 355 | +第五条结果 | |
| 356 | + "spu_id": "68501", | |
| 357 | + | |
| 358 | +Selected SKU | |
| 359 | +{ | |
| 360 | + "selected_sku_id": null, | |
| 361 | + "rerank_suffix": "", | |
| 362 | + "selected_text": "", | |
| 363 | + "final_source": "none", | |
| 364 | + "matched_sources": {}, | |
| 365 | + "resolved_dimensions": { | |
| 366 | + "color": "option1_value" | |
| 367 | + }, | |
| 368 | + "image_pick": null | |
| 369 | +} | |
| 370 | +为什么image_pick也没生效 | |
| 371 | + | |
| 372 | +# 5-response | |
| 373 | + | |
| 374 | +## 为什么当时 `image_pick` 也是 `null` | |
| 375 | + | |
| 376 | +`image_pick` **只**来自 ES 返回的 `inner_hits`(`exact_image_knn_query_hits` 优先,其次 `image_knn_query_hits`),再用其中的 **URL** 去对齐 `skus[].image_src`。下面任一情况都会变成 `null`: | |
| 377 | + | |
| 378 | +1. **没有 `inner_hits`** | |
| 379 | + - 例如:该文档不在 **exact KNN rescore** 的 `window_size` 内(若 `exact_knn_rescore_window` 配得很小,排第 5 条也可能拿不到 exact 的 inner_hits)。你这边配置是 160,第 5 条一般仍在窗口内。 | |
| 380 | + - 或:召回阶段 **nested image KNN** 对该父文档没有产生 inner_hits(只靠文本等子句进结果时,有时 nested 子句侧没有可返回的条目)。 | |
| 381 | + | |
| 382 | +2. **有 inner_hits,但 URL 对不上任何 SKU**(最常见) | |
| 383 | + - `inner_hits` 里是 `https://cdn/a.jpg?w=800`,SKU 里是 `https://cdn/a.jpg` | |
| 384 | + - 或 CDN 域名不同、路径相同 | |
| 385 | + - 或 `_source.url` 与索引里 `image_embedding` 的 canonical 写法不一致,而 SKU 用的是另一种写法 | |
| 386 | + | |
| 387 | +3. **以前预取 `_source` 里没有 `image_embedding`** | |
| 388 | + - ES 在 inner_hit 里给了 `_nested.offset`,但 `_source.url` 不可靠时,**无法**用 `image_embedding[offset].url` 再解析一次。 | |
| 389 | + | |
| 390 | +--- | |
| 391 | + | |
| 392 | +## 本次代码上做了什么(已提交到当前分支) | |
| 393 | + | |
| 394 | +1. **URL 对齐放宽**:去掉 query、路径归一、`host+path` / 仅 path / 文件名 多级比对,减少「明明是一张图却匹配不上」。 | |
| 395 | +2. **`_nested.offset` + `image_embedding[offset].url` 回退**:inner_hit 里若带 offset,会同时尝试 `_source.url` 和父文档里 `image_embedding[offset].url`(与索引侧一致)。 | |
| 396 | +3. **`ImagePick.url`** 改为记录**真正用来对齐成功的那条候选 URL**(便于 debug)。 | |
| 397 | +4. **Searcher**:在存在 **图像查询向量**(`_has_image_signal`)时,预取 `_source` 里增加 **`image_embedding`**,否则 offset 回退拿不到数据。 | |
| 398 | + | |
| 399 | +这样在「藏青百褶半身裙」这类 **文本未命中 SKU、指望图像兜底** 的场景里,只要 ES 给了 inner_hits 且最终能和我们归一化后的 SKU 图对齐,`image_pick` 就会非空;若仍为空,请在 **debug** 里看该条 hit 是否还有 `inner_hits`,以及 `exact_knn_rescore_window`、是否关掉了 rescore 等。 | |
| 400 | + | |
| 401 | +--- | |
| 402 | + | |
| 403 | +## 关于「藏青」文本为什么也可能没命中 | |
| 404 | + | |
| 405 | +词表里 **navy** 已含「藏青、藏蓝、深蓝」。若 SKU 的 `option1_value` 写成 **和词表不一致** 的表述(例如只有英文色号、或「青色系」等),仍可能文本全挂,这时更依赖上面的 **图像对齐**;若图像也对不上,就会是 `final_source: none` + `image_pick: null`。 | |
| 406 | + | |
| 407 | + | |
| 408 | +# 6-prompt | |
| 409 | + | |
| 410 | +# 6-response | |
| 411 | + | |
| 412 | + | |
| 413 | +# TODO | |
| 414 | + | |
| 415 | + | |
| 416 | + "inner_hits": { | |
| 417 | + "name": "exact_image_knn_query_hits", | |
| 418 | + "size": 1, | |
| 419 | + "_source": [ | |
| 420 | + "url" | |
| 421 | + ] | |
| 422 | + } | |
| 423 | + | |
| 424 | +inner_hits获取exact_image_knn_query_hits的目的是:是因为哪个 | |
| 425 | + | |
| 426 | +参考: | |
| 427 | + feat(search): 款式意图下统一 SKU 选取(option/taxonomy/图像)与属性值匹配增强 | |
| 428 | + | |
| 429 | + ## 主要能力 | |
| 430 | + - 在 rerank 窗口内对 hits 做 SKU 预决策:款式意图(多源同义词)+ 图像 KNN inner_hits URL 对齐 SKU.image_src,统一一次决策 | |
| 431 | +、无级联 fallback。 | |
| 432 | + - 区分文本证据强度:final_source ∈ {option, taxonomy, image, none};matched_sources 按意图记录 option 或 taxonomy;selected_text / rerank_suffix 回填真实命中片段(SKU option 原文或 taxonomy value 原文)。 | |
| 433 | + - 权威规则:SKU 在已解析维度上有非空 option 值时仅以该值参与匹配;SPU 级 enriched_taxonomy_attributes 不覆盖与之一致的 SKU 级矛盾值(修复「taxonomy 把白色 SKU 当卡其色命中」)。 | |
| 434 | + - 图像:nested image KNN / exact rescore 增加 inner_hits(url),用于 SKU 置顶时的视觉 tie-break(仅在文本命中集内)或无意图时纯图像置顶。 | |
| 435 | + - 查询侧:DetectedStyleIntent 增加 all_terms(zh+en+attribute 并集),属性值匹配与意图词表一致。 | |
| 436 | + - API:SpuResult 透出 enriched_attributes / enriched_taxonomy_attributes(避免 Pydantic 丢弃 ES 字段)。 | |
| 437 | + | |
| 438 | + ## 属性值匹配(括号和分隔符) | |
| 439 | + - 在分词前对归一化后的 option/taxonomy 字符串执行 _with_segment_boundaries_for_matching:将全/半角括号、斜杠、顿号、中英文标点、中点、各类横线等替换为空格,再 simple_tokenize + 滑窗;无分隔的连续汉字仍走纯中文子串回退(如 卡其色棉)。 | |
| 440 | + - 参数化测试覆盖多种括号与常见电商分隔写法。 | |
| 441 | + | |
| 442 | + ## 编排与配置 | |
| 443 | + - searcher:_should_run_sku_selection = 款式意图激活 或 存在 image_query_vector;prefetch _source 含 skus、option 名、enriched_taxonomy_attributes。 | |
| 444 | + - es_query_builder:image knn / exact image rescore 的 nested 子句带 inner_hits。 | |
| 445 | + | |
| 446 | + ## 测试与仓库 | |
| 447 | + - tests/test_sku_intent_selector.py、tests/test_search_rerank_window.py 更新;移除已废弃的 embedding-fallback 集成断言。 | |
| 448 | + - .gitignore:忽略 artifacts/search_evaluation/datasets/(本地评估大数据集,避免误提交)。 | |
| 449 | + | |
| 450 | + | |
| 451 | + "rescore": { | |
| 452 | + "window_size": 160, | |
| 453 | + "query": { | |
| 454 | + "score_mode": "total", | |
| 455 | + "query_weight": 1, | |
| 456 | + "rescore_query_weight": 0, | |
| 457 | + "rescore_query": { | |
| 458 | + "bool": { | |
| 459 | + "should": [ | |
| 460 | + { | |
| 461 | + "script_score": { | |
| 462 | + "_name": "exact_text_knn_query", | |
| 463 | + "query": { | |
| 464 | + "exists": { | |
| 465 | + "field": "title_embedding" | |
| 466 | + } | |
| 467 | + }, | |
| 468 | + "script": { | |
| 469 | + "source": "((dotProduct(params.query_vector, 'title_embedding') + 1.0) / 2.0) * params.boost", | |
| 470 | + "params": { | |
| 471 | + "boost": 4 | |
| 472 | + } | |
| 473 | + } | |
| 474 | + } | |
| 475 | + }, | |
| 476 | + { | |
| 477 | + "nested": { | |
| 478 | + "path": "image_embedding", | |
| 479 | + "_name": "exact_image_knn_query", | |
| 480 | + "score_mode": "max", | |
| 481 | + "query": { | |
| 482 | + "script_score": { | |
| 483 | + "query": { | |
| 484 | + "exists": { | |
| 485 | + "field": "image_embedding.vector" | |
| 486 | + } | |
| 487 | + }, | |
| 488 | + "script": { | |
| 489 | + "source": "((dotProduct(params.query_vector, 'image_embedding.vector') + 1.0) / 2.0) * params.boost", | |
| 490 | + "params": { | |
| 491 | + "boost": 4 | |
| 492 | + } | |
| 493 | + } | |
| 494 | + } | |
| 495 | + }, | |
| 496 | + "inner_hits": { | |
| 497 | + "name": "exact_image_knn_query_hits", | |
| 498 | + "size": 1, | |
| 499 | + "_source": [ | |
| 500 | + "url" | |
| 501 | + ] | |
| 502 | + } | |
| 503 | + } | |
| 504 | + } | |
| 505 | + ], | |
| 506 | + "minimum_should_match": 1 | |
| 507 | + } | |
| 508 | + } | |
| 509 | + } | |
| 510 | + }, | ... | ... |