Skip to content

Commit f62dbe7

Browse files
committed
上限に当たったら断る。黙って粗くも、黙って切りもしない
区画の天井に当たったときの振る舞いを、切り落としからも「目安を勝手に上げる」からも やめて、理由と必要な値を添えて断る形にした。 数で打ち切ると、打ち切った先の母集団がどの区画にも入らず、どの巡回にも回ってこない (68 万件を目安 150 で割ろうとして 2,000 区画で止まり、40 万件ぶんの範囲が台帳から 消えた)。目安を勝手に上げるのも同じ筋で、頼んだ細かさと違うもので回り続ける。 どちらを選ぶかは人が決めること。 タグで割る道も多い順に切り落としていた。消えるのはいつも細かい分類のほうで、 そこにこそ漏れが溜まる。 そのうえで天井を 2,000 → 20,000 に上げる。目安 150 なら 300 万件まで割れるので、 普通の使い方では当たらない。実測: 68 万件・目安 150 → 8,192 区画・1 区画 83〜84・ 合計は母集団と一致・台帳 1.24 MB。 天井を上げられるのは、定義を 1 収集 1 件に分けたから。まとめて 1 つの JSON に 入れていると、どれか 1 つを直すだけで全部を読み書きすることになり、1 つが壊れれば 全部が読めなくなり、1 つが太れば全部が重くなる。名指しの取得は 1 件しか読まない。 まとめて入れていた頃のものは、読むときに分ける(分け終えてから古い行を消すので、 途中で落ちても次に読むときにやり直せる)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 42796c3 commit f62dbe7

6 files changed

Lines changed: 243 additions & 78 deletions

File tree

CLAUDE.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -613,13 +613,17 @@ GeoNames 全世界地名辞典 = `geonames`(いずれも 348 言語版・195 か
613613
動かすのは区画の印だけで、**進み具合も中身も触らない**(やり直したいのは
614614
見る仕事であって、集めたものではない)。**走らせない** —— 印を外すだけなので、
615615
直したい設定を入れる前に 1 回消費しない。**区画を持つ収集にだけ出す**
616-
- **区画の数が天井(`MAX_PARTITIONS`)に当たったら、削るのは細かさであって
617-
範囲ではない**(`partition._fits`)。打ち切ると、**打ち切った先の母集団が
618-
どの区画にも入らず、どの巡回にも回ってこない** —— 本番で 68 万件を目安 150 で
619-
割ろうとして 2,000 区画(1 区画 200 未満)で止まり、40 万件ぶんの範囲が
620-
台帳から消えた。目安を上げれば 1 回に見る量が増えるだけで済む。
621-
**余裕は 2 倍で見る** —— 二分は割り切れないので、できる区画は目安の半分から
622-
等倍に散らばる。実測: 68 万件・目安 150 → 1,024 区画・1 区画 670・合計は一致
616+
- **区画の数が天井(`MAX_PARTITIONS`)に当たったら断る**(`partition._must_fit`)。
617+
**黙って減らさない** —— 数で打ち切ると、打ち切った先の母集団がどの区画にも
618+
入らず、どの巡回にも回ってこない。本番で 68 万件を目安 150 で割ろうとして
619+
2,000 区画(1 区画 200 未満)で止まり、40 万件ぶんの範囲が台帳から消えた。
620+
**目安を勝手に上げるのも同じ筋で駄目**で、頼んだ細かさと違うもので回り続ける
621+
—— 目安を上げるか母集団を絞るかは人が決めること(理由と必要な値を添えて断る)。
622+
**余裕は 2 倍で見る**(二分は割り切れず、区画は目安の半分から等倍に散らばる)。
623+
タグで割る道も同じで、**多い順に切り落とさない** —— 消えるのはいつも
624+
細かい分類のほうで、そこにこそ漏れが溜まる。
625+
実測: 68 万件・目安 150 → 8,192 区画・1 区画 83〜84・合計は母集団と一致・
626+
台帳 1.24 MB
623627
- **区画の母集団を足しても長期記憶の数には届かない**(画面に差を出す)。
624628
消えたものは数えないし、**座標を持たない 1 件はどの区画にも入らない**
625629
(地図で割る収集)。どちらも仕様だが、出ていないと「区画分けが壊れている」
@@ -1159,6 +1163,14 @@ GeoNames 全世界地名辞典 = `geonames`(いずれも 348 言語版・195 か
11591163
黙って飛ばされる
11601164
- **失敗しても次回の予定を入れる**(`record_result`)。入れないと、一度こけた収集が
11611165
二度と走らなくなる。理由は定義側に控えるので、画面の「前回」の欄から読める
1166+
- **定義は 1 収集 = 1 件**(`collect/<名前>``app/machine_store.py`)。
1167+
**まとめて 1 つの JSON に入れない** —— 区画の台帳は収集 1 つで MB 単位になるので、
1168+
どれか 1 つを直すだけで全部を読み書きすることになる。**1 つが壊れれば全部が
1169+
読めなくなり、1 つが太れば全部が重くなる**`get` は名指しの 1 件しか読まない。
1170+
**並びは作った順**(1 件ずつ置くと配列の順を持てないので `created_at` で並べ直す)。
1171+
**まとめて 1 件に入れていた頃のものは、読むときに分ける**
1172+
(`_split_out_the_old_row`。分け終えてから古い行を消すので、途中で落ちても
1173+
次に読むときにやり直せる)
11621174
- **画面の「削除」は設定と焼いたものをまとめて消す**(`admin_collect_delete`)。
11631175
設定だけ消して中身が残ると、一覧から消えたのに検索には出続けるものができ、
11641176
**どこから来たのかも読めなくなる**(収集の定義が消えているため)。

app/collect.py

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,10 @@ def normalize_focus(raw) -> Focus | None:
941941

942942

943943
def _stored() -> str | None:
944-
"""いまの定義(JSON の文字列)。まだ 1 度も置いていなければ None。"""
944+
"""**まとめて 1 件に入れていた頃**の定義(JSON の文字列)。無ければ None。
945+
946+
いまは 1 収集 = 1 件(`collect/<名前>`)。ここを読むのは移行のためだけ。
947+
"""
945948
return machine_store.get(DEFS_KIND, DEFS_KEY)
946949

947950

@@ -1115,47 +1118,101 @@ def edits_what_is_there(prompt: str, only_new: bool = False) -> bool:
11151118
return any(p in (prompt or "") for p in (MATERIAL_PLACEHOLDER, RECENT_PLACEHOLDER))
11161119

11171120

1118-
def _to_json(items: list[Collection]) -> str:
1119-
return json.dumps(
1120-
{"collections": [c.__dict__ for c in items]},
1121-
ensure_ascii=False,
1122-
indent=2,
1123-
)
1124-
1125-
11261121
def load() -> list[Collection]:
1127-
"""定義の一覧(並びは配列の順)。
1122+
"""定義の一覧(並びは作った順)。**1 収集 = 1 件**(`collect/<名前>`)。
1123+
1124+
**まとめて 1 件に入れない。** 区画の台帳は収集 1 つで MB 単位になりうるので、
1125+
全部を 1 つの JSON に入れると、**どれか 1 つを直すだけで全部を読み書きする**
1126+
ことになる —— 1 つが壊れれば全部が読めなくなり、1 つが太れば全部が重くなる。
1127+
(**上限を置いて逃げない**ための前提でもある。台帳の天井は、この形にして
1128+
初めて「1 つの収集の都合」に閉じられる。)
11281129
11291130
**本文が壊れていたら黙って作り直さない** —— 中身ごと消えるので、読めないことを
1130-
見せて人に直させる(プロジェクトと同じ判断)。
1131+
見せて人に直させる(プロジェクトと同じ判断)。**壊れているのはその 1 件だけ**
1132+
だと分かるように、名前を添えて上げる。
1133+
"""
1134+
_split_out_the_old_row()
1135+
out = []
1136+
for key in machine_store.keys(DEFS_KIND):
1137+
if key == DEFS_KEY:
1138+
continue
1139+
body = machine_store.get(DEFS_KIND, key)
1140+
if body is None:
1141+
continue
1142+
try:
1143+
raw = json.loads(body)
1144+
if not isinstance(raw, dict):
1145+
raise TypeError("object を入れてください")
1146+
except (ValueError, TypeError) as e:
1147+
raise HTTPException(400, {"error": f"{DEFS_BROKEN}(「{key}」): {e}"}) from None
1148+
out.append(_from_json(raw))
1149+
# **並びは作った順**。1 件ずつ置くと配列の順を持てないので、作った時刻で並べ直す
1150+
return sorted(out, key=lambda c: (c.created_at, c.name))
1151+
1152+
1153+
def _split_out_the_old_row() -> None:
1154+
"""まとめて 1 件に入れていた頃のものを、1 収集 1 件へ分ける。**1 度だけ**。
1155+
1156+
**古い行が残っているあいだは、そちらが正**(分け終えてから消す)——
1157+
途中で落ちても、次に読むときにもう一度分け直せる。
11311158
"""
11321159
body = _stored()
11331160
if body is None:
1134-
return []
1161+
return
11351162
try:
1136-
payload = json.loads(body or "{}")
1137-
raw = payload["collections"]
1163+
raw = json.loads(body or "{}")["collections"]
11381164
if not isinstance(raw, list):
1139-
raise ValueError("collections must be a list")
1165+
raise TypeError("collections must be a list")
11401166
except (ValueError, KeyError, TypeError) as e:
11411167
raise HTTPException(400, {"error": f"{DEFS_BROKEN}: {e}"}) from None
1142-
return [_from_json(i) for i in raw if isinstance(i, dict)]
1168+
for item in raw:
1169+
if isinstance(item, dict) and (name := str(item.get("name") or "")):
1170+
machine_store.put(DEFS_KIND, name, json.dumps(item, ensure_ascii=False, indent=2))
1171+
machine_store.drop(DEFS_KIND, DEFS_KEY)
1172+
log.info("split %d collection definitions into one record each", len(raw))
11431173

11441174

11451175
def save(items: list[Collection]) -> None:
1146-
machine_store.put(DEFS_KIND, DEFS_KEY, _to_json(items))
1176+
"""一覧をそのまま書き込む。**消えたものはこの場で落とす**。
1177+
1178+
1 件だけ直すなら `_replace_one` のほうが安い(そちらは 1 件しか書かない)。
1179+
"""
1180+
_split_out_the_old_row()
1181+
keep = {c.name for c in items}
1182+
for item in items:
1183+
_put_one(item)
1184+
for key in machine_store.keys(DEFS_KIND):
1185+
if key != DEFS_KEY and key not in keep:
1186+
machine_store.drop(DEFS_KIND, key)
1187+
1188+
1189+
def _put_one(item: Collection) -> None:
1190+
machine_store.put(
1191+
DEFS_KIND, item.name, json.dumps(item.__dict__, ensure_ascii=False, indent=2),
1192+
)
11471193

11481194

11491195
def get(name: str) -> Collection:
1150-
for item in load():
1151-
if item.name == name:
1152-
return item
1153-
raise HTTPException(404, {"error": f"収集「{name}」がありません"})
1196+
"""名前で 1 つ。**一覧を読まない** —— 台帳を抱えた他の収集まで読む理由が無い。"""
1197+
_split_out_the_old_row()
1198+
body = machine_store.get(DEFS_KIND, name) if name and name != DEFS_KEY else None
1199+
if body is None:
1200+
raise HTTPException(404, {"error": f"収集「{name}」がありません"})
1201+
try:
1202+
raw = json.loads(body)
1203+
if not isinstance(raw, dict):
1204+
raise TypeError("object を入れてください")
1205+
except (ValueError, TypeError) as e:
1206+
raise HTTPException(400, {"error": f"{DEFS_BROKEN}(「{name}」): {e}"}) from None
1207+
return _from_json(raw)
11541208

11551209

11561210
def _replace_one(name: str, updated: Collection) -> None:
1157-
items = load()
1158-
save([updated if c.name == name else c for c in items])
1211+
"""その 1 件だけを書き換える。**他の収集は読みも書きもしない**。"""
1212+
get(name)
1213+
if updated.name != name:
1214+
machine_store.drop(DEFS_KIND, name)
1215+
_put_one(updated)
11591216

11601217

11611218
def create(
@@ -1317,10 +1374,8 @@ def remove(name: str) -> None:
13171374
同じ名前で作り直すのは普通に起きる —— 残しておくと、前の収集が足した・消した
13181375
ものが新しい収集の履歴に混ざって見える。
13191376
"""
1320-
items = load()
1321-
if not any(c.name == name for c in items):
1322-
raise HTTPException(404, {"error": f"収集「{name}」がありません"})
1323-
save([c for c in items if c.name != name])
1377+
get(name)
1378+
machine_store.drop(DEFS_KIND, name)
13241379
collect_log.forget(name)
13251380
# **待ち行列からも外す** —— 消えた収集を抱えたままだと、そのワーカーは
13261381
# 起こそうとして 404 を踏み続ける

app/machine_store.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,24 @@ def drop(kind: str, key: str) -> bool:
214214
return True
215215

216216

217+
def keys(kind: str) -> list[str]:
218+
"""その種類に置いてあるものの名前(見出しの `<種類>/` の後ろ)。
219+
220+
**1 件ずつ置くものを数え上げるのに要る。** まとめて 1 件に入れていたものを
221+
種類の下へ分けると、一覧は「何が置いてあるか」を先に引いてから中身を取る形になる
222+
—— `records()` は全部の種類を並べるので、種類で絞れるほうが読む側も安い。
223+
"""
224+
if not is_enabled():
225+
return []
226+
ensure_db()
227+
head = kind + KEY_SEP
228+
with _connect() as conn:
229+
rows = conn.execute(
230+
"SELECT title FROM docs WHERE title LIKE ? ORDER BY title", (head + "%",)
231+
).fetchall()
232+
return [r["title"][len(head):] for r in rows if r["title"].startswith(head)]
233+
234+
217235
def records() -> list[dict]:
218236
"""置いてあるもの全部の**目録**(中身は運ばない)。
219237

0 commit comments

Comments
 (0)