Spaces:
Sleeping
Sleeping
ktsn-ud commited on
Commit ·
e6ef038
1
Parent(s): 8eb78dc
Add detailed documentation on search algorithms and their mathematical foundations in Japanese
Browse files- Introduced a new document explaining the mathematical background of search algorithms used in the project.
- Covered key topics including BM25F, TF-IDF, word vectors, cosine similarity, and score integration methods.
- Provided examples and explanations tailored for students interested in the mathematical aspects of algorithms.
- docs/api_spec.md +0 -299
- docs/db_connection.md +0 -23
- docs/project_overview.md +0 -224
- docs/project_schema_change_points.md +0 -40
- docs/システムガイド.md +961 -0
- docs/基礎知識.md +547 -0
- docs/改善提案.md +825 -0
- docs/検索アルゴリズム詳説.md +520 -0
docs/api_spec.md
DELETED
|
@@ -1,299 +0,0 @@
|
|
| 1 |
-
# フロントエンド向け API 仕様書
|
| 2 |
-
|
| 3 |
-
本ドキュメントはフロントエンドから本APIを利用するための仕様書です。認証ヘッダ、各エンドポイントの入出力、エラー応答、注意点をまとめています。
|
| 4 |
-
|
| 5 |
-
ベースURLはデプロイ環境に依存します(例: 同一オリジンの `/api/...` または環境変数 `FASTAPI_URL` で指定)。バージョン付けは現状ありません。
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## 目次
|
| 10 |
-
|
| 11 |
-
- [認証](#認証)
|
| 12 |
-
- [エンドポイント一覧](#エンドポイント一覧)
|
| 13 |
-
- [スキーマ定義](#スキーマ定義)
|
| 14 |
-
- [リクエスト/レスポンス例](#リクエストレスポンス例)
|
| 15 |
-
- [エラーとステータスコード](#エラーとステータスコード)
|
| 16 |
-
- [実装メモ(フロントエンド)](#実装メモフロントエンド)
|
| 17 |
-
- [クライアント例](#クライアント例)
|
| 18 |
-
- [環境変数の例(フロントエンド側)](#環境変数の例フロントエンド側)
|
| 19 |
-
|
| 20 |
-
---
|
| 21 |
-
|
| 22 |
-
## 認証
|
| 23 |
-
|
| 24 |
-
本番(HF Spaces・プライベート)では二段階の認証が必要です。
|
| 25 |
-
|
| 26 |
-
- レイヤ1: HF Spaces へのアクセス(プライベート用)
|
| 27 |
-
- ヘッダ: `Authorization: Bearer <API_ACCESS_KEY>`
|
| 28 |
-
- 値の例: 環境変数 `API_ACCESS_KEY`(フロントのサーバーサイドに設定)
|
| 29 |
-
- 失敗時: `401 Unauthorized`(またはプラットフォーム側のエラー)
|
| 30 |
-
|
| 31 |
-
- レイヤ2: アプリ内APIキー
|
| 32 |
-
- ヘッダ: `X-API-KEY: <API_SECRET_KEY>`(ヘッダ名は大小無視: `X-API-Key` でも可)
|
| 33 |
-
- 値の例: 環境変数 `API_SECRET_KEY`(README 参照)
|
| 34 |
-
- 対象: `/api/projects`, `/api/details`, `/api/search`
|
| 35 |
-
- 失敗時: `403 Forbidden`(キー不正または未設定)
|
| 36 |
-
|
| 37 |
-
推奨: ブラウザから直接APIを呼ばず、Next.js等のサーバーサイド(API Routes や server actions)から呼び出してください。クライアントに秘密鍵を露出しないためです。
|
| 38 |
-
|
| 39 |
-
---
|
| 40 |
-
|
| 41 |
-
## エンドポイント一覧
|
| 42 |
-
|
| 43 |
-
1) ヘルスチェック(認証不要)
|
| 44 |
-
- メソッド/URL: `GET /api/health`
|
| 45 |
-
- リクエスト: なし
|
| 46 |
-
- レスポンス: `200 OK`
|
| 47 |
-
- 本文: `{ "status": "ok" }`
|
| 48 |
-
|
| 49 |
-
2) 企画一覧(圧縮レスポンス)
|
| 50 |
-
- メソッド/URL: `GET /api/projects`
|
| 51 |
-
- ヘッダ:
|
| 52 |
-
- `Authorization: Bearer <API_ACCESS_KEY>`
|
| 53 |
-
- `X-API-KEY: <API_SECRET_KEY>`
|
| 54 |
-
- レスポンス: `200 OK`
|
| 55 |
-
- ヘッダ: `Content-Encoding: gzip`, `Content-Type: application/json`
|
| 56 |
-
- 本文: `ProjectSummary[]`(ブラウザは自動で解凍します)
|
| 57 |
-
|
| 58 |
-
3) 企画詳細
|
| 59 |
-
- メソッド/URL: `GET /api/details?projectId=<ID>`
|
| 60 |
-
- ヘッダ:
|
| 61 |
-
- `Authorization: Bearer <API_ACCESS_KEY>`
|
| 62 |
-
- `X-API-KEY: <API_SECRET_KEY>`
|
| 63 |
-
- クエリ: `projectId`(必須)
|
| 64 |
-
- レスポンス:
|
| 65 |
-
- 成功: `200 OK` 本文: `ProjectDetail`
|
| 66 |
-
- 失敗: `404 Not Found`(存在しないID)
|
| 67 |
-
|
| 68 |
-
4) 検索
|
| 69 |
-
- メソッド/URL: `POST /api/search`
|
| 70 |
-
- ヘッダ:
|
| 71 |
-
- `Authorization: Bearer <API_ACCESS_KEY>`
|
| 72 |
-
- `X-API-KEY: <API_SECRET_KEY>`
|
| 73 |
-
- ボディ(JSON): `{ "query": string, "debug": boolean (省略可, 既定:false) }`
|
| 74 |
-
- レスポンス:
|
| 75 |
-
- 成功: `200 OK`
|
| 76 |
-
- `debug=false` のとき: `ProjectIds`
|
| 77 |
-
- `debug=true` のとき: `SearchDebugResponse`
|
| 78 |
-
- 失敗: `400 Bad Request`(`query` が空)
|
| 79 |
-
|
| 80 |
-
---
|
| 81 |
-
|
| 82 |
-
## スキーマ定義
|
| 83 |
-
|
| 84 |
-
型はすべて JSON オブジェクト(配列含む)です。文字列は UTF-8。日付型はありません。
|
| 85 |
-
|
| 86 |
-
共通: `category` の値は次のいずれかです。
|
| 87 |
-
`"パフォーマンス" | "飲食" | "物販" | "展示" | "参加型" | "音楽" | "その他"`
|
| 88 |
-
|
| 89 |
-
1) ProjectSummary(企画一覧用)
|
| 90 |
-
- `projectId`: string(例: "62000A")
|
| 91 |
-
- `circleName`: string(団体名)
|
| 92 |
-
- `name`: string(企画名)
|
| 93 |
-
- `category`: string(上記のいずれか)
|
| 94 |
-
- `day1`: boolean
|
| 95 |
-
- `day2`: boolean
|
| 96 |
-
- `day3`: boolean
|
| 97 |
-
- `location`: string
|
| 98 |
-
- `description`: string
|
| 99 |
-
- `prComment`: string
|
| 100 |
-
- `note`: string | null
|
| 101 |
-
- `imageName`: string
|
| 102 |
-
|
| 103 |
-
2) ProjectDetail(企画詳細用)
|
| 104 |
-
- `projectId`, `circleName`, `name`, `category`, `day1`, `day2`, `day3`, `location`, `description`, `prComment`, `note`, `imageName`: ProjectSummary と同じ
|
| 105 |
-
- 追加フィールド:
|
| 106 |
-
- `prCommentLong`: string | null
|
| 107 |
-
- `optionalImageName1`〜`optionalImageName5`: string | null
|
| 108 |
-
- `urlX`, `urlInstagram`, `urlOfficialWebsite`, `urlYoutube`, `urlOther`: string | null
|
| 109 |
-
|
| 110 |
-
3) ProjectIds(検索の通常応答)
|
| 111 |
-
- `projectIds`: string[](スコアの高い順)
|
| 112 |
-
|
| 113 |
-
4) SearchDebugResponse(`debug=true` のとき)
|
| 114 |
-
- `projectIds`: string[](最終並べ替え順)
|
| 115 |
-
- `scores`: { `projectId`: string, `score`: number }[](最終スコア)
|
| 116 |
-
- `details`: { 下記 }[](内部スコアの内訳)
|
| 117 |
-
- `projectId`: string
|
| 118 |
-
- `bm25`: number(単語一致の強さ)
|
| 119 |
-
- `ws_filter_topk`: number(意味類似: ふるい落とし段階)
|
| 120 |
-
- `ws_rerank_pairavg`: number | null(意味類似: 最終並べ替えに使用)
|
| 121 |
-
- `org_boost`: number(団体名/読みの一致による加点)
|
| 122 |
-
- `fused_filter`: number(フィルタ段階���合成スコア)
|
| 123 |
-
- `fused_final`: number(最終合成スコア = 並べ替え基準)
|
| 124 |
-
|
| 125 |
-
---
|
| 126 |
-
|
| 127 |
-
## リクエスト/レスポンス例
|
| 128 |
-
|
| 129 |
-
1) 企画一覧
|
| 130 |
-
```
|
| 131 |
-
GET /api/projects
|
| 132 |
-
Authorization: Bearer <API_ACCESS_KEY>
|
| 133 |
-
X-API-KEY: <API_SECRET_KEY>
|
| 134 |
-
|
| 135 |
-
200 OK
|
| 136 |
-
Content-Encoding: gzip
|
| 137 |
-
Content-Type: application/json
|
| 138 |
-
|
| 139 |
-
[
|
| 140 |
-
{
|
| 141 |
-
"projectId": "62000A",
|
| 142 |
-
"circleName": "〇〇サークル",
|
| 143 |
-
"name": "△△展示",
|
| 144 |
-
"category": "展示",
|
| 145 |
-
"day1": true,
|
| 146 |
-
"day2": false,
|
| 147 |
-
"day3": true,
|
| 148 |
-
"location": "1号館",
|
| 149 |
-
"description": "…",
|
| 150 |
-
"prComment": "…",
|
| 151 |
-
"note": null,
|
| 152 |
-
"imageName": "image.jpg"
|
| 153 |
-
}
|
| 154 |
-
]
|
| 155 |
-
```
|
| 156 |
-
|
| 157 |
-
2) 企画詳細
|
| 158 |
-
```
|
| 159 |
-
GET /api/details?projectId=62000A
|
| 160 |
-
Authorization: Bearer <API_ACCESS_KEY>
|
| 161 |
-
X-API-KEY: <API_SECRET_KEY>
|
| 162 |
-
|
| 163 |
-
200 OK
|
| 164 |
-
{
|
| 165 |
-
"projectId": "62000A",
|
| 166 |
-
"circleName": "〇〇サークル",
|
| 167 |
-
"name": "△△展示",
|
| 168 |
-
"category": "展示",
|
| 169 |
-
"day1": true,
|
| 170 |
-
"day2": false,
|
| 171 |
-
"day3": true,
|
| 172 |
-
"location": "1号館",
|
| 173 |
-
"description": "…",
|
| 174 |
-
"prComment": "…",
|
| 175 |
-
"prCommentLong": null,
|
| 176 |
-
"note": null,
|
| 177 |
-
"imageName": "image.jpg",
|
| 178 |
-
"optionalImageName1": null,
|
| 179 |
-
"optionalImageName2": null,
|
| 180 |
-
"optionalImageName3": null,
|
| 181 |
-
"optionalImageName4": null,
|
| 182 |
-
"optionalImageName5": null,
|
| 183 |
-
"urlX": null,
|
| 184 |
-
"urlInstagram": null,
|
| 185 |
-
"urlOfficialWebsite": null,
|
| 186 |
-
"urlYoutube": null,
|
| 187 |
-
"urlOther": null
|
| 188 |
-
}
|
| 189 |
-
```
|
| 190 |
-
|
| 191 |
-
3) 検索(通常)
|
| 192 |
-
```
|
| 193 |
-
POST /api/search
|
| 194 |
-
Authorization: Bearer <API_ACCESS_KEY>
|
| 195 |
-
X-API-KEY: <API_SECRET_KEY>
|
| 196 |
-
Content-Type: application/json
|
| 197 |
-
|
| 198 |
-
{
|
| 199 |
-
"query": "焼きそば",
|
| 200 |
-
"debug": false
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
200 OK
|
| 204 |
-
{
|
| 205 |
-
"projectIds": ["62000A", "62000B", "62000C"]
|
| 206 |
-
}
|
| 207 |
-
```
|
| 208 |
-
|
| 209 |
-
4) 検索(デバッグ付き)
|
| 210 |
-
```
|
| 211 |
-
POST /api/search
|
| 212 |
-
Authorization: Bearer <API_ACCESS_KEY>
|
| 213 |
-
X-API-KEY: <API_SECRET_KEY>
|
| 214 |
-
Content-Type: application/json
|
| 215 |
-
|
| 216 |
-
{
|
| 217 |
-
"query": "吹奏楽",
|
| 218 |
-
"debug": true
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
200 OK
|
| 222 |
-
{
|
| 223 |
-
"projectIds": ["62010A", "62003C"],
|
| 224 |
-
"scores": [
|
| 225 |
-
{"projectId": "62010A", "score": 0.83},
|
| 226 |
-
{"projectId": "62003C", "score": 0.78}
|
| 227 |
-
],
|
| 228 |
-
"details": [
|
| 229 |
-
{
|
| 230 |
-
"projectId": "62010A",
|
| 231 |
-
"bm25": 0.62,
|
| 232 |
-
"ws_filter_topk": 0.55,
|
| 233 |
-
"ws_rerank_pairavg": 0.57,
|
| 234 |
-
"org_boost": 0.70,
|
| 235 |
-
"fused_filter": 0.59,
|
| 236 |
-
"fused_final": 1.32
|
| 237 |
-
}
|
| 238 |
-
]
|
| 239 |
-
}
|
| 240 |
-
```
|
| 241 |
-
|
| 242 |
-
---
|
| 243 |
-
|
| 244 |
-
## エラーとステータスコード
|
| 245 |
-
|
| 246 |
-
- `400 Bad Request`: `query` が空(検索)
|
| 247 |
-
- `401 Unauthorized`: HF Spaces のプライベートアクセス認証に失敗
|
| 248 |
-
- `403 Forbidden`: アプリ内APIキー認証に失敗(`X-API-KEY`)
|
| 249 |
-
- `404 Not Found`: 企画が見つからない(詳細)
|
| 250 |
-
- `422 Unprocessable Entity`: リクエスト形式が不正(必須フィールド欠落など、FastAPI標準)
|
| 251 |
-
|
| 252 |
-
---
|
| 253 |
-
|
| 254 |
-
## 実装メモ(フロントエンド)
|
| 255 |
-
|
| 256 |
-
- 圧縮レスポンス: `/api/projects` は `Content-Encoding: gzip`。ブラウザの `fetch` は自動解凍します。低レベルHTTPクライアントを使う場合は自動解凍設定を確認してください。
|
| 257 |
-
- 認証ヘッダ: すべての保護エンドポイントに `X-API-KEY` を付けてください。
|
| 258 |
-
- デバッグ検索: チューニング時のみ利用を推奨。ユーザー向けUIでは通常 `debug=false`。
|
| 259 |
-
- 並び順: 検索結果はスコアの高い順です。`projectIds` の順序をそのまま利用してください。
|
| 260 |
-
|
| 261 |
-
---
|
| 262 |
-
|
| 263 |
-
## クライアント例
|
| 264 |
-
|
| 265 |
-
1) fetch(ブラウザ)
|
| 266 |
-
```js
|
| 267 |
-
// 検索
|
| 268 |
-
const res = await fetch(`${process.env.FASTAPI_URL}/api/search`, {
|
| 269 |
-
method: 'POST',
|
| 270 |
-
headers: {
|
| 271 |
-
'Content-Type': 'application/json',
|
| 272 |
-
// レイヤ1: HF Spaces(プライベート)
|
| 273 |
-
'Authorization': `Bearer ${process.env.API_ACCESS_KEY}`,
|
| 274 |
-
// レイヤ2: アプリ内APIキー
|
| 275 |
-
'X-API-Key': process.env.API_SECRET_KEY,
|
| 276 |
-
},
|
| 277 |
-
body: JSON.stringify({ query: '唐揚げ', debug: false }),
|
| 278 |
-
});
|
| 279 |
-
const data = await res.json();
|
| 280 |
-
console.log(data.projectIds);
|
| 281 |
-
```
|
| 282 |
-
|
| 283 |
-
2) curl
|
| 284 |
-
```bash
|
| 285 |
-
curl -X POST \
|
| 286 |
-
-H "Authorization: Bearer $API_ACCESS_KEY" \
|
| 287 |
-
-H "X-API-KEY: $API_SECRET_KEY" \
|
| 288 |
-
-H "Content-Type: application/json" \
|
| 289 |
-
-d '{"query":"焼きそば"}' \
|
| 290 |
-
"$FASTAPI_URL/api/search"
|
| 291 |
-
```
|
| 292 |
-
|
| 293 |
-
---
|
| 294 |
-
|
| 295 |
-
## 環境変数の例(フロントエンド側)
|
| 296 |
-
|
| 297 |
-
- `FASTAPI_URL`: バックエンドのベースURL(例: `https://<space-name>.<org>.hf.space`)
|
| 298 |
-
- `API_SECRET_KEY`: アプリ内APIキー(バックエンド側 `.env` の `API_SECRET_KEY` と一致させる)
|
| 299 |
-
- `API_ACCESS_KEY`: HF Spaces のプライベートアクセス用トークン(Bearer で送る)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/db_connection.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
# MySQL 接続ガイド
|
| 2 |
-
|
| 3 |
-
## 環境変数
|
| 4 |
-
- `DATABASE_URL`: MySQL の DSN。例: `mysql://user:password@db.example.com:3306/chibafes?charset=utf8mb4`
|
| 5 |
-
- `SSL_ROOT_CERT`: 公式 CA 証明書のパス。サーバ証明書の検証を行う場合に指定します。
|
| 6 |
-
- `DB_POOL_MIN_SIZE` / `DB_POOL_MAX_SIZE`: コネクションプールの初期 / 最大コネクション数。既定値は `1 / 5`。
|
| 7 |
-
- `DB_CONNECT_TIMEOUT`: 接続タイムアウト秒数。未設定時は PyMySQL の既定値を利用します。
|
| 8 |
-
- `DB_CHARSET`: DSN に指定が無い場合の既定文字コード。標準は `utf8mb4`。
|
| 9 |
-
|
| 10 |
-
> パスワードに特殊文字が含まれる場合は、`urllib.parse.quote_plus` で URL エンコードした値を使用してください。
|
| 11 |
-
|
| 12 |
-
## 接続オプション
|
| 13 |
-
- DSN のクエリ文字列で `charset`, `autocommit`, `connect_timeout` など PyMySQL の接続パラメータを上書きできます。
|
| 14 |
-
- `SSL_ROOT_CERT` を指定すると `ssl={"ca": ...}` が PyMySQL に渡されます。証明書ファイルは Secrets Manager や環境変数経由で提供し、リポジトリに含めないでください。
|
| 15 |
-
|
| 16 |
-
## PyMySQL の利用
|
| 17 |
-
- `app/db/session.py` では PyMySQL を利用したシンプルなコネクションプールを実装しています。読み取り用途のため、コネクションはオートコミットで取得します。
|
| 18 |
-
- 追加で書き込み処理を実装する場合は、必要に応じてトランザクション管理やリトライ戦略を検討してください。
|
| 19 |
-
|
| 20 |
-
## 動作確認
|
| 21 |
-
1. `export DATABASE_URL="..."` と必要な環境変数をセット。
|
| 22 |
-
2. `python -c "from app.db.session import get_connection;\nwith get_connection() as conn: print(conn.host)"`
|
| 23 |
-
3. エラーなくホスト名が表示されれば接続成功です。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/project_overview.md
DELETED
|
@@ -1,224 +0,0 @@
|
|
| 1 |
-
# プロジェクトの全体像と調整ガイド
|
| 2 |
-
|
| 3 |
-
このドキュメントは、千葉大祭の「企画情報の提供&検索システム」の中身を、初学者向けにやさしく説明したものです。環境構築の手順は README.md にあるので、ここでは仕組みと調整のポイントに絞ります。
|
| 4 |
-
|
| 5 |
-
---
|
| 6 |
-
|
| 7 |
-
## 目次
|
| 8 |
-
|
| 9 |
-
- [1. 全体像(何をしているか)](#1-全体像何をしているか)
|
| 10 |
-
- [2. ディレクトリと主な役割](#2-ディレクトリと主な役割)
|
| 11 |
-
- [3. 検索システムの仕組み(やさしく解説)](#3-検索システムの仕組みやさしく解説)
|
| 12 |
-
- [4. パラメータ解説(どこをどう調整する?)](#4-パラメータ解説どこをどう調整する)
|
| 13 |
-
- [4.1 search_model.json(主なもの)](#41-search_modeljson主なもの)
|
| 14 |
-
- [4.2 files.json(主なもの)](#42-filesjson主なもの)
|
| 15 |
-
- [5. データ作成パイプライン(いつ何を再実行?)](#5-データ作成パイプラインいつ何を再実行)
|
| 16 |
-
- [6. API の使い方(超概要)](#6-api-の使い方超概要)
|
| 17 |
-
- [7. よくある調整手順の例](#7-よくある調整手順の例)
|
| 18 |
-
- [8. ちょっとした注意点とチェックリスト](#8-ちょっとした注意点とチェックリスト)
|
| 19 |
-
- [9. 用語ミニ辞典](#9-用語ミニ辞典)
|
| 20 |
-
|
| 21 |
-
---
|
| 22 |
-
|
| 23 |
-
## 1. 全体像(何をしているか)
|
| 24 |
-
|
| 25 |
-
- 企画情報(団体名・企画名・説明など)を CSV から取り込み、検索しやすい形に整えます。
|
| 26 |
-
- Web API(FastAPI)を通じて、検索キーワードから「関係が強い企画」を並べて返します。
|
| 27 |
-
- 検索は「単語の一致(BM25F)」と「意味の近さ(単語ベクトル)」を組み合わせ、必要なら同義語も広げます。
|
| 28 |
-
|
| 29 |
-
主な流れは次の通りです。
|
| 30 |
-
1) データ作成スクリプトで各種ファイルを生成(後述の scripts/ 参照)
|
| 31 |
-
2) API 起動時に検索用のデータを読み込み(`api/main.py` → `api/search/engine.py`)
|
| 32 |
-
3) リクエストで受け取ったキーワードを分かち書き(SudachiPy)→ 同義語で拡張 → スコアリング → 上位を返す
|
| 33 |
-
|
| 34 |
-
---
|
| 35 |
-
|
| 36 |
-
## 2. ディレクトリと主な役割
|
| 37 |
-
|
| 38 |
-
- `api/`
|
| 39 |
-
- `main.py`: FastAPI のエントリポイント。エンジン初期化、エンドポイント(`/api/search` など)定義。
|
| 40 |
-
- `search/engine.py`: 検索の本体。トークン化、同義語展開、BM25F、意味類似スコア、並び替え等。
|
| 41 |
-
|
| 42 |
-
- `scripts/`(データ作成パイプライン)
|
| 43 |
-
- `build_all.py`: 0→6 の全ステップを順番に実行するランナー。
|
| 44 |
-
- `0_download_data.py`: 単語ベクトル(fastText)のダウンロード。
|
| 45 |
-
- `1_build_dict.py`: Sudachi のユーザー辞書をビルド(任意だが推奨)。
|
| 46 |
-
- `2_create_projects_data.py`: CSV から `projects.json` を生成。
|
| 47 |
-
- `3_build_synonyms_from_sudachi.py`: 同義語辞書(Sudachi公式+カスタム)から、コーパスにある語だけを抜き出してキャッシュ化。
|
| 48 |
-
- `4_prepare_bm25f_meta.py`: BM25F の IDF や平均長を計算して `bm25_meta.json` に保存。
|
| 49 |
-
- `5_prepare_tf_token.py`: 各文書のフィールド別 TF/長さを `tf_token.json` に保存。
|
| 50 |
-
- `6_build_word_embeddings.py`: コーパス語彙に対応する fastText ベクトルを抽出して保存。
|
| 51 |
-
|
| 52 |
-
- `config/`
|
| 53 |
-
- `search_model.json`: 検索のふるまい(同義語の有無、重み、しきい値など)の設定。
|
| 54 |
-
- `files.json`: 各種入力・出力ファイルのパス定義。
|
| 55 |
-
|
| 56 |
-
- `schemas/`
|
| 57 |
-
- `projects.py`: API レスポンスのデータ形(一覧・詳細・検索結果の ID 群)。
|
| 58 |
-
- `tf_token.py`: 検索前処理で使う中間データの形(TF/長さなど)。
|
| 59 |
-
|
| 60 |
-
- `utils/`
|
| 61 |
-
- `json.py`: 設定ファイルから値を取得、JSON の安全書き出しなど。
|
| 62 |
-
- `io.py`: 行ストリーム読み込みなどの小物。
|
| 63 |
-
- `text_process.py`: 見えない制御文字の除去など。
|
| 64 |
-
|
| 65 |
-
- その他
|
| 66 |
-
- `resources/`: 入力素材(CSV、同義語辞書、ストップワード、ユーザー辞書、ベクトル等)。
|
| 67 |
-
- `Dockerfile`: コンテナ実行向けの定義(概要のみ把握でOK)。
|
| 68 |
-
- `.github/workflows/deploy_to_hf_space.yaml`: Hugging Face Spaces へのデプロイ用ワークフロー(概要のみ把握でOK)。
|
| 69 |
-
|
| 70 |
-
---
|
| 71 |
-
|
| 72 |
-
## 3. 検索システムの仕組み(やさしく解説)
|
| 73 |
-
|
| 74 |
-
検索は「単語での一致」と「意味の近さ」をミックスして、必要なら「団体名などの前方一致」も加点して並び順を決めます。
|
| 75 |
-
|
| 76 |
-
1) トークン化(単語に分ける)
|
| 77 |
-
- SudachiPy を使い、名詞・動詞・形容詞などの重要品詞だけを対象にします。
|
| 78 |
-
- ストップワード(よく出るが意味が薄い語)や禁止語は除外します。
|
| 79 |
-
|
| 80 |
-
2) 同義語の拡張(必要に応じて)
|
| 81 |
-
- 例:「大学祭」→「学園祭」も一緒に検索する、のようにキーワードを広げます。
|
| 82 |
-
- Sudachi の同義語辞書+プロジェクト内語彙にあるものだけを採用。出しすぎないように上限を設けます。
|
| 83 |
-
|
| 84 |
-
3) BM25F で「単語一致の強さ」を計算
|
| 85 |
-
- BM25 は「その単語がどれくらい珍しいか(IDF)」や「どれくらい繰り返し出るか(TF)」をもとにスコア化します。
|
| 86 |
-
- F は Field の F。タイトルは重要、長文は少し抑えめ…といった「フィールドごとの重み」を付けられます。
|
| 87 |
-
|
| 88 |
-
4) 単語ベクトルで「意味の近さ」を計算
|
| 89 |
-
- fastText などの単語ベクトルを使い、「言い換え」でも近さを捉えます。
|
| 90 |
-
- 資料がない単語は、fastText の subword 機能で「なんとなくの方向」を推測して補います(OOV対応)。
|
| 91 |
-
- まずフィルタ用に top-k 集約でざっくり類似度を出し、最終並べ替え時に平均方式でもう一度なめらかに評価します。
|
| 92 |
-
|
| 93 |
-
5) 団体名などへのブースト(加点)
|
| 94 |
-
- 入力文字列が「団体名」や「読み」に完全一致/前方一致/部分一致した場合、加点して上に来やすくします。
|
| 95 |
-
|
| 96 |
-
6) 閾値と上限で絞る → 並べ替え
|
| 97 |
-
- 弱すぎる候補は捨て、最大件数までに絞って、合成スコアの高い順に返します。
|
| 98 |
-
|
| 99 |
-
---
|
| 100 |
-
|
| 101 |
-
## 4. パラメータ解説(どこをどう調整する?)
|
| 102 |
-
|
| 103 |
-
調整の中心は `config/search_model.json` と `config/files.json` です。まずは `search_model.json` から。
|
| 104 |
-
|
| 105 |
-
### 4.1 search_model.json(主なもの)
|
| 106 |
-
|
| 107 |
-
- 同義語関連(`synonyms`)
|
| 108 |
-
- `enable`: 同義語を使うか。誤ヒットが増えたら false も検討。
|
| 109 |
-
- `sources.sudachi`: Sudachi の同義語辞書を使うか。
|
| 110 |
-
- `sources.custom_json`: 追加入力のパス(`resources/synonyms_custom.json`)。独自の言い換えを足せます。
|
| 111 |
-
- `limits.max_expansions_per_term`: 1語あたり何個まで広げるか。多いほど網羅的、誤ヒットも増えやすい。
|
| 112 |
-
- `limits.max_query_variants`: 全体の拡張上限。過剰拡張を防ぐ安全弁。
|
| 113 |
-
- `limits.min_char_len`: これ未満の短い語は拡張しない。
|
| 114 |
-
- `banlist`: 拡張・検索対象から外す語(例: 「部」「会」など一般的すぎる語)。
|
| 115 |
-
|
| 116 |
-
- BM25F(`bm25f`)
|
| 117 |
-
- `k1`/`b`: BM25 の基本パラメータ。通常は標準値(例: 1.2/0.75)から大きく動かさない。
|
| 118 |
-
- `field_weights`: フィールドごとの重要度。`name` を上げるとタイトル一致が強くなります。
|
| 119 |
-
|
| 120 |
-
- 意味類似(`word_sim`)
|
| 121 |
-
- `enable`: 有効化するか。無関係の表現で拾いすぎるなら false を検討。
|
| 122 |
-
- `alpha`: BM25 と意味類似の混ぜ具合。1.0 に近いほど BM25 寄り、0.0 に近いほど意味寄り。
|
| 123 |
-
- `topk_k`: フィルタ段階で使う「上位 k 個」をどれだけ拾うか。小さいと厳しめ。
|
| 124 |
-
- `rerank`: 最終並べ替えに使う類似度の計算方式。`pair_avg` はなめらかで扱いやすい。
|
| 125 |
-
|
| 126 |
-
- サブワード(`query_subword`)
|
| 127 |
-
- `enable`: fastText の subword で未知語を補うか。
|
| 128 |
-
- `oov_weight`: 未知語ベクトルの重み(小さいほど控えめに採用)。
|
| 129 |
-
|
| 130 |
-
- 団体名ブースト(`circleName.boost`)
|
| 131 |
-
- `exact`/`prefix`/`substring`: それぞれ完全一致/前方一致/部分一致の加点量。
|
| 132 |
-
- `min_len`: 短すぎる入力で過剰加点しないための最小文字数。
|
| 133 |
-
|
| 134 |
-
- フィルタ(`filter`)
|
| 135 |
-
- `min_results`/`max_results`: 返す件数の下限/上限。
|
| 136 |
-
- `bm25_min`/`word_sim_min`/`fused_min`: それぞれ BM25・意味類似・合成スコアでの最低ライン。
|
| 137 |
-
- `fused_rel_top_ratio`: 「トップのスコアに対する割合」で足切りする係数。0.5 ならトップの半分未満を落とすイメージ。
|
| 138 |
-
|
| 139 |
-
調整のコツ(例)
|
| 140 |
-
- 「タイトル一致をもっと強く」→ `bm25f.field_weights.name` を上げる。
|
| 141 |
-
- 「言い換えも拾いたいが誤ヒットが増える」→ `word_sim.alpha` を少し上げる(BM25寄りに)。
|
| 142 |
-
- 「似ている語で広がりすぎる」→ `synonyms.enable=false` か `limits` を厳しめに。
|
| 143 |
-
- 「団体名の指名検索を最優先に」→ `circleName.boost.exact` を上げる。
|
| 144 |
-
- 「全体的に弱い」→ `filter.fused_min` を下げるか、`fused_rel_top_ratio` を下げて緩める。
|
| 145 |
-
|
| 146 |
-
### 4.2 files.json(主なもの)
|
| 147 |
-
|
| 148 |
-
- `sudachi.*`: Sudachi の設定(設定 JSON、ユーザー辞書、同義語、ストップワード、同義語キャッシュの保存先)。
|
| 149 |
-
- `projects.*`: 入力 CSV と、出力される `projects.json` の場所。
|
| 150 |
-
- `bm25.*`: BM25 のメタ情報(IDF/平均長)と `tf_token.json` の場所。
|
| 151 |
-
- `embeddings.*`: fastText の `.vec` / `.bin` と、抽出した語彙・ベクトルの保存先。
|
| 152 |
-
|
| 153 |
-
パスを変えると生成物の出力先も変わるので、変更後は該当ステップを作り直してください。
|
| 154 |
-
|
| 155 |
-
---
|
| 156 |
-
|
| 157 |
-
## 5. データ作成パイプライン(いつ何を再実行?)
|
| 158 |
-
|
| 159 |
-
全自動は `python scripts/build_all.py` ですが、調整時は必要な所だけを再実行すると速いです。
|
| 160 |
-
|
| 161 |
-
- CSV を更新したら → `2`、`3`、`4`、`5`(+ `6` も必要なら)
|
| 162 |
-
- `synonyms_custom.json` を更新したら → `3`
|
| 163 |
-
- `search_model.json` の品詞や `target_fields` を変えたら → `3`、`4`、`5`(+ `6`)
|
| 164 |
-
- `field_weights` やフィルタ閾値だけ変えた → 再ビルド不要(API 再起動のみ)
|
| 165 |
-
- fastText を入れ替えた → `6`
|
| 166 |
-
|
| 167 |
-
各ステップの要約
|
| 168 |
-
- 0: fastText のダウンロード(大容量。最初だけ)
|
| 169 |
-
- 1: ユーザー辞書ビルド(任意。固有名詞を強くしたいときに)
|
| 170 |
-
- 2: CSV → `projects.json`
|
| 171 |
-
- 3: 同義語キャッシュ生成
|
| 172 |
-
- 4: BM25F メタ情報(IDF/平均長)
|
| 173 |
-
- 5: フィールド別 TF/長さ
|
| 174 |
-
- 6: 語彙ベクトル抽出(`.vec` がある場合)
|
| 175 |
-
|
| 176 |
-
---
|
| 177 |
-
|
| 178 |
-
## 6. API の使い方(超概要)
|
| 179 |
-
|
| 180 |
-
- `GET /api/health`: ヘルスチェック。
|
| 181 |
-
- `GET /api/projects`: 企画一覧(圧縮返却)。
|
| 182 |
-
- `GET /api/details?projectId=...`: 個別企画の詳細。
|
| 183 |
-
- `POST /api/search`(本文: `{ "query": "...", "debug": false }`): 関連が高い順の `projectIds` を返す。`debug=true` で内部スコアの簡易情報も返却。
|
| 184 |
-
- ヘッダ `X-API-KEY` が必要(キー名のみ言及。値は README 参照)。
|
| 185 |
-
|
| 186 |
-
---
|
| 187 |
-
|
| 188 |
-
## 7. よくある調整手順の例
|
| 189 |
-
|
| 190 |
-
例1: 「タイトル一致を最優先にしたい」
|
| 191 |
-
1) `config/search_model.json` の `bm25f.field_weights.name` を少し上げる(例: 2.0 → 2.5)。
|
| 192 |
-
2) API を再起動して挙動を確認。
|
| 193 |
-
|
| 194 |
-
例2: 「言い換えの拾いすぎを抑えたい」
|
| 195 |
-
1) `synonyms.enable` を false にする、または `limits.max_expansions_per_term` を下げる。
|
| 196 |
-
2) 適宜 `word_sim.alpha` を上げて BM25 寄りに。
|
| 197 |
-
3) 必要に応じて `scripts/3_build_synonyms_from_sudachi.py` を再実行。
|
| 198 |
-
|
| 199 |
-
例3: 「“団体名”の指名検索をもっと強く」
|
| 200 |
-
1) `circleName.boost.exact` や `prefix` を上げる。
|
| 201 |
-
2) 極端になりすぎたら `filter` の閾値で下支え。
|
| 202 |
-
|
| 203 |
-
---
|
| 204 |
-
|
| 205 |
-
## 8. ちょっとした注意点とチェックリスト
|
| 206 |
-
|
| 207 |
-
- Sudachi の対象品詞(`target_pos_l1`)や対象フィールド(`target_fields`)を変えると、多くの中間データを作り直す必要があります(3,4,5,6)。
|
| 208 |
-
- `.bin`(fastText Subword)は OOV 補助用、`.vec` は語彙ベクトル抽出用です。両方あると最もリッチに動きます。
|
| 209 |
-
- デバッグ時は `POST /api/search` で `debug=true` を使うと、BM25/類似/ブースト/合成の内訳が見られます。
|
| 210 |
-
- 値は README の .env を参照(ここではキー名のみ)。例: `API_SECRET_KEY`, `HF_TOKEN`, `HF_EMBEDDINGS_REPO_ID`。
|
| 211 |
-
|
| 212 |
-
---
|
| 213 |
-
|
| 214 |
-
## 9. 用語ミニ辞典
|
| 215 |
-
|
| 216 |
-
- BM25/BM25F: 単語の一致度をスコア化する仕組み。F はフィールド重み付きの拡張版。
|
| 217 |
-
- IDF: 珍しい単語ほど高い値(差別力がある)。
|
| 218 |
-
- fastText: 単語をベクトルにする技術。未知語も subword で推測可能。
|
| 219 |
-
- OOV: 語彙外(Out-Of-Vocabulary)。手元の語彙にない単語のこと。
|
| 220 |
-
|
| 221 |
-
---
|
| 222 |
-
|
| 223 |
-
困ったら、まずは `config/search_model.json` を少しずつ調整して、効果を `debug=true` で確認するのがおすすめです。
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/project_schema_change_points.md
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
# Project Schema / Data Source Migration – Change Points
|
| 2 |
-
|
| 3 |
-
## Background
|
| 4 |
-
- `schemas/projects.py` now defines DB-backed models whose core fields are `description` (企画内容), `prSummary` (PRコメント), `prDetail` (PRコメント詳細) together with `notes`, `images`, and `urls`.
|
| 5 |
-
- Batch scripts under `scripts/` must keep generating search assets from the database export (`projects.json`) instead of the legacy CSV.
|
| 6 |
-
- The API layer (`app/main.py`) serves the DB-derived models directly, while the search engine (`app/search/engine.py`) consumes the statically generated artefacts.
|
| 7 |
-
|
| 8 |
-
## Config (`config/files.json`)
|
| 9 |
-
- Remove the unused `projects.original_csv` entry and expose an explicit `projects.output_json` mapping so scripts resolve the generated JSON path without relying on hard-coded fallbacks.
|
| 10 |
-
|
| 11 |
-
## Batch Scripts (`scripts/`)
|
| 12 |
-
### `scripts/2_create_projects_data.py`
|
| 13 |
-
- Lines `20-32` call `ProjectsRepository.list_projects()` and then pass the resulting Pydantic models straight into `json_dumps`; convert them to serialisable dictionaries via `model_dump(mode="json")` (or equivalent) before writing.
|
| 14 |
-
- Ensure `ProjectsRepository` and `app/repositories/query.sql` surface `prSummary`, `prDetail`, and `notes` so the exported JSON aligns with `schemas.projects.Project`.
|
| 15 |
-
|
| 16 |
-
### `scripts/3_build_synonyms_from_sudachi.py`
|
| 17 |
-
- The loader at `scripts/3_build_synonyms_from_sudachi.py:65-112` instantiates `Project` objects and iterates `target_fields`. Verify that the regenerated `projects.json` exposes `name`, `circleName`, `circleNameKana`, `description`, `prSummary`, and `prDetail`; missing keys will degrade vocabulary extraction for synonyms.
|
| 18 |
-
|
| 19 |
-
### `scripts/4_prepare_bm25f_meta.py`
|
| 20 |
-
- The token loop at `scripts/4_prepare_bm25f_meta.py:52-104` depends on the same field names. Confirm that `projects.json` no longer contains legacy keys such as `organization`/`title`, otherwise IDF statistics will be incorrect.
|
| 21 |
-
|
| 22 |
-
### `scripts/5_prepare_tf_token.py`
|
| 23 |
-
- Update the `tf_token.Fields` construction at `scripts/5_prepare_tf_token.py:108-115` to use `name`, `circleName`, `circleNameKana`, `description`, `prSummary`, and `prDetail`. This keeps the TF schema consistent with the updated models.
|
| 24 |
-
- Continue serialising with `model_dump()` at `scripts/5_prepare_tf_token.py:117-127` once the field names match.
|
| 25 |
-
|
| 26 |
-
### `scripts/7_prepare_circle_names.py`
|
| 27 |
-
- The mapper at `scripts/7_prepare_circle_names.py:68-74` still relies on `Project.organization` / `Project.reading`. Replace these with `circleName` / `circleNameKana` so the generated circle name and substring assets remain accurate.
|
| 28 |
-
|
| 29 |
-
## API Layer (`app/main.py`)
|
| 30 |
-
- `app/main.py:94-105` currently dumps `ProjectSummary` instances directly. Convert to `model_dump()` (or `jsonable_encoder`) before gzipping to avoid serialisation errors.
|
| 31 |
-
- `ProjectSummary.image` should pick the `Image` entry whose `order == 0`. If multiple images share `order == 0`, use the *last* one encountered to mirror repository behaviour.
|
| 32 |
-
- Ensure the summary payload exposes `notes` (renamed from `note`) and the PR fields `prSummary` / `prDetail` as returned by the repository.
|
| 33 |
-
|
| 34 |
-
## Search Engine (`app/search/engine.py`)
|
| 35 |
-
- In `SearchEngine.initialize` (`app/search/engine.py:164-175`) ensure kana norms are stored directly in `self.reading_norms` so substring boosts continue to consider kana values during scoring.
|
| 36 |
-
- Debug responses at `app/search/engine.py:643-645` reference `organization` / `title`; switch these to `circleName` / `name` so diagnostics stay meaningful with the new schema.
|
| 37 |
-
- Rebuild all search assets (`projects.json`, `tf_token.json`, `bm25_meta.json`, `substring_index.json`) after applying the script updates above.
|
| 38 |
-
|
| 39 |
-
## Open Questions / Follow-ups
|
| 40 |
-
1. None – requirements above clarify the PR field names, notes rename, image selection rule, and config key addition.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/システムガイド.md
ADDED
|
@@ -0,0 +1,961 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 千葉大祭 企画情報検索システム - 完全ガイド
|
| 2 |
+
|
| 3 |
+
**作成日**: 2025年12月11日
|
| 4 |
+
**対象**: 大学1~2年生向けのシステム解説
|
| 5 |
+
**言語**: 日本語
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 目次
|
| 10 |
+
|
| 11 |
+
1. [全体像](#1-全体像)
|
| 12 |
+
2. [システムアーキテクチャ](#2-システムアーキテクチャ)
|
| 13 |
+
3. [ディレクトリ構成と役割](#3-ディレクトリ構成と役割)
|
| 14 |
+
4. [検索システムの仕組み](#4-検索システムの仕組み)
|
| 15 |
+
5. [設定ファイルとカスタマイズ](#5-設定ファイルとカスタマイズ)
|
| 16 |
+
6. [データの流れと更新方法](#6-データの流れと更新方法)
|
| 17 |
+
7. [APIの使い方](#7-apiの使い方)
|
| 18 |
+
8. [開発・デプロイ](#8-開発デプロイ)
|
| 19 |
+
9. [トラブルシューティング](#9-トラブルシューティング)
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 1. 全体像
|
| 24 |
+
|
| 25 |
+
### 何をするシステムか
|
| 26 |
+
|
| 27 |
+
このシステムは、千葉大祭に出店する団体の「企画情報」を効率よく提供・検索するためのものです。
|
| 28 |
+
|
| 29 |
+
**具体的な機能:**
|
| 30 |
+
- **企画データの管理**: 団体名、企画名、説明、写真などを一元管理
|
| 31 |
+
- **高度な検索**: キーワードから関連する企画を「正確に」「素早く」見つける
|
| 32 |
+
- **API提供**: Web・モバイルアプリなどのフロントエンドに情報を提供
|
| 33 |
+
|
| 34 |
+
### なぜ「高度な検索」が必要か
|
| 35 |
+
|
| 36 |
+
単純なキーワード完全一致だけでは、ユーザーの満足度が低いです。例えば:
|
| 37 |
+
|
| 38 |
+
- ユーザーが「学園祭」と検索 → 「大学祭」という企画も関連しているはず
|
| 39 |
+
- ユーザーが「ラーメン」と検索 → 「そば」なども同じくらい検索対象にしたい
|
| 40 |
+
- ユーザーが「カレー 販売」と検索 → どちらかのキーワードだけでなく、両方関連する企画を上位に
|
| 41 |
+
|
| 42 |
+
このシステムは、以下の手法を組み合わせて、こうした「意味的に関連した」企画を見つけ出します:
|
| 43 |
+
|
| 44 |
+
1. **BM25F**: 単語の出現頻度と珍しさから「単語一致の強さ」を計算
|
| 45 |
+
2. **単語ベクトル**: 単語の「意味的な近さ」を計算
|
| 46 |
+
3. **同義語展開**: 「学園祭」と「大学祭」など言い換えを自動認識
|
| 47 |
+
4. **団体名マッチング**: 入力が団体名に該当すれば優先的に表示
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## 2. システムアーキテクチャ
|
| 52 |
+
|
| 53 |
+
```
|
| 54 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 55 |
+
│ フロントエンド(Web/App) │
|
| 56 |
+
└─────────────────────────────────────────────────────────────┘
|
| 57 |
+
↓
|
| 58 |
+
┌─────────────────────┐
|
| 59 |
+
│ FastAPI サーバー │
|
| 60 |
+
│ (app/main.py) │
|
| 61 |
+
└─────────────────────┘
|
| 62 |
+
↓ ↓
|
| 63 |
+
┌──────────────────┐ ┌──────────────────┐
|
| 64 |
+
│ 検索エンジン │ │ MySQLデータベース │
|
| 65 |
+
│ (search/engine) │ │ (企画データ) │
|
| 66 |
+
└──────────────────┘ └──────────────────┘
|
| 67 |
+
↓
|
| 68 |
+
┌────────────────────────────────────┐
|
| 69 |
+
│ 静的アセット(JSONファイル等) │
|
| 70 |
+
│ - projects.json (企画データ) │
|
| 71 |
+
│ - bm25_meta.json (検索用統計) │
|
| 72 |
+
│ - synonyms_cache.json (同義語) │
|
| 73 |
+
│ - word_vectors.npz (単語ベクトル) │
|
| 74 |
+
└────────────────────────────────────┘
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
**データの流れ:**
|
| 78 |
+
|
| 79 |
+
1. **初期化時**(サーバー起動)
|
| 80 |
+
- MySQLから最新の企画データを取得
|
| 81 |
+
- 検索用の各種アセット(JSON、ベクトルなど)をメモリに読み込み
|
| 82 |
+
- 検索エンジンを初期化
|
| 83 |
+
|
| 84 |
+
2. **検索時**(ユーザーがキーワード入力)
|
| 85 |
+
- キーワードを形態素解析(単語に分割)
|
| 86 |
+
- BM25F + 単語ベクトル で各企画のスコアを計算
|
| 87 |
+
- スコアの高い順に結果を返す
|
| 88 |
+
|
| 89 |
+
3. **企画データ更新時**
|
| 90 |
+
- MySQLの企画情報を更新
|
| 91 |
+
- スクリプトで検索用アセットを再生成
|
| 92 |
+
- サーバーを再起動
|
| 93 |
+
|
| 94 |
+
---
|
| 95 |
+
|
| 96 |
+
## 3. ディレクトリ構成と役割
|
| 97 |
+
|
| 98 |
+
```
|
| 99 |
+
chibafes-website-api-v2/
|
| 100 |
+
├── app/ # メインアプリケーション
|
| 101 |
+
│ ├── main.py # FastAPI エントリポイント
|
| 102 |
+
│ ├── search/
|
| 103 |
+
│ │ └── engine.py # 検索エンジン実装
|
| 104 |
+
│ ├── repositories/
|
| 105 |
+
│ │ ├── projects_repository.py # DB連携(データ取得)
|
| 106 |
+
│ │ └── query.sql # SQL クエリ
|
| 107 |
+
│ ├── services/
|
| 108 |
+
│ │ └── projects_service.py # ビジネスロジック
|
| 109 |
+
│ ├── db/
|
| 110 |
+
│ │ └── session.py # DB接続管理
|
| 111 |
+
│ └── api/
|
| 112 |
+
│ └── routers/ # API エンドポイント定義
|
| 113 |
+
│
|
| 114 |
+
├── scripts/ # データ生成パイプライン
|
| 115 |
+
│ ├── build_all.py # 全ステップを実行(推奨)
|
| 116 |
+
│ ├── 0_download_data.py # fastText ベクトルをダウンロード
|
| 117 |
+
│ ├── 1_build_dict.py # Sudachi ユーザー辞書を構築
|
| 118 |
+
│ ├── 2_create_projects_data.py # DB → projects.json
|
| 119 |
+
│ ├── 3_build_synonyms_from_sudachi.py # 同義語キャッシュ生成
|
| 120 |
+
│ ├── 4_prepare_bm25f_meta.py # BM25F統計を計算
|
| 121 |
+
│ ├── 5_prepare_tf_token.py # TF/トークン情報を保存
|
| 122 |
+
│ ├── 6_build_word_embeddings.py # 単語ベクトルを抽出
|
| 123 |
+
│ └── 7_prepare_circle_names.py # 団体名インデックスを生成
|
| 124 |
+
│
|
| 125 |
+
├── config/ # 設定ファイル
|
| 126 |
+
│ ├── search_model.json # 検索挙動の詳細設定
|
| 127 |
+
│ └── files.json # ファイルパスマッピング
|
| 128 |
+
│
|
| 129 |
+
├── schemas/ # データモデル定義
|
| 130 |
+
│ └── projects.py # Project, ProjectSummary 等
|
| 131 |
+
│
|
| 132 |
+
├── resources/ # 入力素材
|
| 133 |
+
│ ├── stopwords.json # ストップワード(検索対象外の語)
|
| 134 |
+
│ ├── synonyms_custom.json # カスタム同義語辞書
|
| 135 |
+
│ ├── user_dict.csv # Sudachi ユーザー辞書
|
| 136 |
+
│ └── embeddings/ # 単語ベクトル(fastText)
|
| 137 |
+
│
|
| 138 |
+
├── data/generated/ # 自動生成されるファイル
|
| 139 |
+
│ ├── projects.json # 企画データ(検索用)
|
| 140 |
+
│ ├── bm25_meta.json # BM25F用統計
|
| 141 |
+
│ ├── tf_token.json # TF/トークン情報
|
| 142 |
+
│ ├── synonyms_cache.json # 処理済み同義語
|
| 143 |
+
│ ├── word_vocab.json # 単語→インデックスの対応表
|
| 144 |
+
│ ├── word_vectors.npz # 単語ベクトル行列
|
| 145 |
+
│ ├── circle_names.json # 団体名インデックス
|
| 146 |
+
│ └── substring_index.json # 部分文字列マッチ用インデックス
|
| 147 |
+
│
|
| 148 |
+
├── utils/ # ユーティリティ
|
| 149 |
+
│ ├── json.py # JSON操作
|
| 150 |
+
│ ├── io.py # ファイルI/O
|
| 151 |
+
│ ├── logger.py # ログ出力
|
| 152 |
+
│ └── text_process.py # テキスト前処理
|
| 153 |
+
│
|
| 154 |
+
└── docs/ # ドキュメント
|
| 155 |
+
└── *.md # このファイルを含む説明書
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### 主要ファイルの役割
|
| 159 |
+
|
| 160 |
+
#### `app/main.py` - APIサーバーのエントリポイント
|
| 161 |
+
FastAPIで、フロントエンドからのリクエストを受け取り、検索結果やデータを返します。
|
| 162 |
+
|
| 163 |
+
**主なエンドポイント:**
|
| 164 |
+
- `GET /api/health`: サーバーの状態確認
|
| 165 |
+
- `GET /api/projects`: 全企画の一覧を取得
|
| 166 |
+
- `GET /api/details?projectId=xxx`: 特定の企画の詳細を取得
|
| 167 |
+
- `POST /api/search`: キーワード検索を実行
|
| 168 |
+
|
| 169 |
+
#### `app/search/engine.py` - 検索エンジンの核
|
| 170 |
+
BM25F、単語ベクトル、同義語など各種手法を組み合わせて検索スコアを計算します。
|
| 171 |
+
|
| 172 |
+
**主な処理:**
|
| 173 |
+
- トークン化:キーワードを単語に分割
|
| 174 |
+
- 同義語展開:「学園祭」→「大学祭」なども検索対象に
|
| 175 |
+
- BM25F計算:単語の出現パターンからスコア化
|
| 176 |
+
- ベクトル類似度:意味的な近さを計算
|
| 177 |
+
- スコア融合:複数の指標を組み合わせて最終スコアを算出
|
| 178 |
+
|
| 179 |
+
#### `config/search_model.json` - 検索の挙動を制御
|
| 180 |
+
検索エンジンの細かな動作を調整するための設定ファイルです。後述の「カスタマイズ」で詳しく説明します。
|
| 181 |
+
|
| 182 |
+
---
|
| 183 |
+
|
| 184 |
+
## 4. 検索システムの仕組み
|
| 185 |
+
|
| 186 |
+
### 全体フロー
|
| 187 |
+
|
| 188 |
+
```
|
| 189 |
+
ユーザーの入力 "カレー 販売"
|
| 190 |
+
↓
|
| 191 |
+
┌──────────────────┐
|
| 192 |
+
│ トークン化(形態素解析)│ → ["カレー", "販売"]
|
| 193 |
+
└──────────────────┘
|
| 194 |
+
↓
|
| 195 |
+
┌──────────────────┐
|
| 196 |
+
│ 同義語を展開 │ → ["カレー", "販売", "食事", "販売"]
|
| 197 |
+
└──────────────────┘
|
| 198 |
+
↓
|
| 199 |
+
┌─────────────��───────────────┐
|
| 200 |
+
│ 各企画に対して3つのスコアを計算 │
|
| 201 |
+
│ 1. BM25F (単語一致度) │
|
| 202 |
+
│ 2. 単語ベクトル類似度 │
|
| 203 |
+
│ 3. 団体名マッチングボーナス │
|
| 204 |
+
└─────────────────────────────┘
|
| 205 |
+
↓
|
| 206 |
+
┌──────────────────┐
|
| 207 |
+
│ スコア統合 │ → 複数指標を1つのスコアに
|
| 208 |
+
└──────────────────┘
|
| 209 |
+
↓
|
| 210 |
+
┌──────────────────┐
|
| 211 |
+
│ 閾値フィルタ │ → 低すぎるスコアを除外
|
| 212 |
+
└──────────────────┘
|
| 213 |
+
↓
|
| 214 |
+
┌──────────────────┐
|
| 215 |
+
│ ソート&返却 │ → スコア順に上位30件を返す
|
| 216 |
+
└──────────────────┘
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
### 4.1 トークン化(形態素解析)
|
| 220 |
+
|
| 221 |
+
**概念:** テキストを「意味を持つ最小単位」に分割します。
|
| 222 |
+
|
| 223 |
+
例:
|
| 224 |
+
```
|
| 225 |
+
入力:"カレー販売団体の美味しい食事"
|
| 226 |
+
↓
|
| 227 |
+
トークン化:["カレー", "販売", "団体", "美味しい", "食事"]
|
| 228 |
+
(助詞「の」などは除外)
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
**実装:** SudachiPyという形態素解析エンジンを使用します。これは日本語を高精度に分析できます。
|
| 232 |
+
|
| 233 |
+
**カスタマイズ方法:**
|
| 234 |
+
- `resources/stopwords.json`: 除外する語を指定(例:「のが」など)
|
| 235 |
+
- `resources/user_dict.csv`: 新しい単語を登録(例:ニッチなサークル名など)
|
| 236 |
+
|
| 237 |
+
---
|
| 238 |
+
|
| 239 |
+
### 4.2 同義語展開
|
| 240 |
+
|
| 241 |
+
**概念:** 異なる表現でも同じ意味の語を自動的に広げます。
|
| 242 |
+
|
| 243 |
+
例:
|
| 244 |
+
```
|
| 245 |
+
入力語:"大学祭"
|
| 246 |
+
↓
|
| 247 |
+
同義語展開:["大学祭", "学園祭", "キャンパスフェスタ"]
|
| 248 |
+
(すべてのバリエーションで検索)
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
**メリット:** ユーザーがどのバリエーションで入力しても、関連企画が見つかります。
|
| 252 |
+
|
| 253 |
+
**データソース:** 2つの辞書から同義語を取得
|
| 254 |
+
1. **Sudachi公式辞書**: 標準的な同義語(無料)
|
| 255 |
+
2. **カスタム同義語** (`resources/synonyms_custom.json`): プロジェクト固有の同義語を手動追加
|
| 256 |
+
|
| 257 |
+
**カスタマイズ方法:**
|
| 258 |
+
```json
|
| 259 |
+
// resources/synonyms_custom.json の例
|
| 260 |
+
{
|
| 261 |
+
"カレー": ["食事", "飲食"],
|
| 262 |
+
"ステージ": ["舞台", "パフォーマンス"],
|
| 263 |
+
"学園祭": ["大学祭", "キャンパスフェスタ"]
|
| 264 |
+
}
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
---
|
| 268 |
+
|
| 269 |
+
### 4.3 BM25F - 単語一致の強さを計算
|
| 270 |
+
|
| 271 |
+
**概念:** 単語がどれくらい「珍しいか」「繰り返し出ているか」から検索スコアを計算します。
|
| 272 |
+
|
| 273 |
+
**簡単な説明:**
|
| 274 |
+
|
| 275 |
+
1. **IDF(珍しさスコア)**: 多くの企画に出現する語は価値が低く、少ない企画にしか出現しない語は価値が高い
|
| 276 |
+
- 例:「食べ物」は多くの企画に出現→低い価値
|
| 277 |
+
- 例:「生ビール」は少ない企画にしか出現→高い価値
|
| 278 |
+
|
| 279 |
+
2. **TF(繰り返し度)**: ある企画の中で、その単語がどれくらい繰り返し出ているか
|
| 280 |
+
- 例:「カレー」という企画説明に「カレー」という単語が5回出現→高いTF
|
| 281 |
+
|
| 282 |
+
3. **フィールド重み**: 企画名に出現する単語と、長い説明文に出現する単語では重要度が異なる
|
| 283 |
+
- 例:企画名 (重み 2.0) > 説明文 (重み 1.0)
|
| 284 |
+
|
| 285 |
+
**実装の詳細は「検索アルゴリズム詳説」を参照**
|
| 286 |
+
|
| 287 |
+
---
|
| 288 |
+
|
| 289 |
+
### 4.4 単語ベクトル - 意味的な近さを計算
|
| 290 |
+
|
| 291 |
+
**概念:** 単語を「多次元空間の点」として表現し、距離の近さで「意味の似ている度」を計算します。
|
| 292 |
+
|
| 293 |
+
例:
|
| 294 |
+
```
|
| 295 |
+
← 意味が似ている →
|
| 296 |
+
"カレー" と "スープ" は空間上で近い
|
| 297 |
+
"カレー" と "プログラミング" は空間上で遠い
|
| 298 |
+
```
|
| 299 |
+
|
| 300 |
+
**メリット:**
|
| 301 |
+
- 「カレー」で検索 → 「ラーメン」「そば」なども関連として見つかる
|
| 302 |
+
- 同義語辞書に登録していない言い換えでも対応
|
| 303 |
+
|
| 304 |
+
**実装:** fastTextという機械学習モデルを使用。このモデルは、大量の日本語テキストから事前に学習済みです。
|
| 305 |
+
|
| 306 |
+
**カスタマイズ方法:**
|
| 307 |
+
実装難度が高いため、通常はカスタマイズ不要です。スコアウエイトは設定ファイルで調整できます(後述)。
|
| 308 |
+
|
| 309 |
+
---
|
| 310 |
+
|
| 311 |
+
### 4.5 団体名マッチング - 直接一致の加点
|
| 312 |
+
|
| 313 |
+
**概念:** ユーザーの入力が「団体名」や「団体名の読み仮名」に合致した場合、スコアを加算します。
|
| 314 |
+
|
| 315 |
+
例:
|
| 316 |
+
```
|
| 317 |
+
入力:"写真部"
|
| 318 |
+
↓
|
| 319 |
+
団体名が「写真部」に完全一致 → +1.0点
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
**3段階の加点ルール:**
|
| 323 |
+
- **完全一致** (例:入力 "写真部" = 団体名 "写真部") → 加点: 1.0
|
| 324 |
+
- **前方一致** (例:入力 "写真" = 団体名 "写真部" の前半) → 加点: 0.7
|
| 325 |
+
- **部分一���** (例:入力 "写" = 団体名 "写真部" に含む) → 加点: 0.5
|
| 326 |
+
|
| 327 |
+
**カスタマイズ方法:** `config/search_model.json` の `circleName.boost` で調整可能(後述)
|
| 328 |
+
|
| 329 |
+
---
|
| 330 |
+
|
| 331 |
+
### 4.6 スコア統合と閾値フィルタリング
|
| 332 |
+
|
| 333 |
+
**概念:** 複数の指標(BM25F、ベクトル類似度、団体名加点)を1つのスコアに統合します。
|
| 334 |
+
|
| 335 |
+
**統合式(概略):**
|
| 336 |
+
```
|
| 337 |
+
最終スコア = α × BM25F + (1-α) × 単語ベクトル類似度 + 団体名加点
|
| 338 |
+
```
|
| 339 |
+
|
| 340 |
+
ここで α はウェイト(デフォルト 0.5 = 両者を同じ重要度とみなす)
|
| 341 |
+
|
| 342 |
+
**フィルタリング:** スコアが一定以下の企画は結果から除外します。ノイズ除去と計算効率化のため。
|
| 343 |
+
|
| 344 |
+
---
|
| 345 |
+
|
| 346 |
+
## 5. 設定ファイルとカスタマイズ
|
| 347 |
+
|
| 348 |
+
### 5.1 `config/search_model.json` - 検索挙動の詳細設定
|
| 349 |
+
|
| 350 |
+
このファイルを編集して、検索の細かな動作を調整できます。
|
| 351 |
+
|
| 352 |
+
```json
|
| 353 |
+
{
|
| 354 |
+
"target_pos_l1": [
|
| 355 |
+
"名詞",
|
| 356 |
+
"動詞",
|
| 357 |
+
"形容詞",
|
| 358 |
+
"形容動詞語幹"
|
| 359 |
+
],
|
| 360 |
+
"target_fields": [
|
| 361 |
+
"name",
|
| 362 |
+
"circleName",
|
| 363 |
+
"circleNameKana",
|
| 364 |
+
"description",
|
| 365 |
+
"prSummary",
|
| 366 |
+
"prDetail"
|
| 367 |
+
],
|
| 368 |
+
"synonyms": {
|
| 369 |
+
"enable": true,
|
| 370 |
+
"limits": {
|
| 371 |
+
"max_expansions_per_term": 4,
|
| 372 |
+
"max_query_variants": 5,
|
| 373 |
+
"min_char_len": 2
|
| 374 |
+
}
|
| 375 |
+
},
|
| 376 |
+
"bm25f": {
|
| 377 |
+
"k1": 1.2,
|
| 378 |
+
"b": 0.75,
|
| 379 |
+
"field_weights": {
|
| 380 |
+
"name": 2.0,
|
| 381 |
+
"circleName": 1.5,
|
| 382 |
+
"circleNameKana": 0.6,
|
| 383 |
+
"description": 1.0,
|
| 384 |
+
"prSummary": 1.0,
|
| 385 |
+
"prDetail": 0.8
|
| 386 |
+
}
|
| 387 |
+
},
|
| 388 |
+
"word_sim": {
|
| 389 |
+
"enable": true,
|
| 390 |
+
"alpha": 0.5,
|
| 391 |
+
"topk_k": 3
|
| 392 |
+
},
|
| 393 |
+
"circleName": {
|
| 394 |
+
"boost": {
|
| 395 |
+
"exact": 1.0,
|
| 396 |
+
"prefix": 0.7,
|
| 397 |
+
"substring": 0.5,
|
| 398 |
+
"min_len": 2
|
| 399 |
+
}
|
| 400 |
+
},
|
| 401 |
+
"filter": {
|
| 402 |
+
"min_results": 10,
|
| 403 |
+
"max_results": 30,
|
| 404 |
+
"bm25_min": 0.15,
|
| 405 |
+
"word_sim_min": 0.40,
|
| 406 |
+
"fused_min": 0.15,
|
| 407 |
+
"fused_rel_top_ratio": 0.50
|
| 408 |
+
}
|
| 409 |
+
}
|
| 410 |
+
```
|
| 411 |
+
|
| 412 |
+
**各項目の意味と調整方法:**
|
| 413 |
+
|
| 414 |
+
| 項目 | 説明 | 調整例 |
|
| 415 |
+
|------|------|--------|
|
| 416 |
+
| `target_pos_l1` | トークン化時に対象とする品詞 | 「助詞」を追加したい場合は配列に追加 |
|
| 417 |
+
| `target_fields` | 検索対象にするフィールド | `prDetail`を除きたい場合は削除 |
|
| 418 |
+
| `synonyms.enable` | 同義語展開の有無 | `false`で無効化 |
|
| 419 |
+
| `synonyms.limits.max_expansions_per_term` | 1つの単語から展開する同義語の最大数 | 値を大きくするほど多くの同義語を使用 |
|
| 420 |
+
| `bm25f.field_weights` | 各フィールドの重要度 | 企画名(name)を重視したい場合、値を大きく(例:3.0) |
|
| 421 |
+
| `word_sim.alpha` | BM25FとベクトルのウェイトMIX | 0.5 = 両者を同じ重要度。BM25Fを重視したい場合は0.7に |
|
| 422 |
+
| `circleName.boost.exact` | 団体名完全一致の加点 | 団体名マッチを重視したい場合は2.0に |
|
| 423 |
+
| `filter.max_results` | 返す結果の最大件数 | 結果を少なくしたい場合は20に減らす |
|
| 424 |
+
| `filter.fused_min` | スコアの下限(閾値) | 値を上げるとフィルタリングが厳しくなる |
|
| 425 |
+
|
| 426 |
+
**変更手順:**
|
| 427 |
+
1. `config/search_model.json` をテキストエディタで開く
|
| 428 |
+
2. 該当の値を修正(JSONの形式を崩さないよう注意)
|
| 429 |
+
3. サーバーを再起動
|
| 430 |
+
4. `/api/search` でデバッグ情報を確認 (`debug=true` パラメータ)
|
| 431 |
+
|
| 432 |
+
---
|
| 433 |
+
|
| 434 |
+
### 5.2 `config/files.json` - ファイルパスマッピング
|
| 435 |
+
|
| 436 |
+
各種アセット(JSON、ベクトルなど)の保存場所を指定します。通常は変更不要ですが、ファイルの移動が必要な場合に編集します。
|
| 437 |
+
|
| 438 |
+
```json
|
| 439 |
+
{
|
| 440 |
+
"sudachi": {
|
| 441 |
+
"sudachi_config": "scripts/sudachi.json",
|
| 442 |
+
"stopwords": "resources/stopwords.json"
|
| 443 |
+
},
|
| 444 |
+
"projects": {
|
| 445 |
+
"projects_json": "data/generated/projects.json"
|
| 446 |
+
},
|
| 447 |
+
"bm25": {
|
| 448 |
+
"bm25_meta": "data/generated/bm25_meta.json",
|
| 449 |
+
"tf_token": "data/generated/tf_token.json"
|
| 450 |
+
},
|
| 451 |
+
"embeddings": {
|
| 452 |
+
"fasttext_bin": "resources/embeddings/cc.ja.300.bin",
|
| 453 |
+
"word_vocab": "data/generated/word_vocab.json",
|
| 454 |
+
"word_vectors": "data/generated/word_vectors.npz"
|
| 455 |
+
}
|
| 456 |
+
}
|
| 457 |
+
```
|
| 458 |
+
|
| 459 |
+
---
|
| 460 |
+
|
| 461 |
+
### 5.3 `resources/synonyms_custom.json` - カスタム同義語
|
| 462 |
+
|
| 463 |
+
プロジェクト固有の同義語を手動で登録します。
|
| 464 |
+
|
| 465 |
+
**編集例:**
|
| 466 |
+
```json
|
| 467 |
+
{
|
| 468 |
+
"カレー": ["食事", "飲食"],
|
| 469 |
+
"ステージ": ["舞台", "パフォーマンス"],
|
| 470 |
+
"学園祭": ["大学祭"],
|
| 471 |
+
"AI": ["人工知能", "機械学習"]
|
| 472 |
+
}
|
| 473 |
+
```
|
| 474 |
+
|
| 475 |
+
**変更手順:**
|
| 476 |
+
1. `resources/synonyms_custom.json` を編集
|
| 477 |
+
2. スクリプト `scripts/build_all.py` を実行して、同義語キャッシュを再生成
|
| 478 |
+
3. サーバーを再起動
|
| 479 |
+
|
| 480 |
+
---
|
| 481 |
+
|
| 482 |
+
### 5.4 `resources/stopwords.json` - ストップワード
|
| 483 |
+
|
| 484 |
+
検索対象から除外する語(あまり意味がない頻出語)を登録します。
|
| 485 |
+
|
| 486 |
+
**デフォルト例:**
|
| 487 |
+
```json
|
| 488 |
+
[
|
| 489 |
+
"の",
|
| 490 |
+
"が",
|
| 491 |
+
"を",
|
| 492 |
+
"に",
|
| 493 |
+
"で",
|
| 494 |
+
"は",
|
| 495 |
+
"から",
|
| 496 |
+
"まで"
|
| 497 |
+
]
|
| 498 |
+
```
|
| 499 |
+
|
| 500 |
+
**変更方法:**
|
| 501 |
+
同様���ファイルを編集 → スクリプト実行 → サーバー再起動
|
| 502 |
+
|
| 503 |
+
---
|
| 504 |
+
|
| 505 |
+
## 6. データの流れと更新方法
|
| 506 |
+
|
| 507 |
+
### 6.1 システム起動時の流れ
|
| 508 |
+
|
| 509 |
+
```
|
| 510 |
+
1. サーバー起動 (app/main.py)
|
| 511 |
+
↓
|
| 512 |
+
2. 検索エンジン初期化 (engine.initialize())
|
| 513 |
+
├─ config/search_model.json を読み込み
|
| 514 |
+
├─ config/files.json からファイルパスを取得
|
| 515 |
+
├─ data/generated/*.json を読み込み(企画データ、BM25F統計など)
|
| 516 |
+
├─ resources/embeddings/ から単語ベクトルを読み込み
|
| 517 |
+
└─ Sudachi形態素解析器を初期化
|
| 518 |
+
↓
|
| 519 |
+
3. サーバー起動完了
|
| 520 |
+
フロントエンドからのリクエスト受付開始
|
| 521 |
+
```
|
| 522 |
+
|
| 523 |
+
### 6.2 企画データの更新フロー
|
| 524 |
+
|
| 525 |
+
**状況:** MySQLの企画データが更新された
|
| 526 |
+
|
| 527 |
+
**手順:**
|
| 528 |
+
```
|
| 529 |
+
1. MySQLのデータを確認
|
| 530 |
+
↓
|
| 531 |
+
2. スクリプト実行: python scripts/build_all.py
|
| 532 |
+
├─ Step 0: fastText ベクトルをダウンロード(初回のみ)
|
| 533 |
+
├─ Step 1: Sudachi ユーザー辞書を構築(オプション)
|
| 534 |
+
├─ Step 2: DB → projects.json 生成
|
| 535 |
+
├─ Step 3: 同義語キャッシュを生成
|
| 536 |
+
├─ Step 4: BM25F統計を計算
|
| 537 |
+
├─ Step 5: TF/トークン情報を保存
|
| 538 |
+
├─ Step 6: 単語ベクトルを抽出
|
| 539 |
+
└─ Step 7: 団体名インデックスを生成
|
| 540 |
+
↓
|
| 541 |
+
3. サーバーを再起動
|
| 542 |
+
↓
|
| 543 |
+
4. フロントエンドで新しい企画データが見える
|
| 544 |
+
```
|
| 545 |
+
|
| 546 |
+
**詳細:**
|
| 547 |
+
|
| 548 |
+
#### Step 2: `scripts/2_create_projects_data.py` - DB → JSON変換
|
| 549 |
+
```
|
| 550 |
+
MySQL から企画データを取得
|
| 551 |
+
→ ProjectRepository.list_projects()
|
| 552 |
+
→ 各種テキスト(説明文など)を更新情報から最新版を抽出
|
| 553 |
+
→ projects.json に出力
|
| 554 |
+
```
|
| 555 |
+
|
| 556 |
+
このステップ以降のデータは、すべてこの `projects.json` から生成されます。
|
| 557 |
+
|
| 558 |
+
#### Step 3: `scripts/3_build_synonyms_from_sudachi.py` - 同義語キャッシュ
|
| 559 |
+
```
|
| 560 |
+
projects.json の全テキストをトークン化
|
| 561 |
+
→ 出現語彙を収集
|
| 562 |
+
→ Sudachi同義語辞書 + custom synonyms.json から該当するもののみ抽出
|
| 563 |
+
→ synonyms_cache.json に保存
|
| 564 |
+
```
|
| 565 |
+
|
| 566 |
+
目的:実際のコーパスに出現しない同義語は使わない(ノイズ削減)
|
| 567 |
+
|
| 568 |
+
#### Step 4-5: `scripts/4_prepare_bm25f_meta.py` と `scripts/5_prepare_tf_token.py` - BM25F統計
|
| 569 |
+
```
|
| 570 |
+
projects.json をトークン化
|
| 571 |
+
→ IDF(珍しさスコア)を計算
|
| 572 |
+
→ 各フィールドの平均トークン数を計算
|
| 573 |
+
→ bm25_meta.json に保存
|
| 574 |
+
|
| 575 |
+
各企画ごとのTF(単語の出現数)を計算
|
| 576 |
+
→ tf_token.json に保存
|
| 577 |
+
```
|
| 578 |
+
|
| 579 |
+
#### Step 6: `scripts/6_build_word_embeddings.py` - 単語ベクトル抽出
|
| 580 |
+
```
|
| 581 |
+
tf_token.json から語彙リストを取得
|
| 582 |
+
→ fastText ベクトルから該当する単語のベクトルを抽出
|
| 583 |
+
→ word_vocab.json と word_vectors.npz に保存
|
| 584 |
+
```
|
| 585 |
+
|
| 586 |
+
### 6.3 各ステップの実行時間(目安)
|
| 587 |
+
|
| 588 |
+
| ステップ | 処理内容 | 実行時間 |
|
| 589 |
+
|---------|---------|---------|
|
| 590 |
+
| 0 | fastText ダウンロード | 初回のみ数分 |
|
| 591 |
+
| 1 | Sudachi辞書構築 | 1-2分 |
|
| 592 |
+
| 2 | DB→JSON | 数秒 |
|
| 593 |
+
| 3 | 同義語キャッシュ | 10-30秒 |
|
| 594 |
+
| 4 | BM25F統計 | 1-2分 |
|
| 595 |
+
| 5 | TF/トークン | 1-2分 |
|
| 596 |
+
| 6 | 単語ベクトル | 1-2分 |
|
| 597 |
+
| 7 | 団体名インデックス | 数秒 |
|
| 598 |
+
|
| 599 |
+
**全ステップ実行** → 合計約5-10分
|
| 600 |
+
|
| 601 |
+
---
|
| 602 |
+
|
| 603 |
+
## 7. APIの使い方
|
| 604 |
+
|
| 605 |
+
### 7.1 ヘルスチェック
|
| 606 |
+
|
| 607 |
+
```bash
|
| 608 |
+
curl "http://localhost:7860/api/health"
|
| 609 |
+
```
|
| 610 |
+
|
| 611 |
+
レスポンス:
|
| 612 |
+
```json
|
| 613 |
+
{
|
| 614 |
+
"status": "ok"
|
| 615 |
+
}
|
| 616 |
+
```
|
| 617 |
+
|
| 618 |
+
### 7.2 企画一覧取得
|
| 619 |
+
|
| 620 |
+
```bash
|
| 621 |
+
curl -H "X-API-KEY: YOUR_SECRET_KEY" \
|
| 622 |
+
"http://localhost:7860/api/projects"
|
| 623 |
+
```
|
| 624 |
+
|
| 625 |
+
**認証方法:**
|
| 626 |
+
- ヘッダ `X-API-KEY` に `API_SECRET_KEY` 環境変数の値を指定
|
| 627 |
+
- 環境変数は `.env` ファイルに保存(リポジトリには含めない)
|
| 628 |
+
|
| 629 |
+
**レスポンス(gzip圧縮):**
|
| 630 |
+
```json
|
| 631 |
+
[
|
| 632 |
+
{
|
| 633 |
+
"projectId": "62000A",
|
| 634 |
+
"circleName": "写真部",
|
| 635 |
+
"name": "写真展覧会",
|
| 636 |
+
"projectType": "テント企画",
|
| 637 |
+
"category": "展示",
|
| 638 |
+
"day1": true,
|
| 639 |
+
"day2": true,
|
| 640 |
+
"day3": false,
|
| 641 |
+
"location": "第1キャンパス A-1",
|
| 642 |
+
"description": "写真部による年間作品展...",
|
| 643 |
+
"prSummary": "写真の美しさを...",
|
| 644 |
+
"remark": null,
|
| 645 |
+
"image": {
|
| 646 |
+
"filename": "photo1.jpg",
|
| 647 |
+
"extension": "jpg",
|
| 648 |
+
"order": 0
|
| 649 |
+
}
|
| 650 |
+
},
|
| 651 |
+
...
|
| 652 |
+
]
|
| 653 |
+
```
|
| 654 |
+
|
| 655 |
+
### 7.3 企画詳細取得
|
| 656 |
+
|
| 657 |
+
```bash
|
| 658 |
+
curl -H "X-API-KEY: YOUR_SECRET_KEY" \
|
| 659 |
+
"http://localhost:7860/api/details?projectId=62000A"
|
| 660 |
+
```
|
| 661 |
+
|
| 662 |
+
レスポンス:
|
| 663 |
+
```json
|
| 664 |
+
{
|
| 665 |
+
"projectId": "62000A",
|
| 666 |
+
"circleName": "写真部",
|
| 667 |
+
"name": "写真展覧会",
|
| 668 |
+
...
|
| 669 |
+
"prDetail": "詳細なPR文が入ります...",
|
| 670 |
+
"images": [
|
| 671 |
+
{
|
| 672 |
+
"filename": "photo1.jpg",
|
| 673 |
+
"extension": "jpg",
|
| 674 |
+
"order": 0
|
| 675 |
+
},
|
| 676 |
+
{
|
| 677 |
+
"filename": "photo2.jpg",
|
| 678 |
+
"extension": "jpg",
|
| 679 |
+
"order": 1
|
| 680 |
+
}
|
| 681 |
+
],
|
| 682 |
+
"socialMedia": [
|
| 683 |
+
{
|
| 684 |
+
"service": "Instagram",
|
| 685 |
+
"url": "https://instagram.com/..."
|
| 686 |
+
}
|
| 687 |
+
]
|
| 688 |
+
}
|
| 689 |
+
```
|
| 690 |
+
|
| 691 |
+
### 7.4 検索
|
| 692 |
+
|
| 693 |
+
```bash
|
| 694 |
+
curl -X POST -H "X-API-KEY: YOUR_SECRET_KEY" \
|
| 695 |
+
-H "Content-Type: application/json" \
|
| 696 |
+
-d '{"query": "カレー"}' \
|
| 697 |
+
"http://localhost:7860/api/search"
|
| 698 |
+
```
|
| 699 |
+
|
| 700 |
+
**レスポンス(通常モード):**
|
| 701 |
+
```json
|
| 702 |
+
{
|
| 703 |
+
"projectIds": ["12345A", "67890B", "11111C"]
|
| 704 |
+
}
|
| 705 |
+
```
|
| 706 |
+
|
| 707 |
+
### 7.5 検索(デバッグモード)
|
| 708 |
+
|
| 709 |
+
```bash
|
| 710 |
+
curl -X POST -H "X-API-KEY: YOUR_SECRET_KEY" \
|
| 711 |
+
-H "Content-Type: application/json" \
|
| 712 |
+
-d '{"query": "カレー", "debug": true}' \
|
| 713 |
+
"http://localhost:7860/api/search"
|
| 714 |
+
```
|
| 715 |
+
|
| 716 |
+
**レスポンス(デバッグモード):**
|
| 717 |
+
```json
|
| 718 |
+
{
|
| 719 |
+
"projectIds": ["12345A", "67890B"],
|
| 720 |
+
"scores": [
|
| 721 |
+
{
|
| 722 |
+
"projectId": "12345A",
|
| 723 |
+
"score": 2.15
|
| 724 |
+
},
|
| 725 |
+
{
|
| 726 |
+
"projectId": "67890B",
|
| 727 |
+
"score": 1.83
|
| 728 |
+
}
|
| 729 |
+
],
|
| 730 |
+
"details": [
|
| 731 |
+
{
|
| 732 |
+
"projectId": "12345A",
|
| 733 |
+
"circleName": "飲食部A",
|
| 734 |
+
"name": "カレー販売",
|
| 735 |
+
"bm25": 0.85,
|
| 736 |
+
"ws_filter_topk": 0.72,
|
| 737 |
+
"ws_rerank_pairavg": 0.75,
|
| 738 |
+
"org_boost": 0.0,
|
| 739 |
+
"matched_substring": false,
|
| 740 |
+
"fused_filter": 0.78,
|
| 741 |
+
"fused_final": 2.15
|
| 742 |
+
},
|
| 743 |
+
{}
|
| 744 |
+
]
|
| 745 |
+
}
|
| 746 |
+
```
|
| 747 |
+
|
| 748 |
+
**デバッグ情報の各項目:**
|
| 749 |
+
- `bm25`: BM25Fスコア(単語一致度)
|
| 750 |
+
- `ws_filter_topk`: 単語ベクトル類似度(フィルタリング用、top-k方式)
|
| 751 |
+
- `ws_rerank_pairavg`: 単語ベクトル類似度(リランキング用、pair-average方式)
|
| 752 |
+
- `org_boost`: 団体名マッチングボーナス
|
| 753 |
+
- `matched_substring`: 部分文字列マッチングで団体名に合致したか
|
| 754 |
+
- `fused_filter`: フィルタリング段階での統合スコア
|
| 755 |
+
- `fused_final`: 最終統合スコア
|
| 756 |
+
|
| 757 |
+
### 7.6 データ更新エンドポイント
|
| 758 |
+
|
| 759 |
+
```bash
|
| 760 |
+
curl -X POST -H "X-API-KEY: YOUR_SECRET_KEY" \
|
| 761 |
+
-H "Content-Type: application/json" \
|
| 762 |
+
"http://localhost:7860/tasks/update"
|
| 763 |
+
```
|
| 764 |
+
|
| 765 |
+
**用途:** MySQLの企画データが更新された時に、このエンドポイントを呼び出すと、検索用アセット(`data/generated/` 配下のファイル)が自動的に再生成されます。
|
| 766 |
+
|
| 767 |
+
**レスポンス:**
|
| 768 |
+
```json
|
| 769 |
+
{
|
| 770 |
+
"message": "Data update completed."
|
| 771 |
+
}
|
| 772 |
+
```
|
| 773 |
+
|
| 774 |
+
**注意:** このエンドポイントは認証が必要で、実行に数分かかる場合があります。サーバーのCPU使用率が高くなる可能性があります。
|
| 775 |
+
|
| 776 |
+
---
|
| 777 |
+
|
| 778 |
+
## 補足:開発時のデバッグ方法
|
| 779 |
+
|
| 780 |
+
### デバッグモードの活用
|
| 781 |
+
|
| 782 |
+
スコアリングロジックをチューニングする際は、必ずデバッグモードを使用してください。各スコアの詳細が表示されるため、改善点を特定しやすくなります。
|
| 783 |
+
|
| 784 |
+
```bash
|
| 785 |
+
# テスト検索
|
| 786 |
+
curl -X POST -H "X-API-KEY: key" \
|
| 787 |
+
-H "Content-Type: application/json" \
|
| 788 |
+
-d '{"query": "テスト", "debug": true}' \
|
| 789 |
+
"http://localhost:7860/api/search" | jq '.details[0]'
|
| 790 |
+
```
|
| 791 |
+
|
| 792 |
+
各スコアが期待通りの値になっているか確認してください。異常な値が見つかった場合は、設定ファイルを見直すか、ロジックを確認します。
|
| 793 |
+
|
| 794 |
+
---
|
| 795 |
+
|
| 796 |
+
## 8. 開発・デプロイ
|
| 797 |
+
|
| 798 |
+
### 8.1 ローカル開発環境の構築
|
| 799 |
+
|
| 800 |
+
**前提条件:**
|
| 801 |
+
- Python 3.11以上
|
| 802 |
+
- MySQL(ローカルまたはリモート)
|
| 803 |
+
- pip または conda
|
| 804 |
+
|
| 805 |
+
**手順:**
|
| 806 |
+
```bash
|
| 807 |
+
# 1. リポジトリをクローン
|
| 808 |
+
git clone https://github.com/chibafes-dev/chibafes-website-api-v2.git
|
| 809 |
+
cd chibafes-website-api-v2
|
| 810 |
+
|
| 811 |
+
# 2. Pythonの仮想環境を作成
|
| 812 |
+
python3.11 -m venv venv
|
| 813 |
+
source venv/bin/activate # macOS/Linux
|
| 814 |
+
# or
|
| 815 |
+
venv\Scripts\activate # Windows
|
| 816 |
+
|
| 817 |
+
# 3. 依存パッケージをインストール
|
| 818 |
+
pip install -r requirements.txt
|
| 819 |
+
|
| 820 |
+
# 4. .env ファイルを作成(セキュリティ情報は社内ツールから取得)
|
| 821 |
+
# DATABASE_URL=mysql://user:pass@host:3306/db
|
| 822 |
+
# API_SECRET_KEY=...
|
| 823 |
+
# HF_TOKEN=...
|
| 824 |
+
|
| 825 |
+
# 5. データ生成スクリプトを実行
|
| 826 |
+
python scripts/build_all.py
|
| 827 |
+
|
| 828 |
+
# 6. サーバーを起動
|
| 829 |
+
python -m uvicorn app.main:app --reload --port 7860
|
| 830 |
+
```
|
| 831 |
+
|
| 832 |
+
### 8.2 Docker でのデプロイ
|
| 833 |
+
|
| 834 |
+
```bash
|
| 835 |
+
# ビルド
|
| 836 |
+
docker build -t chibafes-api:latest .
|
| 837 |
+
|
| 838 |
+
# 実行
|
| 839 |
+
docker run -e DATABASE_URL="..." \
|
| 840 |
+
-e API_SECRET_KEY="..." \
|
| 841 |
+
-p 7860:7860 \
|
| 842 |
+
chibafes-api:latest
|
| 843 |
+
```
|
| 844 |
+
|
| 845 |
+
---
|
| 846 |
+
|
| 847 |
+
## 9. トラブルシューティング
|
| 848 |
+
|
| 849 |
+
### 問題: サーバー起動後、検索結果が出ない
|
| 850 |
+
|
| 851 |
+
**原因候補:**
|
| 852 |
+
1. `data/generated/` のファイルが不足している
|
| 853 |
+
2. MySQLのデータが空である
|
| 854 |
+
3. 設定ファイルのパスが間違っている
|
| 855 |
+
|
| 856 |
+
**対処:**
|
| 857 |
+
```bash
|
| 858 |
+
# Step 1: ファイルの確認
|
| 859 |
+
ls -la data/generated/
|
| 860 |
+
|
| 861 |
+
# Step 2: スクリプトを再実行
|
| 862 |
+
python scripts/build_all.py
|
| 863 |
+
|
| 864 |
+
# Step 3: サーバーログを確認
|
| 865 |
+
# 「Initializing search engine」から始まるログをチェック
|
| 866 |
+
```
|
| 867 |
+
|
| 868 |
+
### 問題: 検索結果のスコアがおかしい
|
| 869 |
+
|
| 870 |
+
**対処:**
|
| 871 |
+
1. デバッグモードで詳細情報を確認
|
| 872 |
+
```bash
|
| 873 |
+
curl -X POST -d '{"query": "テスト", "debug": true}' ...
|
| 874 |
+
```
|
| 875 |
+
|
| 876 |
+
2. `config/search_model.json` ��確認
|
| 877 |
+
- `filter.fused_min` が高すぎないか
|
| 878 |
+
- `word_sim.alpha` のウェイトは適切か
|
| 879 |
+
|
| 880 |
+
3. 検索用アセットを再生成
|
| 881 |
+
```bash
|
| 882 |
+
python scripts/build_all.py
|
| 883 |
+
```
|
| 884 |
+
|
| 885 |
+
### 問題: 特定の単語が検索されない
|
| 886 |
+
|
| 887 |
+
**原因候補:**
|
| 888 |
+
1. ストップワードに指定されている
|
| 889 |
+
2. 品詞が `target_pos_l1` に含まれていない
|
| 890 |
+
3. 未知語で、Sudachiで正しく分析されていない
|
| 891 |
+
|
| 892 |
+
**対処:**
|
| 893 |
+
1. `resources/stopwords.json` を確認
|
| 894 |
+
2. `config/search_model.json` の `target_pos_l1` を確認
|
| 895 |
+
3. `resources/user_dict.csv` に未知語を登録
|
| 896 |
+
|
| 897 |
+
### 問題: パフォーマンスが悪い(検索が遅い)
|
| 898 |
+
|
| 899 |
+
**対処:**
|
| 900 |
+
1. `filter.max_results` を減らす
|
| 901 |
+
2. `word_sim.topk_k` を小さくする
|
| 902 |
+
3. `config/search_model.json` の `synonyms.limits.max_expansions_per_term` を減らす
|
| 903 |
+
|
| 904 |
+
---
|
| 905 |
+
|
| 906 |
+
## 付録:よくある調整例
|
| 907 |
+
|
| 908 |
+
### 企画名をもっと重視したい
|
| 909 |
+
|
| 910 |
+
```json
|
| 911 |
+
{
|
| 912 |
+
"bm25f": {
|
| 913 |
+
"field_weights": {
|
| 914 |
+
"name": 3.0, // ← 2.0 から 3.0 に変更
|
| 915 |
+
"circleName": 1.5,
|
| 916 |
+
"description": 1.0,
|
| 917 |
+
...
|
| 918 |
+
}
|
| 919 |
+
}
|
| 920 |
+
}
|
| 921 |
+
```
|
| 922 |
+
|
| 923 |
+
### 同義語を使いたくない
|
| 924 |
+
|
| 925 |
+
```json
|
| 926 |
+
{
|
| 927 |
+
"synonyms": {
|
| 928 |
+
"enable": false // ← true から false に変更
|
| 929 |
+
}
|
| 930 |
+
}
|
| 931 |
+
```
|
| 932 |
+
|
| 933 |
+
### 単語ベクトルよりもBM25Fを重視
|
| 934 |
+
|
| 935 |
+
```json
|
| 936 |
+
{
|
| 937 |
+
"word_sim": {
|
| 938 |
+
"alpha": 0.7 // ← 0.5 から 0.7 に変更
|
| 939 |
+
}
|
| 940 |
+
}
|
| 941 |
+
```
|
| 942 |
+
|
| 943 |
+
(alphaが大きいほどBM25Fのウェイトが大きくなる)
|
| 944 |
+
|
| 945 |
+
### 結果を増やしたい
|
| 946 |
+
|
| 947 |
+
```json
|
| 948 |
+
{
|
| 949 |
+
"filter": {
|
| 950 |
+
"max_results": 50, // ← 30 から 50 に変更
|
| 951 |
+
"fused_min": 0.10 // ← 0.15 から 0.10 に(閾値を下げる)
|
| 952 |
+
}
|
| 953 |
+
}
|
| 954 |
+
```
|
| 955 |
+
|
| 956 |
+
---
|
| 957 |
+
|
| 958 |
+
## さらに学びたい方へ
|
| 959 |
+
|
| 960 |
+
- **基礎知識の補足**: `基礎知識.md` を参照(Python、FastAPI、MySQLの基本など)
|
| 961 |
+
- **検索アルゴリズムの数学的詳説**: `検索アルゴリズム詳説.md` を参照(BM25F、TF-IDF、コサイン類似度など)
|
docs/基礎知識.md
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 基礎知識:このプロジェクトで使われているツール・概念の入門
|
| 2 |
+
|
| 3 |
+
**作成日**: 2025年12月11日
|
| 4 |
+
**対象**: 大学1~2年生向けの背景知識解説
|
| 5 |
+
**言語**: 日本語
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
このドキュメントは、本ガイド(「システムガイド.md」)を読むにあたって、必要な背景知識を提供するものです。すでに以下の内容に詳しい方は、スキップしても構いません。
|
| 10 |
+
|
| 11 |
+
## 目次
|
| 12 |
+
|
| 13 |
+
1. [Pythonの基本](#1-pythonの基本)
|
| 14 |
+
2. [FastAPI](#2-fastapi)
|
| 15 |
+
3. [MySQLとデータベース](#3-mysqlとデータベース)
|
| 16 |
+
4. [JSON形式](#4-json形式)
|
| 17 |
+
5. [環境変数](#5-環境変数)
|
| 18 |
+
6. [Docker](#6-docker)
|
| 19 |
+
7. [Git・バージョン管理](#7-gitバージョン管理)
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 1. Pythonの基本
|
| 24 |
+
|
| 25 |
+
### Pythonとは
|
| 26 |
+
|
| 27 |
+
**Pythonは、初心者にも使いやすい「プログラミング言語」です。**
|
| 28 |
+
|
| 29 |
+
プログラミング言語 = コンピュータに「何をするか」を指示する言葉
|
| 30 |
+
|
| 31 |
+
例えば:
|
| 32 |
+
```python
|
| 33 |
+
# 単純な足し算
|
| 34 |
+
result = 3 + 5
|
| 35 |
+
print(result) # 8 と表示
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
Pythonは以下の理由で、このプロジェクトで選ばれています:
|
| 39 |
+
- **読みやすい**: 日本語に近い構文で、初心者でも分かりやすい
|
| 40 |
+
- **データ処理が得意**: 検索システムのように大量データを扱う時に便利
|
| 41 |
+
- **ライブラリが豊富**: 必要な機能が既に用意されていることが多い
|
| 42 |
+
|
| 43 |
+
### このプロジェクトで使われるPythonの機能
|
| 44 |
+
|
| 45 |
+
#### 1.1 リスト(複数のデータをまとめて保存)
|
| 46 |
+
|
| 47 |
+
```python
|
| 48 |
+
# 企画のIDを複数保存
|
| 49 |
+
projects = ["62000A", "12345B", "67890C"]
|
| 50 |
+
|
| 51 |
+
# 最初の企画を取得
|
| 52 |
+
first = projects[0] # "62000A"
|
| 53 |
+
|
| 54 |
+
# 全企画をループで処理
|
| 55 |
+
for project_id in projects:
|
| 56 |
+
print(project_id)
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
#### 1.2 辞書(キーと値のペア)
|
| 60 |
+
|
| 61 |
+
```python
|
| 62 |
+
# 企画情報を辞書で保存
|
| 63 |
+
project = {
|
| 64 |
+
"projectId": "62000A",
|
| 65 |
+
"circleName": "写真部",
|
| 66 |
+
"name": "写真展覧会"
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
# 値を取得
|
| 70 |
+
print(project["circleName"]) # "写真部" と表示
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
#### 1.3 関数(処理をまとめて再利用)
|
| 74 |
+
|
| 75 |
+
```python
|
| 76 |
+
def greet(name):
|
| 77 |
+
"""名前を受け取って、挨拶を返す関数"""
|
| 78 |
+
return f"こんにちは、{name}さん!"
|
| 79 |
+
|
| 80 |
+
result = greet("太郎")
|
| 81 |
+
print(result) # "こんにちは、太郎さん!" と表示
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
#### 1.4 クラス(データと処理を組み合わせる)
|
| 85 |
+
|
| 86 |
+
```python
|
| 87 |
+
class Project:
|
| 88 |
+
"""企画情報を管理するクラス"""
|
| 89 |
+
|
| 90 |
+
def __init__(self, name, description):
|
| 91 |
+
self.name = name
|
| 92 |
+
self.description = description
|
| 93 |
+
|
| 94 |
+
def get_info(self):
|
| 95 |
+
return f"{self.name}: {self.description}"
|
| 96 |
+
|
| 97 |
+
# インスタンスを作成(実例を作る)
|
| 98 |
+
p1 = Project("写真展覧会", "写真部による年間作品展")
|
| 99 |
+
print(p1.get_info())
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### 仮想環境とは
|
| 103 |
+
|
| 104 |
+
Pythonでプロジェクトを開発する時、**複数のバージョン・複数の依存パッケージが混在しないように**「仮想環境」を作ります。
|
| 105 |
+
|
| 106 |
+
例えば:
|
| 107 |
+
- プロジェクトA: Pythonバージョン3.11、FastAPI 0.95
|
| 108 |
+
- プロジェクトB: Pythonバージョン3.10、FastAPI 0.90
|
| 109 |
+
|
| 110 |
+
各プロジェクトを独立した「仮想環境」で実行すれば、干渉しません。
|
| 111 |
+
|
| 112 |
+
**作成方法:**
|
| 113 |
+
```bash
|
| 114 |
+
python3.11 -m venv venv
|
| 115 |
+
source venv/bin/activate # 仮想環境を有効化
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
---
|
| 119 |
+
|
| 120 |
+
## 2. FastAPI
|
| 121 |
+
|
| 122 |
+
### FastAPIとは
|
| 123 |
+
|
| 124 |
+
**FastAPIは、Pythonで「Web API」を簡単に作るためのフレームワークです。**
|
| 125 |
+
|
| 126 |
+
Web API = ネットワーク経由で、別のコンピュータが呼び出して使えるプログラム
|
| 127 |
+
|
| 128 |
+
このプロジェクトの例:
|
| 129 |
+
```
|
| 130 |
+
フロントエンド(Web画面) ── HTTP通信 ──> FastAPI サーバー
|
| 131 |
+
<── JSON返却 ──
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### エンドポイント(APIの「入口」)
|
| 135 |
+
|
| 136 |
+
APIには複数の「入口」があります。これを「エンドポイント」と呼びます。
|
| 137 |
+
|
| 138 |
+
```python
|
| 139 |
+
from fastapi import FastAPI
|
| 140 |
+
|
| 141 |
+
app = FastAPI()
|
| 142 |
+
|
| 143 |
+
@app.get("/api/health")
|
| 144 |
+
def health_check():
|
| 145 |
+
"""サーバーが動いているか確認するエンドポイント"""
|
| 146 |
+
return {"status": "ok"}
|
| 147 |
+
|
| 148 |
+
@app.post("/api/search")
|
| 149 |
+
def search(query: str):
|
| 150 |
+
"""キーワードで企画を検索するエンドポイント"""
|
| 151 |
+
return {"results": [...]}
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
**アクセス方法:**
|
| 155 |
+
```bash
|
| 156 |
+
# GET リクエスト
|
| 157 |
+
curl "http://localhost:7860/api/health"
|
| 158 |
+
|
| 159 |
+
# POST リクエスト(データを送信)
|
| 160 |
+
curl -X POST -H "Content-Type: application/json" \
|
| 161 |
+
-d '{"query": "カレー"}' \
|
| 162 |
+
"http://localhost:7860/api/search"
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
### HTTPメソッドの種類
|
| 166 |
+
|
| 167 |
+
| メソッド | 用途 | 例 |
|
| 168 |
+
|---------|------|-----|
|
| 169 |
+
| `GET` | データを取得(参照のみ) | `/api/projects` - 全企画を取得 |
|
| 170 |
+
| `POST` | 新しいデータを送信 | `/api/search` - 検索を実行 |
|
| 171 |
+
| `PUT` | データを更新 | `/api/projects/123` - 企画を更新 |
|
| 172 |
+
| `DELETE` | データを削除 | `/api/projects/123` - 企画を削除 |
|
| 173 |
+
|
| 174 |
+
このプロジェクトは主に `GET` と `POST` を使っています。
|
| 175 |
+
|
| 176 |
+
### 認証(セキュリティ)
|
| 177 |
+
|
| 178 |
+
このプロジェクトでは、APIキーで保護されています。
|
| 179 |
+
|
| 180 |
+
**概念:** 誰でもアクセスできるのではなく、正しいキーを持っている人だけがアクセスできる
|
| 181 |
+
|
| 182 |
+
```python
|
| 183 |
+
from fastapi import Security, HTTPException
|
| 184 |
+
from fastapi.security import APIKeyHeader
|
| 185 |
+
|
| 186 |
+
api_key_header = APIKeyHeader(name="X-API-KEY")
|
| 187 |
+
|
| 188 |
+
def get_api_key(key: str = Security(api_key_header)):
|
| 189 |
+
if key != os.getenv("API_SECRET_KEY"):
|
| 190 |
+
raise HTTPException(status_code=403, detail="Invalid key")
|
| 191 |
+
return key
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
**使用方法:**
|
| 195 |
+
```bash
|
| 196 |
+
curl -H "X-API-KEY: your_secret_key" \
|
| 197 |
+
"http://localhost:7860/api/search"
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## 3. MySQLとデータベース
|
| 203 |
+
|
| 204 |
+
### データベースとは
|
| 205 |
+
|
| 206 |
+
**データベースは、大量のデータを効率よく保存・取得するためのシステムです。**
|
| 207 |
+
|
| 208 |
+
例えば、千葉大祭の企画情報:
|
| 209 |
+
- 企画数:数十~数百件
|
| 210 |
+
- 各企画の情報:名前、説明、団体、日程など(10項目以上)
|
| 211 |
+
- 合計:数千以上のデータポイント
|
| 212 |
+
|
| 213 |
+
こを普通のテキストファイルで管理すると、検索や更新が非常に遅くなります。
|
| 214 |
+
|
| 215 |
+
### MySQLの構造
|
| 216 |
+
|
| 217 |
+
```
|
| 218 |
+
┌─────────────────────────────────────┐
|
| 219 |
+
│ MySQL データベース │
|
| 220 |
+
│ (chibafes) │
|
| 221 |
+
├─────────────────────────────────────┤
|
| 222 |
+
│ テーブル: circle_projects │
|
| 223 |
+
├─────────────────────────────────────┤
|
| 224 |
+
│ カラム(列): projectId, circleName, │
|
| 225 |
+
│ name, description, prSummary, ... │
|
| 226 |
+
├─────────────────────────────────────┤
|
| 227 |
+
│ 行1: 62000A, 写真部, 写真展覧会, ... │
|
| 228 |
+
│ 行2: 12345B, 映画研究会, 映画上映会, ... │
|
| 229 |
+
│ 行3: ... │
|
| 230 |
+
└─────────────────────────────────────┘
|
| 231 |
+
```
|
| 232 |
+
|
| 233 |
+
**テーブル = Excelのスプレッドシートのようなもの**
|
| 234 |
+
- **列(カラム)** = 属性(例:企画ID、名前など)
|
| 235 |
+
- **行(レコード)** = 1つのデータ(例:1つの企画)
|
| 236 |
+
|
| 237 |
+
### SQLとは
|
| 238 |
+
|
| 239 |
+
SQL(Structured Query Language)= データベースを操作する言葉
|
| 240 |
+
|
| 241 |
+
**例:**
|
| 242 |
+
```sql
|
| 243 |
+
-- 企画一覧を取得
|
| 244 |
+
SELECT projectId, name, circleName FROM circle_projects;
|
| 245 |
+
|
| 246 |
+
-- 特定の企画を検索
|
| 247 |
+
SELECT * FROM circle_projects WHERE name LIKE '%カレー%';
|
| 248 |
+
|
| 249 |
+
-- データを追加
|
| 250 |
+
INSERT INTO circle_projects (projectId, name) VALUES ('99999Z', '新企画');
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
このプロジェクトでは、`app/repositories/query.sql` にクエリが保存されています。
|
| 254 |
+
|
| 255 |
+
### このプロジェクトでの使用方法
|
| 256 |
+
|
| 257 |
+
```python
|
| 258 |
+
# 1. DB接続を取得
|
| 259 |
+
with get_connection() as conn:
|
| 260 |
+
# 2. クエリを実行
|
| 261 |
+
with conn.cursor() as cur:
|
| 262 |
+
cur.execute("SELECT * FROM circle_projects")
|
| 263 |
+
# 3. 結果を取得
|
| 264 |
+
rows = cur.fetchall()
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
---
|
| 268 |
+
|
| 269 |
+
## 4. JSON形式
|
| 270 |
+
|
| 271 |
+
### JSONとは
|
| 272 |
+
|
| 273 |
+
**JSON(JavaScript Object Notation)は、データを構造化して表現する標準的な形式です。**
|
| 274 |
+
|
| 275 |
+
Web APIの「やり取り」に最も使われています。
|
| 276 |
+
|
| 277 |
+
### JSONの構成要素
|
| 278 |
+
|
| 279 |
+
```json
|
| 280 |
+
{
|
| 281 |
+
"projectId": "62000A",
|
| 282 |
+
"circleName": "写真部",
|
| 283 |
+
"tags": ["展示", "アート"],
|
| 284 |
+
"isActive": true,
|
| 285 |
+
"prDetail": null
|
| 286 |
+
}
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
| 要素 | 説明 | 例 |
|
| 290 |
+
|------|------|-----|
|
| 291 |
+
| キー | データの名前 | `"projectId"` |
|
| 292 |
+
| 値 | データの内容 | `"62000A"` |
|
| 293 |
+
| 文字列 | テキスト(ダブルクォートで囲む) | `"写真部"` |
|
| 294 |
+
| 数値 | 整数・小数 | `123` `45.6` |
|
| 295 |
+
| 真偽値 | true/false | `true` |
|
| 296 |
+
| 配列 | 複数のデータを順序付きで保存 | `["A", "B", "C"]` |
|
| 297 |
+
| オブジェクト | キーと値をまとめた構造 | `{ "key": "value" }` |
|
| 298 |
+
| null | 空・データなし | `null` |
|
| 299 |
+
|
| 300 |
+
### JSONファイルの例
|
| 301 |
+
|
| 302 |
+
`data/generated/projects.json` の一部:
|
| 303 |
+
```json
|
| 304 |
+
[
|
| 305 |
+
{
|
| 306 |
+
"projectId": "62000A",
|
| 307 |
+
"circleName": "写真部",
|
| 308 |
+
"name": "写真展覧会",
|
| 309 |
+
"description": "写真部による年間作品展示...",
|
| 310 |
+
"images": [
|
| 311 |
+
{
|
| 312 |
+
"filename": "photo1.jpg",
|
| 313 |
+
"order": 0
|
| 314 |
+
}
|
| 315 |
+
]
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
"projectId": "12345B",
|
| 319 |
+
"circleName": "映画研究会",
|
| 320 |
+
"name": "映画上映会",
|
| 321 |
+
...
|
| 322 |
+
}
|
| 323 |
+
]
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
### Pythonでの読み書き
|
| 327 |
+
|
| 328 |
+
```python
|
| 329 |
+
import json
|
| 330 |
+
|
| 331 |
+
# JSON ファイルを読む
|
| 332 |
+
with open("data.json", "r", encoding="utf-8") as f:
|
| 333 |
+
data = json.load(f) # JSONをPythonの辞書に変換
|
| 334 |
+
|
| 335 |
+
# 値を取得
|
| 336 |
+
print(data[0]["circleName"]) # "写真部"
|
| 337 |
+
|
| 338 |
+
# Pythonの辞書を JSON に変換して書き出す
|
| 339 |
+
with open("output.json", "w", encoding="utf-8") as f:
|
| 340 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
---
|
| 344 |
+
|
| 345 |
+
## 5. 環境変数
|
| 346 |
+
|
| 347 |
+
### 環境変数とは
|
| 348 |
+
|
| 349 |
+
**環境変数は、プログラムの「設定値」をプログラムの外部に置く仕組みです。**
|
| 350 |
+
|
| 351 |
+
なぜ必要か:
|
| 352 |
+
- **セキュリティ**: パスワードやAPIキーをコードに直書きしない
|
| 353 |
+
- **柔軟性**: 環境ごと(ローカル/本番)で異なる設定を簡単に変更できる
|
| 354 |
+
- **バージョン管理**: センシティブな情報をGitにコミットしない
|
| 355 |
+
|
| 356 |
+
### 設定方法
|
| 357 |
+
|
| 358 |
+
#### 方法1: `.env` ファイル
|
| 359 |
+
|
| 360 |
+
```bash
|
| 361 |
+
# .env ファイル(リポジトリに含めない)
|
| 362 |
+
DATABASE_URL=mysql://user:password@localhost:3306/chibafes
|
| 363 |
+
API_SECRET_KEY=super_secret_key_here
|
| 364 |
+
HF_TOKEN=hf_xxxxxxxxxxxxxx
|
| 365 |
+
```
|
| 366 |
+
|
| 367 |
+
```python
|
| 368 |
+
# Python で読み込み
|
| 369 |
+
from dotenv import load_dotenv
|
| 370 |
+
import os
|
| 371 |
+
|
| 372 |
+
load_dotenv()
|
| 373 |
+
db_url = os.getenv("DATABASE_URL")
|
| 374 |
+
api_key = os.getenv("API_SECRET_KEY")
|
| 375 |
+
```
|
| 376 |
+
|
| 377 |
+
#### 方法2: コマンドラインで設定
|
| 378 |
+
|
| 379 |
+
```bash
|
| 380 |
+
# Linuxの場合
|
| 381 |
+
export DATABASE_URL="mysql://..."
|
| 382 |
+
export API_SECRET_KEY="..."
|
| 383 |
+
|
| 384 |
+
# または、プログラム実行時に指定
|
| 385 |
+
API_SECRET_KEY="..." python app/main.py
|
| 386 |
+
```
|
| 387 |
+
|
| 388 |
+
#### 方法3: Docker/コンテナで設定
|
| 389 |
+
|
| 390 |
+
```bash
|
| 391 |
+
docker run -e DATABASE_URL="..." \
|
| 392 |
+
-e API_SECRET_KEY="..." \
|
| 393 |
+
myapp:latest
|
| 394 |
+
```
|
| 395 |
+
|
| 396 |
+
### このプロジェクトで必要な環境変数
|
| 397 |
+
|
| 398 |
+
| 変数名 | 説明 | 例 |
|
| 399 |
+
|--------|------|-----|
|
| 400 |
+
| `DATABASE_URL` | MySQL接続情報 | `mysql://user:pass@host/db` |
|
| 401 |
+
| `API_SECRET_KEY` | APIキー認証用 | `super_secret_123` |
|
| 402 |
+
| `HF_TOKEN` | Hugging Face API用 | `hf_xxx...` |
|
| 403 |
+
| `POSTHOG_PROJECT_API_KEY` | ログ分析用(オプション) | `phc_xxx...` |
|
| 404 |
+
|
| 405 |
+
---
|
| 406 |
+
|
| 407 |
+
## 6. Docker
|
| 408 |
+
|
| 409 |
+
### Dockerとは
|
| 410 |
+
|
| 411 |
+
**Dockerは、アプリケーションを「箱詰め」にして、どの環境でも同じに実行するための仕組みです。**
|
| 412 |
+
|
| 413 |
+
**メリット:**
|
| 414 |
+
- **再現性**: 開発者のPC、サーバー、他人のPCでも同じ環境で動作
|
| 415 |
+
- **簡潔性**: 複雑な依存関係のインストール不要
|
| 416 |
+
- **スケーラビリティ**: 複数の箱を並べて、負荷分散できる
|
| 417 |
+
|
| 418 |
+
### Dockerイメージとコンテナ
|
| 419 |
+
|
| 420 |
+
```
|
| 421 |
+
Dockerfile(設計図)
|
| 422 |
+
↓
|
| 423 |
+
docker build
|
| 424 |
+
↓
|
| 425 |
+
イメージ(青写真)
|
| 426 |
+
↓
|
| 427 |
+
docker run
|
| 428 |
+
↓
|
| 429 |
+
コンテナ(実行中の箱)
|
| 430 |
+
```
|
| 431 |
+
|
| 432 |
+
**例:**
|
| 433 |
+
```dockerfile
|
| 434 |
+
# Dockerfile - イメージの設計図
|
| 435 |
+
FROM python:3.11
|
| 436 |
+
WORKDIR /app
|
| 437 |
+
COPY . .
|
| 438 |
+
RUN pip install -r requirements.txt
|
| 439 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
|
| 440 |
+
```
|
| 441 |
+
|
| 442 |
+
```bash
|
| 443 |
+
# イメージを構築
|
| 444 |
+
docker build -t chibafes-api:latest .
|
| 445 |
+
|
| 446 |
+
# コンテナを実行
|
| 447 |
+
docker run -p 7860:7860 chibafes-api:latest
|
| 448 |
+
```
|
| 449 |
+
|
| 450 |
+
### このプロジェクトでのDocker使用
|
| 451 |
+
|
| 452 |
+
プロジェクトルートにある `Dockerfile` が、このシステムの配置方法を定義しています。
|
| 453 |
+
|
| 454 |
+
**本番環境(Hugging Face Spaces)では、このDockerイメージが使われます。**
|
| 455 |
+
|
| 456 |
+
---
|
| 457 |
+
|
| 458 |
+
## 7. Git・バージョン管理
|
| 459 |
+
|
| 460 |
+
### Gitとは
|
| 461 |
+
|
| 462 |
+
**Gitは、ソースコードの変更履歴を記録・管理するシステムです。**
|
| 463 |
+
|
| 464 |
+
**なぜ必要か:**
|
| 465 |
+
- **履歴管理**: いつ誰が何を変更したかが記録される
|
| 466 |
+
- **協働開発**: 複数人が同じプロジェクトで作業できる
|
| 467 |
+
- **ロールバック**: 間違った変更を取り消せる
|
| 468 |
+
|
| 469 |
+
### 基本的なGitコマンド
|
| 470 |
+
|
| 471 |
+
```bash
|
| 472 |
+
# 変更をステージング(記録対象に追加)
|
| 473 |
+
git add filename.py
|
| 474 |
+
|
| 475 |
+
# 変更をコミット(履歴に記録)
|
| 476 |
+
git commit -m "検索ロジックを改善"
|
| 477 |
+
|
| 478 |
+
# リモート(サーバー)にアップロード
|
| 479 |
+
git push
|
| 480 |
+
|
| 481 |
+
# 他人の変更を取得
|
| 482 |
+
git pull
|
| 483 |
+
|
| 484 |
+
# 現在のステータス確認
|
| 485 |
+
git status
|
| 486 |
+
|
| 487 |
+
# 変更履歴を確認
|
| 488 |
+
git log
|
| 489 |
+
```
|
| 490 |
+
|
| 491 |
+
### ブランチ
|
| 492 |
+
|
| 493 |
+
**ブランチは、並行して複数の開発を進める仕組みです。**
|
| 494 |
+
|
| 495 |
+
```
|
| 496 |
+
main(本番用)
|
| 497 |
+
├── develop(開発用)
|
| 498 |
+
│ ├── feature/search-improvement(新機能)
|
| 499 |
+
│ └── bugfix/api-error(バグ修正)
|
| 500 |
+
└── ...
|
| 501 |
+
```
|
| 502 |
+
|
| 503 |
+
**ワークフロー例:**
|
| 504 |
+
```bash
|
| 505 |
+
# メインから新しいブランチを作成
|
| 506 |
+
git checkout -b feature/my-feature
|
| 507 |
+
|
| 508 |
+
# 変更を加える
|
| 509 |
+
# ...
|
| 510 |
+
|
| 511 |
+
# 変更をコミット
|
| 512 |
+
git commit -m "新機能を追加"
|
| 513 |
+
|
| 514 |
+
# リモートに反映
|
| 515 |
+
git push origin feature/my-feature
|
| 516 |
+
|
| 517 |
+
# Pull Request を作成 → 他のメンバーがレビュー → マージ
|
| 518 |
+
```
|
| 519 |
+
|
| 520 |
+
### このプロジェクトのバージョン管理
|
| 521 |
+
|
| 522 |
+
```
|
| 523 |
+
GitHub リポジトリ: chibafes-dev/chibafes-website-api-v2
|
| 524 |
+
ブランチ: main(本番用), develop(開発用)
|
| 525 |
+
```
|
| 526 |
+
|
| 527 |
+
**セキュリティ注意:**
|
| 528 |
+
- `.env` や API キーは `.gitignore` に登録し、Git に含めない
|
| 529 |
+
- 秘密情報は GitHub Secrets に保存
|
| 530 |
+
|
| 531 |
+
---
|
| 532 |
+
|
| 533 |
+
## まとめ
|
| 534 |
+
|
| 535 |
+
このドキュメントで説明した概念:
|
| 536 |
+
|
| 537 |
+
| 概念 | 役割 | このプロジェクトでの使用 |
|
| 538 |
+
|------|------|------------------------|
|
| 539 |
+
| Python | プログラミング言語 | 全体のコード |
|
| 540 |
+
| FastAPI | Web API フレームワーク | `/api/...` エンドポイント |
|
| 541 |
+
| MySQL | データベース | 企画情報を保存 |
|
| 542 |
+
| JSON | データ形式 | API のやり取り、設定ファイル |
|
| 543 |
+
| 環境変数 | 設定管理 | DB接続情報、APIキー等 |
|
| 544 |
+
| Docker | 環境仮想化 | 本番環境へのデプロイ |
|
| 545 |
+
| Git | バージョン管理 | ソースコード管理 |
|
| 546 |
+
|
| 547 |
+
各項目の詳細は、同梱の「システムガイド.md」を参照してください。
|
docs/改善提案.md
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# コードベース改善提案書
|
| 2 |
+
|
| 3 |
+
**作成日**: 2025年12月11日
|
| 4 |
+
**対象システム**: ChibaFes Website API v2
|
| 5 |
+
**対象読者**: 技術リーダー、アーキテクト、開発チーム
|
| 6 |
+
**スコープ**: 検索ロジック、コード品質、致命的欠陥の検出
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## 概要
|
| 11 |
+
|
| 12 |
+
本ドキュメントでは、現在のコードベースの包括的な分析を行い、改善が有効な領域を特定しました。以下の3つのカテゴリに分類しています。
|
| 13 |
+
|
| 14 |
+
1. **致命的欠陥・セキュリティ問題**: 即座に対応すべき問題
|
| 15 |
+
2. **検索ロジック改善**: 精度や効率の向上につながる改善
|
| 16 |
+
3. **コード品質改善**: 保守性、リーダビリティ、テスト容易性の向上
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 1. 致命的欠陥・セキュリティ問題
|
| 21 |
+
|
| 22 |
+
### 1.1 コネクションプール管理の例外ハンドリング不足
|
| 23 |
+
|
| 24 |
+
**ファイル**: `app/db/session.py`
|
| 25 |
+
**重要度**: 🔴 高
|
| 26 |
+
|
| 27 |
+
**現在の実装**:
|
| 28 |
+
```python
|
| 29 |
+
def acquire(self) -> Connection:
|
| 30 |
+
while True:
|
| 31 |
+
try:
|
| 32 |
+
conn = self._available.get_nowait()
|
| 33 |
+
except Empty:
|
| 34 |
+
with self._lock:
|
| 35 |
+
if self._total_created < self._max_size:
|
| 36 |
+
conn = self._create_connection()
|
| 37 |
+
self._total_created += 1
|
| 38 |
+
return conn
|
| 39 |
+
conn = self._available.get()
|
| 40 |
+
|
| 41 |
+
if conn.open:
|
| 42 |
+
conn.ping(reconnect=True) # ← 例外ハンドリングがない
|
| 43 |
+
conn.autocommit(True)
|
| 44 |
+
return conn
|
| 45 |
+
self._discard_connection(conn)
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
**具体的な欠陥**:
|
| 49 |
+
1. `ping(reconnect=True)` が例外を発生させた場合、呼び出し元の `get_connection()` でキャッチされず、500エラーが発生する可能性があります。
|
| 50 |
+
2. 接続失敗時に不要なスタックトレースが出力されます。
|
| 51 |
+
3. コネクションプール内のコネクションが「壊れた状態」のままになる可能性があります。
|
| 52 |
+
|
| 53 |
+
**推奨改善**:
|
| 54 |
+
```python
|
| 55 |
+
def acquire(self) -> Connection:
|
| 56 |
+
while True:
|
| 57 |
+
try:
|
| 58 |
+
conn = self._available.get_nowait()
|
| 59 |
+
except Empty:
|
| 60 |
+
with self._lock:
|
| 61 |
+
if self._total_created < self._max_size:
|
| 62 |
+
conn = self._create_connection()
|
| 63 |
+
self._total_created += 1
|
| 64 |
+
return conn
|
| 65 |
+
conn = self._available.get()
|
| 66 |
+
|
| 67 |
+
if conn.open:
|
| 68 |
+
try:
|
| 69 |
+
conn.ping(reconnect=True)
|
| 70 |
+
conn.autocommit(True)
|
| 71 |
+
return conn
|
| 72 |
+
except Exception as e:
|
| 73 |
+
log.warning(f"Stale connection detected and discarded: {e}")
|
| 74 |
+
self._discard_connection(conn)
|
| 75 |
+
### 1.2 APIキー認証のロジック明確化
|
| 76 |
+
|
| 77 |
+
**ファイル**: `app/main.py`
|
| 78 |
+
**重要度**: 🟠 中(機能的には正しいが可読性が低い)
|
| 79 |
+
|
| 80 |
+
**現在の実装**:
|
| 81 |
+
```python
|
| 82 |
+
API_SECRET_KEY = os.getenv("API_SECRET_KEY")
|
| 83 |
+
|
| 84 |
+
async def get_api_key(key: str = Security(api_key_header)):
|
| 85 |
+
if not API_SECRET_KEY or key != API_SECRET_KEY:
|
| 86 |
+
raise HTTPException(status_code=403, detail="Could not validate credentials.")
|
| 87 |
+
return key
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
**具体的な問題**:
|
| 91 |
+
1. `not API_SECRET_KEY` と `key != API_SECRET_KEY` の組み合わせが、コードの意図を曖昧にしています。
|
| 92 |
+
2. セキュリティコードレビューで指摘される可能性が高い(ベストプラクティスに準拠していない)。
|
| 93 |
+
3. 環境変数が未設定の場合、初期化時ではなく、リクエスト時に発見される(デバッグが困難)。
|
| 94 |
+
|
| 95 |
+
**実装は機能的には正しい**(すべてのシナリオで期待通り動作)が、可読性と保守性が低い状態です。
|
| 96 |
+
|
| 97 |
+
**推奨改善**:
|
| 98 |
+
```python
|
| 99 |
+
API_SECRET_KEY = os.getenv("API_SECRET_KEY")
|
| 100 |
+
|
| 101 |
+
if not API_SECRET_KEY:
|
| 102 |
+
raise RuntimeError("API_SECRET_KEY environment variable is not set")
|
| 103 |
+
|
| 104 |
+
async def get_api_key(key: str = Security(api_key_header)):
|
| 105 |
+
if key != API_SECRET_KEY:
|
| 106 |
+
raise HTTPException(status_code=403, detail="Invalid API key")
|
| 107 |
+
return key
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
**改善効果**:
|
| 111 |
+
- 初期化時の明示的なチェック(起動時にエラーが分かる)
|
| 112 |
+
- 認証ロジックの単純化(`if key != API_SECRET_KEY` のみに)
|
| 113 |
+
- セキュリティベストプラクティスに準拠
|
| 114 |
+
|
| 115 |
+
**影響範囲**: 認証が必要なすべてのエンドポイント(`/api/projects`, `/api/search`, `/tasks/update` など)
|
| 116 |
+
```python
|
| 117 |
+
API_SECRET_KEY = os.getenv("API_SECRET_KEY")
|
| 118 |
+
|
| 119 |
+
if not API_SECRET_KEY:
|
| 120 |
+
raise RuntimeError("API_SECRET_KEY environment variable is not set")
|
| 121 |
+
|
| 122 |
+
async def get_api_key(key: str = Security(api_key_header)):
|
| 123 |
+
if key != API_SECRET_KEY:
|
| 124 |
+
raise HTTPException(status_code=403, detail="Invalid API key")
|
| 125 |
+
return key
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
**影響範囲**: 認証が必要なすべてのエンドポイント(`/api/projects`, `/api/search`, `/tasks/update` など)
|
| 129 |
+
|
| 130 |
+
---
|
| 131 |
+
|
| 132 |
+
### 1.3 例外ハンドリングの不完全性
|
| 133 |
+
|
| 134 |
+
**ファイ��**: `app/repositories/projects_repository.py` 他
|
| 135 |
+
**重要度**: 🟠 中
|
| 136 |
+
|
| 137 |
+
**問題内容**:
|
| 138 |
+
```python
|
| 139 |
+
def _transform_row(self, row: Dict[str, Any]) -> Project:
|
| 140 |
+
def _resolve_text_from_updates(updates_key: str) -> str | None:
|
| 141 |
+
updates = sorted(
|
| 142 |
+
_coerce_dict_list(row.get(updates_key)),
|
| 143 |
+
key=lambda item: str(item.get("createdAt") or ""),
|
| 144 |
+
reverse=True,
|
| 145 |
+
)
|
| 146 |
+
# ... 複数の try-except なし処理
|
| 147 |
+
for update in updates:
|
| 148 |
+
status = update.get("updateStatus")
|
| 149 |
+
normalized_status = (
|
| 150 |
+
status.strip().upper() if isinstance(status, str) else None # ← AttributeError可能性
|
| 151 |
+
)
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
**具体的な欠陥**:
|
| 155 |
+
1. `status.strip()` で `AttributeError` が発生する可能性(万が一 `status` が予期しない型の場合)
|
| 156 |
+
2. JSON パース失敗時の黙殺(`except json.JSONDecodeError: return []`)が想定外のデータを見落とさせる可能性
|
| 157 |
+
|
| 158 |
+
**推奨改善**:
|
| 159 |
+
```python
|
| 160 |
+
def _resolve_text_from_updates(updates_key: str) -> str | None:
|
| 161 |
+
try:
|
| 162 |
+
updates = sorted(
|
| 163 |
+
_coerce_dict_list(row.get(updates_key)),
|
| 164 |
+
key=lambda item: str(item.get("createdAt") or ""),
|
| 165 |
+
reverse=True,
|
| 166 |
+
)
|
| 167 |
+
except Exception as e:
|
| 168 |
+
log.warning(f"Failed to sort updates for {updates_key}: {e}")
|
| 169 |
+
return None
|
| 170 |
+
|
| 171 |
+
for update in updates:
|
| 172 |
+
try:
|
| 173 |
+
status = update.get("updateStatus")
|
| 174 |
+
if not isinstance(status, str):
|
| 175 |
+
continue
|
| 176 |
+
normalized_status = status.strip().upper()
|
| 177 |
+
# ...
|
| 178 |
+
except Exception as e:
|
| 179 |
+
log.warning(f"Error processing update: {e}")
|
| 180 |
+
continue
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
**影響範囲**: プロジェクト詳細データの取得時に予期しないエラーで API がクラッシュする可能性
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
## 2. 検索ロジック改善
|
| 188 |
+
|
| 189 |
+
### 2.1 検索フィルタリング戦略の複雑性と曖昧性
|
| 190 |
+
|
| 191 |
+
**ファイル**: `app/search/engine.py` (lines 540-615)
|
| 192 |
+
**重要度**: 🟠 中
|
| 193 |
+
|
| 194 |
+
**問題内容**:
|
| 195 |
+
現在の検索フィルタリングロジックは2段階で実施されています:
|
| 196 |
+
|
| 197 |
+
```python
|
| 198 |
+
# Stage 1: Filtering
|
| 199 |
+
fused_cut = max(self.cfg.fused_min, top * self.cfg.fused_rel_top_ratio)
|
| 200 |
+
keep = (
|
| 201 |
+
(bm25 >= self.cfg.bm25_min)
|
| 202 |
+
| (ws_filter >= self.cfg.word_sim_min)
|
| 203 |
+
| (score_with_boost >= self.cfg.fused_min)
|
| 204 |
+
) & (score_with_boost >= fused_cut)
|
| 205 |
+
|
| 206 |
+
# Stage 2: Fallback (結果が0の場合)
|
| 207 |
+
if not selected_idx_set:
|
| 208 |
+
keep2 = (
|
| 209 |
+
(bm25 >= self.cfg.bm25_min)
|
| 210 |
+
| (ws_filter >= self.cfg.word_sim_min)
|
| 211 |
+
| (score_with_boost >= self.cfg.fused_min)
|
| 212 |
+
)
|
| 213 |
+
```
|
| 214 |
+
|
| 215 |
+
**具体的な問題**:
|
| 216 |
+
1. **論理的矛盾**: `keep2` は `keep` とほぼ同じで、`fused_cut` チェックがないだけです。この相違を明確に文書化する必要があります。
|
| 217 |
+
2. **ハードコードされた戦略**: Fallback 戦略が1段階のみで、より段階的なエスカレーションが考慮されていません。
|
| 218 |
+
3. **テスト不可**: この複雑な条件分岐のカバレッジテストが困難です。
|
| 219 |
+
|
| 220 |
+
**推奨改善**:
|
| 221 |
+
```python
|
| 222 |
+
class FilterStrategy(Enum):
|
| 223 |
+
STRICT = 1 # 厳密: fused_cut を適用
|
| 224 |
+
MODERATE = 2 # 中程度: fused_min のみ
|
| 225 |
+
RELAXED = 3 # 緩和: 個別スコアのみ
|
| 226 |
+
|
| 227 |
+
def _apply_filter(
|
| 228 |
+
self,
|
| 229 |
+
bm25: np.ndarray,
|
| 230 |
+
ws_filter: np.ndarray,
|
| 231 |
+
score_with_boost: np.ndarray,
|
| 232 |
+
strategy: FilterStrategy = FilterStrategy.STRICT,
|
| 233 |
+
) -> np.ndarray:
|
| 234 |
+
"""フィルタリング戦略を統一的に適用"""
|
| 235 |
+
|
| 236 |
+
if strategy == FilterStrategy.STRICT:
|
| 237 |
+
top = float(np.max(score_with_boost)) if score_with_boost.size > 0 else 0.0
|
| 238 |
+
fused_cut = max(
|
| 239 |
+
self.cfg.fused_min,
|
| 240 |
+
top * self.cfg.fused_rel_top_ratio if top > 0 else 0.0
|
| 241 |
+
)
|
| 242 |
+
return (
|
| 243 |
+
(bm25 >= self.cfg.bm25_min)
|
| 244 |
+
| (ws_filter >= self.cfg.word_sim_min)
|
| 245 |
+
| (score_with_boost >= self.cfg.fused_min)
|
| 246 |
+
) & (score_with_boost >= fused_cut)
|
| 247 |
+
|
| 248 |
+
elif strategy == FilterStrategy.MODERATE:
|
| 249 |
+
return (
|
| 250 |
+
(bm25 >= self.cfg.bm25_min)
|
| 251 |
+
| (ws_filter >= self.cfg.word_sim_min)
|
| 252 |
+
| (score_with_boost >= self.cfg.fused_min)
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
elif strategy == FilterStrategy.RELAXED:
|
| 256 |
+
return (bm25 > 0) | (ws_filter > 0) | (score_with_boost > 0)
|
| 257 |
+
|
| 258 |
+
return np.zeros_like(score_with_boost, dtype=bool)
|
| 259 |
+
|
| 260 |
+
# 使用例
|
| 261 |
+
selected_idx = self._select_results(keep, order, selected_idx_set, substring_idx_set)
|
| 262 |
+
if not selected_idx and len(order) > 0:
|
| 263 |
+
log.info("Fallback: Relaxing filter strategy")
|
| 264 |
+
keep_moderate = self._apply_filter(bm25, ws_filter, score_with_boost, FilterStrategy.MODERATE)
|
| 265 |
+
selected_idx = self._select_results(keep_moderate, order, selected_idx_set, substring_idx_set)
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
**改善効果**:
|
| 269 |
+
- テスト容易性向上
|
| 270 |
+
- ロジックの意図が明確化
|
| 271 |
+
- 新しいフィルタリング戦略の追加が容易に
|
| 272 |
+
|
| 273 |
+
---
|
| 274 |
+
|
| 275 |
+
### 2.2 ��ブストリングマッチと BM25F の重複チェック
|
| 276 |
+
|
| 277 |
+
**ファイル**: `app/search/engine.py` (lines 500-530)
|
| 278 |
+
**重要度**: 🟡 中
|
| 279 |
+
|
| 280 |
+
**問題内容**:
|
| 281 |
+
```python
|
| 282 |
+
substring_hits = self._substring_match_project_ids(query) # クエリのサブストリングマッチ
|
| 283 |
+
# ...
|
| 284 |
+
keep = (
|
| 285 |
+
(bm25 >= self.cfg.bm25_min)
|
| 286 |
+
| (ws_filter >= self.cfg.word_sim_min)
|
| 287 |
+
| (score_with_boost >= self.cfg.fused_min)
|
| 288 |
+
) & (score_with_boost >= fused_cut)
|
| 289 |
+
|
| 290 |
+
if substring_idx_set:
|
| 291 |
+
keep = keep | substring_mask # ← サブストリング結果を別途追加
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
**具体的な問題**:
|
| 295 |
+
1. **冗長性**: サブストリングマッチでヒットしたプロジェクトは、既に BM25F でスコアリングされています。これらを別途 `substring_mask` で強制包含するのは、スコアリングの一貫性を損なう可能性があります。
|
| 296 |
+
2. **順序への影響**: 名前一致のプロジェクトが、スコアに関係なく結果に混在します。
|
| 297 |
+
3. **重複**: 実際には、サブストリングマッチのプロジェクトは多くの場合 `keep` に既に含まれている可能性が高いです。
|
| 298 |
+
|
| 299 |
+
**推奨改善**:
|
| 300 |
+
```python
|
| 301 |
+
def _search(self, query: str, debug: bool = False):
|
| 302 |
+
# ...
|
| 303 |
+
|
| 304 |
+
# サブストリングマッチをスコアブースト として活用
|
| 305 |
+
substring_hits = self._substring_match_project_ids(query)
|
| 306 |
+
substring_boost = np.zeros((len(self.projects),), dtype=np.float32)
|
| 307 |
+
|
| 308 |
+
if substring_hits:
|
| 309 |
+
for pid in substring_hits:
|
| 310 |
+
idx = self.project_idx.get(pid)
|
| 311 |
+
if idx is not None:
|
| 312 |
+
# サブストリングマッチは固定ボーナス(例: 0.3)を追加
|
| 313 |
+
substring_boost[idx] = 0.3 # 設定可能にしてもよい
|
| 314 |
+
|
| 315 |
+
# 最終スコアにサブストリングボーナスを統合
|
| 316 |
+
final_scores = fused_rerank + boost + substring_boost
|
| 317 |
+
|
| 318 |
+
# フィルタリングは統一的に
|
| 319 |
+
keep = (
|
| 320 |
+
(bm25 >= self.cfg.bm25_min)
|
| 321 |
+
| (ws_filter >= self.cfg.word_sim_min)
|
| 322 |
+
| (score_with_boost >= self.cfg.fused_min)
|
| 323 |
+
) & (score_with_boost >= fused_cut)
|
| 324 |
+
|
| 325 |
+
# サブストリングマッチは結果にボーナスを与えるが、絶対条件ではない
|
| 326 |
+
# (スコアベースのランキングを保証する)
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
**改善効果**:
|
| 330 |
+
- スコアリングの一貫性向上
|
| 331 |
+
- サブストリングマッチと通常検索の相互作用が明確に
|
| 332 |
+
- デバッグ情報が直感的に
|
| 333 |
+
|
| 334 |
+
---
|
| 335 |
+
|
| 336 |
+
### 2.3 IDF 重み付けが不完全な実装
|
| 337 |
+
|
| 338 |
+
**ファイル**: `app/search/engine.py` (lines 370-450, top-k pooling)
|
| 339 |
+
**重要度**: 🟡 中
|
| 340 |
+
|
| 341 |
+
**問題内容**:
|
| 342 |
+
```python
|
| 343 |
+
def _word_sim_scores_topk(self, terms: List[str]) -> Optional[np.ndarray]:
|
| 344 |
+
# Query term vectors with IDF weighting
|
| 345 |
+
vecs = []
|
| 346 |
+
weights = []
|
| 347 |
+
for t in terms:
|
| 348 |
+
v, oov = self._get_token_vector(t)
|
| 349 |
+
if v is None:
|
| 350 |
+
continue
|
| 351 |
+
w = float(self.idf.get(t, 0.0))
|
| 352 |
+
if oov:
|
| 353 |
+
w *= float(self.cfg.query_subword_oov_weight) # OOV: 0.5 倍など
|
| 354 |
+
if w <= 0:
|
| 355 |
+
continue
|
| 356 |
+
vecs.append(v)
|
| 357 |
+
weights.append(w)
|
| 358 |
+
|
| 359 |
+
# ...
|
| 360 |
+
M = Vd @ Vq.T # Td x Tq
|
| 361 |
+
if Wq.size:
|
| 362 |
+
M = M * Wq[None, :] # ← IDF 重み付けだが正規化なし
|
| 363 |
+
```
|
| 364 |
+
|
| 365 |
+
**具体的な問題**:
|
| 366 |
+
1. **重み付けの不正規化**: `M = M * Wq` で単純に乗算していますが、これにより:
|
| 367 |
+
- 一般的な単語のクエリは高い IDF ウェイトを持ち、相互影響を高めます
|
| 368 |
+
- マイナーな単語は低い IDF で、その寄与度が小さくなります
|
| 369 |
+
- この非対称性は意図的なのか、バグなのか不明確です
|
| 370 |
+
|
| 371 |
+
2. **スコア解釈の困難さ**: IDF で加重されたスコアは、元のコサイン類似度 `[0, 1]` の範囲を逸脱します。
|
| 372 |
+
|
| 373 |
+
**推奨改善**:
|
| 374 |
+
```python
|
| 375 |
+
def _word_sim_scores_topk(self, terms: List[str]) -> Optional[np.ndarray]:
|
| 376 |
+
# ...
|
| 377 |
+
vecs = []
|
| 378 |
+
weights = []
|
| 379 |
+
|
| 380 |
+
for t in terms:
|
| 381 |
+
v, oov = self._get_token_vector(t)
|
| 382 |
+
if v is None:
|
| 383 |
+
continue
|
| 384 |
+
w = float(self.idf.get(t, 0.0))
|
| 385 |
+
if oov:
|
| 386 |
+
w *= float(self.cfg.query_subword_oov_weight)
|
| 387 |
+
if w <= 0:
|
| 388 |
+
continue
|
| 389 |
+
vecs.append(v)
|
| 390 |
+
weights.append(w)
|
| 391 |
+
|
| 392 |
+
if not vecs:
|
| 393 |
+
return None
|
| 394 |
+
|
| 395 |
+
V = np.stack(vecs).astype(np.float32)
|
| 396 |
+
W = np.asarray(weights, dtype=np.float32)
|
| 397 |
+
|
| 398 |
+
# IDF 重み付けベクトルの正規化オプション
|
| 399 |
+
W_normalized = W / (np.sum(W) + 1e-8) # または L2 正規化も検討
|
| 400 |
+
|
| 401 |
+
# ...
|
| 402 |
+
if Wq.size:
|
| 403 |
+
M = M * Wq_normalized[None, :] # 正規化された重みを使用
|
| 404 |
+
```
|
| 405 |
+
|
| 406 |
+
**改善効果**:
|
| 407 |
+
- スコアの解釈が直感的に
|
| 408 |
+
- 異なる長さのクエリでの挙動が一貫的に
|
| 409 |
+
- デバッグが容易に
|
| 410 |
+
|
| 411 |
+
---
|
| 412 |
+
|
| 413 |
+
### 2.4 ペアアベレッジモードのデフォルト値の妥当性
|
| 414 |
+
|
| 415 |
+
**ファイル**: `app/search/engine.py` (lines 477-500)
|
| 416 |
+
**重要度**: 🟡 低~中
|
| 417 |
+
|
| 418 |
+
**問題内容**:
|
| 419 |
+
```python
|
| 420 |
+
def _word_sim_scores_pairavg(self, terms: List[str]) -> Optional[np.ndarray]:
|
| 421 |
+
# Build query term vectors (no weighting for pair-avg, simple mean over all pairs)
|
| 422 |
+
vecs = []
|
| 423 |
+
for t in terms:
|
| 424 |
+
v, _ = self._get_token_vector(t) # ← OOV フラグを無視!
|
| 425 |
+
if v is None:
|
| 426 |
+
continue
|
| 427 |
+
vecs.append(v)
|
| 428 |
+
```
|
| 429 |
+
|
| 430 |
+
**具体的な問題**:
|
| 431 |
+
1. **OOV ベクトルが同等に扱われる**: fastText から生成されたサブワードベクトルと、学習済みベクトルが同じウェイトで扱われます。
|
| 432 |
+
2. **IDF 重み付けが完全に無視される**: これは意図的な設計なのか、実装ミスなのか不明確です。
|
| 433 |
+
3. **設定オプションなし**: ペアアベレッジのウェイト戦略をコンフィグで調整できません。
|
| 434 |
+
|
| 435 |
+
**推奨改善**:
|
| 436 |
+
```python
|
| 437 |
+
# search_model.json に以下を追加
|
| 438 |
+
"word_sim": {
|
| 439 |
+
"enable": true,
|
| 440 |
+
"alpha": 0.5,
|
| 441 |
+
"topk_k": 3,
|
| 442 |
+
"rerank": "pair_avg",
|
| 443 |
+
"pairavg_weighting": "none" # none | idf | oov_penalty
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
def _word_sim_scores_pairavg(self, terms: List[str]) -> Optional[np.ndarray]:
|
| 447 |
+
weighting_mode = self.cfg.word_sim_pairavg_weighting
|
| 448 |
+
|
| 449 |
+
vecs = []
|
| 450 |
+
weights = []
|
| 451 |
+
|
| 452 |
+
for t in terms:
|
| 453 |
+
v, oov = self._get_token_vector(t)
|
| 454 |
+
if v is None:
|
| 455 |
+
continue
|
| 456 |
+
|
| 457 |
+
if weighting_mode == "idf":
|
| 458 |
+
w = float(self.idf.get(t, 0.0))
|
| 459 |
+
elif weighting_mode == "oov_penalty":
|
| 460 |
+
w = 0.5 if oov else 1.0
|
| 461 |
+
else:
|
| 462 |
+
w = 1.0
|
| 463 |
+
|
| 464 |
+
vecs.append(v)
|
| 465 |
+
weights.append(w)
|
| 466 |
+
|
| 467 |
+
if not vecs:
|
| 468 |
+
return None
|
| 469 |
+
|
| 470 |
+
Vq = np.stack(vecs).astype(np.float32)
|
| 471 |
+
W = np.asarray(weights, dtype=np.float32)
|
| 472 |
+
|
| 473 |
+
# ... 後続処理
|
| 474 |
+
```
|
| 475 |
+
|
| 476 |
+
**改善効果**:
|
| 477 |
+
- OOV ベクトルの扱いが一貫的に
|
| 478 |
+
- テスト可能な設定オプションが増加
|
| 479 |
+
- より柔軟な検索戦略が可能に
|
| 480 |
+
|
| 481 |
+
---
|
| 482 |
+
|
| 483 |
+
## 3. コード品質改善
|
| 484 |
+
|
| 485 |
+
### 3.1 設定管理の分散化
|
| 486 |
+
|
| 487 |
+
**ファイル**: `config/search_model.json`, `app/search/engine.py`
|
| 488 |
+
**重要度**: 🟡 中
|
| 489 |
+
|
| 490 |
+
**問題内容**:
|
| 491 |
+
```python
|
| 492 |
+
# app/search/engine.py で SearchConfig を動的に構築
|
| 493 |
+
self.cfg = SearchConfig(
|
| 494 |
+
target_pos_l1=search("target_pos_l1"),
|
| 495 |
+
target_fields=search("target_fields"),
|
| 496 |
+
# ... 20+ フィールド
|
| 497 |
+
fused_rel_top_ratio=float(search("filter.fused_rel_top_ratio", 0.5)),
|
| 498 |
+
)
|
| 499 |
+
```
|
| 500 |
+
|
| 501 |
+
**具体的な問題**:
|
| 502 |
+
1. **マジックナンバーが散在**: デフォルト値がコード内にハードコード(例:`0.5`)されており、統一的に管理されていません。
|
| 503 |
+
2. **設定の完全性チェックなし**: JSON から値が欠落している場合のハンドリングが不一貫(時には例外、時にはデフォルト)です。
|
| 504 |
+
3. **型安全性なし**: すべての値を手動で `float()` や `int()` で変換しており、型エラーの可能性があります。
|
| 505 |
+
|
| 506 |
+
**推奨改善**:
|
| 507 |
+
```python
|
| 508 |
+
# schemas/search_config.py を新規作成
|
| 509 |
+
from pydantic import BaseModel, Field
|
| 510 |
+
|
| 511 |
+
class BM25Config(BaseModel):
|
| 512 |
+
k1: float = Field(default=1.5, ge=0)
|
| 513 |
+
b: float = Field(default=0.75, ge=0, le=1)
|
| 514 |
+
field_weights: Dict[str, float] = Field(default_factory=dict)
|
| 515 |
+
|
| 516 |
+
class WordSimConfig(BaseModel):
|
| 517 |
+
enable: bool = True
|
| 518 |
+
alpha: float = Field(default=0.5, ge=0, le=1)
|
| 519 |
+
topk_k: int = Field(default=3, ge=1)
|
| 520 |
+
rerank: str = "pair_avg" # or "topk"
|
| 521 |
+
pairavg_weighting: str = "none" # idf, oov_penalty, none
|
| 522 |
+
|
| 523 |
+
class FilterConfig(BaseModel):
|
| 524 |
+
min_results: int = Field(default=20, ge=1)
|
| 525 |
+
max_results: int = Field(default=50, ge=1)
|
| 526 |
+
bm25_min: float = Field(default=0.1, ge=0)
|
| 527 |
+
word_sim_min: float = Field(default=0.35, ge=0, le=1)
|
| 528 |
+
fused_min: float = Field(default=0.12, ge=0)
|
| 529 |
+
fused_rel_top_ratio: float = Field(default=0.5, ge=0, le=1)
|
| 530 |
+
|
| 531 |
+
class SearchConfig(BaseModel):
|
| 532 |
+
target_pos_l1: List[str]
|
| 533 |
+
target_fields: List[str]
|
| 534 |
+
bm25f: BM25Config
|
| 535 |
+
word_sim: WordSimConfig
|
| 536 |
+
synonyms: Dict[str, Any]
|
| 537 |
+
filter: FilterConfig
|
| 538 |
+
# ... その他
|
| 539 |
+
|
| 540 |
+
# 使用時
|
| 541 |
+
config = SearchConfig.model_validate_json(open("config/search_model.json").read())
|
| 542 |
+
```
|
| 543 |
+
|
| 544 |
+
**改善効果**:
|
| 545 |
+
- 設定の型安全性が保証される
|
| 546 |
+
- バリデーション が自動化される
|
| 547 |
+
- デフォルト値が一元管理される
|
| 548 |
+
|
| 549 |
+
---
|
| 550 |
+
|
| 551 |
+
### 3.2 テスト戦略の欠如
|
| 552 |
+
|
| 553 |
+
**ファイル**: リポジトリ全体(テストファイルなし)
|
| 554 |
+
**重要度**: 🟡 中
|
| 555 |
+
|
| 556 |
+
**問題内容**:
|
| 557 |
+
```
|
| 558 |
+
テストファイルが見当たりません。以下のコンポーネントのテストが必須です:
|
| 559 |
+
- SearchEngine の各スコアリング関数
|
| 560 |
+
- ProjectsRepository のデータ変換ロジック
|
| 561 |
+
- APIエンドポイントの動作
|
| 562 |
+
- フィルタリング戦略
|
| 563 |
+
```
|
| 564 |
+
|
| 565 |
+
**推奨改善**:
|
| 566 |
+
```python
|
| 567 |
+
# tests/test_search_engine.py
|
| 568 |
+
import pytest
|
| 569 |
+
import numpy as np
|
| 570 |
+
from app.search.engine import SearchEngine
|
| 571 |
+
|
| 572 |
+
class TestBM25FScores:
|
| 573 |
+
def setup_method(self):
|
| 574 |
+
self.engine = SearchEngine()
|
| 575 |
+
self.engine.initialize()
|
| 576 |
+
|
| 577 |
+
def test_bm25f_scores_empty_terms(self):
|
| 578 |
+
scores = self.engine._bm25f_scores([])
|
| 579 |
+
assert np.all(scores == 0.0)
|
| 580 |
+
|
| 581 |
+
def test_bm25f_scores_single_term(self):
|
| 582 |
+
scores = self.engine._bm25f_scores(["テスト"])
|
| 583 |
+
assert scores.shape == (len(self.engine.projects),)
|
| 584 |
+
assert np.all(scores >= 0.0)
|
| 585 |
+
|
| 586 |
+
def test_bm25f_idempotence(self):
|
| 587 |
+
scores1 = self.engine._bm25f_scores(["テスト", "企画"])
|
| 588 |
+
scores2 = self.engine._bm25f_scores(["テスト", "企画"])
|
| 589 |
+
np.testing.assert_array_equal(scores1, scores2)
|
| 590 |
+
|
| 591 |
+
class TestFilterStrategies:
|
| 592 |
+
def test_filter_strategy_consistency(self):
|
| 593 |
+
# STRICT > MODERATE > RELAXED
|
| 594 |
+
# つまり、同じ結果セットで RELAXED の方が多くマッチする
|
| 595 |
+
pass
|
| 596 |
+
|
| 597 |
+
# tests/test_api.py
|
| 598 |
+
import pytest
|
| 599 |
+
from fastapi.testclient import TestClient
|
| 600 |
+
from app.main import app
|
| 601 |
+
|
| 602 |
+
client = TestClient(app)
|
| 603 |
+
|
| 604 |
+
def test_health_check():
|
| 605 |
+
response = client.get("/api/health")
|
| 606 |
+
assert response.status_code == 200
|
| 607 |
+
assert response.json()["status"] == "ok"
|
| 608 |
+
|
| 609 |
+
def test_search_without_api_key():
|
| 610 |
+
response = client.post("/api/search", json={"query": "test"})
|
| 611 |
+
assert response.status_code == 403
|
| 612 |
+
|
| 613 |
+
def test_search_with_api_key():
|
| 614 |
+
response = client.post(
|
| 615 |
+
"/api/search",
|
| 616 |
+
json={"query": "test"},
|
| 617 |
+
headers={"X-API-KEY": "test-key"} # テスト用キー
|
| 618 |
+
)
|
| 619 |
+
assert response.status_code in (200, 400) # 検索は成功、クエリが空なら 400
|
| 620 |
+
```
|
| 621 |
+
|
| 622 |
+
**改善効果**:
|
| 623 |
+
- リグレッション防止
|
| 624 |
+
- リファクタリング時の信頼度向上
|
| 625 |
+
- 動作の仕様書として機能
|
| 626 |
+
|
| 627 |
+
---
|
| 628 |
+
|
| 629 |
+
### 3.3 ロギングの戦略的活用不足
|
| 630 |
+
|
| 631 |
+
**ファイル**: `app/search/engine.py`, `app/main.py` 他
|
| 632 |
+
**重要度**: 🟡 低~中
|
| 633 |
+
|
| 634 |
+
**問題内容**:
|
| 635 |
+
```python
|
| 636 |
+
# ロギングが少なく、デバッグが困難
|
| 637 |
+
def _bm25f_scores(self, terms: List[str]) -> np.ndarray:
|
| 638 |
+
# ... 長いアルゴリズム処理
|
| 639 |
+
# ログなし
|
| 640 |
+
return scores
|
| 641 |
+
|
| 642 |
+
# 一方で不要なログがある
|
| 643 |
+
log.info(f"Project summaries fetched: {len(summaries_payload)} items")
|
| 644 |
+
```
|
| 645 |
+
|
| 646 |
+
**具体的な問題**:
|
| 647 |
+
1. **パフォーマンスデバッグの困難さ**: どの処理が遅いのか不明確
|
| 648 |
+
2. **エラー追跡の困難さ**: 検索がなぜ失敗したのか、スコアが低いのか不明確
|
| 649 |
+
3. **本番環境の可視性低下**: PostHog でログを追跡していますが、内部的なキャッシュ状態などが見えません
|
| 650 |
+
|
| 651 |
+
**推奨改善**:
|
| 652 |
+
```python
|
| 653 |
+
import logging
|
| 654 |
+
from dataclasses import dataclass
|
| 655 |
+
from typing import Optional
|
| 656 |
+
|
| 657 |
+
@dataclass
|
| 658 |
+
class SearchMetrics:
|
| 659 |
+
query: str
|
| 660 |
+
terms_count: int
|
| 661 |
+
documents_count: int
|
| 662 |
+
bm25_max: float
|
| 663 |
+
ws_filter_max: float
|
| 664 |
+
final_results_count: int
|
| 665 |
+
fallback_applied: bool
|
| 666 |
+
elapsed_ms: float
|
| 667 |
+
|
| 668 |
+
class SearchEngine:
|
| 669 |
+
def search(self, query: str, debug: bool = False) -> ...:
|
| 670 |
+
import time
|
| 671 |
+
start = time.time()
|
| 672 |
+
|
| 673 |
+
terms = self._tokenize(query)
|
| 674 |
+
log.debug(f"Query tokenized: {len(terms)} terms from '{query}'")
|
| 675 |
+
|
| 676 |
+
# ... スコアリング処理
|
| 677 |
+
|
| 678 |
+
bm25 = self._bm25f_scores(terms)
|
| 679 |
+
log.debug(f"BM25F scores: min={bm25.min():.3f}, max={bm25.max():.3f}, "
|
| 680 |
+
f"mean={bm25.mean():.3f}")
|
| 681 |
+
|
| 682 |
+
# ... 検索処理続行
|
| 683 |
+
|
| 684 |
+
# メトリクス記録
|
| 685 |
+
metrics = SearchMetrics(
|
| 686 |
+
query=query,
|
| 687 |
+
terms_count=len(terms),
|
| 688 |
+
documents_count=len(self.projects),
|
| 689 |
+
bm25_max=float(bm25.max()),
|
| 690 |
+
ws_filter_max=float(ws_filter.max()) if ws_filter is not None else 0.0,
|
| 691 |
+
final_results_count=len(pairs),
|
| 692 |
+
fallback_applied=fallback_was_applied,
|
| 693 |
+
elapsed_ms=(time.time() - start) * 1000,
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
log.info(f"Search completed: {len(pairs)} results in {metrics.elapsed_ms:.1f}ms")
|
| 697 |
+
|
| 698 |
+
if posthog and GET_LOGS:
|
| 699 |
+
posthog.capture(event="search_metrics", properties=vars(metrics))
|
| 700 |
+
|
| 701 |
+
return pairs
|
| 702 |
+
```
|
| 703 |
+
|
| 704 |
+
**改善効果**:
|
| 705 |
+
- パフォーマンス最適化の根拠が得られる
|
| 706 |
+
- 検索品質の測定が可能に
|
| 707 |
+
- ユーザー行動の詳細な分析が可能に
|
| 708 |
+
|
| 709 |
+
---
|
| 710 |
+
|
| 711 |
+
### 3.4 型注釈の不完全性
|
| 712 |
+
|
| 713 |
+
**ファイル**: `app/search/engine.py` 他
|
| 714 |
+
**重要度**: 🟡 低
|
| 715 |
+
|
| 716 |
+
**問題内容**:
|
| 717 |
+
```python
|
| 718 |
+
def _bm25f_scores(self, terms: List[str]) -> np.ndarray:
|
| 719 |
+
# np.ndarray の形状や dtype が未指定
|
| 720 |
+
pass
|
| 721 |
+
|
| 722 |
+
def search(
|
| 723 |
+
self,
|
| 724 |
+
query: str,
|
| 725 |
+
debug: bool = False,
|
| 726 |
+
) -> List[Tuple[str, float]] | Tuple[List[Tuple[str, float]], Dict[str, Any]]:
|
| 727 |
+
# Union 型が複雑で読みにくい
|
| 728 |
+
pass
|
| 729 |
+
```
|
| 730 |
+
|
| 731 |
+
**推奨改善**:
|
| 732 |
+
```python
|
| 733 |
+
from typing import TypeAlias, overload
|
| 734 |
+
import numpy as np
|
| 735 |
+
from numpy.typing import NDArray
|
| 736 |
+
|
| 737 |
+
ScoreArray: TypeAlias = NDArray[np.float32]
|
| 738 |
+
SearchResult: TypeAlias = List[Tuple[str, float]]
|
| 739 |
+
DebugInfo: TypeAlias = Dict[str, Any]
|
| 740 |
+
|
| 741 |
+
class SearchEngine:
|
| 742 |
+
def _bm25f_scores(self, terms: List[str]) -> ScoreArray:
|
| 743 |
+
"""
|
| 744 |
+
BM25F スコアを計算
|
| 745 |
+
|
| 746 |
+
Returns:
|
| 747 |
+
Shape (n_documents,) の float32 配列
|
| 748 |
+
"""
|
| 749 |
+
return np.zeros((len(self.tf_token_docs),), dtype=np.float32)
|
| 750 |
+
|
| 751 |
+
@overload
|
| 752 |
+
def search(self, query: str, debug: bool = False) -> SearchResult:
|
| 753 |
+
...
|
| 754 |
+
|
| 755 |
+
@overload
|
| 756 |
+
def search(self, query: str, debug: bool = True) -> Tuple[SearchResult, DebugInfo]:
|
| 757 |
+
...
|
| 758 |
+
|
| 759 |
+
def search(self, query: str, debug: bool = False):
|
| 760 |
+
# ...
|
| 761 |
+
if debug:
|
| 762 |
+
return pairs, debug_info
|
| 763 |
+
return pairs
|
| 764 |
+
```
|
| 765 |
+
|
| 766 |
+
**改善効果**:
|
| 767 |
+
- IDE のオートコンプリート精度向上
|
| 768 |
+
- 型チェッカー(mypy)による自動検証可能に
|
| 769 |
+
- ドキュメント性が向上
|
| 770 |
+
|
| 771 |
+
---
|
| 772 |
+
|
| 773 |
+
## 4. 推奨実装優先度
|
| 774 |
+
|
| 775 |
+
### フェーズ 1(即座 - 数時間)
|
| 776 |
+
1. ✅ コネクションプール管理の例外ハンドリング追加
|
| 777 |
+
2. ✅ APIキー認証のロジック明確化(環境変数チェック)
|
| 778 |
+
|
| 779 |
+
### フェーズ 2(短期:1-2週間)
|
| 780 |
+
1. 例外ハンドリングの強化(ロギング付き)
|
| 781 |
+
2. フィルタリング戦略の統一(FilterStrategy 導入)
|
| 782 |
+
3. 設定管理の Pydantic 化
|
| 783 |
+
|
| 784 |
+
### フェーズ 3(中期:1-2ヶ月)
|
| 785 |
+
1. テストスイートの構築(pytest)
|
| 786 |
+
2. IDF 重み付けの正規化処理
|
| 787 |
+
3. サブストリングマッチの統合改善
|
| 788 |
+
|
| 789 |
+
### フェーズ 4(長期:随時)
|
| 790 |
+
1. 型注釈の完全化
|
| 791 |
+
2. パフォーマンス最適化(ベクトル計算の GPU 化など)
|
| 792 |
+
3. 検索ロジックの A/B テスト環境構築
|
| 793 |
+
|
| 794 |
+
---
|
| 795 |
+
|
| 796 |
+
## 5. まとめ
|
| 797 |
+
|
| 798 |
+
本コードベースは**機能的には堅牢**ですが、以下の領域で改善が有効です:
|
| 799 |
+
|
| 800 |
+
| カテゴリ | 優先度 | 影響度 | 実装時間 |
|
| 801 |
+
|---------|--------|--------|---------|
|
| 802 |
+
| セキュリティ・堅牢性 | 🔴 高 | 高い | 数時間 |
|
| 803 |
+
| エラーハンドリング | 🟠 中 | 中程度 | 1-2日 |
|
| 804 |
+
| 検索ロジック(フィルタリング) | 🟡 中 | 中程度 | 1週間 |
|
| 805 |
+
| テスト戦略 | 🟡 中 | 高い(長期) | 2週間+ |
|
| 806 |
+
| ロギング | 🟡 低~中 | 低~中 | 3-5日 |
|
| 807 |
+
| 型安全性 | 🟡 低 | 低い | 1-2日 |
|
| 808 |
+
|
| 809 |
+
**即座に対応すべき項目**は**フェーズ 1** の 2 つです。これらを解決することで、API のセキュリティと安定性が大幅に向上します。
|
| 810 |
+
|
| 811 |
+
### 実装推奨工程
|
| 812 |
+
|
| 813 |
+
1. **Phase 1 実装**(本日中)
|
| 814 |
+
- コネクション例外ハンドリング追加
|
| 815 |
+
- API_SECRET_KEY チェック追加
|
| 816 |
+
- デプロイして検証
|
| 817 |
+
|
| 818 |
+
2. **Phase 2 実装**(1-2週間以内)
|
| 819 |
+
- 例外ハンドリング強化
|
| 820 |
+
- フィルタリング戦略統一
|
| 821 |
+
|
| 822 |
+
3. **Phase 3 以降**(優先度と人員に応じて)
|
| 823 |
+
- テストスイート構築
|
| 824 |
+
- 検索ロジック最適化
|
| 825 |
+
|
docs/検索アルゴリズム詳説.md
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 検索アルゴリズム詳説:数学的理解
|
| 2 |
+
|
| 3 |
+
**作成日**: 2025年12月11日
|
| 4 |
+
**対象**: アルゴリズムの数学的背景に興味のある学生
|
| 5 |
+
**言語**: 日本語
|
| 6 |
+
**前提知識**: 「システムガイド.md」を読了していること
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
このドキュメントは、このプロジェクトで使われている検索アルゴリズムの数学的な背景を説明するものです。「システムガイド.md」の「4. 検索システムの仕組み」で紹介した概念を、より厳密に解説します。
|
| 11 |
+
|
| 12 |
+
数学が得意でない方は、概念的な説明だけを読んで、数式は参考程度で構いません。
|
| 13 |
+
|
| 14 |
+
## 目次
|
| 15 |
+
|
| 16 |
+
1. [BM25F(単語一致の強さ)](#1-bm25f単語一致の強さ)
|
| 17 |
+
2. [TF-IDF](#2-tf-idf)
|
| 18 |
+
3. [単語ベクトルとコサイン類似度](#3-単語ベクトルとコサイン類似度)
|
| 19 |
+
4. [このプロジェクトでのスコア統合](#4-このプロジェクトでのスコア統合)
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 1. BM25F(単語一致の強さ)
|
| 24 |
+
|
| 25 |
+
### 概要
|
| 26 |
+
|
| 27 |
+
BM25Fは、情報検索の分野で最も実績のあるスコアリング関数です。Fは「Field」を意味し、複数のフィールド(企画名、説明文など)を扱う場合に拡張されています。
|
| 28 |
+
|
| 29 |
+
### BM25 の基本形
|
| 30 |
+
|
| 31 |
+
$$\text{BM25}(q, d) = \sum_{i=1}^{|q|} \text{IDF}(q_i) \cdot \frac{(k_1 + 1) \cdot \text{TF}(q_i, d)}{k_1 \cdot (1 - b + b \cdot |d| / \text{avgdl}) + \text{TF}(q_i, d)}$$
|
| 32 |
+
|
| 33 |
+
テキスト形式:
|
| 34 |
+
```
|
| 35 |
+
BM25(q, d) = Σ_{i=1}^{|q|} IDF(q_i) · (k1 + 1) · TF(q_i, d) / (k1 · (1 - b + b · |d| / avgdl) + TF(q_i, d))
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
各記号の意味:
|
| 39 |
+
- $q$ = クエリ(検索語)
|
| 40 |
+
- $d$ = 文書(企画データ)
|
| 41 |
+
- $q_i$ = クエリの i 番目の単語
|
| 42 |
+
- $|q|$ = クエリの単語数
|
| 43 |
+
- $TF(q_i, d)$ = 単語 $q_i$ がドキュメント $d$ に出現した回数
|
| 44 |
+
- $|d|$ = 文書の長さ(単語数)
|
| 45 |
+
- $avgdl$ = 全文書の平均長さ
|
| 46 |
+
- $k1$, $b$ = ハイパーパラメータ
|
| 47 |
+
- $IDF(q_i)$ = 逆文書頻度
|
| 48 |
+
|
| 49 |
+
### IDF(Inverse Document Frequency)
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
IDF(w) = log( (N - df(w) + 0.5) / (df(w) + 0.5) )
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
- $N$ = 全文書数
|
| 56 |
+
- $df(w)$ = 単語 $w$ が出現する文書の数
|
| 57 |
+
|
| 58 |
+
**直感的な意味:**
|
| 59 |
+
- $df(w)$ が小さい(珍しい単語)→ IDF が大きい → スコアへの寄与が大きい
|
| 60 |
+
- $df(w)$ が大きい(よくある単語)→ IDF が小さい → スコアへの寄与が小さい
|
| 61 |
+
|
| 62 |
+
**例:**
|
| 63 |
+
企画が100個あるとき
|
| 64 |
+
- 「食べ物」は80個の企画に出現 → $df = 80$ → IDF = $\log(20.5/80.5) \approx -1.37$ (低い)
|
| 65 |
+
- 「生ビール」は3個の企画に出現 → $df = 3$ → IDF = $\log(97.5/3.5) \approx 3.43$ (高い)
|
| 66 |
+
|
| 67 |
+
### BM25F への拡張(複数フィールド対応)
|
| 68 |
+
|
| 69 |
+
複数のフィールド(企画名、説明文など)がある場合、各フィールドごとに TF を計算してから、重みを付けます。
|
| 70 |
+
|
| 71 |
+
$$\text{BM25F}(q, d) = \sum_{i=1}^{|q|} \text{IDF}(q_i) \cdot \frac{\sum_{f} w_f \cdot (k_1 + 1) \cdot \text{TF}_f(q_i, d)}{\sum_{f} w_f \cdot (\text{TF}_f(q_i, d) + k_1 \cdot (1 - b + b \cdot \text{len}_f / \text{avglen}_f))}$$
|
| 72 |
+
|
| 73 |
+
**注意:** このプロジェクトの実装では、分子と分母にそれぞれフィールドごとの加重合計を計算してから、統合スコアを求めます。これは複数フィールドの BM25 における一般的なアプローチです。
|
| 74 |
+
|
| 75 |
+
テキスト形式:
|
| 76 |
+
```
|
| 77 |
+
BM25F(q, d) = Σ_{i=1}^{|q|} IDF(q_i) ·
|
| 78 |
+
(Σ_f w_f · TF_f · (k1 + 1)) /
|
| 79 |
+
(Σ_f w_f · (TF_f + k1 · (1 - b + b · len_f / avglen_f)))
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
- $f$ = フィールド(例:name, description)
|
| 83 |
+
- $w_f$ = フィールド $f$ の重み(例:name は 2.0、description は 1.0)
|
| 84 |
+
- $TF_f(q_i, d)$ = 単語 $q_i$ がフィールド $f$ に出現した回数
|
| 85 |
+
- $len_f$ = フィールド $f$ の長さ
|
| 86 |
+
- $avglen_f$ = フィールド $f$ の全文書平均長さ
|
| 87 |
+
|
| 88 |
+
### パラメータの意味と調整
|
| 89 |
+
|
| 90 |
+
#### $k1$(デフォルト 1.2)
|
| 91 |
+
TFの影響度を制御します。
|
| 92 |
+
- $k1$ が大きい → TF の差がスコアに大きく反映(繰り返しが重要)
|
| 93 |
+
- $k1$ が小さい → TF の差がスコアに反映されにくい
|
| 94 |
+
|
| 95 |
+
#### $b$(デフォルト 0.75)
|
| 96 |
+
文書長さの正規化度合いを制御します。
|
| 97 |
+
- $b = 0$ → 文書長さを無視
|
| 98 |
+
- $b = 1$ → 文書長さを完全に考慮
|
| 99 |
+
- $b = 0.75$ → 中程度に考慮(推奨値)
|
| 100 |
+
|
| 101 |
+
**例:** 企画説明が極端に長い場合、$b$ を小さくすると、長さの影響を減らせます。
|
| 102 |
+
|
| 103 |
+
#### フィールド重み
|
| 104 |
+
```json
|
| 105 |
+
{
|
| 106 |
+
"name": 2.0,
|
| 107 |
+
"circleName": 1.5,
|
| 108 |
+
"description": 1.0
|
| 109 |
+
}
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
企画名に出現する単語は、説明文に出現する単語の2倍の価値があるということです。
|
| 113 |
+
|
| 114 |
+
### このプロジェクトの設定値
|
| 115 |
+
|
| 116 |
+
```json
|
| 117 |
+
{
|
| 118 |
+
"bm25f": {
|
| 119 |
+
"k1": 1.2,
|
| 120 |
+
"b": 0.75,
|
| 121 |
+
"field_weights": {
|
| 122 |
+
"name": 2.0,
|
| 123 |
+
"circleName": 1.5,
|
| 124 |
+
"circleNameKana": 0.6,
|
| 125 |
+
"description": 1.0,
|
| 126 |
+
"prSummary": 1.0,
|
| 127 |
+
"prDetail": 0.8
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
---
|
| 134 |
+
|
| 135 |
+
## 2. TF-IDF
|
| 136 |
+
|
| 137 |
+
### 概要
|
| 138 |
+
|
| 139 |
+
TF-IDF は、単語の重要度を計算する最も基本的な手法です。
|
| 140 |
+
|
| 141 |
+
```
|
| 142 |
+
TF-IDF(w, d) = TF(w, d) · IDF(w)
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
### TF(Term Frequency)
|
| 146 |
+
|
| 147 |
+
単語 $w$ がドキュメント $d$ に出現した回数。通常は正規化されます。
|
| 148 |
+
|
| 149 |
+
#### 単純な TF
|
| 150 |
+
```
|
| 151 |
+
TF(w, d) = 単語 w が d に出現した回数
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
#### 正規化 TF
|
| 155 |
+
```
|
| 156 |
+
TF(w, d) = (単語 w が d に出現した回数) / (d の総単語数)
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
#### 対数正規化 TF(BM25で使用)
|
| 160 |
+
```
|
| 161 |
+
TF(w, d) = log(1 + (単語 w が d に出現した回数))
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
**利点:** 出現回数が多いほどスコアが上がるが、100回と101回の差は1回と2回ほど大きくない(飽和性)
|
| 165 |
+
|
| 166 |
+
### IDF(Inverse Document Frequency)
|
| 167 |
+
|
| 168 |
+
前述の「1. BM25F」を参照。
|
| 169 |
+
|
| 170 |
+
### TF-IDF の解釈
|
| 171 |
+
|
| 172 |
+
```
|
| 173 |
+
TF-IDF = 「その単語がその文書にどれだけ重要か」
|
| 174 |
+
= 「珍しく」かつ「繰り返し出現する」単語ほど重要
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
**例:**
|
| 178 |
+
|
| 179 |
+
クエリ:「カレー 販売」、 企画A:「カレー販売|カレーはインドの料理です。当団体が作るカレーは...(以下詳細)」
|
| 180 |
+
|
| 181 |
+
- 「カレー」: TF(カレー, A) 高い、IDF(カレー) 中程度 → TF-IDF 高い
|
| 182 |
+
- 「販売」: TF(販売, A) 低い、IDF(販売) 高い → TF-IDF 中程度
|
| 183 |
+
- 「料理」: TF(料理, A) 中程度、IDF(料理) 中程度 → TF-IDF 中程度
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
## 3. 単語ベクトルとコサイン類似度
|
| 188 |
+
|
| 189 |
+
### 単語ベクトルとは
|
| 190 |
+
|
| 191 |
+
単語を「意味を反映した高次元ベクトル」として表現する手法です。
|
| 192 |
+
|
| 193 |
+
```
|
| 194 |
+
"カレー" → [0.12, -0.34, 0.67, ..., 0.23] (例:300次元)
|
| 195 |
+
"ラーメン" → [0.11, -0.35, 0.68, ..., 0.24]
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
**重要な性質:** 意味が近い単語のベクトルは、空間上で近い位置にあります。
|
| 199 |
+
|
| 200 |
+
### fastText
|
| 201 |
+
|
| 202 |
+
このプロジェクトで使われているfastTextは、以下の特徴を持っています:
|
| 203 |
+
|
| 204 |
+
1. **事前学習済み**: 大量の日本語テキストから学習済みのベクトルを使用
|
| 205 |
+
2. **部分語情報を活用**: 未知語(コーパスに出現しない単語)でも推測できる
|
| 206 |
+
3. **300次元**: 各単語は300次元のベクトルで表現
|
| 207 |
+
|
| 208 |
+
### コサイン類似度
|
| 209 |
+
|
| 210 |
+
2つのベクトルの「角度」から、意味的な近さを計算します。
|
| 211 |
+
|
| 212 |
+
```
|
| 213 |
+
cos_sim(v1, v2) = (v1 · v2) / (||v1|| · ||v2||)
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
- $v_1 \cdot v_2$ = 内積(ドット積)
|
| 217 |
+
- $||v_1||$ = ベクトル $v_1$ のノルム(長さ)
|
| 218 |
+
|
| 219 |
+
### 内積の計算
|
| 220 |
+
|
| 221 |
+
```
|
| 222 |
+
v1 = [a1, a2, ..., an]
|
| 223 |
+
v2 = [b1, b2, ..., bn]
|
| 224 |
+
|
| 225 |
+
v1 · v2 = a1*b1 + a2*b2 + ... + an*bn
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
### ノルム(ベクトルの長さ)
|
| 229 |
+
|
| 230 |
+
```
|
| 231 |
+
||v|| = sqrt(a1^2 + a2^2 + ... + an^2)
|
| 232 |
+
```
|
| 233 |
+
|
| 234 |
+
### コサイン類似度の範囲
|
| 235 |
+
|
| 236 |
+
```
|
| 237 |
+
-1 ≤ cos_sim(v1, v2) ≤ 1
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
- 1に近い → 同じ方向(意味が非常に近い)
|
| 241 |
+
- 0に近い → 直交(無関係)
|
| 242 |
+
- -1に近い → 反対方向(反対の意味)
|
| 243 |
+
|
| 244 |
+
**例:**
|
| 245 |
+
```
|
| 246 |
+
cos_sim("カレー", "スープ") ≈ 0.85 (近い)
|
| 247 |
+
cos_sim("カレー", "本棚") ≈ 0.10 (遠い)
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
### Python での計算
|
| 251 |
+
|
| 252 |
+
```python
|
| 253 |
+
import numpy as np
|
| 254 |
+
|
| 255 |
+
# ベクトルを正規化(長さを1にする)
|
| 256 |
+
v1 = np.array([0.12, -0.34, 0.67])
|
| 257 |
+
v2 = np.array([0.11, -0.35, 0.68])
|
| 258 |
+
|
| 259 |
+
# L2 正規化
|
| 260 |
+
v1_norm = v1 / np.linalg.norm(v1)
|
| 261 |
+
v2_norm = v2 / np.linalg.norm(v2)
|
| 262 |
+
|
| 263 |
+
# コサイン類似度 = 内積(正規化済み)
|
| 264 |
+
cos_sim = np.dot(v1_norm, v2_norm)
|
| 265 |
+
print(cos_sim) # 例: 0.9995
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
### このプロジェクトでのベクトル類似度計算
|
| 269 |
+
|
| 270 |
+
2つのモード:
|
| 271 |
+
|
| 272 |
+
#### Mode 1: Top-k Pooling(フィルタリング用)
|
| 273 |
+
|
| 274 |
+
クエリの各単語とドキュメント内の各単語のペアについて、コサイン類似度を計算し、高い上位k個を抽出して平均を取ります。
|
| 275 |
+
|
| 276 |
+
**実装の詳細:**
|
| 277 |
+
1. クエリの各単語ベクトルに対して、IDF による重み付けを行います
|
| 278 |
+
2. ドキュメント内のすべての単語について、クエリの各単語とのコサイン類似度を計算(行列計算)
|
| 279 |
+
3. 計算結果の行列 M(サイズ: ドキュメント単語数 × クエリ単語数)について、IDF重みを乗算
|
| 280 |
+
4. 負の値を0に截断( ReLU)
|
| 281 |
+
5. 全要素から top-k を抽出し、その平均を取る
|
| 282 |
+
|
| 283 |
+
$$\text{sim}_{\text{topk}}(q, d) = \text{mean}(\text{top-k}(\cos\text{-sim}(q_i, d_j) \cdot w_i \text{ for all } i, j))$$
|
| 284 |
+
|
| 285 |
+
**目的:** 高速なフィルタリング(クイックスクリーニング)
|
| 286 |
+
|
| 287 |
+
#### Mode 2: Pair-Average(リランキング用)
|
| 288 |
+
|
| 289 |
+
クエリの全単語とドキュメント内の全単語のペアについて、コサイン類似度を計算し、全体の平均を取ります。
|
| 290 |
+
|
| 291 |
+
**実装の詳細:**
|
| 292 |
+
1. クエリの各単語ベクトルを集める(重み付けなし)
|
| 293 |
+
2. ドキュメント内のすべての単語について、クエリの各単語とのコサイン類似度を計算(行列計算)
|
| 294 |
+
3. 計算結果の行列 M(サイズ: ドキュメント単語数 × クエリ単語数)に��いて、負の値を0に截断( ReLU)
|
| 295 |
+
4. 全要素の平均を取る
|
| 296 |
+
|
| 297 |
+
$$\text{sim}_{\text{pair\_avg}}(q, d) = \text{mean}(\cos\text{-sim}(q_i, d_j) \text{ for all pairs } (i, j))$$
|
| 298 |
+
|
| 299 |
+
**目的:** より精密なスコアリング
|
| 300 |
+
|
| 301 |
+
---
|
| 302 |
+
|
| 303 |
+
## 4. このプロジェクトでのスコア統合
|
| 304 |
+
|
| 305 |
+
### 複数スコアの融合
|
| 306 |
+
|
| 307 |
+
このプロジェクトでは、複数の指標を組み合わせて、最終的なスコアを計算します。
|
| 308 |
+
|
| 309 |
+
```
|
| 310 |
+
最終スコア = α × BM25F + (1 - α) × ベクトル類似度 + 団体名ボーナス
|
| 311 |
+
```
|
| 312 |
+
|
| 313 |
+
### パラメータ
|
| 314 |
+
|
| 315 |
+
```json
|
| 316 |
+
{
|
| 317 |
+
"word_sim": {
|
| 318 |
+
"alpha": 0.5
|
| 319 |
+
}
|
| 320 |
+
}
|
| 321 |
+
```
|
| 322 |
+
|
| 323 |
+
- $\alpha = 0.5$ の場合、BM25F と ベクトル類似度を同じ重要度で扱う
|
| 324 |
+
- $\alpha = 0.7$ の場合、BM25F を重視
|
| 325 |
+
|
| 326 |
+
### スコア正規化の考慮
|
| 327 |
+
|
| 328 |
+
BM25F と ベクトル類似度のスケールが異なるため、実装では工夫がされています:
|
| 329 |
+
|
| 330 |
+
1. **フィルタリング段階**(候補を絞る)
|
| 331 |
+
- Top-k モードでベクトル類似度を素早く計算
|
| 332 |
+
- BM25F の最小閾値を設定
|
| 333 |
+
|
| 334 |
+
2. **リランキング段階**(最終順序を決める)
|
| 335 |
+
- Pair-average モードでより正確なベクトル類似度を計算
|
| 336 |
+
- BM25F と統合
|
| 337 |
+
|
| 338 |
+
### 団体名ボーナス
|
| 339 |
+
|
| 340 |
+
入力文字列が団体名に合致した場合、スコアを加算します。複数の条件が満たされた場合、それぞれのボーナスが加算されます。
|
| 341 |
+
|
| 342 |
+
```
|
| 343 |
+
団体名ボーナス = (完全一致 ? exact_boost : 0) + (前方一致 ? prefix_boost : 0) + (部分一致 ? substring_boost : 0)
|
| 344 |
+
```
|
| 345 |
+
|
| 346 |
+
デフォルト値:
|
| 347 |
+
- 完全一致: +1.0
|
| 348 |
+
- 前方一致: +0.7
|
| 349 |
+
- 部分一致: +0.5
|
| 350 |
+
|
| 351 |
+
**例:**
|
| 352 |
+
- 入力 "写真部" = 団体名 "写真部" → 完全一致のボーナス +1.0
|
| 353 |
+
- 入力 "写" = 団体名 "写真部" → 部分一致のボーナス +0.5
|
| 354 |
+
- 入力 "写真" = 団体名 "写真部" → 前方一致のボーナス +0.7
|
| 355 |
+
|
| 356 |
+
**注意:** 複数条件が同時に満たされることはありません。最も強い条件(完全一致 > 前方一致 > 部分一致)のみが適用されます。
|
| 357 |
+
|
| 358 |
+
### 最終的なフィルタリング戦略
|
| 359 |
+
|
| 360 |
+
```
|
| 361 |
+
keep = (BM25F >= min_bm25)
|
| 362 |
+
OR (ベクトル類似度 >= min_sim)
|
| 363 |
+
OR (統合スコア >= min_fused)
|
| 364 |
+
```
|
| 365 |
+
|
| 366 |
+
いずれかの条件を満たすものを結果として返します。
|
| 367 |
+
|
| 368 |
+
---
|
| 369 |
+
|
| 370 |
+
## 計算例
|
| 371 |
+
|
| 372 |
+
このセクションは、実装の複雑さを反映するため、簡略化した例を示します。実際の計算はより複雑です。
|
| 373 |
+
|
| 374 |
+
### 設定
|
| 375 |
+
- クエリ: 「カレー」
|
| 376 |
+
- 企画数: 100個
|
| 377 |
+
- 「カレー」が出現する企画: 5個(df = 5)
|
| 378 |
+
- α(BM25F vs ベクトル重み): 0.5
|
| 379 |
+
|
| 380 |
+
### Step 1: トークン化と同義語展開
|
| 381 |
+
|
| 382 |
+
```
|
| 383 |
+
入力: "カレー"
|
| 384 |
+
↓ トークン化
|
| 385 |
+
tokens = ["カレー"]
|
| 386 |
+
↓ 同義語展開(有効な場合)
|
| 387 |
+
expanded_terms = ["カレー", "食事"]
|
| 388 |
+
```
|
| 389 |
+
|
| 390 |
+
### Step 2: IDF 計算
|
| 391 |
+
|
| 392 |
+
```
|
| 393 |
+
N = 100(全企画数)
|
| 394 |
+
df("カレー") = 5(5個の企画に出現)
|
| 395 |
+
|
| 396 |
+
IDF("カレー") = log((100 - 5 + 0.5) / (5 + 0.5))
|
| 397 |
+
= log(95.5 / 5.5)
|
| 398 |
+
≈ log(17.36)
|
| 399 |
+
≈ 2.85
|
| 400 |
+
```
|
| 401 |
+
|
| 402 |
+
### Step 3: 特定の企画Aに対するスコア計算
|
| 403 |
+
|
| 404 |
+
企画A: 「カレー販売 - 当団体は美味しいカレーを販売します。カレーはインドの料理です。」
|
| 405 |
+
|
| 406 |
+
#### BM25F スコア
|
| 407 |
+
|
| 408 |
+
フィールド別の TF:
|
| 409 |
+
- name: "カレー販売" → TF("カレー") = 1
|
| 410 |
+
- description: "カレー...カレーは..." → TF("カレー") = 2
|
| 411 |
+
|
| 412 |
+
各フィールドの長さと平均:
|
| 413 |
+
- name の長さ: 6(文字列ベース)、平均: 5
|
| 414 |
+
- description の長さ: 25、平均: 20
|
| 415 |
+
|
| 416 |
+
パラメータ: k1=1.2, b=0.75
|
| 417 |
+
|
| 418 |
+
BM25F スコア計算(簡略化):
|
| 419 |
+
```
|
| 420 |
+
num_name = 2.0 * 1 * (1.2 + 1) = 4.4
|
| 421 |
+
denom_name = 2.0 * (1 + 1.2 * (1 - 0.75 + 0.75 * 6/5)) = 4.08
|
| 422 |
+
→ name部分スコア = 4.4 / 4.08 ≈ 1.08
|
| 423 |
+
|
| 424 |
+
num_desc = 1.0 * 2 * 2.2 = 4.4
|
| 425 |
+
denom_desc = 1.0 * (2 + 1.2 * (1 - 0.75 + 0.75 * 25/20)) = 4.35
|
| 426 |
+
→ desc部分スコア = 4.4 / 4.35 ≈ 1.01
|
| 427 |
+
|
| 428 |
+
BM25F = IDF("カレー") * (1.08 + 1.01) ≈ 2.85 * 2.09 ≈ 5.96
|
| 429 |
+
```
|
| 430 |
+
|
| 431 |
+
#### ベクトル類似度(Top-k)
|
| 432 |
+
|
| 433 |
+
```
|
| 434 |
+
cos_sim("カレー", "カレー") = 1.0
|
| 435 |
+
cos_sim("カレー", "販売") = 0.65
|
| 436 |
+
cos_sim("カレー", "料理") = 0.82
|
| 437 |
+
...(他の単語)
|
| 438 |
+
|
| 439 |
+
top-3: [1.0, 0.82, 0.65] → mean = 0.82
|
| 440 |
+
```
|
| 441 |
+
|
| 442 |
+
#### スコア統合(フィルタリング段階)
|
| 443 |
+
|
| 444 |
+
```
|
| 445 |
+
α = 0.5
|
| 446 |
+
|
| 447 |
+
fused_filter = 0.5 * 5.96 + 0.5 * 0.82 = 3.39
|
| 448 |
+
```
|
| 449 |
+
|
| 450 |
+
#### 団体名ボーナス
|
| 451 |
+
|
| 452 |
+
```
|
| 453 |
+
query_normalized = "カレー"
|
| 454 |
+
circleName_normalized = "飲食部カレー販売班"
|
| 455 |
+
|
| 456 |
+
完全一致: "カレー" != "飲食部カレー販売班" → false
|
| 457 |
+
前方一致: "飲食部カレー販売班".startswith("カレー") → false
|
| 458 |
+
部分一致: "カレー" in "飲食部カレー販売班" → true
|
| 459 |
+
|
| 460 |
+
org_boost = 0.5
|
| 461 |
+
```
|
| 462 |
+
|
| 463 |
+
#### 最終スコア(フィルタリング段階)
|
| 464 |
+
|
| 465 |
+
```
|
| 466 |
+
score_with_boost = fused_filter + org_boost = 3.39 + 0.5 = 3.89
|
| 467 |
+
```
|
| 468 |
+
|
| 469 |
+
### Step 4: リランキング(Pair-Average)
|
| 470 |
+
|
| 471 |
+
フィルタリングを通った企画については、Pair-Average モードでより正確なベクトル類似度を再計算し、最終順序を決定します。
|
| 472 |
+
|
| 473 |
+
```
|
| 474 |
+
sim_pair_avg = mean of all (cos_sim(qi, dj))
|
| 475 |
+
|
| 476 |
+
final_score = 0.5 * BM25F + 0.5 * sim_pair_avg + org_boost
|
| 477 |
+
```
|
| 478 |
+
|
| 479 |
+
---
|
| 480 |
+
|
| 481 |
+
## 参考資料・式の出典
|
| 482 |
+
|
| 483 |
+
1. **BM25F**: Zaragoza et al. (2004). "Relevance weighting for query independent evidence"
|
| 484 |
+
2. **fastText**: Bojanowski et al. (2017). "Enriching Word Vectors with Subword Information"
|
| 485 |
+
3. **コサイン類似度**: 情報検索の標準的な手法
|
| 486 |
+
|
| 487 |
+
---
|
| 488 |
+
|
| 489 |
+
## よくある質問
|
| 490 |
+
|
| 491 |
+
### Q: なぜBM25FとベクトルのMIXが必要か
|
| 492 |
+
|
| 493 |
+
**A:**
|
| 494 |
+
- **BM25F**は「正確性」が高い(クエリに含まれる単語の頻度をよく反映)
|
| 495 |
+
- **ベクトル類似度**は「リコール」が高い(クエリの意図に近い企画も見つかる)
|
| 496 |
+
- 両者を組み合わせることで、正確性と再現性のバランスを取ります。
|
| 497 |
+
|
| 498 |
+
### Q: αを大きくしたら何が変わるか
|
| 499 |
+
|
| 500 |
+
**A:**
|
| 501 |
+
- **α = 1.0**: BM25Fのみを使用(単語一致重視、保守的)
|
| 502 |
+
- **α = 0.5**: BM25FとベクトルMIX(バランス型、推奨)
|
| 503 |
+
- **α = 0.0**: ベクトルのみを使用(意味重視、広い検索)
|
| 504 |
+
|
| 505 |
+
### Q: なぜ700次元ではなく300次元か
|
| 506 |
+
|
| 507 |
+
**A:** このプロジェクトで使用しているfastTextの日本語事前学習モデル(`cc.ja.300.bin`)が300次元です。より高い次元のモデルもありますが、以下の理由から300次元が採用されています:
|
| 508 |
+
|
| 509 |
+
- **標準的な選択**: fastTextの推奨モデルであり、多くのプロジェクトで使用されている
|
| 510 |
+
- **計算効率**: 高次元になるほど、行列演算(内積計算)にかかるコストが増加
|
| 511 |
+
- **精度とのバランス**: 300次元で既に十分な表現力がある
|
| 512 |
+
- **メモリ効率**: ベクトル行列をメモリに保持する際のサイズ
|
| 513 |
+
|
| 514 |
+
### Q: Top-kの「k」は何か
|
| 515 |
+
|
| 516 |
+
**A:** このプロジェクトでは k=3 です。つまり、クエリ語×ドキュメント語の全ペアから、最も類似度の高い3ペアを選んで平均を取ります。
|
| 517 |
+
|
| 518 |
+
---
|
| 519 |
+
|
| 520 |
+
本ドキュメントでわかりづらい点や、さらに詳しく知りたいことがあれば、「システムガイド.md」の関連セクションを参照してください。
|