ANCHORS_AND_SEMANTIC_ATTRIBUTES.md 15.2 KB

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)。

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。
  • 当前支持的维度名(在 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_languagesSUPPORTED_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. 总结与扩展方向

  1. 功能定位
    • qanchors.{lang}:更好地贴近用户真实查询词,用于召回与 suggestion;
    • enriched_attributes:以结构化形式承载 LLM 抽取的语义维度,用于 filter / facet。
  2. 多语言对齐
    • 完全复用租户级 index_languages 配置;
    • 对每种语言单独生成锚文本与语义属性,不互相混用。
  3. 默认开启 / 自动降级
    • 索引流程始终可用;
    • 当 LLM/配置异常时,只是“缺少增强特征”,不影响基础搜索能力。
  4. 未来扩展
    • 可以在 dim_keys 中新增维度名(如 style, benefit 等),只要在 prompt 与解析逻辑中增加对应列即可;
    • 可以为 enriched_attributes 增加额外字段(如 confidencesource),用于更精细的控制(当前 mapping 为简单版)。

如需在查询层面增加基于 enriched_attributes 的统一 DSL(类似 specifications 的过滤/分面规则),推荐在 docs/搜索API对接指南-01-搜索接口.mddocs/搜索API对接指南-08-数据模型与字段速查.md 中新增一节,并在 search/es_query_builder.py 里封装构造逻辑,避免前端直接拼 nested 查询。