qanchors 与 enriched_attributes 设计与索引逻辑说明
本文档详细说明:
- 锚文本字段
qanchors.{lang}的作用与来源 - 语义属性字段
enriched_attributes的结构、用途与写入流程 - 多语言支持策略(zh / en / de / ru / fr)
- 索引阶段与 LLM 调用的集成方式
本设计已默认开启,无需额外开关;在上游 LLM 不可用时会自动降级为“无锚点/语义属性”,不影响主索引流程。
1. 字段设计概览
1.1 qanchors.{lang}:面向查询的锚文本
- Mapping 位置:
mappings/search_products.json中的qanchors对象。 - 结构(与
title.{lang}一致):
"qanchors": {
"type": "object",
"properties": {
"zh": { "type": "text", "analyzer": "index_ik", "search_analyzer": "query_ik" },
"en": { "type": "text", "analyzer": "english" },
"de": { "type": "text", "analyzer": "german" },
"ru": { "type": "text", "analyzer": "russian" },
"fr": { "type": "text", "analyzer": "french" },
...
}
}
语义:
用于承载“更接近用户自然搜索行为”的词/短语(query-style anchors),包括:- 品类 + 细分类别表达;
- 使用场景(通勤、约会、度假、office outfit 等);
- 适用人群(年轻女性、plus size、teen boys 等);
- 材质 / 关键属性 / 功能特点等。
使用场景:
- 主搜索:作为额外的全文字段参与 BM25 召回与打分(可在
search/query_config.py中给一定权重); - Suggestion:
suggestion/builder.py会从qanchors.{lang}中拆分词条作为候选(source="qanchor",权重大于title)。
- 主搜索:作为额外的全文字段参与 BM25 召回与打分(可在
1.2 enriched_attributes:面向过滤/分面的通用语义属性
- Mapping 位置:
mappings/search_products.json,追加的 nested 字段。 - 结构:
"enriched_attributes": {
"type": "nested",
"properties": {
"lang": { "type": "keyword" }, // 语言:zh / en / de / ru / fr
"name": { "type": "keyword" }, // 维度名:usage_scene / target_audience / material / ...
"value": { "type": "keyword" } // 维度值:通勤 / office / Baumwolle ...
}
}
语义:
- 将 LLM 输出的各维度信息统一规约到
name/value/lang三元组; - 维度名稳定、值内容可变,便于后续扩展新的语义维度而不需要修改 mapping。
- 将 LLM 输出的各维度信息统一规约到
当前支持的维度名(在
document_transformer.py中固定列表):tags:细分标签/风格标签;target_audience:适用人群;usage_scene:使用场景;season:适用季节;key_attributes:关键属性;material:材质说明;features:功能特点。
使用场景:
- 按语义维度过滤:
- 例:只要“适用人群=年轻女性”的商品;
- 例:
usage_scene包含 “office” 或 “通勤”。 - 按语义维度分面 / 展示筛选项:
- 例:展示当前结果中所有
usage_scene的分布,供前端勾选; - 例:展示所有
material值 + 命中文档数。
- 按语义维度过滤:
2. LLM 分析服务:indexer/product_annotator.py
2.1 入口函数:analyze_products
- 文件:
indexer/product_annotator.py - 函数签名:
def analyze_products(
products: List[Dict[str, str]],
target_lang: str = "zh",
batch_size: Optional[int] = None,
) -> List[Dict[str, Any]]:
"""
库调用入口:根据输入+语言,返回锚文本及各维度信息。
Args:
products: [{"id": "...", "title": "..."}]
target_lang: 输出语言,需在 SUPPORTED_LANGS 内
batch_size: 批大小,默认使用全局 BATCH_SIZE
"""
...
- 支持的输出语言(在同文件中定义):
LANG_LABELS: Dict[str, str] = {
"zh": "中文",
"en": "英文",
"de": "德文",
"ru": "俄文",
"fr": "法文",
}
SUPPORTED_LANGS = set(LANG_LABELS.keys())
- 返回结构(每个商品一条记录):
{
"id": "<SPU_ID>",
"lang": "<zh|en|de|ru|fr>",
"title_input": "<原始输入标题>",
"title": "<目标语言的标题>",
"category_path": "<LLM 生成的品类路径>",
"tags": "<逗号分隔的细分标签>",
"target_audience": "<逗号分隔的适用人群>",
"usage_scene": "<逗号分隔的使用场景>",
"season": "<逗号分隔的适用季节>",
"key_attributes": "<逗号分隔的关键属性>",
"material": "<逗号分隔的材质说明>",
"features": "<逗号分隔的功能特点>",
"anchor_text": "<逗号分隔的锚文本短语>",
# 若发生错误,还会附带:
# "error": "<异常信息>"
}
注意:表格中的多值字段(标签/场景/人群/材质等)约定为使用逗号分隔,后续索引端会统一按正则
[,;|/\\n\\t]+再拆分为短语。
2.2 Prompt 设计与语言控制
- Prompt 中会明确要求“所有输出内容使用目标语言”,并给出中英文示例:
def create_prompt(products: List[Dict[str, str]], target_lang: str = "zh") -> str:
"""创建LLM提示词(根据目标语言输出)"""
lang_label = LANG_LABELS.get(target_lang, "对应语言")
prompt = f"""请对输入的每条商品标题,分析并提取以下信息,所有输出内容请使用{lang_label}:
1. 商品标题:将输入商品名称翻译为{lang_label}
2. 品类路径:从大类到细分品类,用">"分隔(例如:服装>女装>裤子>工装裤)
3. 细分标签:商品的风格、特点、功能等(例如:碎花,收腰,法式)
4. 适用人群:性别/年龄段等(例如:年轻女性)
5. 使用场景
6. 适用季节
7. 关键属性
8. 材质说明
9. 功能特点
10. 商品卖点:分析和提取一句话核心卖点,用于推荐理由
11. 锚文本:生成一组能够代表该商品、并可能被用户用于搜索的词语或短语。这些词语应覆盖用户需求的各个维度,如品类、细分标签、功能特性、需求场景等等。
"""
- 返回格式固定为 Markdown 表格,首行头为:
| 序号 | 商品标题 | 品类路径 | 细分标签 | 适用人群 | 使用场景 | 适用季节 | 关键属性 | 材质说明 | 功能特点 | 商品卖点 | 锚文本 |
|----|----|----|----|----|----|----|----|----|----|----|----|
parse_markdown_table 会按表格列顺序解析成字段。
3. 索引阶段集成:SPUDocumentTransformer._fill_llm_attributes
3.1 调用时机
在 SPUDocumentTransformer.transform_spu_to_doc(...) 的末尾,在所有基础字段(多语言文本、类目、SKU/规格、价格、库存等)填充完成后,会调用:
# 文本字段处理(翻译等)
self._fill_text_fields(doc, spu_row, primary_lang)
# 标题向量化
if self.enable_title_embedding and self.encoder:
self._fill_title_embedding(doc)
...
# 时间字段
...
# 基于 LLM 的锚文本与语义属性(默认开启,失败时仅记录日志)
self._fill_llm_attributes(doc, spu_row)
也就是说,每个 SPU 文档默认会尝试补充 qanchors 与 enriched_attributes。
3.2 语言选择策略
在 _fill_llm_attributes 内部:
try:
index_langs = self.tenant_config.get("index_languages") or ["en", "zh"]
except Exception:
index_langs = ["en", "zh"]
# 只在支持的语言集合内调用
llm_langs = [lang for lang in index_langs if lang in SUPPORTED_LANGS]
if not llm_langs:
return
tenant_config.index_languages决定该租户希望在索引中支持哪些语言;- 实际调用 LLM 的语言集合 =
index_languages ∩ SUPPORTED_LANGS; - 当前 SUPPORTED_LANGS:
{"zh", "en", "de", "ru", "fr"}。
这保证了:
- 如果租户只索引
zh,就只跑中文; - 如果租户同时索引
en+de,就为这两种语言各跑一次 LLM; - 如果
index_languages里包含暂不支持的语言(例如es),会被自动忽略。
3.3 调用 LLM 并写入字段
核心逻辑(简化描述):
spu_id = str(spu_row.get("id") or "").strip()
title = str(spu_row.get("title") or "").strip()
if not spu_id or not title:
return
semantic_list = doc.get("enriched_attributes") or []
qanchors_obj = doc.get("qanchors") or {}
dim_keys = [
"tags",
"target_audience",
"usage_scene",
"season",
"key_attributes",
"material",
"features",
]
for lang in llm_langs:
try:
rows = analyze_products(
products=[{"id": spu_id, "title": title}],
target_lang=lang,
batch_size=1,
)
except Exception as e:
logger.warning("LLM attribute fill failed for SPU %s, lang=%s: %s", spu_id, lang, e)
continue
if not rows:
continue
row = rows[0] or {}
# qanchors.{lang}
anchor_text = str(row.get("anchor_text") or "").strip()
if anchor_text:
qanchors_obj[lang] = anchor_text
# 语义属性
for name in dim_keys:
raw = row.get(name)
if not raw:
continue
parts = re.split(r"[,;|/\n\t]+", str(raw))
for part in parts:
value = part.strip()
if not value:
continue
semantic_list.append(
{
"lang": lang,
"name": name,
"value": value,
}
)
if qanchors_obj:
doc["qanchors"] = qanchors_obj
if semantic_list:
doc["enriched_attributes"] = semantic_list
要点:
- 每种语言单独调用一次
analyze_products,传入同一 SPU 的原始标题; - 将返回的
anchor_text直接写入qanchors.{lang},其内部仍是逗号分隔短语,后续 suggestion builder 会再拆分; - 对各维度字段(tags/usage_scene/...)用统一正则进行“松散拆词”,过滤空串后,以
(lang,name,value)三元组追加到 nested 数组; - 如果某个维度在该语言下为空,则跳过,不写入任何条目。
3.4 容错 & 降级策略
- 如果:
- 没有
title; - 或者
tenant_config.index_languages与SUPPORTED_LANGS没有交集; - 或
DASHSCOPE_API_KEY未配置 / LLM 请求报错;
- 没有
- 则
_fill_llm_attributes会在日志中输出warning,不会抛异常,索引流程继续,只是该 SPU 在这一轮不会得到qanchors/enriched_attributes。
这保证了整个索引服务在 LLM 不可用时表现为一个普通的“传统索引”,而不会中断。
4. 查询与 Suggestion 中的使用建议
4.1 主搜索(Search API)
在 search/query_config.py 或构建 ES 查询时,可以:
- 将
qanchors.{lang}作为额外的should字段参与匹配,并给一个略高的权重,例如:
{
"multi_match": {
"query": "<user_query>",
"fields": [
"title.zh^3.0",
"brief.zh^1.5",
"description.zh^1.0",
"vendor.zh^1.5",
"category_path.zh^1.5",
"category_name_text.zh^1.5",
"tags^1.0",
"qanchors.zh^2.0" // 建议新增
]
}
}
- 当用户做维度过滤时(例如“只看通勤场景 + 夏季 + 棉质”),可以在 filter 中增加 nested 查询:
{
"nested": {
"path": "enriched_attributes",
"query": {
"bool": {
"must": [
{ "term": { "enriched_attributes.lang": "zh" } },
{ "term": { "enriched_attributes.name": "usage_scene" } },
{ "term": { "enriched_attributes.value": "通勤" } }
]
}
}
}
}
多个维度可以通过多个 nested 子句组合(AND/OR 逻辑与 specifications 的设计类似)。
4.2 Suggestion(联想词)
现有 suggestion/builder.py 已经支持从 qanchors.{lang} 中提取候选:
# Step 1: product title/qanchors
hits = self._scan_products(tenant_id, batch_size=batch_size)
...
title_obj = src.get("title") or {}
qanchor_obj = src.get("qanchors") or {}
...
for lang in index_languages:
...
q_raw = None
if isinstance(qanchor_obj, dict):
q_raw = qanchor_obj.get(lang)
for q_text in self._split_qanchors(q_raw):
text_norm = self._normalize_text(q_text)
if self._looks_noise(text_norm):
continue
key = (lang, text_norm)
c = key_to_candidate.get(key)
if c is None:
c = SuggestionCandidate(text=q_text, text_norm=text_norm, lang=lang)
key_to_candidate[key] = c
c.add_product("qanchor", spu_id=spu_id, score=product_score + 0.6)
_split_qanchors使用与索引端一致的分隔符集合,确保:- 无论 LLM 用逗号、分号还是换行分隔,只要符合约定,都能被拆成单独候选词;
add_product("qanchor", ...)会:- 将来源标记为
qanchor; - 在排序打分时,
qanchor命中会比纯title更有权重。
- 将来源标记为
5. 总结与扩展方向
- 功能定位:
qanchors.{lang}:更好地贴近用户真实查询词,用于召回与 suggestion;enriched_attributes:以结构化形式承载 LLM 抽取的语义维度,用于 filter / facet。
- 多语言对齐:
- 完全复用租户级
index_languages配置; - 对每种语言单独生成锚文本与语义属性,不互相混用。
- 完全复用租户级
- 默认开启 / 自动降级:
- 索引流程始终可用;
- 当 LLM/配置异常时,只是“缺少增强特征”,不影响基础搜索能力。
- 未来扩展:
- 可以在
dim_keys中新增维度名(如style,benefit等),只要在 prompt 与解析逻辑中增加对应列即可; - 可以为
enriched_attributes增加额外字段(如confidence、source),用于更精细的控制(当前 mapping 为简单版)。
- 可以在
如需在查询层面增加基于 enriched_attributes 的统一 DSL(类似 specifications 的过滤/分面规则),推荐在 docs/搜索API对接指南-01-搜索接口.md 或 docs/搜索API对接指南-08-数据模型与字段速查.md 中新增一节,并在 search/es_query_builder.py 里封装构造逻辑,避免前端直接拼 nested 查询。