商品分类字段对齐实录:Product.category 映射 Google 商品分类体系的三条规则

商品分类字段对齐实录:Product.category 映射 Google 商品分类体系的三条规则

适用读者:在零售电商站负责商品结构化数据的前端、后端与内容工程同学;已经在商品页埋了 Schema.org Product 标记,但商品在 AI 搜索回答里几乎不被提及、想从类目信号下手做改造的团队。

8 月中旬接了一个母婴零售站的生成式引擎优化(Generative Engine Optimization, GEO)诊断,翻遍全站 1,200 个商品页的 JSON-LD,category 字段清一色写着「商品」两个字。AI 搜索引擎拿到的类目信号等于零,商品自然进不了候选集。这篇把当时的排查过程和改造方案拆成三条规则讲清楚:映射表怎么建、三个字段各管什么、AI 引擎拿类目词去干什么。

现场还原:1,200 个商品页,category 全是「商品」

用脚本顺着 sitemap 扫了一遍商品页的 application/ld+json,结果比预想的还整齐:

商品分类树与类目对齐

  • 1,183 个商品页的 Product.category 值为「商品」,占比 97.4%;
  • 剩下 17 个页面写了「母婴用品」「家居日用」这类一级大类,同样细不到叶子层;
  • 站内真实的类目树其实有四层:大类 > 品类 > 子类 > 款式,共 214 个末级类目,但这套树只活在导航栏和筛选器里,从未进入结构化数据。

先做了一轮基线测试:挑 60 条品类问句喂给主流 AI 搜索(比如「推荐一款适合新生儿的分腿睡袋」「有无纯棉 A 类标准的婴儿床品」),记录回答里是否出现本站域名、是否出现与本站商品对应的细分类目词。结果 60 条里只有 4 条回答提到本站,出现「婴儿睡袋」这类类目锚点词并同时带出本站的,只有 1 条。

类目栏全站一个泛分类,等于告诉 AI 引擎:这个站的商品没有品类属性,没法归档,也就没法在回答里被按品类召回。

AI 引擎读类目的机制:类目是召回的分诊台

这里把机制说透。AI 搜索引擎抓到商品页后,并不会像传统爬虫那样只存正文,而是把结构化数据解析成一堆实体槽位:品牌是什么、价格区间、属于哪个品类、有什么属性。其中类目槽位承担「分诊」职能——回答引擎生成内容时,先根据问句语义锁定一个候选品类集(比如「婴儿睡袋」),再从已索引的商品实体里筛出类目匹配的那些进入候选池。

泛分类「商品」

Taxonomy 叶子类目

商品页 JSON-LD

抓取并解析结构化数据

category 能否归档

无法进入候选品类集

锁定候选品类集

回答文本落下类目锚点词

商品实体被跳过或降权

如果 category 是「商品」,这个实体在分诊环节就挂了:候选池建立阶段根本轮不到它,后面的价格对比、属性补充、链接引用全部无从谈起。这就是为什么那家站 60 条问句只命中 4 条——不是内容不行,是入口就被关了。

类目词在最终回答里的「落点」也有讲究。回答引擎倾向把品类词作为叙述锚点,比如「挑选婴儿睡袋时可以关注……」,锚点词一旦出现,引擎会顺手挂上同品类的商品链接。类目词命中回答文本,是被推荐的前置条件。

映射表怎么建:站内类目树对齐 Google 商品分类体系

Google 维护了一套公开的商品分类体系(Google Product Taxonomy),每条分类是一条全路径,形如「服装与配饰 > 婴幼儿服装 > 婴儿睡袋」,官方提供包括简体中文在内的多语言版本,并有明确的版本号和更新节奏。做 GEO 改造时,映射表是地基,我们的表结构长这样:

字段说明示例
站内类目 ID站内类目树的主键cat_0182
站内全路径大类到末级的完整路径母婴用品 > 婴儿寝居 > 睡袋
Taxonomy 全路径对齐后的官方分类全路径服装与配饰 > 婴幼儿服装 > 婴儿睡袋
匹配方式精确匹配 / 关键词匹配 / 人工复核末级精确匹配
在售 SKU 数用于排定人工复核优先级342
复核状态待复核 / 已通过 / 有争议已通过

建表流程没有秘密,就是把「按末级类目名查官方分类」这件事自动化,剩下的疑难杂症走人工队列。当时的脚本逻辑如下:

# -*- coding: utf-8 -*-
"""
build_category_map.py
站内类目 CSV 与 Google 商品分类体系文本文件自动比对,生成映射表。

依赖:Python 3.10+,无第三方库
环境:内网跳板机运行,taxonomy 文件用官方发布的简体中文版文本文件
用法:python build_category_map.py shop_categories.csv taxonomy_zh-CN.txt

输入:
  1. shop_categories.csv —— 站内类目导出,字段:
     id, full_path, name, sku_count
     示例:cat_0182,母婴用品 > 婴儿寝居 > 睡袋,睡袋,342
  2. taxonomy_zh-CN.txt —— Google 官方分类简体中文版,每行一条全路径:
     服装与配饰 > 婴幼儿服装 > 婴儿睡袋

输出(stdout,每行一条):
  id, full_path, taxonomy, method
  method 取值:末级精确匹配 / 关键词匹配 / 人工复核
  人工复核的行 taxonomy 为空,需人工补全后回填映射表。
"""
import csv
import sys
import unicodedata


def load_taxonomy(path):
    """
    读官方分类文件,建立「末级分类名 -> 全路径列表」倒排索引。

    输入:taxonomy 文本文件路径
    输出:dict,key 为 NFKC 归一化后的末级分类名,value 为该末级名对应的
          所有全路径列表(同名分类可能有多条,如不同父级下的「套装」)。
    边界情况:
      - 空行直接跳过;
      - 全路径用「 > 」分隔,取最后一段作为末级分类名;
      - 对末级名做 NFKC 归一化,吸收全角括号、不可见空格等导出噪音。
    """
    leaf_index = {}
    with open(path, encoding="utf-8") as fh:
        for line in fh:
            full = line.strip()
            if not full:
                continue
            # 全路径形如:服装与配饰 > 婴幼儿服装 > 婴儿睡袋
            leaf = full.split(" > ")[-1]
            key = unicodedata.normalize("NFKC", leaf)
            leaf_index.setdefault(key, []).append(full)
    return leaf_index


def match(row, leaf_index):
    """
    单条站内类目与官方分类倒排索引比对,返回 (官方全路径, 匹配方式)。

    输入:row 为站内类目导出的 dict(含 name 字段),leaf_index 为倒排索引
    输出:官方分类全路径 + 匹配方式;全路径为空则必须人工复核。

    匹配策略(按优先级):
      1. 末级类目名精确命中且唯一 -> 自动通过,标记「末级精确匹配」;
      2. 精确命中多条(同名分类) -> 无法自动消歧,标记「人工复核」;
      3. 精确未命中,退化为包含匹配(双向包含) -> 标记「关键词匹配」,
         结果仍需人工复核确认;
      4. 兜底也未命中(站内自造类目词) -> 标记「人工复核」。

    边界情况:
      - 同名分类多条:必须人工按上级路径消歧,不能自动取第一条;
      - 包含匹配只做兜底,命中结果同样要过人工复核再落表;
      - 站内类目名与官方完全无关(如「宝宝辅食盒」跨界品)时,
        返回空路径,由人工按最接近父级落位并标注争议。
    """
    leaf = unicodedata.normalize("NFKC", row["名称"])
    hits = leaf_index.get(leaf, [])

    if len(hits) == 1:
        # 恰好一条命中才算自动通过,两条以上同名分类必须消歧
        return hits[0], "末级精确匹配"

    if len(hits) > 1:
        # 命中多条说明有同名分类,交给人工按上级路径消歧
        return "", "人工复核"

    # 精确未命中,退化成包含匹配,命中同样进人工队列
    # 包含匹配只做兜底,结果同样要过人工复核再落表
    for leaf_name, paths in leaf_index.items():
        if leaf_name in leaf or leaf in leaf_name:
            return paths[0], "关键词匹配"

    # 兜底也没命中:站内自造类目词,落人工复核
    return "", "人工复核"


def main():
    """主流程:加载索引 -> 按 SKU 数倒序处理 -> 输出映射结果。"""
    if len(sys.argv) != 3:
        print("用法:python build_category_map.py shop_categories.csv taxonomy_zh-CN.txt")
        sys.exit(1)

    leaf_index = load_taxonomy(sys.argv[2])

    with open(sys.argv[1], encoding="utf-8") as fh:
        # 站内类目按在售 SKU 数倒序排,优先处理流量大的类目
        rows = sorted(csv.DictReader(fh), key=lambda r: -int(r["sku_count"]))
        for row in rows:
            taxonomy, method = match(row, leaf_index)
            print(f"{row['id']},{row['full_path']},{taxonomy},{method}")


if __name__ == "__main__":
    main()

跑完这版脚本,214 个末级类目里 137 个自动通过,61 个走关键词匹配进人工队列,16 个确实在官方体系里找不到对应(比如「宝宝辅食盒」这类跨界品),按最接近的父级分类落位并在映射表里标注了争议。映射表是资产不是一次性脚本,官方分类每年更新版本,映射表要跟着比对 diff。

category、AdditionalProperty 与 BreadcrumbList:三个字段各管一段

规则二解决字段分工。很多团队的困惑是:类目语境到底写在哪、要不要重复写。我们定下的口径是三条线,各管一段:

  1. Product.category 是单值字符串,只写映射后 Taxonomy 全路径的最细一级,整条全路径用「 > 」连接。不要塞数组、不要堆同义词、不要把品牌词混进去。
  2. AdditionalProperty 承担属性语境,类目之外的尺码、材质、适用月龄、执行标准都放这里,用 PropertyValue 的 name/value 成对表达,帮 AI 引擎补全商品属性槽位。
  3. BreadcrumbList 表达站内导航语境,面包屑链上的类目词必须与 category 的分类语境一致——category 写了「婴幼儿服装 > 婴儿睡袋」,面包屑就不能是「首页 > 母婴专区 > 精选推荐」这种运营位链路。

语境一致性是最容易被忽略的一条。AI 引擎会把面包屑和 category 做交叉校验,两边对不上,实体可信度就打折。三个字段如何被引擎组合使用,见下面的时序:

BreadcrumbListAdditionalPropertyProduct.categoryAI 搜索引擎BreadcrumbListAdditionalPropertyProduct.categoryAI 搜索引擎读取单值类目(Taxonomy 全路径)归入候选品类集读取属性语境(月龄/材质/标准)补全商品属性槽位校验导航链与类目语境一致站内层级可信度确认回答文本落下类目锚点词并引用商品

落到商品页 JSON-LD 里就是下面这个样子(注释仅作讲解用,实际部署时去掉注释行):

{
  "@context": "https://schema.org",
  "@type": "Product",
  // name 与品牌语境保持常规写法,本例重点看 category 与属性块
  "name": "纯棉分腿婴儿睡袋 春秋款",
  // category 只写一个值:官方分类体系里最细一级的全路径
  // 全路径用「 > 」连接,层级从粗到细,不塞数组不堆同义词
  "category": "服装与配饰 > 婴幼儿服装 > 婴儿睡袋",
  "brand": { "@type": "Brand", "name": "Example Baby" },
  "offers": {
    "@type": "Offer",
    "price": "129.00",
    "priceCurrency": "CNY",
    // 价格与库存语境放 offers,与类目字段互不干扰
    "availability": "https://schema.org/InStock"
  },
  // 类目之外的属性语境全部收进 additionalProperty
  // 每条属性 name/value 成对出现,帮引擎补全商品属性槽位
  "additionalProperty": [
    { "@type": "PropertyValue", "name": "适用月龄", "value": "0-18个月" },
    { "@type": "PropertyValue", "name": "面料", "value": "100%棉" },
    // 执行标准这类权威属性对可信度加成明显,建议单独成条
    { "@type": "PropertyValue", "name": "执行标准", "value": "GB/T 23158" }
  ],
  // 面包屑的末级词必须与 category 的类目语境对齐
  // 不能写「首页 > 活动专区 > 爆款推荐」这类运营位链路
  "breadcrumb": {
    "@type": "BreadcrumbList",
    "itemListElement": [
      // position 从 1 开始,逐级对应类目树的上层与末级
      { "@type": "ListItem", "position": 1, "name": "婴幼儿服装" },
      { "@type": "ListItem", "position": 2, "name": "婴儿睡袋" }
    ]
  }
}

模板批量下发到商品详情页渲染层,1,200 个页面两天内全部替换完成,category 字段覆盖率从 2.6% 拉到 100%。

改造前后:AI 回答里的类目词命中对照

结构化数据改完不等于立刻见效,抓取和索引有滞后。8 月 20 日全量下发,9 月 5 日用同一组 60 条品类问句复测,对照数据如下:

指标改造前(8 月 12 日)改造后(9 月 5 日)
回答提及本站的问句数4 / 6017 / 60
回答中出现细分类目词7 / 6041 / 60
类目词与本站 category 一致的回答1 / 6023 / 60
回答同时带商品链接与类目锚点词1 / 6012 / 60

几个具体的对比例子更直观:

抽样问句改造前回答改造后回答
推荐适合新生儿的分腿睡袋泛泛列选购要点,无站点引用出现「婴儿睡袋」锚点词,引用本站商品页
纯棉 A 类婴儿床品有哪些只提跨境平台出现「婴儿床品」类目词并带出本站列表页
婴儿温奶器怎么选无本站内容类目锚点词命中,引用本站测评长文

复测结论和当时预判一致:类目信号修好之后,AI 搜索回答里最先回来的是品类锚点词,其次才是商品引用。 类目词命中率翻到六成多,站点被引用的量翻了三倍,中间的差距是属性语境(AdditionalProperty)和站内内容厚度补的,那是另一个话题。

三个容易踩的坑

  • category 填了但只填到大类。 「母婴用品」这种一级大类对分诊环节几乎没有增益,务必写到映射表里 Taxonomy 的叶子层。宁可部分类目暂时空缺走人工复核,也别全站先上一个粗粒度值凑数。
  • 面包屑用运营位链路。 首页 > 活动专区 > 爆款推荐这种链路和 category 完全对不上,引擎交叉校验直接扣可信度。面包屑必须走类目树的实时数据源。
  • 官方分类更新后映射表不动。 Google 商品分类体系有版本号,每年都会增删调整。我们把这步挂进了季度任务:下载新版文件,diff 出变化项,回放映射表,改动超过 20 条就重新过一遍人工队列。

排查清单

  • 抽查 10 个商品页 JSON-LD,确认 category 值是否已到叶子层。 用脚本抓取页面 application/ld+json,检查 category 是否为映射表里 Taxonomy 的最细一级;只要有一个页面仍是大类或「商品」,即判定未达标,触发整改。
  • 对比面包屑末级词与 category 末级词是否一致。 取 BreadcrumbList 最后一级 name 与 category 全路径的末级词比对;不一致则触发告警,说明导航链路与类目语境脱节。
  • 检查官方分类版本号与映射表记录版本是否匹配。 比对 Google 商品分类体系当前版本号与映射表里记录的版本;不一致则触发告警,需下载新版文件并回放映射表。
  • 确认 category 字段是单值字符串而非数组。 校验 JSON-LD 中 category 的类型与长度,必须是单个字符串且不含数组、同义词堆叠;出现数组或多值即判定违规,触发修正。
  • 用 AI 搜索实测 3 条品类问句,看回答是否出现类目锚点词。 挑 3 条与站内商品对应的品类问句喂给主流 AI 搜索;回答里未出现对应类目锚点词或未引用本站,则判定信号未生效,需复查抓取与索引。

批量校验脚本:上线后如何自动盯住 category 质量

改造上线不等于一劳永逸。商品上新、模板改版、运营误操作都可能让 category 悄悄退回泛分类。与其靠人工抽查,不如写一个批量校验脚本,把「单值、叶子层、面包屑一致」这三条硬指标变成可自动执行的检查,挂进 CI 或定时任务,违规页面当天就能暴露。

脚本逻辑如下:

# -*- coding: utf-8 -*-
"""
validate_category.py
输入商品页 URL 列表,自动抓取 JSON-LD,校验 category 质量。

依赖:Python 3.10+,requests(需 pip install requests)
环境:CI 或定时任务运行,需能访问商品页公网地址
用法:python validate_category.py urls.txt

输入:
  urls.txt —— 每行一个商品页 URL

输出(stdout,每行一条违规记录):
  url, 违规类型
  违规类型取值:category 非单值 / category 未到叶子层 / 面包屑末级不一致

退出码:
  0 —— 全部通过
  1 —— 存在违规(供 CI 判定失败)
"""
import json
import re
import sys
import unicodedata

import requests


# 官方分类叶子层末级词白名单,来自映射表 Taxonomy 全路径的末级
# 实际使用时应从映射表导出,避免硬编码
LEAF_WHITELIST = {"婴儿睡袋", "婴儿床品", "温奶器"}


def fetch_json_ld(url):
    """
    抓取商品页并解析 application/ld+json 块。

    输入:商品页 URL
    输出:解析后的 JSON 对象;页面无 JSON-LD 或解析失败返回 None。
    边界情况:
      - 页面可能内联多个 JSON-LD 块,取第一个 @type 为 Product 的;
      - 网络异常或非 200 状态码直接返回 None,由调用方标记为抓取失败。
    """
    resp = requests.get(url, timeout=10, headers={"User-Agent": "Mozilla/5.0"})
    resp.raise_for_status()
    # 匹配 <script type="application/ld+json">...</script>
    for match in re.finditer(
        r'<script[^>]*type=["\']application/ld\+json["\'][^>]*>(.*?)</script>',
        resp.text,
        re.DOTALL,
    ):
        try:
            data = json.loads(match.group(1))
        except json.JSONDecodeError:
            continue
        # 兼容单对象与数组两种写法
        candidates = data if isinstance(data, list) else [data]
        for item in candidates:
            if item.get("@type") == "Product":
                return item
    return None


def check_category(data):
    """
    校验 category 是否为单值字符串且已到叶子层。

    输入:Product 的 JSON-LD 对象
    输出:违规类型列表;空列表表示通过。
    边界情况:
      - category 缺失、为数组、为对象都算「非单值」;
      - 字符串但末级词不在白名单,算「未到叶子层」;
      - 白名单未覆盖的类目会误报,需随映射表同步维护。
    """
    violations = []
    category = data.get("category")

    if not isinstance(category, str):
        violations.append("category 非单值")
        return violations

    # 取全路径末级词,如「服装与配饰 > 婴幼儿服装 > 婴儿睡袋」->「婴儿睡袋」
    leaf = unicodedata.normalize("NFKC", category).split(" > ")[-1]
    if leaf not in LEAF_WHITELIST:
        violations.append("category 未到叶子层")
    return violations


def check_breadcrumb(data):
    """
    校验面包屑末级词与 category 末级词是否一致。

    输入:Product 的 JSON-LD 对象
    输出:违规类型列表;空列表表示通过。
    边界情况:
      - 无 breadcrumb 或 itemListElement 为空,直接判违规;
      - 取 position 最大的 ListItem 的 name 作为末级词;
      - 与 category 末级词做 NFKC 归一化后比对。
    """
    violations = []
    category = data.get("category")
    if not isinstance(category, str):
        return violations  # category 本身已违规,不再重复报

    breadcrumb = data.get("breadcrumb", {})
    items = breadcrumb.get("itemListElement", [])
    if not items:
        violations.append("面包屑末级不一致")
        return violations

    # 取 position 最大的作为末级
    last_item = max(items, key=lambda i: i.get("position", 0))
    crumb_leaf = unicodedata.normalize("NFKC", last_item.get("name", ""))
    category_leaf = unicodedata.normalize("NFKC", category.split(" > ")[-1])
    if crumb_leaf != category_leaf:
        violations.append("面包屑末级不一致")
    return violations


def main():
    """主流程:逐 URL 抓取校验,输出违规清单,按违规数决定退出码。"""
    if len(sys.argv) != 2:
        print("用法:python validate_category.py urls.txt")
        sys.exit(2)

    with open(sys.argv[1], encoding="utf-8") as fh:
        urls = [line.strip() for line in fh if line.strip()]

    has_violation = False
    for url in urls:
        try:
            data = fetch_json_ld(url)
        except requests.RequestException:
            print(f"{url},抓取失败")
            has_violation = True
            continue

        if data is None:
            print(f"{url},无 Product JSON-LD")
            has_violation = True
            continue

        violations = check_category(data) + check_breadcrumb(data)
        if violations:
            has_violation = True
            for v in violations:
                print(f"{url},{v}")
        else:
            print(f"{url},通过")

    sys.exit(1 if has_violation else 0)


if __name__ == "__main__":
    main()

接入方式有两种,按团队习惯二选一:

  • CI 门禁:在商品发布流水线里加一步 python validate_category.py urls.txt,退出码非 0 即阻断合并。适合上新频繁、希望问题在发布前拦截的团队。
  • 定时任务:用 cron 或 GitHub Actions 每天跑一次全量商品 URL 列表,输出违规清单到告警群。适合已经上线、主要防回退的场景。

脚本里的 LEAF_WHITELIST 是硬编码的末级词白名单,实际使用时应从映射表导出,并随官方分类版本更新同步维护,否则新类目会被误报成「未到叶子层」。

误区澄清与趋势判断

常见误区是把「填了 category」当成终点。类目字段的价值不在字段本身,而在它把商品实体接进了 AI 引擎的品类召回体系——分诊台进不去,后面所有优化都是空转。另一个反向误区是过度填写:往 category 里堆关键词、塞多个值,引擎解析到的是脏数据,效果反而差,单值、叶子层、语境一致这三点守住了就够了。

趋势上看,主流 AI 搜索正在把购物类问答往「品类意图识别 + 商品实体推荐」的方向收紧,类目层级的语义利用会越来越深。零售站与其等着引擎猜自己的类目,不如主动把站内类目树翻译成官方分类语言送上去。改造细节或映射表字段设计有疑问,欢迎评论区交流。

参考与延伸

  • Schema.org Product 定义与属性说明:https://schema.org/Product
  • Schema.org BreadcrumbList 定义:https://schema.org/BreadcrumbList
  • Schema.org PropertyValue 与 AdditionalProperty:https://schema.org/PropertyValue
  • Google 商品分类体系官方文档与多语言文件下载:https://developers.google.com/shopping-content/guides/product-taxonomy

GEO · AI 搜索 · Product.category · Google 商品分类 · 类目映射 · 结构化数据 · 零售电商

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亨贵的GEO优化

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值