Search Consoleのクエリ次元だけで「表示0」と判断してはいけない

Search Console(以下 SC)の API からデータを取り、「このページは表示0だからテーマ選びが外れている」と判断する仕組みを作った。週1で回していたところ、ある日登録した20ページの全件が「表示0」になった。

全件そろって外れるのは、テーマ選びの問題としては不自然である。調べたら、判定の入力が構造的に足りていなかった。この記事では、何が起きていたのかと、同じ誤判定を検出する検査の書き方を書く。確認した環境は Windows 11 / Node.js v24.11.1 で、数字は 2026-09-19 に実際に取ったデータである。

SC の「次元」は互いに一致しない

SC の searchanalytics.querydimensions で集計軸を指定する。よく使うのは次の3つである。

次元 何が返るか
date 日ごとの表示・クリック
page URL ごとの表示・クリック
query 検索語ごとの表示・クリック

ここで踏みやすい罠が2つある。

1つめ。page 次元の合計は date 次元の合計と一致しない。 1回の検索結果に自サイトのページが複数出ると、ページ側では両方に1表示ずつ計上されるためである。実測では page 合計 16,431 に対し date 合計 14,826 だった。比率の比較には使えるが、総量は日付次元を正とするべきである。

2つめ。query 次元は全表示を説明しない。 SC は少量のクエリを匿名化して返さないので、クエリ次元の合計は日付次元の合計より必ず小さくなる。手元の実測では クエリ次元は表示の 32% しか説明していなかった(4,760 / 14,826)。

問題は2つめのほうである。表示があるページでも、そのページのクエリが全部匿名化されていれば、クエリ次元には1行も出てこない。

何が起きていたか

判定に使っていた入力は、日次で保存しているスナップショットの sc_query_pages、つまり querypage を組み合わせた次元だった。ページごとの表示を、検索語つきで見たかったからである。

既存のガードは1つだけあった。「SC 側の行が全部0なら、その回は判定しない」というものである。

1
2
// フリート全体で表示 > 0 の行が1つでもあるか
const scAlive = rows.some((r) => (r.impressions || 0) > 0);

これはサイト全体が欠測している回を弾くためのガードで、そこは正しく効いていた。ところがこの判定は全体で1行でも表示があれば true になる。表示の多いサイトが1つあるだけで scAlive は常に true になり、クエリ次元に1行も返っていないサイトのページまで「表示0」に落ちていた

実測:20サイト中11サイトが測れていなかった

その日のスナップショットを開いて数えた。sc_query_pages59行・ホスト7つしか無い。判定対象のページは20サイトぶんある。

そこで、site_daily(日付次元のサイトごとの表示)と突き合わせた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const fs = require("fs");
const j = JSON.parse(fs.readFileSync("metrics/2026-09-18.json", "utf8"));

// クエリ次元に「表示>0の行」が返っているホスト
const liveHosts = new Set();
for (const r of j.sc_query_pages || []) {
if (!(r.impressions > 0)) continue;
try { liveHosts.add(new URL(r.page || r.url).host); } catch {}
}

// 日付次元のサイトごとの表示合計
const imp = {};
for (const s of j.site_daily || []) {
if (s.sc_impressions) imp[s.site_key] = (imp[s.site_key] || 0) + s.sc_impressions;
}

// 「表示はあるのにクエリ次元に1行も無い」サイト
for (const [key, total] of Object.entries(imp)) {
// host は自分の台帳から引く
const host = hostOf(key);
if (host && !liveHosts.has(host)) console.log(key, host, "SC表示", total);
}

結果はこうだった。

区分 件数
A: クエリ次元に出ている 5
B: 日付次元には表示があるのにクエリ次元に1行も無い 11
C: 日付次元でも表示が0 4

B の11サイトは、表示が 1〜14 の小さい値である。小さいから匿名化で落ちる。そして小さいサイトほど「まだ表示が付いていない、テーマが外れている」と判定されやすいので、いちばん誤判定してほしくないところで誤判定していた

直し方:ガードをサイト単位にする

全体の scAlive に加えて、ホスト単位でも同じ形のガードを掛ける。判定は純粋関数に切り出してテストできるようにした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function verdictFor({ tool, stats, today, scAlive, host, liveHosts, siteImpressions = 0 }) {
// 1) SC 側が丸ごと欠測の回
if (!scAlive) return { verdict: "sc-unavailable", note: "SC 側に表示が1件も無い回" };

// 2) 日付次元には表示があるのに、クエリ次元にそのホストの行が1つも無い
const hosts = liveHosts || new Set();
if (host && !hosts.has(host) && siteImpressions > 0) {
return {
verdict: "sc-unavailable",
note: `${host} は表示 ${siteImpressions} があるのにクエリ次元に1行も無い(匿名化)。表示0ではなく測れていない`,
};
}

// 3) ここまで来たものだけ本来の判定へ
const c = classify(tool, stats, today);

// 4) サイト全体の表示が0なら、ページのテーマの問題とは言えない
if (c.verdict === "no-impressions" && siteImpressions === 0) {
return { ...c, siteSilent: true, note: c.note + "(サイト全体の表示も0)" };
}
return c;
}

4つめの段も足した。サイト全体の表示が0なら、個別ページのテーマ選びを疑っても意味がない。 そのサイトが検索に出ていないという別の問題なので、ページを差し替えても直らない。ここを区別しないと、直しようのない作業指示が積み上がる。

直したあとの結果はこうなった。

判定 直す前 直したあと
表示0(テーマを見直すべきもの) 20 9
判定不可(測れていない) 0 11

さらに9件のうち4件は「サイト全体の表示も0」の注記が付く。本当にページのテーマを疑うべきなのは5件だった。

テストは「壊れ方」をそのまま固定する

この種の判定はテストを書きにくいが、実際に起きた誤判定をそのままテストにすると書ける。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
test("表示はあるのにクエリ次元に行が無いサイトは «判定不可» になる", () => {
const c = verdictFor({
tool: TOOL, stats: NO_ROWS, today: TODAY,
scAlive: true, // 他サイトに表示があるので true
host: "coffee.example.net", siteImpressions: 14,
liveHosts: new Set(["other.example.com"]), // このホストは入っていない
});
assert.equal(c.verdict, "sc-unavailable");
assert.match(c.note, /測れていない/);
assert.doesNotMatch(c.note, /テーマ/); // 差し替えの指示を出さない
});

test("ホストがクエリ次元に出ているサイトの表示0は、これまでどおり «表示0»", () => {
const c = verdictFor({
tool: TOOL, stats: NO_ROWS, today: TODAY, scAlive: true,
host: "cve.example.net", siteImpressions: 59,
liveHosts: new Set(["cve.example.net"]),
});
assert.equal(c.verdict, "no-impressions");
});

2本目が大事である。ガードを足したせいで本来の判定まで止まっていないことを固定しておかないと、次に「誤判定が出た」と言われたときに全部を sc-unavailable に倒してしまう。

まとめ

SC のデータを機械判定に使うときの勘所は3つある。

  1. 次元ごとに母集団が違う。 date が総量の正、page は重複計上あり、query は匿名化で欠ける。同じ「表示回数」という名前でも足し合わせられない
  2. 「0」には2種類ある。 測って0だったのか、測れていないのか。この2つを同じ 0 で持つと、後段の判定が必ず間違える
  3. 全体のガードはサイト単位の欠測を隠す。 「1件でもデータがあれば健全」という形の判定は、規模の小さい対象を構造的に切り捨てる

そして、判定を変えたときは直す前と直したあとの件数を並べて記録しておくこと。今回なら「20件 → 9件」という数字がそのまま、この修正が何を止めたかの説明になる。

なお、この仕組みで測っている各サイトのツールはハシトシステムなどフリート各サイトに置いてある。判定の入力になっているのは、日次で保存している SC と GA のスナップショットである。


Search Consoleのクエリ次元だけで「表示0」と判断してはいけない
https://blog.hashito.biz/2026/09/19/search-console-query-dimension-zero-impressions-trap/
著者
hashito
作成日
2026年9月19日
著作権