AIエージェント用検索APIの重複URLをPythonで測る
朝、テスト用に保存した二つの検索応答を見比べていたら、片方にだけ utm_source が付いていた。同じ Python ドキュメントを返しているのに、そのまま集合を取ると別URL扱いになる。こういう地味な差で「二つの検索APIは違う結果を返した」と思い込む。
8月18日に、AIエージェント向け検索APIを品質・コスト・速度で比較するベンチマークが報じられた1。ただ、エージェントに検索を任せる側が最初に見る数字は順位ではなく、結果がどれだけ同じ一次情報に寄っているかだと思う。上位10件のうち8件が同じURLなら、APIを二本用意しても調査の逃げ道は増えない。
ここでは、各APIの生JSONを保存してから、URLを正規化して重複を数える小さなスクリプトを作る。APIキーもSDKも要らない。比較の前に、今使っている検索結果を観察するための道具として使う。
URLをそのまま比べると重複を取り逃がす
検索APIの応答形式はまちまちだ。results の配列だったり、web.results の中だったりする。下のコードはJSONを再帰的にたどり、HTTP URLだけを拾う。
比較前に次をそろえる。
-
utm_*、gclid、fbclidを落とす -
www.と末尾の/をそろえる - フラグメントを落とす
- それ以外のクエリは残す
最後の点は意外と大事。検索条件を含むURLまで全部同じページ扱いにすると、今度は別物を混ぜてしまう。正規化は強くしすぎない方が、後で出力を読める。
#!/usr/bin/env python3
"""複数の検索 API 応答に含まれる URL の重複を集計する。"""
import argparse
import itertools
import json
from pathlib import Path
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
def iter_urls(value):
if isinstance(value, dict):
for child in value.values():
yield from iter_urls(child)
elif isinstance(value, list):
for child in value:
yield from iter_urls(child)
elif isinstance(value, str):
parsed = urlsplit(value)
if parsed.scheme in {"http", "https"} and parsed.netloc:
yield value
def canonicalize(url):
parsed = urlsplit(url)
host = parsed.netloc.lower().removeprefix("www.")
path = parsed.path.rstrip("/") or "/"
query = [
(key, value)
for key, value in parse_qsl(parsed.query, keep_blank_values=True)
if not (key.lower().startswith("utm_") or key.lower() in {"gclid", "fbclid"})
]
return urlunsplit(("https", host, path, urlencode(sorted(query)), ""))
def read_input(spec):
name, separator, filename = spec.partition("=")
if not separator or not name or not filename:
raise ValueError("--input は provider=response.json の形式で指定してください")
with Path(filename).open(encoding="utf-8") as handle:
document = json.load(handle)
return name, {canonicalize(url) for url in iter_urls(document)}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", action="append", required=True)
args = parser.parse_args()
try:
results = dict(read_input(spec) for spec in args.input)
except ValueError as error:
parser.error(str(error))
if len(results) != len(args.input):
parser.error("provider 名は重複させないでください")
provider_names = sorted(results)
shared_by_all = sorted(set.intersection(*(results[name] for name in provider_names)))
pairwise_overlap = []
for left, right in itertools.combinations(provider_names, 2):
shared = results[left] & results[right]
union = results[left] | results[right]
pairwise_overlap.append(
{
"providers": [left, right],
"shared_urls": len(shared),
"jaccard": round(len(shared) / len(union), 3) if union else 0.0,
}
)
print(json.dumps({
"providers": {name: {"unique_urls": len(results[name])} for name in provider_names},
"shared_by_all": shared_by_all,
"pairwise_overlap": pairwise_overlap,
}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
保存した応答を brave.json と tavily.json とした場合はこう実行する。
python audit_search_overlap.py \
--input brave=brave.json \
--input tavily=tavily.json
自分が動かしたテストでは、片方だけに付けた utm_source と、もう片方の #url-parsing が消え、Python公式ドキュメントが1件の共通URLになった。Python 3.14.6で構文確認も通している。
{
"providers": {
"brave": {"unique_urls": 2},
"tavily": {"unique_urls": 2}
},
"shared_by_all": [
"https://docs.python.org/3/library/urllib.parse.html"
],
"pairwise_overlap": [
{
"providers": ["brave", "tavily"],
"shared_urls": 1,
"jaccard": 0.333
}
]
}
ここだけ見れば、速度の違う二社が実際には同じ一件を返していないか確認できる。
数字の読み方と、集めすぎないための使い方
shared_by_all は全社で共通のURL、jaccard は二社の和集合に対する共通URLの比率だ。0.333なら、今回のように3URLのうち1URLが共通ということになる。
値が低ければ良い、で終わらせない。低い理由が低品質なページの混入なら、エージェントの根拠はむしろ弱くなる。上位URLを実際に開き、公式ドキュメント、一次発表、転載記事のどれが増えたかを見る。ここはスコアで自動判定しない方がいい。ドメイン単位に畳みたくなるけれど、公式Docsと公式Blogを分けて見たい案件もある。
自分は、まず同じ5クエリを各APIへ投げて応答をそのまま保存し、このスクリプトを回す運用にしている。重複が高いなら片方を冗長化用に置く判断もできる。低いなら、回答を合成する前に出典の優先順位を決める。検索APIの契約を増やす話より先に、そこを見ておくと設計がかなり静かになる。
たとえば一般Web検索と公式ドキュメント専用の検索を並べるなら、重複が少ないこと自体に意味がある。逆に二社とも同じニュース転載へ寄るなら、プロバイダを替えても調査の視野は広がらない。出力を一度眺めるだけで、クエリの書き方を直す場所も見えてくる。
おわりに
検索APIの評価は、平均レイテンシや正解率だけで決めにくい。エージェントでは検索結果が次のプロンプトに入り、その偏りが回答まで持ち越されるからだ。
まずは保存済みの応答でURLの重複を出す。共通の一次情報しか取れていないなら、検索先を増やすより、クエリやソース制約を変えた方が効く。比較表の数字を読む前に、自分のワークロードで出典の分散を一度測るのがおすすめ。