Skip to content

Commit 7967e08

Browse files
committed
地図から周辺のスポットを探して追加できるようにする
地図の右クリック/長押しに「この周辺を探す」を足す(spot_admin/admin 専用)。 検索語・半径・件数を入れると候補が地図に番号つきの印で並び、右のパネルで 選んでまとめて登録できる。候補そのものは保存せず、追加したものだけがスポットになる。 探し方は 2 段で、既定は速いほう。地図データ(chiezo の OSM 辞典)は 240ms 以下で 返り、AI の web 検索は 17 秒〜2 分かかる代わりに地図に無い新しい店と説明が付く。 まず地図データで雑に集め、足りなければ AI で探し足す(同じ一覧に混ざる)。 その場で答えが欲しい機能なので、正確さより速さを採る。実測で効いたのは 3 つ。 検索の回数に上限を言い渡すこと(41 秒 → 17 秒。エージェントとして動く相手は 放っておくと候補ごとに裏取りの検索を回す)、頼む項目を最小限にすること、 位置の確認を候補ごとの直列な地名検索からローカルの並列問い合わせに変えること。 AI の答えは信用せず確かめてから出す。位置は OSM 辞典を並列に引き、当たれば その座標に置き換え、外れれば未確認の印(新しい店は載っていないのが普通)。 重複は名前一致か 50m 以内で登録済みの印。口コミ本文・点数・順位の転記は プロンプトで禁じ、説明文には出どころと日付を必ず添える。 相手・モデル・深さは探索の画面で選ぶ。その場の目的で軽くも重くもしたい設定なので、 全員ぶんを 1 つに決めさせない(選択は端末に覚え、サーバーには持たない)。 何を頼んで何が返ったかも画面から見られる —— 遅い・少ない・的外れの原因は、 プロンプトなのか相手なのか件数なのか、そこを見ないと切り分けられない。 任意機能で、知識サーバーへの接続先を設定し、種別ごとに有効にしたときだけ出る。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 6297cc9 commit 7967e08

17 files changed

Lines changed: 3001 additions & 2 deletions

.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,18 @@ PHOTO_STORAGE=fs
9898
# 自分だけ・身内だけで使うインスタンスでは有効にしないこと
9999
# (既定のままなら、新規アカウントは管理者が /[type]/admin から作成する)
100100
# GOOGLE_AUTO_SIGNUP=true
101+
102+
# --- 周辺のAI探索(任意) ---
103+
# 地図の右クリック/長押しメニューに「この周辺をAIで探す」(spot_admin/admin専用)を出す。
104+
# AIへの中継は知識サーバー chiezo の /v1/ai/complete で、鍵はあちらが握る
105+
# (このアプリはAPIキーを持たない)。未設定なら機能そのものが出ない。
106+
# CHIEZO_BASE_URL=http://chiezo.example.lan:7010
107+
108+
# 中継先で使う相手の既定(chiezo の /v1/ai/backends に出る id。未設定なら antigravity)。
109+
# 探索の画面の「AIの設定」で選ぶとそちらが優先される。web検索を持つ相手を選ぶこと。
110+
# モデルと考える深さも同じ画面で選ぶ(速さはこの2つで数倍変わる)。
111+
# CHIEZO_AI_BACKEND=codex
112+
113+
# AIの答えを待つ上限(秒。既定 180)。待たされた末に失敗するより早く諦めて
114+
# 条件を変えるほうがよいので短くしてある。長い調査に使うときだけ伸ばす
115+
# CHIEZO_AI_TIMEOUT_SECONDS=180

CLAUDE.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,109 @@ CSVなので、アプリ側は「気づいた」を貯めて渡すところま
553553

554554
`/[type]/admin`の「travel-log-dataへの還元用エクスポート」(spot_admin/admin)が、①`origin='manual'`の公開スポット(スポットCSVへの収録候補。key列なし=収録時にtravel-log-data側の規則で振る)と、②削除の墓標(exclude.txtへの追記候補、GET `/api/spot-deletions?type=`)を1つのMarkdownにまとめてダウンロードする。**還元作業はClaude Code等が読む前提のため、意図的に機械取り込み可能な形式にしていない**。還元後にkeyを付けたCSVを再インポートすると、一致した行は内容が同一でも`origin='csv'`にPATCHされ(インポートの同一判定が`origin !== 'csv'`を「変更あり」とみなす)、次回のエクスポートから自動的に外れる。
555555

556+
### 周辺を探す(`/api/spots/nearby``/api/spots/discover``AiSpotDiscoverySearchModal``AiSpotDiscoveryPanel`)
557+
558+
地図の右クリック/長押しメニューの「この周辺を探す」(spot_admin/admin専用)。
559+
検索語・半径・件数(10/20/30)と**探し方**を選ぶと、候補が地図に番号つきの印で並び、
560+
右のパネルで選んでまとめて登録できる。
561+
562+
**探し方は2段。既定は速いほう。**
563+
564+
||| かかる時間 | 得意なもの |
565+
|---|---|---|---|
566+
| **地図データ**(既定) | `GET /api/spots/nearby` | **240ms以下**(実測) | 正確な座標。AIの枠を使わない |
567+
| **AIでweb検索** | `POST /api/spots/discover` | 17秒〜2分 | 地図に無い新しい店、一言の説明 |
568+
569+
**この順にしたのは「近くの飲食店をちょっと見たい」が主な使い方だから。** AIだけの頃は
570+
1回に3分近くかかって使い物にならなかった。**まず地図データで雑に集め、足りなければ
571+
AIで探し足す**(パネルの「もう一度探す」。同じ一覧に混ざり、行に出どころの印が付く)。
572+
573+
**どちらも同じ`DiscoveryResult`を返す**ので、地図の印もパネルも出どころを問わず
574+
同じものを並べられる。違いは`source`(`osm`/`ai`)だけで、画面はそれを印にする。
575+
576+
**出す条件は3つそろって初めて**(GET `/api/spots/discover?type=``enabled`を返し、
577+
`MapView`はfalseならメニュー項目を出さない): ①接続先`CHIEZO_BASE_URL`がある
578+
(どちらの段もchiezo経由。未設定なら機能そのものが出ない。IPはコードにもDBにも置かない)、
579+
②種別の`ai_discovery_enabled`がon(**既定off**)、③呼んだ人がspot_admin/admin。
580+
581+
#### 1段目: 地図データ(`app/api/spots/nearby/route.ts`)
582+
583+
chiezoのOSM辞典(`osm_japan`)を引くだけ。**探し方は2つ投げて混ぜる**(`lib/osmNearby.ts`):
584+
585+
- **全文検索**(`/search`)は「ラーメン」のように**名前に出る語**に強い
586+
- **種別での絞り込み**(`/filter?feature=`)は「ランチ」のように**名前に出ない語**に要る。
587+
片方だけだと、どちらかの語で何も出ない。`feature`はOSMの主タグ1つしか受け付けない
588+
(`cuisine=ramen`で絞ろうとすると0件。実測)ので、語→主タグの表を持つ
589+
- 全文検索の応答には座標が載らないので、当たった題名で`doc`を引き直す。**並列に投げる**
590+
591+
**座標は地図データそのもの**なので`location_verified`は常にtrue。ジャンルは`cuisine`
592+
主タグの順で日本語に直す。**説明文は持たない**(OSMに無いので`summary`はnull)。
593+
**日本の種別だけ**(chiezoのOSM辞典が日本の抽出)。
594+
595+
**限界は測ってある**: 新宿1kmに飲食店884件あるのに、AIが挙げた有名店4件のうち
596+
OSMで引けたのは1件だけだった(タカノフルーツパーラーのような老舗も載っていない)。
597+
営業時間が入っているのは新宿で23%・郡山で1%。**だからAIの段を残してある。**
598+
599+
#### 2段目: AIでweb検索(`app/api/spots/discover/route.ts`)
600+
601+
**その場で答えが欲しい機能なので、正確さより速さを採る。** 実測で効いたのは3つ:
602+
603+
| 変えたところ | 理由 |
604+
|---|---|
605+
| **検索の回数に上限を言い渡す**(`quick`) | **これが一番効いた。41秒→17秒**(同じ相手・同じ件数)。エージェントとして動く相手は、放っておくと候補ごとに裏取りの検索を回す。「webの検索は多くても2回」と書き、URLも必須にしないだけで半分以下になり、**遅い側のばらつきも収まる**。裏取りが要るときは画面で「しっかり」を選ぶ |
606+
| **プロンプトを短くし、頼む項目を最小限にする** | 項目を1つ増やすと候補の数だけ文が増える。`region`(中心座標から引ける)・`name_kana``rank_reason`は落とし、`rank`**その種別がランクを使うときだけ**聞く |
607+
| **モデルとeffortを選べるようにする** | 同じ問いで **32秒〜3分半** の開きが出た(実測・単発: antigravity+low 32秒で10件 / haiku+low 64秒 / codex 100秒 / sonnet+medium 215秒)。未選択なら`low`(`DEFAULT_DISCOVERY_EFFORT`**相手がeffortを持つときだけ送る** —— 持たない相手=codexに送るとchiezoが400で断る) |
608+
| **位置の確認を候補ごとに直列でNominatimに聞くのをやめる** | 1秒1回の間隔制限がそのまま待ちになり、10件で10秒以上。しかもchiezoに無い店(=新店)は同じOSM由来のNominatimにも無いことが多く、待った割に当たらなかった |
609+
610+
**相手・モデル・深さは探索の画面で選ぶ**(「AIの設定」。AIを選んだときだけ出る折り畳み)。
611+
**管理画面に置かない**のは、その場の目的で軽くも重くもしたい設定だから —— 全員ぶんを
612+
1つに決めさせると使う人が選び直せない。**選択はサーバーに保存せず、その端末の
613+
localStorageに覚える**(**DBに列を持たせない**判断でもある —— 種別ごとのon/offと違い、
614+
これは人ごと・回ごとに変わる値)。選ばなければ`CHIEZO_AI_BACKEND`か既定に落ちる。
615+
選択肢はGET `/api/spots/discover?type=`が配る(**web検索を持つ相手だけ**)。POSTは
616+
`resolveDiscoveryChoice`で確かめてから投げるが、**相手の一覧が取れなければ素通しする**
617+
(確かめられないことを理由に探索そのものを止めない)。
618+
619+
**AIの答えは信用せず確かめてから出す。** 位置はchiezoのOSM辞典を候補ぶん並列に引き、
620+
当たれば「位置確認済み」、当たらなければAIの座標のまま「未確認」(新店はOSMに無いのが
621+
普通なので珍しくない)。重複は名前一致か50m以内で「登録済み」。**口コミ本文・点数・
622+
ランキング順位の転記はプロンプトで禁じ**、要約はAI自身の言葉に限る。
623+
地域はAIに聞かず中心座標から引いたものを全候補に使う。
624+
625+
**待つ上限は180秒**(`CHIEZO_AI_TIMEOUT_SECONDS`)。**待たされた末に失敗するより、
626+
早く諦めて条件を変えて出し直せるほうがよい**。失敗の文言には**必ず相手の名前を入れる**
627+
—— どの相手が失敗したのか分からないと切り替えの判断ができない。
628+
629+
**投げた本文と返ってきた本文は結果に載せ、画面から見られるようにしてある**
630+
(`DiscoveryExchange``AiExchangeDialog`。パネルの「AIとのやり取りを見る」)。
631+
**手を入れずに出す** —— 整形すると直したいプロンプトの実物と食い違う。
632+
遅い・少ない・的外れの原因は、プロンプトなのか相手なのか件数なのか、ここを見ないと
633+
切り分けられない。**JSONとして読み取れなかったときはエラー文にも本文の頭を載せる**
634+
(そのときこそ何が返ったか見たい)。
635+
636+
**相手によっては同時に1本しか動かない。** chiezoのブリッジは認証情報がホーム配下で回る
637+
CLI(codex / antigravity)の起動を直列にするので、2人が同時に探すと後の人は待つ
638+
(実測: 3本同時で1本目238秒、残りは600秒待っても返らなかった。1本ずつなら32秒)。
639+
640+
#### 画面と登録
641+
642+
候補は番号つきの印(AIは紫、位置未確認は破線の縁、登録済みは灰色、チェックを外したものは薄く)で、
643+
**行を押すとそのピンへ寄り、ピンを押すとその行が目立つ**。パネルでできるのは、
644+
チェック・×(候補から外す)・行ごとのランク・「もう一度探す」(**同じ一覧に足す**
645+
正規化した名前が同じ候補は二度入れない)・「追加」・「終了」。
646+
647+
**探索の結果は保存しない。** 追加したものだけが通常のPOST `/api/spots`(`origin='manual'`)で
648+
入る。説明文は`buildDiscoveredDescription`が組み、**出どころを必ず添える**
649+
(地図データなら「OpenStreetMapから取得(日付)」、AIなら「AIがwebから収集(日付)、参照: URL」)
650+
—— 手で書いた説明と見分けるため。ジャンルはカテゴリに入れる。状態・シリーズは全行共通。
651+
公開で追加した行は`spotCache.applySpotChange`で次のダウンロードを待たずに地図へ載せる。
652+
653+
**OSMのライセンス(ODbL)について。** この機能が引くのは**自分のインスタンスで使うため**で、
654+
配布ではない(地図表示には`© OpenStreetMap contributors`を出している)。ただし
655+
`origin='manual'`は還元用エクスポートの抽出条件なので、**OSM由来の名前・座標を
656+
travel-log-data(MIT側)へ持ち込まないよう、還元するときは出どころを見て外すこと**
657+
(「外部データソース…注意」の「ヒントに留める」と同じ線)。
658+
556659
### 公開スポットの全削除・スポット種別の削除
557660

558661
管理画面の`/[type]/admin`にはadmin専用の「公開スポットの全削除」(`app/api/spots/purge/route.ts`)と「スポット種別の削除」(`DELETE /api/spot-types/[id]`、同ファイルのPATCHと同居)がある。前者は`spot_types`の行自体は消さず、対象種別の公開(published)スポットのみを全件削除する(承認待ち・却下・非公開のスポットは残す。CSVで作り直す対象=CSVインポートが取り込む公開スポットに限定するため)。削除される公開スポットに紐づく`visits`/`visit_plans`/`reviews`(FKの`on delete cascade`)・写真ファイルと、対象種別のルート(`spot_routes`。status問わず丸ごと)も一括で消す。後者はstatus問わず対象種別の全スポットを削除(紐づくデータ・写真ファイルの扱いは前者と同じ)した上で`spot_types`の行自体も削除する(「別のスポット種別の管理」一覧には現在表示中の種別も含めて全種別を出すが、現在表示中の種別だけはリンク化・削除ボタンをUI側で出さないことで自分が今開いている種別を誤って消せないようにしている)。後者は`public_visible`がtrue(一般公開中)の種別、または対象種別が`app_settings.active_spot_type_id`(ルート`/`リダイレクトのフォールバック既定)の場合はAPIレベルで拒否する(既定の種別は常にpublic_visible=trueであるため後者は実質前者に含まれるが、防御的に両方チェックしている)。どちらもCSVでデータを作り直す前提の機能で、spot_adminには許可していない(ユーザー管理と同様、他ユーザーのデータを巻き込むため)。ルート`/`アクセス時に開く既定の種別(最後に開いていた種別のCookieが無い・開けないときのフォールバック)の変更は、この一括削除等の管理系操作とは別の独立したセレクトボックス(`app_settings.active_spot_type_id`を更新)として`/[type]/admin`に置いている。

README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
| **重ね表示** | 別のスポット種別を半透明で同じ地図に重ねられる |
3636
| **対象地域** | 種別ごとに日本(都道府県)・特定の国(州・県)・世界全体(国)から選べる |
3737
| **AIに聞く** | スポットの詳細から、Claude・ChatGPT・Perplexity・Grok・Geminiに所在地と座標を添えて質問できる(営業時間とアクセスを先に答えるよう頼んである) |
38+
| **周辺を探す** | 管理者が地図の右クリック/長押しから検索語(「ランチ」など)を入れると、周辺の候補を地図に番号つきで並べる。**まず地図データ(OpenStreetMap)から1秒かからず出し**、足りなければAIのweb検索で同じ一覧に探し足せる。右側のパネルで外す・選ぶ・まとめて追加ができる。任意機能で、知識サーバー(chiezo)への接続先を設定し、種別ごとに有効にしたときだけ出る |
3839
| **PWA対応** | ホーム画面に追加して、アドレスバーなしの独立アプリとして起動できる(オフライン対応は未実装) |
3940

4041
## スクリーンショット
@@ -137,6 +138,67 @@ PUBLIC_BASE_URL=https://travel.example.com
137138

138139
</details>
139140

141+
<details>
142+
<summary>周辺を探す(任意)</summary>
143+
144+
地図の右クリック/長押しメニューに「この周辺を探す」(スポット管理者・管理者のみ)を
145+
出す機能。検索語・半径・件数(10〜30)と**探し方**を選ぶと、周辺の候補が地図に番号つきの
146+
印で並ぶ。右側のパネルで外す・チェックする・ランクを直す・まとめて追加する。
147+
追加したものだけが通常のスポット(手動追加扱い)になり、候補そのものは保存されない。
148+
149+
探し方は2つあり、**既定は速いほう**
150+
151+
| 探し方 | かかる時間 | 得意なもの |
152+
|---|---|---|
153+
| **地図データ**(既定) | 1秒かからない | 正確な座標。AIの枠を使わない |
154+
| **AIでweb検索** | 15秒〜2分 | 地図に無い新しい店、一言の説明 |
155+
156+
まず地図データで雑に集め、足りなければパネルの「もう一度探す」からAIで探し足す
157+
(同じ一覧に混ざり、行に出どころの印が付く)、が想定の流れ。地図データは日本の種別のみ。
158+
159+
AI で探すときは**念の入れ方**も選ぶ(既定は「さっくり」)。
160+
161+
| | 頼み方 | 実測(10件) |
162+
|---|---|---|
163+
| **さっくり**(既定) | web の検索を 2 回までに抑え、裏取りをさせない | 17 秒 |
164+
| **しっかり** | 1 件ずつ確かめ、根拠の URL を必須にする | 41 秒 |
165+
166+
遅さの一番の原因は**裏取りの検索回数**で、上限を言い渡すだけで半分以下になる。
167+
168+
AI で探した後は、パネルの「AIとのやり取りを見る」で**頼んだ本文と返ってきた本文を
169+
そのまま**確かめられる(整形しないので、プロンプトを直すときはこれを見る)。
170+
171+
AIへの中継は知識サーバー [chiezo](https://github.com/rtcode337/chiezo)`/v1/ai/complete` で、
172+
APIキーはあちらが持つ(このアプリには置かない)。使うには2段の設定が要る。
173+
174+
1. `.env`(Docker Compose)か `.env.local` に接続先を書く。未設定なら機能そのものが出ない
175+
2. `/[type]/admin` の「スポット種別の設定」で、その種別の「周辺をAIで探す」を有効にする
176+
(**既定は無効**。AIの枠を使うので、使う種別だけ開ける)
177+
178+
```
179+
CHIEZO_BASE_URL=http://chiezo.example.lan:7010
180+
# 相手の既定(任意。未設定なら antigravity)。管理画面で選んだ相手があればそちらが優先
181+
CHIEZO_AI_BACKEND=codex
182+
```
183+
184+
AI で探すときの相手(chiezo に登録された AI のうち web 検索を持つもの)と、そのモデル・
185+
考える深さは、**探索の画面の「AIの設定」で選ぶ**(既定は畳んである)。相手が落ちている間に
186+
逃がすためと、その場の目的で速さを選ぶためのもの。選んだ内容はその端末に覚える。
187+
188+
- 軽いモデル・低い深さほど早い。深さは未選択なら自動で `low` を使う(相手が対応するとき)
189+
- AI の候補の位置は chiezo の OSM 辞典で確かめる。新しい店は OSM に無いのが普通なので
190+
「位置未確認」は珍しくない。地図で見て違えば、追加後に「位置を修正」で直す
191+
- OpenStreetMap のデータは ODbL。**自分のインスタンスで周辺を引く用途**を想定していて、
192+
地図表示には出典を出している。travel-log-data へ還元するときは、OSM 由来の名前・座標を
193+
そのまま持ち込まないこと(本体は MIT)
194+
- 位置の確認と重複判定はこちらで行うが、**内容の真偽はAIの答え次第**。参照URLの無い
195+
候補は薄く出し、既定では選ばれない。説明文には「AIがwebから収集(日付)」と参照URLが
196+
自動で入るので、手で書いた説明と見分けがつく
197+
- 商用のグルメサイト・地図サービスのAPIは「自前DBへの保存・再配布禁止」の条件があり
198+
出どころにできないため、web検索を持つAIに調べさせる形にしている
199+
200+
</details>
201+
140202
### 本番運用
141203

142204
サーバーを持たずに公開する場合は

0 commit comments

Comments
 (0)