を運用し始めると、必ず出てくるのが鮮度の問題です。元の文書は更新されたのに、検索結果は古いまま。
厄介なのは、壊れているように見えないことです。検索は成功し、それらしい回答が返ります。内容だけが古い。この回はその対処を扱います。
1. なぜ気づけないのか
── 元は更新されたのに索引に反映されていない状態 ── は、エラーとして現れません。
| 状態 | 利用者から見て | 気づけるか |
|---|---|---|
| 索引が壊れている | 検索できない | すぐ気づく |
| 索引が空 | 何も出ない | すぐ気づく |
| 索引が古い | それらしい結果が出る | 気づけない |
| 削除された文書が残る | 存在しない文書が出る | 指摘されるまで気づかない |
3行目と4行目が問題です。動いているように見えて、内容が正しくない。この構造は 前処理の現場 EP.22 で扱った「静かに壊れる」と同じです。
4行目は特に危険です。廃止された規程、終了したサービスの説明が検索結果に出続ける。しかも出典として表示されるため、利用者は正しい情報だと受け取ります。
2. 更新の方式
方式は3つに整理できます。それぞれ得意な条件が違います。
| 方式 | 速さ | 確実さ | 削除の反映 |
|---|---|---|---|
| 全件を作り直す | 遅い | 最も確実 | 確実 |
| 速い | 条件による | 漏れやすい | |
| 変更を検知して即時 | 最速 | 仕組み次第 | 検知できれば確実 |
2行目の削除の反映漏れが、差分方式の最大の弱点です。「更新された文書」は検知できても、「消えた文書」は検知しにくい。元のシステムが削除の記録を残していなければ、気づけません。
実務的な組み合わせは、差分を基本にして、定期的に全件を作り直すことです。レガシー再生の現場 EP.06 や Snowflake EP.11 で扱った差分更新と同じで、取りこぼしを定期的に回収します。
3. 削除をどう扱うか
削除の反映は、方式を決めておく必要があります。
- 1元のシステムで削除の記録を残す — 最も確実だが、上流の協力が要る
- 2全件の一覧と突き合わせる — 索引にあって元に無いものを消す
- 3有効期限を設ける — 一定期間、再取得されなければ消す
- 4元の文書の存在を、回答時に確認する — 確実だが遅くなる
def reconcile_index(index_client, source_client, dry_run: bool = True): """索引と元の文書一覧を突き合わせ、消えたものを索引から除く。""" indexed_ids = set(index_client.list_all_ids()) source_ids = set(source_client.list_all_ids())
to_delete = indexed_ids - source_ids # 索引にあるが元に無い to_add = source_ids - indexed_ids # 元にあるが索引に無い
print(f" 索引 {len(indexed_ids):,} 件 / 元 {len(source_ids):,} 件") print(f" 削除すべき: {len(to_delete):,} 件") print(f" 追加すべき: {len(to_add):,} 件")
# 大量に消える場合は、元の取得が失敗している可能性を疑う if to_delete and len(to_delete) > len(indexed_ids) * 0.1: print(" ※ 削除対象が1割を超えています。元の取得を確認してください") if dry_run is False: raise RuntimeError("安全のため中断しました")
if dry_run: print(" (確認のみ。実行するには dry_run=False)") return {"to_delete": to_delete, "to_add": to_add}
for doc_id in to_delete: index_client.delete(doc_id) return {"deleted": len(to_delete), "to_add": to_add}削除対象が多すぎる場合に中断するのが重要な安全装置です。元の一覧の取得が失敗して空が返ってきた場合、索引を全消ししてしまいます。「1割を超えたら止める」という上限を入れておいてください。
これは 前処理の現場 EP.22 で扱った件数の検査と同じ発想です。急激な変化は、正しい変化より、失敗である可能性が高い。
4. 遅れを可視化する
完全な即時反映は難しいので、遅れがあることを前提に設計します。要点は、遅れを測れる状態にすることです。
from datetime import datetime, timedelta, timezone
def check_freshness(index_client, source_client, threshold_hours=24): """索引が元より古くなっている文書を見つける。""" stale = [] now = datetime.now(timezone.utc)
for doc in source_client.list_documents(): indexed = index_client.get_metadata(doc["id"]) if indexed is None: stale.append((doc["id"], "索引に存在しない", None)) continue
lag = doc["updated_at"] - indexed["indexed_at"] if lag > timedelta(hours=threshold_hours): stale.append((doc["id"], "索引が古い", lag))
print(f" 確認 {source_client.count():,} 件 / 古い {len(stale):,} 件") for doc_id, reason, lag in stale[:10]: lag_str = f"{lag.total_seconds() / 3600:.1f}時間" if lag else "-" print(f" {doc_id}: {reason}(遅れ {lag_str})")
return staleこの確認を定期的に回すと、更新の仕組みが正しく動いているかが分かります。特定の文書だけ更新されない、といった偏りも見つかります。
「この情報は◯月◯日時点です」と回答に添えるだけで、利用者が判断できます。GEO/LLMO EP.08 で扱った鮮度の明示と同じで、時点が分かれば、使う側が古さを考慮できます。
5. 即時性が要る場合
価格、在庫、空き状況。古い情報を返すと実害が出る種類のデータは、索引に載せる設計自体を見直します。
| データの性質 | 扱い |
|---|---|
| めったに変わらない(規程、マニュアル) | 索引に載せる。日次更新で十分 |
| 日単位で変わる(案件の状況) | 索引に載せる。頻度を上げる |
| 刻々と変わる(価格、在庫) | 索引に載せず、回答時に取得 |
| 個人ごとに違う(契約内容) | 回答時に取得 |
下2行が要点です。索引に載せるのは、変わりにくい情報だけにする。刻々と変わる値は、回答を作る時点で元のシステムに問い合わせる。
この設計にすると、「規程の説明は索引から、現在の価格は問い合わせて」という組み合わせになります。実装は複雑になりますが、古い価格を答えるという事故が構造的に起きなくなります。仕組みで頑張るのではなく、設計で問題を消すほうが確実です。
6. 運用の整理
最後に、運用として決めておくことをまとめます。
- 1更新の頻度を、文書の性質ごとに決める — 一律にしない
- 2差分を基本に、定期的な全件再構築を組み合わせる — 取りこぼしを回収
- 3削除の反映方式を決める — 突き合わせか、有効期限か
- 4遅れを測って監視する — 古くなっている文書を検出
- 5時点を回答に添える — 利用者が判断できるように
- 6即時性が要るものは索引に載せない — 設計で防ぐ
6番目が最も確実な対処です。仕組みで頑張るより、そもそも古くなりうるものを載せない。判断の順序としては、まずここを検討してください。
古い索引 はエラーにならず、それらしい結果が返るので気づけない。特に削除の反映漏れが危険(廃止された規程が出典として出る)。差分を基本に、定期的な全件再構築で取りこぼしを回収する。突き合わせでの削除には上限の安全装置を入れる。遅れを測って監視し、時点を回答に添える。そして即時性が要るものは、そもそも索引に載せないのが最も確実です。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。