Zun Iguchi commited on
Commit
ccf6ffb
·
unverified ·
2 Parent(s): dd595fcacff396

Merge pull request #3 from chibafes-dev/develop

Browse files
.github/workflows/deploy_to_hf_space.yaml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ deploy-to-space:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout repository
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Push to Hugging Face Space
17
+ env:
18
+ # GitHub Secretsに設定したHF_TOKENを利用
19
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
20
+ run: |
21
+ # Hugging Face Spaceのリポジトリをクローン
22
+ git clone https://chibafes-dev-admin:$HF_TOKEN@huggingface.co/spaces/chibafes-dev/chibafes-website-api-v2 space-repo
23
+
24
+ # .gitディレクトリとクローン先ディレクトリを除き、全ファイルをコピー
25
+ # これにより、Actions内で生成したデータファイルもSpaceに同期
26
+ rsync -av --delete --exclude='.git/' --exclude='space-repo/' ./ space-repo/
27
+
28
+ # Spaceリポジトリ内で変更をコミットし、プッシュ
29
+ cd space-repo
30
+ git config user.name "GitHub Actions"
31
+ git config user.email "actions@github.com"
32
+ git add .
33
+ # 変更がある場合のみコミットを実行
34
+ if ! git diff-index --quiet HEAD; then
35
+ git commit -m "Update from GitHub Actions"
36
+ fi
37
+ # chibafes-dev-admin の部分はトークンを発行した Hugging Face のユーザー名
38
+ git push https://chibafes-dev-admin:$HF_TOKEN@huggingface.co/spaces/chibafes-dev/chibafes-website-api-v2 main
.gitignore CHANGED
@@ -42,3 +42,6 @@ resources/**/*.bin
42
  .Trashes
43
  ehthumbs.db
44
  Thumbs.db
 
 
 
 
42
  .Trashes
43
  ehthumbs.db
44
  Thumbs.db
45
+
46
+ # generated data
47
+ data/generated
.vscode/extensions.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "recommendations": [
3
+ "ms-python.python",
4
+ "charliermarsh.ruff", // formatter + linter
5
+ "usernamehw.errorlens",
6
+ "mhutchie.git-graph",
7
+ "mechatroner.rainbow-csv",
8
+ ]
9
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "editor.defaultFormatter": "charliermarsh.ruff",
3
+ "editor.formatOnSave": true
4
+ }
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12.11-slim
2
+
3
+ # 作業ディレクトリの設定
4
+ WORKDIR /app
5
+
6
+ # 依存関係のインストール
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir --upgrade pip && \
9
+ pip install --no-cache-dir -r requirements.txt
10
+
11
+ # プロジェクトのソースコードをコピー
12
+ COPY . .
13
+
14
+ # データディレクトリを作成し、権限を付与
15
+ RUN mkdir -p data/generated && chmod -R 755 data
16
+
17
+ # データファイルをビルド
18
+ RUN python scripts/build_all.py
19
+
20
+ # Hugging Face Hub のキャッシュディレクトリを設定
21
+ ENV HF_HOME /tmp/hf_cache
22
+
23
+ # アプリケーションがリッスンするポート
24
+ EXPOSE 7860
25
+
26
+ # uvicornでFastAPIアプリケーションを起動
27
+ # Hugging Face Spacesはデフォルトでポート7860を公開します
28
+ CMD ["uvicorn", "api.index:app", "--host", "0.0.0.0", "--port", "7860"]
api/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __all__ = []
2
+
api/main.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import gzip
4
+ from contextlib import asynccontextmanager
5
+
6
+ from fastapi import FastAPI, Response, Depends, HTTPException, Security, Query
7
+ from fastapi.responses import RedirectResponse, JSONResponse
8
+ from fastapi.security import APIKeyHeader
9
+ from pydantic import BaseModel
10
+
11
+ from dotenv import load_dotenv
12
+
13
+ from utils.logger import setup_logger
14
+ from utils.json import get_file_path_from_config
15
+ from api.search.engine import SearchEngine
16
+ import schemas.projects as schema_projects
17
+
18
+ log = setup_logger(__name__)
19
+
20
+ load_dotenv()
21
+
22
+ # --- Auth ---
23
+ API_KEY_NAME = "X-API-KEY"
24
+ API_SECRET_KEY = os.getenv("API_SECRET_KEY")
25
+ api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True)
26
+
27
+
28
+ async def get_api_key(key: str = Security(api_key_header)):
29
+ """APIキーを検証する依存関係"""
30
+ if not API_SECRET_KEY or key != API_SECRET_KEY:
31
+ raise HTTPException(status_code=403, detail="Could not validate credentials.")
32
+ return key
33
+
34
+
35
+ # --- App ---
36
+ engine = SearchEngine()
37
+
38
+
39
+ def download_embeddings() -> None:
40
+ """Hugging Face Datasets から Embeddings をダウンロードする"""
41
+ from huggingface_hub import hf_hub_download
42
+
43
+ # フォルダがなかったら新規作成
44
+ target_dir = "resources/embeddings"
45
+ os.makedirs(target_dir, exist_ok=True)
46
+
47
+ # ダウンロード
48
+ path = hf_hub_download(
49
+ repo_id=os.getenv("HF_EMBEDDINGS_REPO_ID"),
50
+ repo_type="dataset",
51
+ filename="cc.ja.300.bin",
52
+ local_dir=target_dir,
53
+ local_dir_use_symlinks=False,
54
+ token=os.getenv("HF_TOKEN"),
55
+ )
56
+
57
+ log.info(f"Embeddings downloaded: {path}")
58
+ return
59
+
60
+
61
+ @asynccontextmanager
62
+ async def lifespan(app: FastAPI):
63
+ """アプリケーションの起動時と終了時に実行されるコード"""
64
+ # --- 起動時処理 ---
65
+ log.info("Initializing search engine and loading assets...")
66
+ load_dotenv()
67
+ if os.path.exists(get_file_path_from_config("embeddings.fasttext_bin")):
68
+ log.info("FastText embeddings found locally.")
69
+ else:
70
+ log.info("FastText embeddings not found locally. Downloading...")
71
+ download_embeddings()
72
+ engine.initialize()
73
+ log.info("Initialization complete.")
74
+ yield
75
+
76
+ # --- 終了時処理 ---
77
+ log.info("Shutting down search engine...")
78
+
79
+
80
+ app = FastAPI(lifespan=lifespan)
81
+
82
+
83
+ @app.get("/", include_in_schema=False)
84
+ def root():
85
+ return RedirectResponse("/docs")
86
+
87
+
88
+ @app.get("/api/health")
89
+ def health_check():
90
+ return {"status": "ok"}
91
+
92
+
93
+ @app.get(
94
+ "/api/projects",
95
+ response_model=list[schema_projects.ProjectSummary],
96
+ dependencies=[Depends(get_api_key)],
97
+ )
98
+ def get_summary_data():
99
+ summary_fields = set(schema_projects.ProjectSummary.model_fields.keys())
100
+ summaries = [
101
+ {k: v for k, v in p.items() if k in summary_fields}
102
+ for p in engine.get_projects()
103
+ ]
104
+ content = json.dumps(summaries, ensure_ascii=False).encode("utf-8")
105
+ return Response(
106
+ content=gzip.compress(content),
107
+ headers={"Content-Encoding": "gzip", "Content-Type": "application/json"},
108
+ )
109
+
110
+
111
+ @app.get(
112
+ "/api/details",
113
+ response_model=schema_projects.ProjectDetail,
114
+ dependencies=[Depends(get_api_key)],
115
+ )
116
+ def get_project_detail(projectId: str = Query(..., description="取得したい企画のID")):
117
+ p = engine.get_project_map().get(projectId)
118
+ if not p:
119
+ raise HTTPException(status_code=404, detail="Project not found")
120
+ fields = set(schema_projects.ProjectDetail.model_fields.keys())
121
+ return {k: v for k, v in p.items() if k in fields}
122
+
123
+
124
+ class SearchRequest(BaseModel):
125
+ query: str
126
+ debug: bool = False
127
+
128
+
129
+ @app.post(
130
+ "/api/search",
131
+ response_model=schema_projects.ProjectIds,
132
+ dependencies=[Depends(get_api_key)],
133
+ )
134
+ def search(request: SearchRequest):
135
+ if not request.query:
136
+ raise HTTPException(status_code=400, detail="Query cannot be empty")
137
+
138
+ result = engine.search(request.query, debug=request.debug)
139
+ if request.debug:
140
+ pairs, diag = result # type: ignore
141
+ ids = [pid for pid, _ in pairs]
142
+ return JSONResponse(
143
+ content={
144
+ "projectIds": ids,
145
+ "scores": [
146
+ {"projectId": pid, "score": float(score)} for pid, score in pairs
147
+ ],
148
+ "details": diag.get("details", []),
149
+ }
150
+ )
151
+ pairs = result # type: ignore
152
+ ids = [pid for pid, _ in pairs]
153
+ return schema_projects.ProjectIds(projectIds=ids)
api/search/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __all__ = []
2
+
api/search/engine.py ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, List, Optional, Tuple
5
+
6
+ import numpy as np
7
+ from sudachipy import dictionary, tokenizer
8
+
9
+ from utils.json import field_getter
10
+ from utils.logger import setup_logger
11
+
12
+ log = setup_logger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class SearchConfig:
17
+ target_pos_l1: List[str]
18
+ target_fields: List[str]
19
+ k1: float
20
+ b: float
21
+ field_weights: Dict[str, float]
22
+ synonyms_enable: bool
23
+ syn_limits: Dict[str, int]
24
+ banlist: List[str]
25
+ word_sim_enable: bool
26
+ word_sim_alpha: float
27
+ word_sim_topk_k: int
28
+ word_sim_rerank: str
29
+ query_subword_enable: bool
30
+ query_subword_path: str
31
+ query_subword_oov_weight: float
32
+ org_boost_exact: float
33
+ org_boost_prefix: float
34
+ org_boost_substring: float
35
+ org_boost_min_len: int
36
+ # filter
37
+ min_results: int
38
+ max_results: int
39
+ bm25_min: float
40
+ word_sim_min: float
41
+ fused_min: float
42
+ fused_rel_top_ratio: float
43
+
44
+
45
+ def normalize_text_for_org(s: str) -> str:
46
+ try:
47
+ import unicodedata
48
+
49
+ s = unicodedata.normalize("NFKC", s)
50
+ except Exception:
51
+ pass
52
+ s = " ".join(s.split())
53
+ return s
54
+
55
+
56
+ class SearchEngine:
57
+ def __init__(self):
58
+ self.cfg: Optional[SearchConfig] = None
59
+ self.tokenizer = None
60
+ self.mode = None
61
+ self.stopwords: set[str] = set()
62
+ self.custom_synonyms: Dict[str, List[str]] = {}
63
+ self.synonyms_cache: Dict[str, List[str]] = {}
64
+
65
+ # Data
66
+ self.projects: List[Dict[str, Any]] = []
67
+ self.project_map: Dict[str, Dict[str, Any]] = {}
68
+ self.org_norms: Dict[str, str] = {}
69
+ self.reading_norms: Dict[str, str] = {}
70
+
71
+ # BM25F assets
72
+ self.idf: Dict[str, float] = {}
73
+ self.avg_len: Dict[str, float] = {}
74
+ self.tf_token_docs: List[Dict[str, Any]] = []
75
+
76
+ # Vectors
77
+ self.word_vocab: Dict[str, int] = {}
78
+ self.word_vectors: Optional[np.ndarray] = None
79
+ self.ft_model = None
80
+
81
+ # ----- Init / Load -----
82
+ def initialize(self):
83
+ files = field_getter("config/files.json")
84
+ search = field_getter("config/search_model.json")
85
+
86
+ # Config
87
+ self.cfg = SearchConfig(
88
+ target_pos_l1=search("target_pos_l1"),
89
+ target_fields=search("target_fields"),
90
+ k1=float(search("bm25f.k1")),
91
+ b=float(search("bm25f.b")),
92
+ field_weights=search("bm25f.field_weights"),
93
+ synonyms_enable=bool(search("synonyms.enable")),
94
+ syn_limits=search("synonyms.limits"),
95
+ banlist=search("synonyms.banlist"),
96
+ word_sim_enable=bool(search("word_sim.enable")),
97
+ word_sim_alpha=float(search("word_sim.alpha")),
98
+ word_sim_topk_k=int(search("word_sim.topk_k", 3)),
99
+ word_sim_rerank=(
100
+ search("word_sim.rerank", "pair_avg") or "pair_avg"
101
+ ).lower(),
102
+ query_subword_enable=bool(search("query_subword.enable")),
103
+ query_subword_path=files("embeddings.fasttext_bin"),
104
+ query_subword_oov_weight=float(search("query_subword.oov_weight")),
105
+ org_boost_exact=float(search("organization.boost.exact", 1.0)),
106
+ org_boost_prefix=float(search("organization.boost.prefix", 0.7)),
107
+ org_boost_substring=float(search("organization.boost.substring", 0.5)),
108
+ org_boost_min_len=int(search("organization.boost.min_len", 2)),
109
+ min_results=int(search("filter.min_results", 20)),
110
+ max_results=int(search("filter.max_results", 50)),
111
+ bm25_min=float(search("filter.bm25_min", 0.1)),
112
+ word_sim_min=float(search("filter.word_sim_min", 0.35)),
113
+ fused_min=float(search("filter.fused_min", 0.12)),
114
+ fused_rel_top_ratio=float(search("filter.fused_rel_top_ratio", 0.5)),
115
+ )
116
+
117
+ # Tokenizer
118
+ sudachi_config_path = files("sudachi.sudachi_config")
119
+ tok = dictionary.Dictionary(config_path=sudachi_config_path).create()
120
+ self.tokenizer = tok
121
+ self.mode = tokenizer.Tokenizer.SplitMode.A
122
+
123
+ # Stopwords
124
+ with open(files("sudachi.stopwords"), encoding="utf-8") as f:
125
+ self.stopwords = set(json.load(f))
126
+
127
+ # Synonyms assets
128
+ try:
129
+ syn_cache_path = files("sudachi.synonyms_cache")
130
+ if os.path.exists(syn_cache_path):
131
+ with open(syn_cache_path, encoding="utf-8") as f:
132
+ self.synonyms_cache = json.load(f)
133
+ except Exception as e:
134
+ log.warning(f"failed to load synonyms_cache: {e}")
135
+
136
+ try:
137
+ custom_path = field_getter("config/search_model.json")(
138
+ "synonyms.sources.custom_json"
139
+ )
140
+ if custom_path and os.path.exists(custom_path):
141
+ with open(custom_path, encoding="utf-8") as f:
142
+ self.custom_synonyms = json.load(f)
143
+ except Exception:
144
+ pass
145
+
146
+ # Projects
147
+ with open(files("projects.projects_json"), encoding="utf-8") as f:
148
+ self.projects = json.load(f)
149
+ self.project_map = {p["projectId"]: p for p in self.projects}
150
+ self.org_norms = {
151
+ p["projectId"]: normalize_text_for_org(p.get("organization") or "")
152
+ for p in self.projects
153
+ }
154
+ self.reading_norms = {
155
+ p["projectId"]: normalize_text_for_org(p.get("reading") or "")
156
+ for p in self.projects
157
+ }
158
+
159
+ # BM25F assets
160
+ with open(files("bm25.bm25_meta"), encoding="utf-8") as f:
161
+ meta = json.load(f)
162
+ self.idf = meta.get("idf", {})
163
+ self.avg_len = meta.get("avg_len", {})
164
+
165
+ with open(files("bm25.tf_token"), encoding="utf-8") as f:
166
+ self.tf_token_docs = json.load(f)
167
+
168
+ # Vectors
169
+ try:
170
+ vocab_path = files("embeddings.word_vocab")
171
+ vec_path = files("embeddings.word_vectors")
172
+ if os.path.exists(vocab_path) and os.path.exists(vec_path):
173
+ with open(vocab_path, encoding="utf-8") as f:
174
+ self.word_vocab = {k: int(v) for k, v in json.load(f).items()}
175
+ self.word_vectors = np.load(vec_path)["vectors"]
176
+ except Exception as e:
177
+ log.warning(f"word vectors not ready: {e}")
178
+
179
+ # doc_vectors.npy は topk 方式では不要
180
+
181
+ # fastText OOV
182
+ if (
183
+ self.cfg.query_subword_enable
184
+ and self.cfg.query_subword_path
185
+ and os.path.exists(self.cfg.query_subword_path)
186
+ ):
187
+ try:
188
+ import fasttext
189
+
190
+ self.ft_model = fasttext.load_model(self.cfg.query_subword_path)
191
+ log.info("fastText .bin loaded for OOV")
192
+ except Exception as e:
193
+ log.warning(f"failed to load fastText .bin: {e}")
194
+
195
+ # ----- Tokenize / Synonyms -----
196
+ def _tokenize(self, text: str) -> List[str]:
197
+ if not text:
198
+ return []
199
+ out: List[str] = []
200
+ for m in self.tokenizer.tokenize(text, self.mode):
201
+ base = m.normalized_form().lower().strip()
202
+ if not base:
203
+ continue
204
+ pos = m.part_of_speech()
205
+ if pos[0] not in self.cfg.target_pos_l1:
206
+ continue
207
+ if base in self.stopwords or base in self.cfg.banlist:
208
+ continue
209
+ out.append(base)
210
+ return out
211
+
212
+ def _expand_synonyms(self, terms: List[str]) -> List[str]:
213
+ if not self.cfg.synonyms_enable:
214
+ return terms
215
+ max_exp = int(self.cfg.syn_limits.get("max_expansions_per_term", 4))
216
+ min_len = int(self.cfg.syn_limits.get("min_char_len", 2))
217
+ expanded: List[str] = []
218
+ for t in terms:
219
+ expanded.append(t)
220
+ cands = []
221
+ cands.extend(self.synonyms_cache.get(t, []))
222
+ cands.extend(self.custom_synonyms.get(t, []))
223
+ # filter/unique
224
+ uniq = []
225
+ seen = set()
226
+ for c in cands:
227
+ if c in seen or len(c) < min_len or c in self.cfg.banlist:
228
+ continue
229
+ seen.add(c)
230
+ uniq.append(c)
231
+ if len(uniq) >= max_exp:
232
+ break
233
+ expanded.extend(uniq)
234
+ # overall limit
235
+ max_q = int(self.cfg.syn_limits.get("max_query_variants", 5))
236
+ return expanded[: max_q * max_exp + len(terms)]
237
+
238
+ # ----- BM25F -----
239
+ def _bm25f_scores(self, terms: List[str]) -> np.ndarray:
240
+ N = len(self.tf_token_docs)
241
+ if N == 0:
242
+ return np.zeros((0,), dtype=np.float32)
243
+ k1 = self.cfg.k1
244
+ b = self.cfg.b
245
+ fw = self.cfg.field_weights
246
+
247
+ scores = np.zeros((N,), dtype=np.float32)
248
+
249
+ idf = self.idf
250
+ avg_len = self.avg_len
251
+
252
+ # For quick access, build list of per-doc per-field structures
253
+ for i, d in enumerate(self.tf_token_docs):
254
+ fields = d.get("fields") or {}
255
+ s = 0.0
256
+ for t in terms:
257
+ idf_t = float(idf.get(t, 0.0))
258
+ if idf_t <= 0.0:
259
+ continue
260
+ denom_sum = 0.0
261
+ num_sum = 0.0
262
+ for field, weight in fw.items():
263
+ fobj = fields.get(field) or {}
264
+ tf = float((fobj.get("tf") or {}).get(t, 0))
265
+ if tf <= 0.0:
266
+ continue
267
+ len_f = float(fobj.get("len", 0))
268
+ avg_f = float(avg_len.get(field, 0.0)) or 1.0
269
+ norm = k1 * (1 - b + b * (len_f / avg_f))
270
+ num_sum += weight * tf * (k1 + 1.0)
271
+ denom_sum += weight * (tf + norm)
272
+ if denom_sum > 0:
273
+ s += idf_t * (num_sum / denom_sum)
274
+ scores[i] = s
275
+ return scores
276
+
277
+ # ----- Word similarity -----
278
+ def _get_token_vector(self, t: str) -> Tuple[Optional[np.ndarray], bool]:
279
+ if self.word_vectors is not None and t in self.word_vocab:
280
+ v = self.word_vectors[self.word_vocab[t]]
281
+ return v, False
282
+ if self.ft_model is not None:
283
+ try:
284
+ v = self.ft_model.get_word_vector(t)
285
+ v = v.astype(np.float32)
286
+ n = np.linalg.norm(v)
287
+ if n > 0:
288
+ v = v / n
289
+ return v, True
290
+ except Exception:
291
+ return None, True
292
+ return None, True
293
+
294
+ def _word_sim_scores_topk(self, terms: List[str]) -> Optional[np.ndarray]:
295
+ if not self.cfg.word_sim_enable:
296
+ return None
297
+ if self.word_vectors is None:
298
+ return None
299
+
300
+ # Build per-term vectors with weights (IDF; OOV down-weighted)
301
+ weights = []
302
+ vecs = []
303
+ for t in terms:
304
+ v, oov = self._get_token_vector(t)
305
+ if v is None:
306
+ continue
307
+ w = float(self.idf.get(t, 0.0))
308
+ if oov:
309
+ w *= float(self.cfg.query_subword_oov_weight)
310
+ if w <= 0:
311
+ continue
312
+ vecs.append(v)
313
+ weights.append(w)
314
+
315
+ if not vecs:
316
+ return None
317
+
318
+ V = np.stack(vecs).astype(np.float32) # T x D
319
+ W = np.asarray(weights, dtype=np.float32) # T
320
+
321
+ # top-k pooling over term-term cosine contributions (query terms x document terms)
322
+ k = max(1, int(self.cfg.word_sim_topk_k))
323
+ n_docs = len(self.tf_token_docs)
324
+ sims_all = np.zeros((n_docs,), dtype=np.float32)
325
+ Vq = V # Tq x D (normalized)
326
+ Wq = W # Tq
327
+ for i, d in enumerate(self.tf_token_docs):
328
+ fields = d.get("fields") or {}
329
+ doc_terms = set()
330
+ for fname in self.cfg.target_fields:
331
+ fobj = fields.get(fname) or {}
332
+ tf = fobj.get("tf") or {}
333
+ doc_terms.update(tf.keys())
334
+ if not doc_terms:
335
+ sims_all[i] = 0.0
336
+ continue
337
+ Vd_list = []
338
+ for t in doc_terms:
339
+ idx = self.word_vocab.get(t)
340
+ if idx is None:
341
+ continue
342
+ Vd_list.append(self.word_vectors[idx])
343
+ if not Vd_list:
344
+ sims_all[i] = 0.0
345
+ continue
346
+ Vd = np.stack(Vd_list).astype(np.float32) # Td x D
347
+ M = Vd @ Vq.T # Td x Tq
348
+ if Wq.size:
349
+ M = M * Wq[None, :]
350
+ M = np.maximum(M, 0.0)
351
+ Td, Tq = M.shape
352
+ total = Td * Tq
353
+ kk = min(k, total) if total > 0 else 0
354
+ if kk == 0:
355
+ sims_all[i] = 0.0
356
+ continue
357
+ flat = M.reshape(-1)
358
+ if kk == total:
359
+ top_vals = flat
360
+ else:
361
+ idxk = np.argpartition(flat, -kk)[-kk:]
362
+ top_vals = flat[idxk]
363
+ sims_all[i] = float(top_vals.mean()) if top_vals.size else 0.0
364
+ return sims_all
365
+
366
+ def _word_sim_scores_pairavg(self, terms: List[str]) -> Optional[np.ndarray]:
367
+ if not self.cfg.word_sim_enable:
368
+ return None
369
+ if self.word_vectors is None:
370
+ return None
371
+ # Build query term vectors (no weighting for pair-avg, simple mean over all pairs)
372
+ vecs = []
373
+ for t in terms:
374
+ v, _ = self._get_token_vector(t)
375
+ if v is None:
376
+ continue
377
+ vecs.append(v)
378
+ if not vecs:
379
+ return None
380
+ Vq = np.stack(vecs).astype(np.float32) # Tq x D
381
+
382
+ n_docs = len(self.tf_token_docs)
383
+ sims_all = np.zeros((n_docs,), dtype=np.float32)
384
+ for i, d in enumerate(self.tf_token_docs):
385
+ fields = d.get("fields") or {}
386
+ doc_terms = set()
387
+ for fname in self.cfg.target_fields:
388
+ fobj = fields.get(fname) or {}
389
+ tf = fobj.get("tf") or {}
390
+ doc_terms.update(tf.keys())
391
+ if not doc_terms:
392
+ sims_all[i] = 0.0
393
+ continue
394
+ Vd_list = []
395
+ for t in doc_terms:
396
+ idx = self.word_vocab.get(t)
397
+ if idx is None:
398
+ continue
399
+ Vd_list.append(self.word_vectors[idx])
400
+ if not Vd_list:
401
+ sims_all[i] = 0.0
402
+ continue
403
+ Vd = np.stack(Vd_list).astype(np.float32) # Td x D
404
+ M = Vd @ Vq.T # Td x Tq
405
+ M = np.maximum(M, 0.0)
406
+ sims_all[i] = float(M.mean()) if M.size else 0.0
407
+ return sims_all
408
+
409
+ # ----- Public API -----
410
+ def search(
411
+ self,
412
+ query: str,
413
+ debug: bool = False,
414
+ ) -> List[Tuple[str, float]] | Tuple[List[Tuple[str, float]], Dict[str, Any]]:
415
+ terms = self._tokenize(query)
416
+ if self.cfg.synonyms_enable:
417
+ terms = self._expand_synonyms(terms)
418
+
419
+ # BM25F
420
+ bm25 = self._bm25f_scores(terms)
421
+
422
+ # word sim (filtering): top-k pooling
423
+ ws_filter = self._word_sim_scores_topk(terms)
424
+ if ws_filter is None:
425
+ ws_filter = np.zeros_like(bm25)
426
+ a = float(self.cfg.word_sim_alpha)
427
+ fused_filter = a * bm25 + (1.0 - a) * ws_filter
428
+
429
+ # Organization/reading auto-boost based on raw query substring match
430
+ qn = normalize_text_for_org(query)
431
+ if len(qn) >= int(self.cfg.org_boost_min_len):
432
+ exact = np.zeros((len(self.projects),), dtype=bool)
433
+ prefix = np.zeros_like(exact)
434
+ substr = np.zeros_like(exact)
435
+ for i, d in enumerate(self.projects):
436
+ pid = d.get("projectId")
437
+ on = self.org_norms.get(pid, "")
438
+ rn = self.reading_norms.get(pid, "")
439
+ if qn and (qn == on or (rn and qn == rn)):
440
+ exact[i] = True
441
+ elif qn and (on.startswith(qn) or (rn and rn.startswith(qn))):
442
+ prefix[i] = True
443
+ elif qn and ((qn in on) or (rn and qn in rn)):
444
+ substr[i] = True
445
+
446
+ boost = (
447
+ exact.astype(np.float32) * float(self.cfg.org_boost_exact)
448
+ + prefix.astype(np.float32) * float(self.cfg.org_boost_prefix)
449
+ + substr.astype(np.float32) * float(self.cfg.org_boost_substring)
450
+ )
451
+ pass
452
+
453
+ # collect results
454
+ ids = [d.get("projectId") for d in self.projects]
455
+ # Filtering to reduce false positives while keeping recall
456
+ # Relative threshold anchored to the top fused score
457
+ score_with_boost = fused_filter + boost
458
+ top = float(np.max(score_with_boost)) if score_with_boost.size > 0 else 0.0
459
+ rel_cut = (
460
+ top * float(self.cfg.fused_rel_top_ratio) if top > 0 else self.cfg.fused_min
461
+ )
462
+ fused_cut = max(float(self.cfg.fused_min), rel_cut)
463
+ keep = (
464
+ (bm25 >= self.cfg.bm25_min)
465
+ | (ws_filter >= self.cfg.word_sim_min)
466
+ | (score_with_boost >= self.cfg.fused_min)
467
+ ) & (score_with_boost >= fused_cut)
468
+ order = np.argsort(-score_with_boost) # descending by fused
469
+ selected_idx: List[int] = []
470
+ for i in order:
471
+ if keep[i]:
472
+ selected_idx.append(int(i))
473
+ if len(selected_idx) >= self.cfg.max_results:
474
+ break
475
+ # Single-step fallback: if zero, relax the relative cut and use absolute thresholds only
476
+ if len(selected_idx) == 0:
477
+ keep2 = (
478
+ (bm25 >= self.cfg.bm25_min)
479
+ | (ws_filter >= self.cfg.word_sim_min)
480
+ | (score_with_boost >= self.cfg.fused_min)
481
+ )
482
+ for i in order:
483
+ if keep2[i]:
484
+ selected_idx.append(int(i))
485
+ if len(selected_idx) >= self.cfg.max_results:
486
+ break
487
+ # Rerank with pair-avg word similarity (if enabled)
488
+ ws_rerank = None
489
+ if self.cfg.word_sim_rerank == "pair_avg":
490
+ ws_rerank = self._word_sim_scores_pairavg(terms)
491
+ if ws_rerank is None:
492
+ ws_rerank = ws_filter
493
+ fused_rerank = a * bm25 + (1.0 - a) * ws_rerank
494
+ final_scores = fused_rerank + boost
495
+ pairs = [(ids[i], float(final_scores[i])) for i in selected_idx]
496
+
497
+ # sort
498
+ pairs.sort(key=lambda x: (-x[1], x[0]))
499
+ if not debug:
500
+ return pairs
501
+ # build debug details for selected docs
502
+ details = []
503
+ for i in selected_idx:
504
+ details.append(
505
+ {
506
+ "projectId": ids[i],
507
+ "bm25": float(bm25[i]),
508
+ "ws_filter_topk": float(ws_filter[i]),
509
+ "ws_rerank_pairavg": float(ws_rerank[i])
510
+ if ws_rerank is not None
511
+ else None,
512
+ "org_boost": float(boost[i]),
513
+ "fused_filter": float(fused_filter[i]),
514
+ "fused_final": float(final_scores[i]),
515
+ }
516
+ )
517
+ return pairs, {"details": details}
518
+
519
+ def get_projects(self) -> List[Dict[str, Any]]:
520
+ return self.projects
521
+
522
+ def get_project_map(self) -> Dict[str, Dict[str, Any]]:
523
+ return self.project_map
config/files.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "sudachi": {
3
+ "sudachi_config": "scripts/sudachi.json",
4
+ "user_dict": "resources/user_dict.csv",
5
+ "user_dict_generated": "data/generated/user.dic",
6
+ "synonyms": "resources/synonyms_DO_NOT_EDIT.txt",
7
+ "synonyms_cache": "data/generated/synonyms_cache.json",
8
+ "stopwords": "resources/stopwords.json"
9
+ },
10
+ "projects": {
11
+ "original_csv": "resources/test_data.csv",
12
+ "projects_json": "data/generated/projects.json"
13
+ },
14
+ "bm25": {
15
+ "bm25_meta": "data/generated/bm25_meta.json",
16
+ "tf_token": "data/generated/tf_token.json"
17
+ },
18
+ "embeddings": {
19
+ "fasttext_vec": "resources/embeddings/cc.ja.300.vec",
20
+ "fasttext_bin": "resources/embeddings/cc.ja.300.bin",
21
+ "word_vocab": "data/generated/word_vocab.json",
22
+ "word_vectors": "data/generated/word_vectors.npz",
23
+ "doc_vectors": "data/generated/doc_vectors.npy"
24
+ }
25
+ }
config/search_model.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "target_pos_l1": [
3
+ "名詞",
4
+ "動詞",
5
+ "形容詞",
6
+ "形容動詞語幹"
7
+ ],
8
+ "target_fields": [
9
+ "title",
10
+ "organization",
11
+ "description",
12
+ "prComment",
13
+ "prCommentLong",
14
+ "reading"
15
+ ],
16
+ "synonyms": {
17
+ "enable": true,
18
+ "sources": {
19
+ "sudachi": true,
20
+ "custom_json": "resources/synonyms_custom.json"
21
+ },
22
+ "limits": {
23
+ "max_expansions_per_term": 4,
24
+ "max_query_variants": 5,
25
+ "min_char_len": 2
26
+ },
27
+ "banlist": [
28
+ "部",
29
+ "会",
30
+ "サークル"
31
+ ]
32
+ },
33
+ "bm25f": {
34
+ "k1": 1.2,
35
+ "b": 0.75,
36
+ "field_weights": {
37
+ "title": 2.0,
38
+ "organization": 1.5,
39
+ "description": 1.0,
40
+ "prComment": 1.0,
41
+ "prCommentLong": 0.8,
42
+ "reading": 0.6
43
+ }
44
+ },
45
+ "word_sim": {
46
+ "enable": true,
47
+ "mode": "topk",
48
+ "alpha": 0.3,
49
+ "topk_k": 3,
50
+ "rerank": "pair_avg"
51
+ },
52
+ "query_subword": {
53
+ "enable": true,
54
+ "oov_weight": 0.8,
55
+ "cache_size": 50000
56
+ },
57
+ "organization": {
58
+ "boost": {
59
+ "exact": 1.0,
60
+ "prefix": 0.7,
61
+ "substring": 0.5,
62
+ "min_len": 2
63
+ }
64
+ },
65
+ "filter": {
66
+ "min_results": 10,
67
+ "max_results": 30,
68
+ "bm25_min": 0.15,
69
+ "word_sim_min": 0.40,
70
+ "fused_min": 0.15,
71
+ "fused_rel_top_ratio": 0.50
72
+ }
73
+ }
docs/files.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # オフライン生成
2
+ - `cc.ja.300.bin`: OOV(Out of Vocabrary)時に使用 更新不要
3
+ - `generated/`: 毎年更新
4
+ - `projects.json`: API返却用データ
5
+ - `scripts/1_build_dict.py`で生成
6
+ - `bm25_meta.json`: BM25Fメタデータ
7
+ - `scripts/4_prepare_bm25f_meta.py`で生成
8
+ - `tf_token.json`: フィールド別TF/トークン
9
+ - `scripts/5_prepare_tf_token.py`で生成
10
+ - `synonyms_cache.json`: Sudachi同義語キャッシュ
11
+ - `scripts/3_build_synonyms_from_sudachi.py`から生成
12
+ - `word_vocab.json`
13
+ - `scripts/6_build_word_embeddings.py`で生成
14
+ - `word_vectors.npz`
15
+ - `scripts/6_build_word_embeddings.py`で生成
docs/old_index.md ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```py
2
+ import os
3
+ import sys
4
+ import json
5
+ import gzip
6
+ import logging
7
+ from dotenv import load_dotenv
8
+ from fastapi import FastAPI, Response, Depends, HTTPException, Security, Query
9
+ from fastapi.responses import RedirectResponse
10
+ from fastapi.security import APIKeyHeader
11
+ from pydantic import BaseModel
12
+ from sudachipy import dictionary
13
+ from dataclasses import asdict, fields
14
+ from typing import Optional, Literal, List, Dict
15
+ from pathlib import Path
16
+
17
+ # デバッグ用
18
+ from contextlib import asynccontextmanager
19
+ from fastapi.routing import APIRoute
20
+
21
+ # ローカルモジュールのインポート
22
+ from api import search_preprocess
23
+ from api import data_fetch
24
+ from api.search import pipeline as search_pipeline
25
+ from api.model import Project, ProjectSummary, ProjectDetail, ProjectIds
26
+
27
+ # 親ディレクトリをパスに追加して設定ファイルをインポート
28
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
29
+ from scripts.config import cfg_file, cfg_search_model, cfg_search_params
30
+
31
+ # --- ロギング設定 ---
32
+ _level_name = os.getenv("LOG_LEVEL", "INFO").upper()
33
+ _level = getattr(logging, _level_name, logging.INFO)
34
+ logging.basicConfig(level=_level, format="%(asctime)s [%(levelname)s]: %(message)s")
35
+ log = logging.getLogger(__name__)
36
+ log.setLevel(_level)
37
+
38
+
39
+ # --- グローバル変数 ---
40
+ # このファイルの場所を基準にプロジェクトルートを特定
41
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
42
+
43
+ # 設定情報
44
+ settings_file = cfg_file()
45
+ settings_search_model = cfg_search_model()
46
+ settings_search_params = cfg_search_params()
47
+
48
+ # モデルとデータ
49
+ tokenizer_obj = None
50
+ sentence_model = None # kept for compatibility; no longer used
51
+ all_projects: List[Project] = [] # populated via search_pipeline
52
+ project_map: Dict[str, Project] = {}
53
+ docs_for_search: Dict[str, Dict] = {}
54
+ bm25_meta: Dict = {}
55
+ synonyms_cache: Dict = {}
56
+ custom_synonyms: Dict = {}
57
+
58
+
59
+ # --- 初期化処理 ---
60
+
61
+
62
+ def initialize_sudachi_tokenizer() -> dictionary.Dictionary:
63
+ """SudachiPyトークナイザをユーザー辞書と共に初期化する"""
64
+ sudachi_config_path = PROJECT_ROOT / "scripts/sudachi.json"
65
+ try:
66
+ if sudachi_config_path.exists():
67
+ log.info(
68
+ "SudachiPy設定ファイルが見つかりました。ユーザー辞書で初期化します。"
69
+ )
70
+ with open(sudachi_config_path, "r", encoding="utf-8") as f:
71
+ config = json.load(f)
72
+
73
+ # userDictのパスを絶対パスに変換
74
+ if "userDict" in config:
75
+ config["userDict"] = [str(PROJECT_ROOT / p) for p in config["userDict"]]
76
+
77
+ # configオブジェクトをJSON文字列に変換して渡す
78
+ return dictionary.Dictionary(config=json.dumps(config)).create()
79
+ else:
80
+ log.info(
81
+ "SudachiPy設定ファイルが見つかりません。システム辞書のみ使用します。"
82
+ )
83
+ return dictionary.Dictionary().create()
84
+ except Exception as e:
85
+ log.error(f"SudachiPyの初期化に失敗しました: {e}")
86
+ raise
87
+
88
+
89
+ def load_all_data():
90
+ """Deprecated: Data is now loaded by api.search.pipeline.initialize()."""
91
+ log.info("load_all_data() is deprecated; using search_pipeline.initialize().")
92
+
93
+
94
+ @asynccontextmanager
95
+ async def lifespan(app: FastAPI):
96
+ """アプリケーションの起動時と終了時に実行される処理"""
97
+ # --- 起動時処理 ---
98
+ global tokenizer_obj, sentence_model, all_projects, project_map
99
+
100
+ log.info("アプリケーションを起動します...")
101
+
102
+ # 環境変数をロード
103
+ load_dotenv()
104
+
105
+ # トークナイザの初期化
106
+ tokenizer_obj = initialize_sudachi_tokenizer()
107
+ search_preprocess.set_tokenizer(tokenizer_obj)
108
+
109
+ # 生成物の確保(ローカル or HF Datasets からフェッチ)
110
+ try:
111
+ fetch_summary = data_fetch.orchestrate_fetch_and_validate(settings_search_model)
112
+ missing = fetch_summary.get("missing_before", [])
113
+ fetched = fetch_summary.get("fetched", {})
114
+ checks = fetch_summary.get("checks", {})
115
+ ok_list = [k for k, v in checks.items() if v]
116
+ log.info(
117
+ f"DATA PREP: missing_before={missing}, fetched={list(k for k,v in fetched.items() if v)}, checks_ok={ok_list}"
118
+ )
119
+ ws_conf = settings_search_model.get("word_sim", {}) or {}
120
+ ws_enabled = bool(ws_conf.get("enable"))
121
+ ws_mode = (ws_conf.get("mode") or "avg").lower()
122
+ ws_ready = (
123
+ checks.get("word_vectors.npz", False)
124
+ and checks.get("word_vocab.json", False)
125
+ and (True if ws_mode != "avg" else checks.get("doc_vectors.npy", False))
126
+ )
127
+ if ws_enabled:
128
+ log.info(f"WORD_SIM: requested mode={ws_mode}, assets_ready={ws_ready}")
129
+ except Exception as e:
130
+ log.warning(f"Data fetch/validate step failed: {e}")
131
+
132
+ # 検索パイプラインの初期化���データロード含む)
133
+ search_pipeline.initialize(tokenizer_obj)
134
+ # 既存エンドポイント互換のためローカル参照も持つ
135
+ all_projects = search_pipeline.get_projects()
136
+ project_map = search_pipeline.get_project_map()
137
+
138
+ # ルート情報のログ出力
139
+ for r in app.routes:
140
+ if isinstance(r, APIRoute):
141
+ log.info(f"ROUTE {list(r.methods)} {r.path}")
142
+ log.info(f"DOCS={app.docs_url} OPENAPI={app.openapi_url} REDOC={app.redoc_url}")
143
+ log.info("アプリケーションの準備が整いました。")
144
+
145
+ yield
146
+
147
+ # --- 終了時処理 ---
148
+ log.info("アプリケーションをシャットダウンします。")
149
+
150
+
151
+ # --- FastAPIアプリケーション設定 ---
152
+ app = FastAPI(lifespan=lifespan)
153
+
154
+ # .envファイルから環境変数を読み込む
155
+ load_dotenv()
156
+
157
+
158
+ # --- 認証設定 ---
159
+ API_KEY_NAME = "X-API-KEY"
160
+ API_SECRET_KEY = os.getenv("API_SECRET_KEY")
161
+ api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True)
162
+
163
+
164
+ async def get_api_key(key: str = Security(api_key_header)):
165
+ """APIキーを検証する依存関係"""
166
+ if not API_SECRET_KEY or key != API_SECRET_KEY:
167
+ raise HTTPException(status_code=403, detail="Could not validate credentials.")
168
+ return key
169
+
170
+
171
+ # --- APIエンドポイント ---
172
+
173
+
174
+ @app.get("/", include_in_schema=False)
175
+ def root():
176
+ """ルートURLへのアクセスはドキュメントへリダイレクト"""
177
+ return RedirectResponse("/docs")
178
+
179
+
180
+ @app.get("/api/health")
181
+ def health_check():
182
+ """ヘルスチェック用エンドポイント"""
183
+ return {"status": "ok"}
184
+
185
+
186
+ @app.get(
187
+ "/api/projects",
188
+ response_model=List[ProjectSummary],
189
+ dependencies=[Depends(get_api_key)],
190
+ )
191
+ def get_summary_data():
192
+ """全企画のサマリー情報をGZIP圧縮して返す"""
193
+ summary_fields = {f.name for f in fields(ProjectSummary)}
194
+ summaries = [
195
+ {k: v for k, v in asdict(p).items() if k in summary_fields}
196
+ for p in search_pipeline.get_projects()
197
+ ]
198
+
199
+ content = json.dumps(summaries, ensure_ascii=False).encode("utf-8")
200
+ return Response(
201
+ content=gzip.compress(content),
202
+ headers={"Content-Encoding": "gzip", "Content-Type": "application/json"},
203
+ )
204
+
205
+
206
+ @app.get(
207
+ "/api/details",
208
+ response_model=ProjectDetail,
209
+ dependencies=[Depends(get_api_key)],
210
+ )
211
+ def get_project_detail(projectId: str = Query(..., description="取得したい企画のID")):
212
+ """指定されたIDの企画詳細情報を返す"""
213
+ project = search_pipeline.get_project_map().get(projectId)
214
+ if not project:
215
+ raise HTTPException(status_code=404, detail="Project not found")
216
+
217
+ detail_fields = {f.name for f in fields(ProjectDetail)}
218
+ project_dict = asdict(project)
219
+ return {key: project_dict[key] for key in detail_fields if key in project_dict}
220
+
221
+
222
+ # --- 検索エンドポイント ---
223
+
224
+
225
+ class SearchRequest(BaseModel):
226
+ query: str
227
+ # fusion は非推奨: 設定ファイルで制御し、ここでは受け取っても無視する
228
+ fusion: Optional[Literal["add", "mul"]] = None
229
+ debug: bool = False
230
+
231
+
232
+ @app.post(
233
+ "/api/search",
234
+ response_model=ProjectIds,
235
+ dependencies=[Depends(get_api_key)],
236
+ )
237
+ def search(request: SearchRequest) -> ProjectIds:
238
+ """BM25F中心の新パイプラインで検索し、企画IDを返す。"""
239
+ if not request.query:
240
+ raise HTTPException(status_code=400, detail="Query cannot be empty")
241
+ result_ids = search_pipeline.search(request.query, request.debug)
242
+ log.info(f"検索完了: {len(result_ids)} 件を返却")
243
+ return ProjectIds(projectIds=result_ids)
244
+
245
+ ```
docs/search_implementation_plan.md ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 検索実装計画書(BM25F + fastText OOV, .vec/.bin 運用)
2
+
3
+ 目的
4
+ - BM25F を主軸に、任意の語義近接スコア(fastText 由来の事前生成行列)を融合し、精度と速度を両立する。
5
+ - OOV(語彙外)クエリは fastText のサブワードを `.bin` でランタイム生成して補う。
6
+ - Hugging Face Spaces 無料CPU(RAM 16GB)で安定運用できる構成にする。
7
+
8
+ 結論(運用方針)
9
+ - `.vec`(1.82GB)は「ビルド専用」。語彙を間引いた行列 `word_vectors.npz` と `doc_vectors.npy` を生成して配布する。
10
+ - `.bin`(838MB)は「ランタイムの OOV 専用」。起動時に1回ロードし、未知語のベクトルを生成する。
11
+ - 量子化 `.ftz` は不使用(fastText 公式の quantize は教師ありモデルのみサポート)。
12
+
13
+ ---
14
+
15
+ ## 1. 全体アーキテクチャ
16
+ - 検索スコア
17
+ - BM25F: 既存のトークン化・フィールド重み(`config/search_model.json`)。
18
+ - 語義近接(word_sim): `word_vectors.npz/doc_vectors.npy` を利用。`mode=avg|soft` を設定可能。
19
+ - 融合: `score = alpha * bm25f + (1 - alpha) * word_sim`(`alpha` は 0.6–0.8 目安)。
20
+ - OOV 対応
21
+ - 既知語: `word_vocab.json + word_vectors.npz` から即時ルックアップ(L2 正規化済)。
22
+ - 未知語: fastText `.bin` をロードし `get_word_vector` でサブワードから生成(取得後に L2 正規化)。
23
+ - キャッシュ: OOV 語ベクトルに LRU キャッシュを適用(例: 50k 語)。
24
+ - フォールバック
25
+ - `.bin` が未配置/取得失敗時: OOV をゼロベクトル扱い → word_sim 寄与が自然に減衰、BM25F 単体でも整合。
26
+
27
+ ## 2. ビルド(ローカル・年次)
28
+ - 入力
29
+ - `resources/embeddings/cc.ja.300.vec`(1.82GB):fastText の公開ベクトル、または自前学習の `.vec`。
30
+ - 注意: `.vec` と `.bin` は必ず同一モデル(同じ学習元/版)を用意する。
31
+ - 実行
32
+ - `python scripts/build_all.py`(Step 6 が `.vec` を検出した場合に実行)
33
+ - 出力(`data/generated/`)
34
+ - `projects.json`, `bm25_meta.json`, `tf_token.json`(BM25F 用)
35
+ - `word_vocab.json`, `word_vectors.npz`, `doc_vectors.npy`(word_sim 用)
36
+ - 配布
37
+ - Hugging Face Datasets(private)に `generated/` 一式をアップロード。年度タグ(例: `2025.0`)で管理。
38
+
39
+ ## 3. ランタイム(Spaces, 16GB RAM)
40
+ - 起動時取得
41
+ - Datasets から `generated/` をダウンロード → `data/generated/` へ配置。
42
+ - Models から `cc.ja.300.bin` をダウンロード → `resources/embeddings/` へ配置。
43
+ - 起動時ロード
44
+ - `word_vectors.npz/doc_vectors.npy/word_vocab.json` を読み込み。
45
+ - 設定 `query_subword.enable=true` なら `fasttext.load_model(cc.ja.300.bin)` を1回だけ実行。
46
+ - メモリ目安
47
+ - `.bin` ロード: 1.0–1.5GB 程度
48
+ - 事前生成行列ほか: 10–40MB 程度
49
+ - 余裕を見て合計 1.5–2.5GB 程度(RAM 16GB 内に十分収まる)。
50
+ - コールドスタート
51
+ - `.bin` の取得(~0.8GB)+ロードで数十秒〜数分(I/O 依存)。起動時のみ発生。
52
+
53
+ ## 4. クエリ処理フロー(擬似コード)
54
+ 1) クエリを正規化 → トークン化(SudachiPy)。
55
+ 2) 各トークンについて:
56
+ - 語彙に存在 → `word_vectors` から取得
57
+ - なければ(OOV) → `.bin` から `get_word_vector`(あれば LRU から)
58
+ - ベクトルは L2 正規化
59
+ 3) word_sim を計算
60
+ - `avg`: IDF 加重平均ベクトルと `doc_vectors.npy` のコサイン類似
61
+ - `soft`: 各クエリ語に対し文書語群との最大コサインを合算(短クエリに強い)
62
+ 4) BM25F と融合
63
+ - `score = alpha * bm25f + (1 - alpha) * word_sim`
64
+ - OOV が多いクエリは `oov_weight < 1.0` で寄与を減衰可能
65
+
66
+ ## 5. 設定(例: `config/search_model.json`)
67
+ - 主要キー
68
+ - `bm25f`: 既存のフィールド定義/重み
69
+ - `word_sim.enable`: `true|false`
70
+ - `word_sim.mode`: `"soft"|"avg"`(既定は `soft` 推奨)
71
+ - `word_sim.alpha`: `0.6–0.8` 目安
72
+ - `query_subword.enable`: `true|false`
73
+ - `query_subword.path`: `"resources/embeddings/cc.ja.300.bin"`
74
+ - `query_subword.oov_weight`: `0.6–1.0`(既定 0.8 例)
75
+ - `query_subword.cache_size`: `50000` など
76
+
77
+ ## 6. 環境変数(Spaces/ローカル)
78
+ - 取得制御
79
+ - `FETCH_FROM_HF_ON_STARTUP=true`
80
+ - `DATASET_REPO`, `DATASET_REVISION`, `HF_TOKEN`
81
+ - `MODEL_REPO`, `MODEL_REVISION`, `MODEL_FILENAME=cc.ja.300.bin`
82
+ - ログ
83
+ - `LOG_LEVEL=INFO|DEBUG`
84
+ - API
85
+ - `API_SECRET_KEY`(必須)
86
+
87
+ ## 7. 運用(HF Hub レイアウト)
88
+ - Datasets(private): `generated/` 一式(年度タグで版管理)
89
+ - Models(private): `cc.ja.300.bin`(年度非依存、基本更新不要)
90
+ - フォールバック: 取得失敗時は BM25F のみで起動可(word_sim を自動無効化)
91
+
92
+ ## 8. 検証・品質
93
+ - スモーク
94
+ - `GET /api/health` → 200
95
+ - `GET /api/projects`(要 `X-API-KEY`)
96
+ - 精度
97
+ - 代表クエリの MRR/nDCG を比較、`alpha` と `mode` を探索
98
+ - `.vec`/`.bin` 同一モデルであることを確認(空間ずれ防止)
99
+ - 性能
100
+ - OOV キャッシュを有効化(ヒット率確認)
101
+ - 大文字/小文字/正規化の統一でキャッシュ効率を上げる
102
+
103
+ ## 9. 既存ドキュメントとの整合
104
+ - 量子化 `.ftz` は使用しない(fastText 公式の quantize は教師ありのみ)。
105
+ - 該当箇所の表記を `.bin` 前提へ統一(本コミットで主要2ファイルを修正)。
106
+
107
+ ## 10. リスクと対策
108
+ - モデル不一致(`.vec` と `.bin` の学習元が異なる)
109
+ - 同一ソース/版を必須とし、ハッシュで管理
110
+ - コールドスタート時間
111
+ - `.bin` は Models(private)からの取得を前提。I/O が遅い場合は Space をスリープさせない運用も検討
112
+ - メモリ
113
+ - 16GB で十分だが、同時常駐データに注意。OOV キャッシュサイズを調整
114
+
115
+ ---
116
+
117
+ 実装タスクリスト(要約)
118
+ 1) `.vec` を `resources/embeddings/` に配置し `build_all.py` 実行 → `generated/` 生成
119
+ 2) `generated/` を HF Datasets(private, 年度タグ)へアップロード
120
+ 3) `.bin` を HF Models(private)へアップロード
121
+ 4) 起動時取得ロジックを有効化(環境変数と設定を反映)
122
+ 5) `query_subword`(.bin ロード + LRU キャッシュ)を有効化
123
+ 6) パラメータ(`alpha`, `mode`, `oov_weight`)をスモーク+代表クエリでチューニング
124
+
125
+ ## 11. 組織名(organization)フィルタ/ブースト
126
+ - 目的: 固有名詞(団体名)に対して、シンプルな包含一致を検索条件として扱えるようにする。
127
+ - 正規化: NFKC 正規化 → 全角/半角の統一、空白の正規化(連続空白の1個化、前後トリム)。必要に応じてひらがな/カタカナの片寄せは行わない(誤爆回避)。
128
+ - モード
129
+ - filter: `organization` に部分一致するドキュメントのみを候補集合にする(候補ゼロ時は自動フォールバックで無視するオプションも可)。
130
+ - boost: 部分一致ドキュメントに定数加点(例: `org_boost=+0.2`)または乗算(例: `org_boost_mul=1.1`)。
131
+ - パラメータ(API; 例)
132
+ - `org`: 文字列。部分一致の対象
133
+ - `org_mode`: `filter|boost`(既定 `boost`)
134
+ - `org_boost`: `0.0–2.0`(既定 `0.2`)
135
+ - 実装は検索前処理で `organization` フィールドの正規化文字列へ substring 判定を実施
136
+ - 融合との関係
137
+ - filter: BM25F/word_sim の計算対象集合を限定
138
+ - boost: 最終スコアに加点(または BM25F スコアに加点)
139
+ - 注意点
140
+ - 正規化は検索側/索引側で同一処理を適用(ビルド時に `org_norm` を持たせると高速)
141
+ - 候補ゼロの UX を考慮し、`filter` 選択時のみ `fallback_if_empty=true` を許可
docs/test.md ADDED
@@ -0,0 +1,2063 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 電子
2
+ {
3
+ "projectIds": [
4
+ "dummy-01",
5
+ "dummy-48",
6
+ "dummy-33",
7
+ "dummy-25",
8
+ "dummy-18",
9
+ "dummy-47",
10
+ "dummy-36",
11
+ "dummy-40",
12
+ "dummy-24",
13
+ "dummy-21",
14
+ "dummy-11",
15
+ "dummy-10",
16
+ "dummy-02",
17
+ "dummy-15",
18
+ "dummy-13",
19
+ "dummy-17",
20
+ "dummy-35",
21
+ "dummy-34",
22
+ "dummy-38",
23
+ "dummy-07",
24
+ "dummy-42",
25
+ "dummy-31",
26
+ "dummy-06",
27
+ "dummy-19",
28
+ "dummy-45",
29
+ "dummy-43",
30
+ "dummy-03",
31
+ "dummy-30",
32
+ "dummy-04",
33
+ "dummy-37",
34
+ "dummy-23",
35
+ "dummy-46",
36
+ "dummy-32",
37
+ "dummy-08",
38
+ "dummy-27",
39
+ "dummy-14",
40
+ "dummy-09",
41
+ "dummy-29",
42
+ "dummy-39",
43
+ "dummy-20",
44
+ "dummy-22",
45
+ "dummy-44",
46
+ "dummy-05",
47
+ "dummy-50",
48
+ "dummy-41",
49
+ "dummy-28",
50
+ "dummy-49",
51
+ "dummy-12",
52
+ "dummy-26",
53
+ "dummy-16"
54
+ ],
55
+ "scores": [
56
+ {
57
+ "projectId": "dummy-01",
58
+ "score": 3.431400775909424
59
+ },
60
+ {
61
+ "projectId": "dummy-48",
62
+ "score": 0.12993203103542328
63
+ },
64
+ {
65
+ "projectId": "dummy-33",
66
+ "score": 0.12524525821208954
67
+ },
68
+ {
69
+ "projectId": "dummy-25",
70
+ "score": 0.1251281201839447
71
+ },
72
+ {
73
+ "projectId": "dummy-18",
74
+ "score": 0.11960740387439728
75
+ },
76
+ {
77
+ "projectId": "dummy-47",
78
+ "score": 0.11708493530750275
79
+ },
80
+ {
81
+ "projectId": "dummy-36",
82
+ "score": 0.11684656143188477
83
+ },
84
+ {
85
+ "projectId": "dummy-40",
86
+ "score": 0.11507218331098557
87
+ },
88
+ {
89
+ "projectId": "dummy-24",
90
+ "score": 0.11465374380350113
91
+ },
92
+ {
93
+ "projectId": "dummy-21",
94
+ "score": 0.11193694919347763
95
+ },
96
+ {
97
+ "projectId": "dummy-11",
98
+ "score": 0.10999565571546555
99
+ },
100
+ {
101
+ "projectId": "dummy-10",
102
+ "score": 0.1098007932305336
103
+ },
104
+ {
105
+ "projectId": "dummy-02",
106
+ "score": 0.10925329476594925
107
+ },
108
+ {
109
+ "projectId": "dummy-15",
110
+ "score": 0.10827929526567459
111
+ },
112
+ {
113
+ "projectId": "dummy-13",
114
+ "score": 0.10723763704299927
115
+ },
116
+ {
117
+ "projectId": "dummy-17",
118
+ "score": 0.10687872767448425
119
+ },
120
+ {
121
+ "projectId": "dummy-35",
122
+ "score": 0.10368791967630386
123
+ },
124
+ {
125
+ "projectId": "dummy-34",
126
+ "score": 0.10351861268281937
127
+ },
128
+ {
129
+ "projectId": "dummy-38",
130
+ "score": 0.1032002866268158
131
+ },
132
+ {
133
+ "projectId": "dummy-07",
134
+ "score": 0.10318154841661453
135
+ },
136
+ {
137
+ "projectId": "dummy-42",
138
+ "score": 0.10294974595308304
139
+ },
140
+ {
141
+ "projectId": "dummy-31",
142
+ "score": 0.10262186080217361
143
+ },
144
+ {
145
+ "projectId": "dummy-06",
146
+ "score": 0.10227564722299576
147
+ },
148
+ {
149
+ "projectId": "dummy-19",
150
+ "score": 0.1011614054441452
151
+ },
152
+ {
153
+ "projectId": "dummy-45",
154
+ "score": 0.10093759745359421
155
+ },
156
+ {
157
+ "projectId": "dummy-43",
158
+ "score": 0.10040473192930222
159
+ },
160
+ {
161
+ "projectId": "dummy-03",
162
+ "score": 0.0997525006532669
163
+ },
164
+ {
165
+ "projectId": "dummy-30",
166
+ "score": 0.09739913791418076
167
+ },
168
+ {
169
+ "projectId": "dummy-04",
170
+ "score": 0.09715365618467331
171
+ },
172
+ {
173
+ "projectId": "dummy-37",
174
+ "score": 0.09643036872148514
175
+ },
176
+ {
177
+ "projectId": "dummy-23",
178
+ "score": 0.09422503411769867
179
+ },
180
+ {
181
+ "projectId": "dummy-46",
182
+ "score": 0.09410383552312851
183
+ },
184
+ {
185
+ "projectId": "dummy-32",
186
+ "score": 0.09380743652582169
187
+ },
188
+ {
189
+ "projectId": "dummy-08",
190
+ "score": 0.09357142448425293
191
+ },
192
+ {
193
+ "projectId": "dummy-27",
194
+ "score": 0.09273825585842133
195
+ },
196
+ {
197
+ "projectId": "dummy-14",
198
+ "score": 0.09264349937438965
199
+ },
200
+ {
201
+ "projectId": "dummy-09",
202
+ "score": 0.09263898432254791
203
+ },
204
+ {
205
+ "projectId": "dummy-29",
206
+ "score": 0.0914716124534607
207
+ },
208
+ {
209
+ "projectId": "dummy-39",
210
+ "score": 0.09003062546253204
211
+ },
212
+ {
213
+ "projectId": "dummy-20",
214
+ "score": 0.08615680038928986
215
+ },
216
+ {
217
+ "projectId": "dummy-22",
218
+ "score": 0.08371936529874802
219
+ },
220
+ {
221
+ "projectId": "dummy-44",
222
+ "score": 0.08343670517206192
223
+ },
224
+ {
225
+ "projectId": "dummy-05",
226
+ "score": 0.08266183733940125
227
+ },
228
+ {
229
+ "projectId": "dummy-50",
230
+ "score": 0.08221913129091263
231
+ },
232
+ {
233
+ "projectId": "dummy-41",
234
+ "score": 0.08188144117593765
235
+ },
236
+ {
237
+ "projectId": "dummy-28",
238
+ "score": 0.07737957686185837
239
+ },
240
+ {
241
+ "projectId": "dummy-49",
242
+ "score": 0.07631941884756088
243
+ },
244
+ {
245
+ "projectId": "dummy-12",
246
+ "score": 0.07623115181922913
247
+ },
248
+ {
249
+ "projectId": "dummy-26",
250
+ "score": 0.0692358985543251
251
+ },
252
+ {
253
+ "projectId": "dummy-16",
254
+ "score": 0.06431126594543457
255
+ }
256
+ ]
257
+ }
258
+
259
+ # こうさく
260
+ {
261
+ "projectIds": [
262
+ "dummy-01",
263
+ "dummy-19",
264
+ "dummy-40",
265
+ "dummy-42",
266
+ "dummy-25",
267
+ "dummy-13",
268
+ "dummy-34",
269
+ "dummy-48",
270
+ "dummy-38",
271
+ "dummy-27",
272
+ "dummy-02",
273
+ "dummy-17",
274
+ "dummy-24",
275
+ "dummy-35",
276
+ "dummy-03",
277
+ "dummy-10",
278
+ "dummy-45",
279
+ "dummy-26",
280
+ "dummy-14",
281
+ "dummy-33",
282
+ "dummy-29",
283
+ "dummy-23",
284
+ "dummy-50",
285
+ "dummy-21",
286
+ "dummy-32",
287
+ "dummy-36",
288
+ "dummy-49",
289
+ "dummy-04",
290
+ "dummy-30",
291
+ "dummy-05",
292
+ "dummy-12",
293
+ "dummy-15",
294
+ "dummy-39",
295
+ "dummy-31",
296
+ "dummy-37",
297
+ "dummy-08",
298
+ "dummy-11",
299
+ "dummy-09",
300
+ "dummy-44",
301
+ "dummy-06",
302
+ "dummy-43",
303
+ "dummy-18",
304
+ "dummy-46",
305
+ "dummy-20",
306
+ "dummy-22",
307
+ "dummy-28",
308
+ "dummy-41",
309
+ "dummy-07",
310
+ "dummy-47",
311
+ "dummy-16"
312
+ ],
313
+ "scores": [
314
+ {
315
+ "projectId": "dummy-01",
316
+ "score": 2.958343744277954
317
+ },
318
+ {
319
+ "projectId": "dummy-19",
320
+ "score": 0.10967567563056946
321
+ },
322
+ {
323
+ "projectId": "dummy-40",
324
+ "score": 0.1078089252114296
325
+ },
326
+ {
327
+ "projectId": "dummy-42",
328
+ "score": 0.10442163795232773
329
+ },
330
+ {
331
+ "projectId": "dummy-25",
332
+ "score": 0.10362518578767776
333
+ },
334
+ {
335
+ "projectId": "dummy-13",
336
+ "score": 0.10124309360980988
337
+ },
338
+ {
339
+ "projectId": "dummy-34",
340
+ "score": 0.10120446979999542
341
+ },
342
+ {
343
+ "projectId": "dummy-48",
344
+ "score": 0.10025734454393387
345
+ },
346
+ {
347
+ "projectId": "dummy-38",
348
+ "score": 0.09987800568342209
349
+ },
350
+ {
351
+ "projectId": "dummy-27",
352
+ "score": 0.09944233298301697
353
+ },
354
+ {
355
+ "projectId": "dummy-02",
356
+ "score": 0.0974903553724289
357
+ },
358
+ {
359
+ "projectId": "dummy-17",
360
+ "score": 0.09713833779096603
361
+ },
362
+ {
363
+ "projectId": "dummy-24",
364
+ "score": 0.09597335755825043
365
+ },
366
+ {
367
+ "projectId": "dummy-35",
368
+ "score": 0.09521206468343735
369
+ },
370
+ {
371
+ "projectId": "dummy-03",
372
+ "score": 0.09464053064584732
373
+ },
374
+ {
375
+ "projectId": "dummy-10",
376
+ "score": 0.09453081339597702
377
+ },
378
+ {
379
+ "projectId": "dummy-45",
380
+ "score": 0.09366767108440399
381
+ },
382
+ {
383
+ "projectId": "dummy-26",
384
+ "score": 0.09298509359359741
385
+ },
386
+ {
387
+ "projectId": "dummy-14",
388
+ "score": 0.09244585782289505
389
+ },
390
+ {
391
+ "projectId": "dummy-33",
392
+ "score": 0.09236441552639008
393
+ },
394
+ {
395
+ "projectId": "dummy-29",
396
+ "score": 0.09188427776098251
397
+ },
398
+ {
399
+ "projectId": "dummy-23",
400
+ "score": 0.0905442014336586
401
+ },
402
+ {
403
+ "projectId": "dummy-50",
404
+ "score": 0.09030000120401382
405
+ },
406
+ {
407
+ "projectId": "dummy-21",
408
+ "score": 0.09017764776945114
409
+ },
410
+ {
411
+ "projectId": "dummy-32",
412
+ "score": 0.09004317224025726
413
+ },
414
+ {
415
+ "projectId": "dummy-36",
416
+ "score": 0.08987368643283844
417
+ },
418
+ {
419
+ "projectId": "dummy-49",
420
+ "score": 0.0897449180483818
421
+ },
422
+ {
423
+ "projectId": "dummy-04",
424
+ "score": 0.08967691659927368
425
+ },
426
+ {
427
+ "projectId": "dummy-30",
428
+ "score": 0.08961175382137299
429
+ },
430
+ {
431
+ "projectId": "dummy-05",
432
+ "score": 0.08941705524921417
433
+ },
434
+ {
435
+ "projectId": "dummy-12",
436
+ "score": 0.08816639333963394
437
+ },
438
+ {
439
+ "projectId": "dummy-15",
440
+ "score": 0.08616558462381363
441
+ },
442
+ {
443
+ "projectId": "dummy-39",
444
+ "score": 0.0843149945139885
445
+ },
446
+ {
447
+ "projectId": "dummy-31",
448
+ "score": 0.08405950665473938
449
+ },
450
+ {
451
+ "projectId": "dummy-37",
452
+ "score": 0.08368293941020966
453
+ },
454
+ {
455
+ "projectId": "dummy-08",
456
+ "score": 0.08284597098827362
457
+ },
458
+ {
459
+ "projectId": "dummy-11",
460
+ "score": 0.08270086348056793
461
+ },
462
+ {
463
+ "projectId": "dummy-09",
464
+ "score": 0.08264289796352386
465
+ },
466
+ {
467
+ "projectId": "dummy-44",
468
+ "score": 0.08196865022182465
469
+ },
470
+ {
471
+ "projectId": "dummy-06",
472
+ "score": 0.08190039545297623
473
+ },
474
+ {
475
+ "projectId": "dummy-43",
476
+ "score": 0.08157528936862946
477
+ },
478
+ {
479
+ "projectId": "dummy-18",
480
+ "score": 0.08130253106355667
481
+ },
482
+ {
483
+ "projectId": "dummy-46",
484
+ "score": 0.07963813096284866
485
+ },
486
+ {
487
+ "projectId": "dummy-20",
488
+ "score": 0.07923799008131027
489
+ },
490
+ {
491
+ "projectId": "dummy-22",
492
+ "score": 0.07898101210594177
493
+ },
494
+ {
495
+ "projectId": "dummy-28",
496
+ "score": 0.07825343310832977
497
+ },
498
+ {
499
+ "projectId": "dummy-41",
500
+ "score": 0.07740511000156403
501
+ },
502
+ {
503
+ "projectId": "dummy-07",
504
+ "score": 0.07709077000617981
505
+ },
506
+ {
507
+ "projectId": "dummy-47",
508
+ "score": 0.07670128345489502
509
+ },
510
+ {
511
+ "projectId": "dummy-16",
512
+ "score": 0.07050929963588715
513
+ }
514
+ ]
515
+ }
516
+
517
+ # 空想地図
518
+ {
519
+ "projectIds": [
520
+ "dummy-02",
521
+ "dummy-32",
522
+ "dummy-20",
523
+ "dummy-27",
524
+ "dummy-50",
525
+ "dummy-19",
526
+ "dummy-31",
527
+ "dummy-37",
528
+ "dummy-18",
529
+ "dummy-21",
530
+ "dummy-03",
531
+ "dummy-30",
532
+ "dummy-39",
533
+ "dummy-01",
534
+ "dummy-06",
535
+ "dummy-09",
536
+ "dummy-10",
537
+ "dummy-23",
538
+ "dummy-11",
539
+ "dummy-46",
540
+ "dummy-08",
541
+ "dummy-35",
542
+ "dummy-22",
543
+ "dummy-07",
544
+ "dummy-17",
545
+ "dummy-48",
546
+ "dummy-40",
547
+ "dummy-47",
548
+ "dummy-45",
549
+ "dummy-26",
550
+ "dummy-05",
551
+ "dummy-36",
552
+ "dummy-24",
553
+ "dummy-44",
554
+ "dummy-13",
555
+ "dummy-15",
556
+ "dummy-38",
557
+ "dummy-42",
558
+ "dummy-14",
559
+ "dummy-41",
560
+ "dummy-29",
561
+ "dummy-34",
562
+ "dummy-25",
563
+ "dummy-43",
564
+ "dummy-33",
565
+ "dummy-12",
566
+ "dummy-04",
567
+ "dummy-28",
568
+ "dummy-49",
569
+ "dummy-16"
570
+ ],
571
+ "scores": [
572
+ {
573
+ "projectId": "dummy-02",
574
+ "score": 5.931369781494141
575
+ },
576
+ {
577
+ "projectId": "dummy-32",
578
+ "score": 2.6352267265319824
579
+ },
580
+ {
581
+ "projectId": "dummy-20",
582
+ "score": 0.1919010877609253
583
+ },
584
+ {
585
+ "projectId": "dummy-27",
586
+ "score": 0.1807546764612198
587
+ },
588
+ {
589
+ "projectId": "dummy-50",
590
+ "score": 0.1795060932636261
591
+ },
592
+ {
593
+ "projectId": "dummy-19",
594
+ "score": 0.17730551958084106
595
+ },
596
+ {
597
+ "projectId": "dummy-31",
598
+ "score": 0.17708048224449158
599
+ },
600
+ {
601
+ "projectId": "dummy-37",
602
+ "score": 0.17596064507961273
603
+ },
604
+ {
605
+ "projectId": "dummy-18",
606
+ "score": 0.1759600043296814
607
+ },
608
+ {
609
+ "projectId": "dummy-21",
610
+ "score": 0.1722804307937622
611
+ },
612
+ {
613
+ "projectId": "dummy-03",
614
+ "score": 0.16904908418655396
615
+ },
616
+ {
617
+ "projectId": "dummy-30",
618
+ "score": 0.16836203634738922
619
+ },
620
+ {
621
+ "projectId": "dummy-39",
622
+ "score": 0.16769906878471375
623
+ },
624
+ {
625
+ "projectId": "dummy-01",
626
+ "score": 0.16594183444976807
627
+ },
628
+ {
629
+ "projectId": "dummy-06",
630
+ "score": 0.1649368703365326
631
+ },
632
+ {
633
+ "projectId": "dummy-09",
634
+ "score": 0.16446015238761902
635
+ },
636
+ {
637
+ "projectId": "dummy-10",
638
+ "score": 0.16347283124923706
639
+ },
640
+ {
641
+ "projectId": "dummy-23",
642
+ "score": 0.16043199598789215
643
+ },
644
+ {
645
+ "projectId": "dummy-11",
646
+ "score": 0.1600879430770874
647
+ },
648
+ {
649
+ "projectId": "dummy-46",
650
+ "score": 0.1599961817264557
651
+ },
652
+ {
653
+ "projectId": "dummy-08",
654
+ "score": 0.15916672348976135
655
+ },
656
+ {
657
+ "projectId": "dummy-35",
658
+ "score": 0.15472930669784546
659
+ },
660
+ {
661
+ "projectId": "dummy-22",
662
+ "score": 0.15420827269554138
663
+ },
664
+ {
665
+ "projectId": "dummy-07",
666
+ "score": 0.15418641269207
667
+ },
668
+ {
669
+ "projectId": "dummy-17",
670
+ "score": 0.15405218303203583
671
+ },
672
+ {
673
+ "projectId": "dummy-48",
674
+ "score": 0.1535654515028
675
+ },
676
+ {
677
+ "projectId": "dummy-40",
678
+ "score": 0.15275800228118896
679
+ },
680
+ {
681
+ "projectId": "dummy-47",
682
+ "score": 0.15079064667224884
683
+ },
684
+ {
685
+ "projectId": "dummy-45",
686
+ "score": 0.14808325469493866
687
+ },
688
+ {
689
+ "projectId": "dummy-26",
690
+ "score": 0.1474611759185791
691
+ },
692
+ {
693
+ "projectId": "dummy-05",
694
+ "score": 0.14566561579704285
695
+ },
696
+ {
697
+ "projectId": "dummy-36",
698
+ "score": 0.14557917416095734
699
+ },
700
+ {
701
+ "projectId": "dummy-24",
702
+ "score": 0.14543066918849945
703
+ },
704
+ {
705
+ "projectId": "dummy-44",
706
+ "score": 0.14542552828788757
707
+ },
708
+ {
709
+ "projectId": "dummy-13",
710
+ "score": 0.14524437487125397
711
+ },
712
+ {
713
+ "projectId": "dummy-15",
714
+ "score": 0.14392796158790588
715
+ },
716
+ {
717
+ "projectId": "dummy-38",
718
+ "score": 0.14303478598594666
719
+ },
720
+ {
721
+ "projectId": "dummy-42",
722
+ "score": 0.1425190418958664
723
+ },
724
+ {
725
+ "projectId": "dummy-14",
726
+ "score": 0.13991902768611908
727
+ },
728
+ {
729
+ "projectId": "dummy-41",
730
+ "score": 0.1398436278104782
731
+ },
732
+ {
733
+ "projectId": "dummy-29",
734
+ "score": 0.13916610181331635
735
+ },
736
+ {
737
+ "projectId": "dummy-34",
738
+ "score": 0.13802099227905273
739
+ },
740
+ {
741
+ "projectId": "dummy-25",
742
+ "score": 0.13733211159706116
743
+ },
744
+ {
745
+ "projectId": "dummy-43",
746
+ "score": 0.13702505826950073
747
+ },
748
+ {
749
+ "projectId": "dummy-33",
750
+ "score": 0.13551051914691925
751
+ },
752
+ {
753
+ "projectId": "dummy-12",
754
+ "score": 0.13336017727851868
755
+ },
756
+ {
757
+ "projectId": "dummy-04",
758
+ "score": 0.1325426697731018
759
+ },
760
+ {
761
+ "projectId": "dummy-28",
762
+ "score": 0.1268766224384308
763
+ },
764
+ {
765
+ "projectId": "dummy-49",
766
+ "score": 0.1265995353460312
767
+ },
768
+ {
769
+ "projectId": "dummy-16",
770
+ "score": 0.12136942893266678
771
+ }
772
+ ]
773
+ }
774
+
775
+ # ジェミニシティ
776
+ {
777
+ "projectIds": [
778
+ "dummy-02",
779
+ "dummy-07",
780
+ "dummy-09",
781
+ "dummy-30",
782
+ "dummy-01",
783
+ "dummy-06",
784
+ "dummy-45",
785
+ "dummy-05",
786
+ "dummy-13",
787
+ "dummy-21",
788
+ "dummy-50",
789
+ "dummy-27",
790
+ "dummy-36",
791
+ "dummy-40",
792
+ "dummy-43",
793
+ "dummy-46",
794
+ "dummy-34",
795
+ "dummy-47",
796
+ "dummy-32",
797
+ "dummy-42",
798
+ "dummy-18",
799
+ "dummy-20",
800
+ "dummy-31",
801
+ "dummy-25",
802
+ "dummy-29",
803
+ "dummy-15",
804
+ "dummy-04",
805
+ "dummy-39",
806
+ "dummy-10",
807
+ "dummy-19",
808
+ "dummy-37",
809
+ "dummy-11",
810
+ "dummy-24",
811
+ "dummy-38",
812
+ "dummy-03",
813
+ "dummy-23",
814
+ "dummy-08",
815
+ "dummy-17",
816
+ "dummy-22",
817
+ "dummy-33",
818
+ "dummy-35",
819
+ "dummy-14",
820
+ "dummy-12",
821
+ "dummy-41",
822
+ "dummy-26",
823
+ "dummy-48",
824
+ "dummy-44",
825
+ "dummy-49",
826
+ "dummy-16",
827
+ "dummy-28"
828
+ ],
829
+ "scores": [
830
+ {
831
+ "projectId": "dummy-02",
832
+ "score": 2.456648588180542
833
+ },
834
+ {
835
+ "projectId": "dummy-07",
836
+ "score": 0.162680983543396
837
+ },
838
+ {
839
+ "projectId": "dummy-09",
840
+ "score": 0.15619903802871704
841
+ },
842
+ {
843
+ "projectId": "dummy-30",
844
+ "score": 0.15498758852481842
845
+ },
846
+ {
847
+ "projectId": "dummy-01",
848
+ "score": 0.15436750650405884
849
+ },
850
+ {
851
+ "projectId": "dummy-06",
852
+ "score": 0.15225842595100403
853
+ },
854
+ {
855
+ "projectId": "dummy-45",
856
+ "score": 0.1520552635192871
857
+ },
858
+ {
859
+ "projectId": "dummy-05",
860
+ "score": 0.1514219343662262
861
+ },
862
+ {
863
+ "projectId": "dummy-13",
864
+ "score": 0.15118378400802612
865
+ },
866
+ {
867
+ "projectId": "dummy-21",
868
+ "score": 0.15101128816604614
869
+ },
870
+ {
871
+ "projectId": "dummy-50",
872
+ "score": 0.1507887840270996
873
+ },
874
+ {
875
+ "projectId": "dummy-27",
876
+ "score": 0.1502055525779724
877
+ },
878
+ {
879
+ "projectId": "dummy-36",
880
+ "score": 0.14961348474025726
881
+ },
882
+ {
883
+ "projectId": "dummy-40",
884
+ "score": 0.14926813542842865
885
+ },
886
+ {
887
+ "projectId": "dummy-43",
888
+ "score": 0.14891408383846283
889
+ },
890
+ {
891
+ "projectId": "dummy-46",
892
+ "score": 0.1488681137561798
893
+ },
894
+ {
895
+ "projectId": "dummy-34",
896
+ "score": 0.14862272143363953
897
+ },
898
+ {
899
+ "projectId": "dummy-47",
900
+ "score": 0.14832982420921326
901
+ },
902
+ {
903
+ "projectId": "dummy-32",
904
+ "score": 0.14759908616542816
905
+ },
906
+ {
907
+ "projectId": "dummy-42",
908
+ "score": 0.14659354090690613
909
+ },
910
+ {
911
+ "projectId": "dummy-18",
912
+ "score": 0.1465129405260086
913
+ },
914
+ {
915
+ "projectId": "dummy-20",
916
+ "score": 0.14555582404136658
917
+ },
918
+ {
919
+ "projectId": "dummy-31",
920
+ "score": 0.14508406817913055
921
+ },
922
+ {
923
+ "projectId": "dummy-25",
924
+ "score": 0.14451518654823303
925
+ },
926
+ {
927
+ "projectId": "dummy-29",
928
+ "score": 0.14375324547290802
929
+ },
930
+ {
931
+ "projectId": "dummy-15",
932
+ "score": 0.1436637043952942
933
+ },
934
+ {
935
+ "projectId": "dummy-04",
936
+ "score": 0.14246971905231476
937
+ },
938
+ {
939
+ "projectId": "dummy-39",
940
+ "score": 0.14222663640975952
941
+ },
942
+ {
943
+ "projectId": "dummy-10",
944
+ "score": 0.14208325743675232
945
+ },
946
+ {
947
+ "projectId": "dummy-19",
948
+ "score": 0.14109577238559723
949
+ },
950
+ {
951
+ "projectId": "dummy-37",
952
+ "score": 0.140916109085083
953
+ },
954
+ {
955
+ "projectId": "dummy-11",
956
+ "score": 0.14039728045463562
957
+ },
958
+ {
959
+ "projectId": "dummy-24",
960
+ "score": 0.13975919783115387
961
+ },
962
+ {
963
+ "projectId": "dummy-38",
964
+ "score": 0.13965779542922974
965
+ },
966
+ {
967
+ "projectId": "dummy-03",
968
+ "score": 0.13717900216579437
969
+ },
970
+ {
971
+ "projectId": "dummy-23",
972
+ "score": 0.13443152606487274
973
+ },
974
+ {
975
+ "projectId": "dummy-08",
976
+ "score": 0.13416489958763123
977
+ },
978
+ {
979
+ "projectId": "dummy-17",
980
+ "score": 0.13373295962810516
981
+ },
982
+ {
983
+ "projectId": "dummy-22",
984
+ "score": 0.13138151168823242
985
+ },
986
+ {
987
+ "projectId": "dummy-33",
988
+ "score": 0.13107235729694366
989
+ },
990
+ {
991
+ "projectId": "dummy-35",
992
+ "score": 0.1308753341436386
993
+ },
994
+ {
995
+ "projectId": "dummy-14",
996
+ "score": 0.1284683346748352
997
+ },
998
+ {
999
+ "projectId": "dummy-12",
1000
+ "score": 0.12798088788986206
1001
+ },
1002
+ {
1003
+ "projectId": "dummy-41",
1004
+ "score": 0.12646889686584473
1005
+ },
1006
+ {
1007
+ "projectId": "dummy-26",
1008
+ "score": 0.12549281120300293
1009
+ },
1010
+ {
1011
+ "projectId": "dummy-48",
1012
+ "score": 0.12391983717679977
1013
+ },
1014
+ {
1015
+ "projectId": "dummy-44",
1016
+ "score": 0.11978904157876968
1017
+ },
1018
+ {
1019
+ "projectId": "dummy-49",
1020
+ "score": 0.11888212710618973
1021
+ },
1022
+ {
1023
+ "projectId": "dummy-16",
1024
+ "score": 0.10489151626825333
1025
+ },
1026
+ {
1027
+ "projectId": "dummy-28",
1028
+ "score": 0.09725114703178406
1029
+ }
1030
+ ]
1031
+ }
1032
+
1033
+ # ボードゲーム
1034
+ {
1035
+ "projectIds": [
1036
+ "dummy-03",
1037
+ "dummy-18",
1038
+ "dummy-14",
1039
+ "dummy-31",
1040
+ "dummy-26",
1041
+ "dummy-32",
1042
+ "dummy-01",
1043
+ "dummy-05",
1044
+ "dummy-38",
1045
+ "dummy-40",
1046
+ "dummy-23",
1047
+ "dummy-11",
1048
+ "dummy-13",
1049
+ "dummy-25",
1050
+ "dummy-48",
1051
+ "dummy-45",
1052
+ "dummy-43",
1053
+ "dummy-34",
1054
+ "dummy-15",
1055
+ "dummy-37",
1056
+ "dummy-21",
1057
+ "dummy-09",
1058
+ "dummy-08",
1059
+ "dummy-35",
1060
+ "dummy-29",
1061
+ "dummy-36",
1062
+ "dummy-10",
1063
+ "dummy-30",
1064
+ "dummy-39",
1065
+ "dummy-46",
1066
+ "dummy-42",
1067
+ "dummy-24",
1068
+ "dummy-41",
1069
+ "dummy-19",
1070
+ "dummy-47",
1071
+ "dummy-27",
1072
+ "dummy-07",
1073
+ "dummy-06",
1074
+ "dummy-02",
1075
+ "dummy-20",
1076
+ "dummy-44",
1077
+ "dummy-12",
1078
+ "dummy-50",
1079
+ "dummy-22",
1080
+ "dummy-17",
1081
+ "dummy-33",
1082
+ "dummy-49",
1083
+ "dummy-04",
1084
+ "dummy-28",
1085
+ "dummy-16"
1086
+ ],
1087
+ "scores": [
1088
+ {
1089
+ "projectId": "dummy-03",
1090
+ "score": 4.6026997566223145
1091
+ },
1092
+ {
1093
+ "projectId": "dummy-18",
1094
+ "score": 3.9451470375061035
1095
+ },
1096
+ {
1097
+ "projectId": "dummy-14",
1098
+ "score": 1.7676340341567993
1099
+ },
1100
+ {
1101
+ "projectId": "dummy-31",
1102
+ "score": 1.6230387687683105
1103
+ },
1104
+ {
1105
+ "projectId": "dummy-26",
1106
+ "score": 0.1631765365600586
1107
+ },
1108
+ {
1109
+ "projectId": "dummy-32",
1110
+ "score": 0.15479955077171326
1111
+ },
1112
+ {
1113
+ "projectId": "dummy-01",
1114
+ "score": 0.15239496529102325
1115
+ },
1116
+ {
1117
+ "projectId": "dummy-05",
1118
+ "score": 0.15059471130371094
1119
+ },
1120
+ {
1121
+ "projectId": "dummy-38",
1122
+ "score": 0.15034374594688416
1123
+ },
1124
+ {
1125
+ "projectId": "dummy-40",
1126
+ "score": 0.14852960407733917
1127
+ },
1128
+ {
1129
+ "projectId": "dummy-23",
1130
+ "score": 0.14833544194698334
1131
+ },
1132
+ {
1133
+ "projectId": "dummy-11",
1134
+ "score": 0.14826737344264984
1135
+ },
1136
+ {
1137
+ "projectId": "dummy-13",
1138
+ "score": 0.147711381316185
1139
+ },
1140
+ {
1141
+ "projectId": "dummy-25",
1142
+ "score": 0.1473732441663742
1143
+ },
1144
+ {
1145
+ "projectId": "dummy-48",
1146
+ "score": 0.1472303867340088
1147
+ },
1148
+ {
1149
+ "projectId": "dummy-45",
1150
+ "score": 0.14652344584465027
1151
+ },
1152
+ {
1153
+ "projectId": "dummy-43",
1154
+ "score": 0.14301139116287231
1155
+ },
1156
+ {
1157
+ "projectId": "dummy-34",
1158
+ "score": 0.14235596358776093
1159
+ },
1160
+ {
1161
+ "projectId": "dummy-15",
1162
+ "score": 0.14173288643360138
1163
+ },
1164
+ {
1165
+ "projectId": "dummy-37",
1166
+ "score": 0.14097197353839874
1167
+ },
1168
+ {
1169
+ "projectId": "dummy-21",
1170
+ "score": 0.14076118171215057
1171
+ },
1172
+ {
1173
+ "projectId": "dummy-09",
1174
+ "score": 0.1392628401517868
1175
+ },
1176
+ {
1177
+ "projectId": "dummy-08",
1178
+ "score": 0.13878723978996277
1179
+ },
1180
+ {
1181
+ "projectId": "dummy-35",
1182
+ "score": 0.13797332346439362
1183
+ },
1184
+ {
1185
+ "projectId": "dummy-29",
1186
+ "score": 0.13737985491752625
1187
+ },
1188
+ {
1189
+ "projectId": "dummy-36",
1190
+ "score": 0.1361878663301468
1191
+ },
1192
+ {
1193
+ "projectId": "dummy-10",
1194
+ "score": 0.133790522813797
1195
+ },
1196
+ {
1197
+ "projectId": "dummy-30",
1198
+ "score": 0.1336868703365326
1199
+ },
1200
+ {
1201
+ "projectId": "dummy-39",
1202
+ "score": 0.13292568922042847
1203
+ },
1204
+ {
1205
+ "projectId": "dummy-46",
1206
+ "score": 0.13286401331424713
1207
+ },
1208
+ {
1209
+ "projectId": "dummy-42",
1210
+ "score": 0.1324540674686432
1211
+ },
1212
+ {
1213
+ "projectId": "dummy-24",
1214
+ "score": 0.1314363181591034
1215
+ },
1216
+ {
1217
+ "projectId": "dummy-41",
1218
+ "score": 0.1313837617635727
1219
+ },
1220
+ {
1221
+ "projectId": "dummy-19",
1222
+ "score": 0.1310305893421173
1223
+ },
1224
+ {
1225
+ "projectId": "dummy-47",
1226
+ "score": 0.13075384497642517
1227
+ },
1228
+ {
1229
+ "projectId": "dummy-27",
1230
+ "score": 0.130295529961586
1231
+ },
1232
+ {
1233
+ "projectId": "dummy-07",
1234
+ "score": 0.1289217174053192
1235
+ },
1236
+ {
1237
+ "projectId": "dummy-06",
1238
+ "score": 0.12773872911930084
1239
+ },
1240
+ {
1241
+ "projectId": "dummy-02",
1242
+ "score": 0.12749534845352173
1243
+ },
1244
+ {
1245
+ "projectId": "dummy-20",
1246
+ "score": 0.12316787987947464
1247
+ },
1248
+ {
1249
+ "projectId": "dummy-44",
1250
+ "score": 0.12286143749952316
1251
+ },
1252
+ {
1253
+ "projectId": "dummy-12",
1254
+ "score": 0.12097669392824173
1255
+ },
1256
+ {
1257
+ "projectId": "dummy-50",
1258
+ "score": 0.11809731274843216
1259
+ },
1260
+ {
1261
+ "projectId": "dummy-22",
1262
+ "score": 0.11734956502914429
1263
+ },
1264
+ {
1265
+ "projectId": "dummy-17",
1266
+ "score": 0.11496858298778534
1267
+ },
1268
+ {
1269
+ "projectId": "dummy-33",
1270
+ "score": 0.11183813214302063
1271
+ },
1272
+ {
1273
+ "projectId": "dummy-49",
1274
+ "score": 0.10802316665649414
1275
+ },
1276
+ {
1277
+ "projectId": "dummy-04",
1278
+ "score": 0.10751412063837051
1279
+ },
1280
+ {
1281
+ "projectId": "dummy-28",
1282
+ "score": 0.105849988758564
1283
+ },
1284
+ {
1285
+ "projectId": "dummy-16",
1286
+ "score": 0.09239037334918976
1287
+ }
1288
+ ]
1289
+ }
1290
+
1291
+ # インタラクティブ
1292
+ {
1293
+ "projectIds": [
1294
+ "dummy-01",
1295
+ "dummy-18",
1296
+ "dummy-03",
1297
+ "dummy-21",
1298
+ "dummy-45",
1299
+ "dummy-32",
1300
+ "dummy-37",
1301
+ "dummy-05",
1302
+ "dummy-47",
1303
+ "dummy-27",
1304
+ "dummy-11",
1305
+ "dummy-13",
1306
+ "dummy-46",
1307
+ "dummy-25",
1308
+ "dummy-24",
1309
+ "dummy-31",
1310
+ "dummy-35",
1311
+ "dummy-09",
1312
+ "dummy-30",
1313
+ "dummy-34",
1314
+ "dummy-29",
1315
+ "dummy-15",
1316
+ "dummy-43",
1317
+ "dummy-48",
1318
+ "dummy-40",
1319
+ "dummy-50",
1320
+ "dummy-39",
1321
+ "dummy-20",
1322
+ "dummy-10",
1323
+ "dummy-26",
1324
+ "dummy-38",
1325
+ "dummy-14",
1326
+ "dummy-06",
1327
+ "dummy-02",
1328
+ "dummy-23",
1329
+ "dummy-36",
1330
+ "dummy-08",
1331
+ "dummy-07",
1332
+ "dummy-17",
1333
+ "dummy-22",
1334
+ "dummy-42",
1335
+ "dummy-19",
1336
+ "dummy-33",
1337
+ "dummy-41",
1338
+ "dummy-44",
1339
+ "dummy-49",
1340
+ "dummy-04",
1341
+ "dummy-12",
1342
+ "dummy-28",
1343
+ "dummy-16"
1344
+ ],
1345
+ "scores": [
1346
+ {
1347
+ "projectId": "dummy-01",
1348
+ "score": 2.0224697589874268
1349
+ },
1350
+ {
1351
+ "projectId": "dummy-18",
1352
+ "score": 0.1717991828918457
1353
+ },
1354
+ {
1355
+ "projectId": "dummy-03",
1356
+ "score": 0.15875877439975739
1357
+ },
1358
+ {
1359
+ "projectId": "dummy-21",
1360
+ "score": 0.14773328602313995
1361
+ },
1362
+ {
1363
+ "projectId": "dummy-45",
1364
+ "score": 0.13539406657218933
1365
+ },
1366
+ {
1367
+ "projectId": "dummy-32",
1368
+ "score": 0.13098303973674774
1369
+ },
1370
+ {
1371
+ "projectId": "dummy-37",
1372
+ "score": 0.1267457902431488
1373
+ },
1374
+ {
1375
+ "projectId": "dummy-05",
1376
+ "score": 0.12650640308856964
1377
+ },
1378
+ {
1379
+ "projectId": "dummy-47",
1380
+ "score": 0.12536215782165527
1381
+ },
1382
+ {
1383
+ "projectId": "dummy-27",
1384
+ "score": 0.1250198483467102
1385
+ },
1386
+ {
1387
+ "projectId": "dummy-11",
1388
+ "score": 0.12491045892238617
1389
+ },
1390
+ {
1391
+ "projectId": "dummy-13",
1392
+ "score": 0.12490887939929962
1393
+ },
1394
+ {
1395
+ "projectId": "dummy-46",
1396
+ "score": 0.12423489987850189
1397
+ },
1398
+ {
1399
+ "projectId": "dummy-25",
1400
+ "score": 0.12377213686704636
1401
+ },
1402
+ {
1403
+ "projectId": "dummy-24",
1404
+ "score": 0.12340930104255676
1405
+ },
1406
+ {
1407
+ "projectId": "dummy-31",
1408
+ "score": 0.12207731604576111
1409
+ },
1410
+ {
1411
+ "projectId": "dummy-35",
1412
+ "score": 0.12206392735242844
1413
+ },
1414
+ {
1415
+ "projectId": "dummy-09",
1416
+ "score": 0.12170739471912384
1417
+ },
1418
+ {
1419
+ "projectId": "dummy-30",
1420
+ "score": 0.12082985788583755
1421
+ },
1422
+ {
1423
+ "projectId": "dummy-34",
1424
+ "score": 0.1205349788069725
1425
+ },
1426
+ {
1427
+ "projectId": "dummy-29",
1428
+ "score": 0.1192193254828453
1429
+ },
1430
+ {
1431
+ "projectId": "dummy-15",
1432
+ "score": 0.11767151951789856
1433
+ },
1434
+ {
1435
+ "projectId": "dummy-43",
1436
+ "score": 0.11537515372037888
1437
+ },
1438
+ {
1439
+ "projectId": "dummy-48",
1440
+ "score": 0.11534177511930466
1441
+ },
1442
+ {
1443
+ "projectId": "dummy-40",
1444
+ "score": 0.11425547301769257
1445
+ },
1446
+ {
1447
+ "projectId": "dummy-50",
1448
+ "score": 0.11390717327594757
1449
+ },
1450
+ {
1451
+ "projectId": "dummy-39",
1452
+ "score": 0.11368876695632935
1453
+ },
1454
+ {
1455
+ "projectId": "dummy-20",
1456
+ "score": 0.11208880692720413
1457
+ },
1458
+ {
1459
+ "projectId": "dummy-10",
1460
+ "score": 0.11158151179552078
1461
+ },
1462
+ {
1463
+ "projectId": "dummy-26",
1464
+ "score": 0.11129496991634369
1465
+ },
1466
+ {
1467
+ "projectId": "dummy-38",
1468
+ "score": 0.10989216715097427
1469
+ },
1470
+ {
1471
+ "projectId": "dummy-14",
1472
+ "score": 0.10894030332565308
1473
+ },
1474
+ {
1475
+ "projectId": "dummy-06",
1476
+ "score": 0.10804328322410583
1477
+ },
1478
+ {
1479
+ "projectId": "dummy-02",
1480
+ "score": 0.10758388787508011
1481
+ },
1482
+ {
1483
+ "projectId": "dummy-23",
1484
+ "score": 0.10621065646409988
1485
+ },
1486
+ {
1487
+ "projectId": "dummy-36",
1488
+ "score": 0.10569079965353012
1489
+ },
1490
+ {
1491
+ "projectId": "dummy-08",
1492
+ "score": 0.1055106520652771
1493
+ },
1494
+ {
1495
+ "projectId": "dummy-07",
1496
+ "score": 0.10404599457979202
1497
+ },
1498
+ {
1499
+ "projectId": "dummy-17",
1500
+ "score": 0.10389190167188644
1501
+ },
1502
+ {
1503
+ "projectId": "dummy-22",
1504
+ "score": 0.10377786308526993
1505
+ },
1506
+ {
1507
+ "projectId": "dummy-42",
1508
+ "score": 0.10317900776863098
1509
+ },
1510
+ {
1511
+ "projectId": "dummy-19",
1512
+ "score": 0.10277500003576279
1513
+ },
1514
+ {
1515
+ "projectId": "dummy-33",
1516
+ "score": 0.10180061310529709
1517
+ },
1518
+ {
1519
+ "projectId": "dummy-41",
1520
+ "score": 0.09484352916479111
1521
+ },
1522
+ {
1523
+ "projectId": "dummy-44",
1524
+ "score": 0.09008049964904785
1525
+ },
1526
+ {
1527
+ "projectId": "dummy-49",
1528
+ "score": 0.08740860968828201
1529
+ },
1530
+ {
1531
+ "projectId": "dummy-04",
1532
+ "score": 0.08725807070732117
1533
+ },
1534
+ {
1535
+ "projectId": "dummy-12",
1536
+ "score": 0.08704513311386108
1537
+ },
1538
+ {
1539
+ "projectId": "dummy-28",
1540
+ "score": 0.08287956565618515
1541
+ },
1542
+ {
1543
+ "projectId": "dummy-16",
1544
+ "score": 0.06936758756637573
1545
+ }
1546
+ ]
1547
+ }
1548
+
1549
+ # LED
1550
+ {
1551
+ "projectIds": [
1552
+ "dummy-13",
1553
+ "dummy-01",
1554
+ "dummy-29",
1555
+ "dummy-33",
1556
+ "dummy-45",
1557
+ "dummy-38",
1558
+ "dummy-35",
1559
+ "dummy-43",
1560
+ "dummy-22",
1561
+ "dummy-39",
1562
+ "dummy-25",
1563
+ "dummy-24",
1564
+ "dummy-44",
1565
+ "dummy-04",
1566
+ "dummy-31",
1567
+ "dummy-09",
1568
+ "dummy-05",
1569
+ "dummy-08",
1570
+ "dummy-46",
1571
+ "dummy-27",
1572
+ "dummy-10",
1573
+ "dummy-37",
1574
+ "dummy-11",
1575
+ "dummy-42",
1576
+ "dummy-48",
1577
+ "dummy-07",
1578
+ "dummy-03",
1579
+ "dummy-41",
1580
+ "dummy-49",
1581
+ "dummy-18",
1582
+ "dummy-21",
1583
+ "dummy-06",
1584
+ "dummy-28",
1585
+ "dummy-30",
1586
+ "dummy-16",
1587
+ "dummy-14",
1588
+ "dummy-50",
1589
+ "dummy-47",
1590
+ "dummy-15",
1591
+ "dummy-40",
1592
+ "dummy-12",
1593
+ "dummy-20",
1594
+ "dummy-36",
1595
+ "dummy-19",
1596
+ "dummy-23",
1597
+ "dummy-17",
1598
+ "dummy-02",
1599
+ "dummy-32",
1600
+ "dummy-34",
1601
+ "dummy-26"
1602
+ ],
1603
+ "scores": [
1604
+ {
1605
+ "projectId": "dummy-13",
1606
+ "score": 2.0778872966766357
1607
+ },
1608
+ {
1609
+ "projectId": "dummy-01",
1610
+ "score": 1.6898093223571777
1611
+ },
1612
+ {
1613
+ "projectId": "dummy-29",
1614
+ "score": 0.10570766776800156
1615
+ },
1616
+ {
1617
+ "projectId": "dummy-33",
1618
+ "score": 0.09935978055000305
1619
+ },
1620
+ {
1621
+ "projectId": "dummy-45",
1622
+ "score": 0.0951630100607872
1623
+ },
1624
+ {
1625
+ "projectId": "dummy-38",
1626
+ "score": 0.09483207762241364
1627
+ },
1628
+ {
1629
+ "projectId": "dummy-35",
1630
+ "score": 0.09441396594047546
1631
+ },
1632
+ {
1633
+ "projectId": "dummy-43",
1634
+ "score": 0.0936182290315628
1635
+ },
1636
+ {
1637
+ "projectId": "dummy-22",
1638
+ "score": 0.0923035740852356
1639
+ },
1640
+ {
1641
+ "projectId": "dummy-39",
1642
+ "score": 0.09194866567850113
1643
+ },
1644
+ {
1645
+ "projectId": "dummy-25",
1646
+ "score": 0.09147751331329346
1647
+ },
1648
+ {
1649
+ "projectId": "dummy-24",
1650
+ "score": 0.09100349992513657
1651
+ },
1652
+ {
1653
+ "projectId": "dummy-44",
1654
+ "score": 0.08923175185918808
1655
+ },
1656
+ {
1657
+ "projectId": "dummy-04",
1658
+ "score": 0.08885731548070908
1659
+ },
1660
+ {
1661
+ "projectId": "dummy-31",
1662
+ "score": 0.08837521076202393
1663
+ },
1664
+ {
1665
+ "projectId": "dummy-09",
1666
+ "score": 0.08757206797599792
1667
+ },
1668
+ {
1669
+ "projectId": "dummy-05",
1670
+ "score": 0.08745519071817398
1671
+ },
1672
+ {
1673
+ "projectId": "dummy-08",
1674
+ "score": 0.08742401003837585
1675
+ },
1676
+ {
1677
+ "projectId": "dummy-46",
1678
+ "score": 0.08670942485332489
1679
+ },
1680
+ {
1681
+ "projectId": "dummy-27",
1682
+ "score": 0.08608520030975342
1683
+ },
1684
+ {
1685
+ "projectId": "dummy-10",
1686
+ "score": 0.08546361327171326
1687
+ },
1688
+ {
1689
+ "projectId": "dummy-37",
1690
+ "score": 0.08545397222042084
1691
+ },
1692
+ {
1693
+ "projectId": "dummy-11",
1694
+ "score": 0.08495649695396423
1695
+ },
1696
+ {
1697
+ "projectId": "dummy-42",
1698
+ "score": 0.08428287506103516
1699
+ },
1700
+ {
1701
+ "projectId": "dummy-48",
1702
+ "score": 0.08367615938186646
1703
+ },
1704
+ {
1705
+ "projectId": "dummy-07",
1706
+ "score": 0.08359388262033463
1707
+ },
1708
+ {
1709
+ "projectId": "dummy-03",
1710
+ "score": 0.08242101967334747
1711
+ },
1712
+ {
1713
+ "projectId": "dummy-41",
1714
+ "score": 0.08240765333175659
1715
+ },
1716
+ {
1717
+ "projectId": "dummy-49",
1718
+ "score": 0.08240342140197754
1719
+ },
1720
+ {
1721
+ "projectId": "dummy-18",
1722
+ "score": 0.08168061077594757
1723
+ },
1724
+ {
1725
+ "projectId": "dummy-21",
1726
+ "score": 0.08138646185398102
1727
+ },
1728
+ {
1729
+ "projectId": "dummy-06",
1730
+ "score": 0.08057239651679993
1731
+ },
1732
+ {
1733
+ "projectId": "dummy-28",
1734
+ "score": 0.07947935163974762
1735
+ },
1736
+ {
1737
+ "projectId": "dummy-30",
1738
+ "score": 0.07906509935855865
1739
+ },
1740
+ {
1741
+ "projectId": "dummy-16",
1742
+ "score": 0.07739663869142532
1743
+ },
1744
+ {
1745
+ "projectId": "dummy-14",
1746
+ "score": 0.07670343667268753
1747
+ },
1748
+ {
1749
+ "projectId": "dummy-50",
1750
+ "score": 0.07632601261138916
1751
+ },
1752
+ {
1753
+ "projectId": "dummy-47",
1754
+ "score": 0.07593720406293869
1755
+ },
1756
+ {
1757
+ "projectId": "dummy-15",
1758
+ "score": 0.07524865865707397
1759
+ },
1760
+ {
1761
+ "projectId": "dummy-40",
1762
+ "score": 0.07505342364311218
1763
+ },
1764
+ {
1765
+ "projectId": "dummy-12",
1766
+ "score": 0.0750349760055542
1767
+ },
1768
+ {
1769
+ "projectId": "dummy-20",
1770
+ "score": 0.07358665019273758
1771
+ },
1772
+ {
1773
+ "projectId": "dummy-36",
1774
+ "score": 0.07355395704507828
1775
+ },
1776
+ {
1777
+ "projectId": "dummy-19",
1778
+ "score": 0.0733858272433281
1779
+ },
1780
+ {
1781
+ "projectId": "dummy-23",
1782
+ "score": 0.07254036515951157
1783
+ },
1784
+ {
1785
+ "projectId": "dummy-17",
1786
+ "score": 0.07079058140516281
1787
+ },
1788
+ {
1789
+ "projectId": "dummy-02",
1790
+ "score": 0.06835603713989258
1791
+ },
1792
+ {
1793
+ "projectId": "dummy-32",
1794
+ "score": 0.06082003191113472
1795
+ },
1796
+ {
1797
+ "projectId": "dummy-34",
1798
+ "score": 0.056489575654268265
1799
+ },
1800
+ {
1801
+ "projectId": "dummy-26",
1802
+ "score": 0.05596642196178436
1803
+ }
1804
+ ]
1805
+ }
1806
+
1807
+ # カレー
1808
+ {
1809
+ "projectIds": [
1810
+ "dummy-04",
1811
+ "dummy-49",
1812
+ "dummy-22",
1813
+ "dummy-12",
1814
+ "dummy-28",
1815
+ "dummy-14",
1816
+ "dummy-33",
1817
+ "dummy-24",
1818
+ "dummy-41",
1819
+ "dummy-44",
1820
+ "dummy-31",
1821
+ "dummy-30",
1822
+ "dummy-39",
1823
+ "dummy-38",
1824
+ "dummy-25",
1825
+ "dummy-10",
1826
+ "dummy-07",
1827
+ "dummy-23",
1828
+ "dummy-09",
1829
+ "dummy-05",
1830
+ "dummy-26",
1831
+ "dummy-32",
1832
+ "dummy-43",
1833
+ "dummy-29",
1834
+ "dummy-03",
1835
+ "dummy-16",
1836
+ "dummy-47",
1837
+ "dummy-45",
1838
+ "dummy-20",
1839
+ "dummy-02",
1840
+ "dummy-06",
1841
+ "dummy-01",
1842
+ "dummy-37",
1843
+ "dummy-48",
1844
+ "dummy-15",
1845
+ "dummy-35",
1846
+ "dummy-36",
1847
+ "dummy-27",
1848
+ "dummy-18",
1849
+ "dummy-13",
1850
+ "dummy-40",
1851
+ "dummy-08",
1852
+ "dummy-19",
1853
+ "dummy-34",
1854
+ "dummy-50",
1855
+ "dummy-11",
1856
+ "dummy-21",
1857
+ "dummy-46",
1858
+ "dummy-17",
1859
+ "dummy-42"
1860
+ ],
1861
+ "scores": [
1862
+ {
1863
+ "projectId": "dummy-04",
1864
+ "score": 2.6254711151123047
1865
+ },
1866
+ {
1867
+ "projectId": "dummy-49",
1868
+ "score": 0.18292959034442902
1869
+ },
1870
+ {
1871
+ "projectId": "dummy-22",
1872
+ "score": 0.1800815910100937
1873
+ },
1874
+ {
1875
+ "projectId": "dummy-12",
1876
+ "score": 0.1737019568681717
1877
+ },
1878
+ {
1879
+ "projectId": "dummy-28",
1880
+ "score": 0.16631010174751282
1881
+ },
1882
+ {
1883
+ "projectId": "dummy-14",
1884
+ "score": 0.14709749817848206
1885
+ },
1886
+ {
1887
+ "projectId": "dummy-33",
1888
+ "score": 0.14541301131248474
1889
+ },
1890
+ {
1891
+ "projectId": "dummy-24",
1892
+ "score": 0.13671286404132843
1893
+ },
1894
+ {
1895
+ "projectId": "dummy-41",
1896
+ "score": 0.13063141703605652
1897
+ },
1898
+ {
1899
+ "projectId": "dummy-44",
1900
+ "score": 0.1156868264079094
1901
+ },
1902
+ {
1903
+ "projectId": "dummy-31",
1904
+ "score": 0.1143571063876152
1905
+ },
1906
+ {
1907
+ "projectId": "dummy-30",
1908
+ "score": 0.1113339439034462
1909
+ },
1910
+ {
1911
+ "projectId": "dummy-39",
1912
+ "score": 0.11087211966514587
1913
+ },
1914
+ {
1915
+ "projectId": "dummy-38",
1916
+ "score": 0.11070612818002701
1917
+ },
1918
+ {
1919
+ "projectId": "dummy-25",
1920
+ "score": 0.1086856871843338
1921
+ },
1922
+ {
1923
+ "projectId": "dummy-10",
1924
+ "score": 0.10852580517530441
1925
+ },
1926
+ {
1927
+ "projectId": "dummy-07",
1928
+ "score": 0.10763086378574371
1929
+ },
1930
+ {
1931
+ "projectId": "dummy-23",
1932
+ "score": 0.10729862004518509
1933
+ },
1934
+ {
1935
+ "projectId": "dummy-09",
1936
+ "score": 0.10714579373598099
1937
+ },
1938
+ {
1939
+ "projectId": "dummy-05",
1940
+ "score": 0.10656001418828964
1941
+ },
1942
+ {
1943
+ "projectId": "dummy-26",
1944
+ "score": 0.10479174554347992
1945
+ },
1946
+ {
1947
+ "projectId": "dummy-32",
1948
+ "score": 0.10145216435194016
1949
+ },
1950
+ {
1951
+ "projectId": "dummy-43",
1952
+ "score": 0.09996487945318222
1953
+ },
1954
+ {
1955
+ "projectId": "dummy-29",
1956
+ "score": 0.09960292279720306
1957
+ },
1958
+ {
1959
+ "projectId": "dummy-03",
1960
+ "score": 0.0987028107047081
1961
+ },
1962
+ {
1963
+ "projectId": "dummy-16",
1964
+ "score": 0.09857852756977081
1965
+ },
1966
+ {
1967
+ "projectId": "dummy-47",
1968
+ "score": 0.09719719737768173
1969
+ },
1970
+ {
1971
+ "projectId": "dummy-45",
1972
+ "score": 0.09627760946750641
1973
+ },
1974
+ {
1975
+ "projectId": "dummy-20",
1976
+ "score": 0.09507821500301361
1977
+ },
1978
+ {
1979
+ "projectId": "dummy-02",
1980
+ "score": 0.09484118968248367
1981
+ },
1982
+ {
1983
+ "projectId": "dummy-06",
1984
+ "score": 0.09443477541208267
1985
+ },
1986
+ {
1987
+ "projectId": "dummy-01",
1988
+ "score": 0.09416937828063965
1989
+ },
1990
+ {
1991
+ "projectId": "dummy-37",
1992
+ "score": 0.09407279640436172
1993
+ },
1994
+ {
1995
+ "projectId": "dummy-48",
1996
+ "score": 0.09399788826704025
1997
+ },
1998
+ {
1999
+ "projectId": "dummy-15",
2000
+ "score": 0.09398657828569412
2001
+ },
2002
+ {
2003
+ "projectId": "dummy-35",
2004
+ "score": 0.09319526702165604
2005
+ },
2006
+ {
2007
+ "projectId": "dummy-36",
2008
+ "score": 0.09293168038129807
2009
+ },
2010
+ {
2011
+ "projectId": "dummy-27",
2012
+ "score": 0.09266433119773865
2013
+ },
2014
+ {
2015
+ "projectId": "dummy-18",
2016
+ "score": 0.09237142652273178
2017
+ },
2018
+ {
2019
+ "projectId": "dummy-13",
2020
+ "score": 0.0918302908539772
2021
+ },
2022
+ {
2023
+ "projectId": "dummy-40",
2024
+ "score": 0.0913495421409607
2025
+ },
2026
+ {
2027
+ "projectId": "dummy-08",
2028
+ "score": 0.091004878282547
2029
+ },
2030
+ {
2031
+ "projectId": "dummy-19",
2032
+ "score": 0.09041927009820938
2033
+ },
2034
+ {
2035
+ "projectId": "dummy-34",
2036
+ "score": 0.09039682894945145
2037
+ },
2038
+ {
2039
+ "projectId": "dummy-50",
2040
+ "score": 0.08850666880607605
2041
+ },
2042
+ {
2043
+ "projectId": "dummy-11",
2044
+ "score": 0.08840479701757431
2045
+ },
2046
+ {
2047
+ "projectId": "dummy-21",
2048
+ "score": 0.08591099828481674
2049
+ },
2050
+ {
2051
+ "projectId": "dummy-46",
2052
+ "score": 0.08536650240421295
2053
+ },
2054
+ {
2055
+ "projectId": "dummy-17",
2056
+ "score": 0.08424936980009079
2057
+ },
2058
+ {
2059
+ "projectId": "dummy-42",
2060
+ "score": 0.08145570755004883
2061
+ }
2062
+ ]
2063
+ }
requirements.txt ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ anyio==4.10.0
3
+ certifi==2025.8.3
4
+ charset-normalizer==3.4.3
5
+ click==8.2.1
6
+ dotenv==0.9.9
7
+ fastapi==0.116.1
8
+ fasttext==0.9.3
9
+ filelock==3.19.1
10
+ fsspec==2025.7.0
11
+ h11==0.16.0
12
+ hf-xet==1.1.9
13
+ huggingface-hub==0.34.4
14
+ idna==3.10
15
+ numpy==2.3.2
16
+ packaging==25.0
17
+ pandas==2.3.2
18
+ pybind11==3.0.1
19
+ pydantic==2.11.7
20
+ pydantic_core==2.33.2
21
+ python-dateutil==2.9.0.post0
22
+ python-dotenv==1.1.1
23
+ pytz==2025.2
24
+ PyYAML==6.0.2
25
+ requests==2.32.5
26
+ setuptools==80.9.0
27
+ six==1.17.0
28
+ sniffio==1.3.1
29
+ starlette==0.47.3
30
+ SudachiDict-full==20250825
31
+ SudachiPy==0.6.10
32
+ tqdm==4.67.1
33
+ typing-inspection==0.4.1
34
+ typing_extensions==4.15.0
35
+ tzdata==2025.2
36
+ urllib3==2.5.0
37
+ uvicorn==0.35.0
resources/stopwords.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "こと", "もの", "よう", "ため", "ところ", "する", "ある", "いる",
3
+ "方", "人", "他", "の", "に", "を", "と", "が", "で", "です", "ます", "くださる", "下さる",
4
+ "ました", "ません", "いい", "良い", "楽しい", "面白い", "すごい",
5
+ "また", "ぜひ", "いろいろ", "様々", "誰でも", "どなたでも",
6
+ "1", "2", "3", "4", "5", "10", "100", "円", "人", "個", "回", "分",
7
+ "第", "日", "年", "〜", "・", "!", "?", "。", "、", "%",
8
+ "the", "a", "an", "in", "on", "for", "with", "from", "of", "and",
9
+ "to", "by", "this", "that", "it", "is", "are", "be", "have", "has",
10
+ "will", "can"
11
+ ]
resources/synonyms_DO_NOT_EDIT.txt ADDED
The diff for this file is too large to render. See raw diff
 
resources/synonyms_custom.json ADDED
@@ -0,0 +1,608 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "千葉大学": ["千葉大", "Chiba Univ."],
3
+ "焼きそば": ["やきそば"],
4
+ "軽音": ["バンド", "軽音楽", "軽音部"],
5
+ "バンド": ["軽音", "バンド演奏", "バンドサークル"],
6
+
7
+ "学園祭": ["大学祭", "文化祭", "学祭"],
8
+ "大学祭": ["学園祭", "学祭"],
9
+ "文化祭": ["学園祭", "学祭"],
10
+ "学祭": ["学園祭", "大学祭"],
11
+
12
+ "模擬店": ["出店", "屋台", "食販"],
13
+ "出店": ["模擬店", "屋台"],
14
+ "屋台": ["模擬店", "出店"],
15
+ "食販": ["飲食", "フード"],
16
+ "飲食": ["食販", "フード"],
17
+ "キッチンカー": ["フードトラック", "移動販売"],
18
+ "フードトラック": ["キッチンカー"],
19
+ "物販": ["販売", "グッズ"],
20
+ "グッズ": ["物販", "公式グッズ"],
21
+
22
+ "ステージ": ["屋外ステージ", "屋内ステージ"],
23
+ "ライブ": ["コンサート", "音楽ライブ"],
24
+ "コンサート": ["ライブ", "音楽ライブ"],
25
+ "音楽ライブ": ["ライブ", "コンサート"],
26
+ "吹奏楽": ["ブラスバンド", "吹奏"],
27
+ "ブラスバンド": ["吹奏楽"],
28
+ "合唱": ["コーラス", "混声合唱"],
29
+ "コーラス": ["合唱"],
30
+ "ダンス": ["ダンスショー", "パフォーマンス"],
31
+ "演劇": ["芝居", "劇"],
32
+ "お笑い": ["漫才", "コント"],
33
+ "芸人ライブ": ["お笑いライブ", "トークライブ"],
34
+ "トークショー": ["トークイベント"],
35
+ "マジック": ["手品", "奇術"],
36
+ "ジャグリング": ["パフォーマンス", "ジャグ"],
37
+
38
+ "ワークショップ": ["体験", "体験会"],
39
+ "体験": ["体験会", "参加型"],
40
+ "参加型": ["体験型", "体験"],
41
+ "展示": ["エキシビション", "展覧会"],
42
+ "研究発表": ["研究展示", "ポスター発表"],
43
+ "作品展示": ["展示", "個展"],
44
+ "写真展": ["フォト展", "写真展示"],
45
+ "美術": ["アート", "造形"],
46
+ "書道": ["書作品", "書展"],
47
+ "茶道": ["茶会", "お点前"],
48
+ "華道": ["生け花", "花道"],
49
+
50
+ "科学実験": ["サイエンスショー", "実験ショー"],
51
+ "模型": ["モデリング", "ジオラマ"],
52
+ "電子工作": ["マイコン工作", "回路工作"],
53
+ "プログラミング": ["コーディング", "ソフト開発"],
54
+ "ロボット": ["ロボティクス", "ロボコン"],
55
+ "eスポーツ": ["ゲーム大会", "esports"],
56
+ "ボードゲーム": ["アナログゲーム", "テーブルゲーム"],
57
+ "カードゲーム": ["TCG", "カード対戦"],
58
+ "アニメ": ["アニメーション"],
59
+ "漫画": ["コミック", "マンガ"],
60
+ "映画": ["シネマ", "フィルム"],
61
+ "鉄道": ["乗り鉄", "鉄道研究"],
62
+ "天文": ["天文学", "星空観察"],
63
+ "天体観測": ["星空観察", "望遠鏡観測"],
64
+ "旅行": ["トラベル", "旅"],
65
+ "写真": ["フォト", "カメラ"],
66
+ "手芸": ["ハンドメイド", "クラフト"],
67
+ "工芸": ["クラフト", "ものづくり"],
68
+ "ものづくり": ["メイカーズ", "ファブ"],
69
+ "DIY": ["自作", "ハンドメイド"],
70
+
71
+ "音楽": ["ミュージック", "サウンド"],
72
+ "合奏": ["アンサンブル", "セッション"],
73
+ "合バン": ["合同バンド", "コラボ演奏"],
74
+ "バンド演奏": ["ライブ演奏", "ステージ演奏"],
75
+
76
+ "模擬講義": ["体験授業", "体験講義"],
77
+ "模擬裁判": ["法学イベント", "体験裁判"],
78
+
79
+ "屋内": ["インドア", "室内"],
80
+ "屋外": ["アウトドア", "屋外会場"],
81
+ "会場": ["会場内", "会場エリア"],
82
+ "本部": ["運営本部", "インフォメーション"],
83
+ "インフォメーション": ["案内所", "総合案内"],
84
+ "案内": ["ガイド", "ナビ"],
85
+ "マップ": ["地図", "会場図"],
86
+ "タイムテーブル": ["スケジュール", "日程表"],
87
+ "パンフレット": ["パンフ", "冊子"],
88
+ "整理券": ["入場券", "整理札"],
89
+ "受付": ["レジストレーション", "チェックイン"],
90
+ "行列": ["待機列", "待ち列"],
91
+ "混雑": ["混み具合", "混雑状況"],
92
+
93
+ "雨天": ["雨天時", "雨天決行"],
94
+ "中止": ["キャンセル", "取り止め"],
95
+ "順延": ["延期", "延期開催"],
96
+ "注意事項": ["ご注意", "ルール"],
97
+ "禁止事項": ["持ち込み禁止", "禁止ルール"],
98
+
99
+ "ゴミ": ["廃棄物", "ごみ"],
100
+ "分別": ["仕分け", "セパレート"],
101
+ "清掃": ["クリーンアップ", "掃除"],
102
+
103
+ "ボランティア": ["スタッフ", "運営補助"],
104
+ "実行委員": ["実委", "実行委員会"],
105
+ "協賛": ["スポンサー", "サポート"],
106
+ "寄付": ["寄附", "ドネーション"],
107
+ "広報": ["PR", "プロモーション"],
108
+ "公式": ["オフィシャル", "公式サイト"],
109
+
110
+ "SNS": ["ソーシャル", "ソーシャルメディア"],
111
+ "X": ["Twitter", "旧Twitter"],
112
+ "Instagram": ["インスタ", "IG"],
113
+ "YouTube": ["ユーチューブ", "動画配信"],
114
+ "Webサイト": ["公式サイト", "ホームページ"],
115
+
116
+ "アクセス": ["交通", "行き方"],
117
+ "最寄り駅": ["駅", "近隣駅"],
118
+ "バス": ["シャトルバス", "送迎バス"],
119
+ "シャトル": ["シャトルバス", "送迎"],
120
+ "駐車場": ["パーキング", "駐車スペース"],
121
+
122
+ "家族": ["ファミリー", "子連れ"],
123
+ "子ども": ["キッズ", "こども"],
124
+ "学生": ["スチューデント", "学徒"],
125
+ "教員": ["先生", "教職員"],
126
+ "OB": ["卒業生", "同窓生"],
127
+ "同窓会": ["OB会", "同窓"],
128
+
129
+ "パフォーマンス": ["実演", "ショー"],
130
+ "デモンストレーション": ["デモ", "実演"],
131
+ "実演": ["デモ", "実技"],
132
+
133
+ "合気道": ["武道", "合気"],
134
+ "柔道": ["Judo", "武道"],
135
+ "剣道": ["Kendo", "武道"],
136
+ "弓道": ["Kyudo", "武道"],
137
+ "陸上": ["トラック&フィールド", "陸上競技"],
138
+ "サッカー": ["フットボール", "soccer"],
139
+ "バスケットボール": ["バスケ", "basketball"],
140
+ "野球": ["ベースボール", "baseball"],
141
+ "テニス": ["テニス部", "テニスサークル"],
142
+ "バドミントン": ["badminton", "シャトル"],
143
+ "卓球": ["ピンポン", "table tennis"],
144
+
145
+ "フリマ": ["フリーマーケット", "バザー"],
146
+ "バザー": ["チャリティバザー", "フリマ"],
147
+ "チャリティ": ["慈善", "募金"],
148
+ "募金": ["寄付", "ドネーション"],
149
+
150
+ "たこ焼き": ["たこやき", "タコ焼き"],
151
+ "焼き鳥": ["やきとり", "串焼き"],
152
+ "からあげ": ["唐揚げ", "フライドチキン"],
153
+ "フランクフルト": ["ソーセージ", "ホットソーセージ"],
154
+ "綿あめ": ["わたあめ", "コットンキャンディ"],
155
+ "かき氷": ["かきごおり", "シェーブアイス"],
156
+ "クレープ": ["スイーツクレープ", "デザートクレープ"],
157
+ "チュロス": ["揚げドーナツ", "シナモンスティック"],
158
+ "たい焼き": ["鯛焼き", "タイ焼き"],
159
+ "おでん": ["関東煮", "煮込みおでん"],
160
+ "肉まん": ["豚まん", "ぶたまん", "中華まん", "肉饅頭", "にくまん"],
161
+ "豚まん": ["肉まん", "ぶたまん", "中華まん", "肉饅頭"],
162
+ "ぶたまん": ["豚まん", "肉まん", "中華まん", "肉饅頭"],
163
+ "中華まん": ["肉まん", "豚まん", "ぶたまん", "肉饅頭"],
164
+ "うどん": ["温うどん", "ぶっかけうどん"],
165
+ "そば": ["蕎麦", "日本そば"],
166
+ "カレー": ["カレーライス", "CURRY"],
167
+ "ホットドッグ": ["ホットドッグパン", "ドッグパン"],
168
+ "ハンバーガー": ["バーガー", "ハンバーグサンド"],
169
+ "ポップコーン": ["ポップコーン販売", "塩ポップコーン"],
170
+ "ジュース": ["ソフトドリンク", "清涼飲料"],
171
+ "コーヒー": ["珈琲", "ホットコーヒー"],
172
+ "紅茶": ["ティー", "ホットティー"],
173
+ "レモネード": ["レモンソーダ", "レモンドリンク"],
174
+ "タピオカ": ["タピオカドリンク", "ミルクティー"],
175
+ "スムージー": ["フルーツスムージー", "スムージードリンク"],
176
+
177
+ "射的": ["的当て", "射撃ゲーム"],
178
+ "くじ引き": ["抽選会", "ガラポン"],
179
+ "輪投げ": ["わなげ", "リングトス"],
180
+ "スーパーボールすくい": ["スーパーボール", "ボールすくい"],
181
+ "金魚すくい": ["金魚救い", "ヨーヨー釣り"],
182
+ "ヨーヨー釣り": ["ヨーヨーつり", "水風船釣り"],
183
+
184
+ "体育館": ["ジム", "アリーナ"],
185
+ "講堂": ["大講堂", "ホール"],
186
+ "大講義室": ["大教室", "大教室棟"],
187
+ "中講義室": ["中教室", "中規模教室"],
188
+ "小講義室": ["小教室", "セミナー室"],
189
+ "屋上": ["ルーフ", "屋上スペース"],
190
+ "中庭": ["パティオ", "コートヤード"],
191
+ "正門": ["メインゲート", "正面入口"],
192
+ "西門": ["ウェストゲート", "西口"],
193
+ "東門": ["イーストゲート", "東口"],
194
+ "南門": ["サウスゲート", "南口"],
195
+ "北門": ["ノースゲート", "北口"],
196
+
197
+ "初日": ["1日目", "一日目"],
198
+ "二日目": ["2日目", "二日目"],
199
+ "三日目": ["3日目", "三日目"],
200
+ "前夜祭": ["前日祭", "イブフェス"],
201
+ "当日": ["当日開催", "当日券"] ,
202
+
203
+ "電源": ["コンセント", "AC電源"],
204
+ "Wi-Fi": ["ワイファイ", "無線LAN"],
205
+ "充電": ["モバイル充電", "チャージ"],
206
+ "休憩所": ["レストスペース", "休憩スペース"],
207
+ "授乳室": ["ベビーケアルーム", "授乳ブース"],
208
+ "おむつ替え": ["おむつ交換", "ベビーベッド"],
209
+ "多目的トイレ": ["バリアフリートイレ", "ユニバーサルトイレ"],
210
+ "エレベーター": ["昇降機", "リフト"],
211
+ "スロープ": ["斜路", "バリアフリースロープ"],
212
+
213
+ "迷子": ["はぐれ", "迷子案内"],
214
+ "迷子センター": ["迷子案内所", "インフォ迷子"],
215
+ "落とし物": ["遺失物", "忘れ物"],
216
+ "遺失物": ["落とし物", "忘れ物"],
217
+ "救護": ["救急", "医療ブース"],
218
+ "救護室": ["救護所", "メディカルルーム"],
219
+ "体調不良": ["具合が悪い", "気分が悪い"],
220
+ "AED": ["自動体外式除細動器", "AED設置"],
221
+ "警備": ["セキュリティ", "ガード"],
222
+ "消防": ["消防隊", "消火活動"],
223
+
224
+ "環境": ["エコ", "サステナビリティ"],
225
+ "SDGs": ["持続可能性", "サステナブル"],
226
+ "リサイクル": ["資源回収", "再資源化"],
227
+ "マイボトル": ["リユースボトル", "水筒"],
228
+ "リユース食器": ["再利用食器", "リユースカップ"],
229
+ "給水": ["ウォーターサーバー", "給水所"],
230
+ "フードロス": ["食品ロス", "食ロス"],
231
+
232
+ "入場": ["エントリー", "入館"],
233
+ "入場制限": ["入場規制", "人数制限"],
234
+ "事前予約": ["予約制", "事前申込"],
235
+ "抽選": ["ロッタリー", "くじ"],
236
+
237
+ "工学部": ["エンジニアリング", "工学"],
238
+ "理学部": ["サイエンス", "理学"],
239
+ "医学部": ["メディカル", "医"],
240
+ "薬学部": ["ファーマシー", "薬学"],
241
+ "法学部": ["ロー", "法"],
242
+ "経済学部": ["エコノミクス", "経済"],
243
+ "文学部": ["リベラルアーツ", "文"],
244
+ "教育学部": ["教育", "教育学"],
245
+ "看護学部": ["ナース", "看護"],
246
+ "園芸学部": ["園芸", "アグリ"],
247
+
248
+ "軽音楽部": ["軽音部", "音楽サークル"],
249
+ "吹奏楽部": ["吹奏", "ブラス"],
250
+ "和太鼓": ["太鼓", "和太鼓演奏"],
251
+ "落語": ["寄席", "落研"],
252
+ "漫研": ["漫画研究会", "コミック研究"],
253
+ "鉄研": ["鉄道研究会", "鉄道サークル"],
254
+ "写真部": ["フォト部", "写真サークル"],
255
+ "映画研究会": ["映研", "映画サークル"],
256
+ "演劇部": ["演劇サークル", "劇団"],
257
+ "美術部": ["アート部", "美術サークル"],
258
+ "書道部": ["書道サークル", "書芸"],
259
+ "茶道部": ["茶道サークル", "お茶"],
260
+ "華道部": ["華道サークル", "生け花"],
261
+ "囲碁": ["囲碁部", "碁"],
262
+ "将棋": ["将棋部", "チェス将棋"],
263
+ "合唱部": ["コーラス部", "合唱サークル"],
264
+ "ゴスペル": ["ゴスペル合唱", "アカペラ"],
265
+ "アカペラ": ["ヴォーカル", "ボイスパーカッション"],
266
+
267
+ "ギター": ["Gt.", "エレキギター"],
268
+ "ベース": ["Ba.", "ベースギター"],
269
+ "ドラム": ["Dr.", "ドラムス"],
270
+ "キーボード": ["Kb.", "シンセサイザー"],
271
+ "ピアノ": ["Piano", "鍵盤"],
272
+ "バイオリン": ["ヴァイオリン", "Vn."],
273
+ "チェロ": ["Vc.", "チェロ演奏"],
274
+ "トランペット": ["Tp.", "ラッパ"],
275
+ "トロンボーン": ["Tb.", "骨"],
276
+ "サックス": ["サクソフォン", "Sax"],
277
+ "フルート": ["Fl.", "横笛"],
278
+ "クラリネット": ["Cl.", "クラリネット演奏"],
279
+ "オーボエ": ["Ob.", "オーボエ演奏"],
280
+ "ファゴット": ["Fg.", "バスーン"],
281
+ "パーカッション": ["打楽器", "Perc."],
282
+ "マリンバ": ["鍵盤打楽器", "マレット"],
283
+ "ティンパニ": ["Kettledrum", "ティンパニー"],
284
+ "三味線": ["津軽三味線", "三味線演奏"],
285
+ "尺八": ["竹笛", "尺八演奏"],
286
+ "琴": ["箏", "和琴"],
287
+
288
+ "ロック": ["ROCK", "ロックバンド"],
289
+ "ポップス": ["J-POP", "ポップ"],
290
+ "ジャズ": ["JAZZ", "スウィング"],
291
+ "フュージョン": ["Fusion", "クロスオーバー"],
292
+ "クラシック": ["クラシカル", "古典音楽"],
293
+ "ブルース": ["Blues", "ブルースロック"],
294
+ "メタル": ["ヘヴィメタル", "HM"],
295
+ "ハードロック": ["HR", "ハード系ロック"],
296
+ "ヒップホップ": ["HIPHOP", "ラップ"],
297
+ "ラップ": ["RAP", "MC"],
298
+ "EDM": ["ダンスミュージック", "エレクトロ"],
299
+ "テクノ": ["Techno", "電子音楽"],
300
+ "ハウス": ["House", "クラブミュージック"],
301
+ "ゴスペル合唱": ["ゴスペル", "クワイア"],
302
+
303
+ "ラグビー": ["Rugby", "楕円球"],
304
+ "バレーボール": ["バレー", "volleyball"],
305
+ "ハンドボール": ["ハンド", "handball"],
306
+ "ラクロス": ["Lacrosse", "ラクロス部"],
307
+ "アメフト": ["アメリカンフットボール", "フットボール"],
308
+ "フットサル": ["室内サッカー", "Futsal"],
309
+ "ソフトボール": ["ソフト", "softball"],
310
+ "ソフトテニス": ["軟式テニス", "soft tennis"],
311
+ "ランニング": ["ジョギング", "走る"],
312
+ "マラソン": ["長距離走", "ロードレース"],
313
+ "短距離走": ["スプリント", "ダッシュ"],
314
+ "長距離走": ["ディスタンス", "スタミナ走"],
315
+ "水泳": ["スイミング", "競泳"],
316
+ "水球": ["ウォーターポロ", "水球競技"],
317
+ "登山": ["ハイキング", "トレッキング"],
318
+ "自転車": ["サイクリング", "ロードバイク"],
319
+
320
+ "大道芸": ["ストリートパフォーマンス", "大道パフォーマンス"],
321
+ "コスプレ": ["コスチュームプレイ", "レイヤー"],
322
+ "撮影会": ["フォトセッション", "写真撮影"],
323
+ "メイド喫茶": ["メイドカフェ", "執事喫茶"],
324
+ "カジノ企画": ["カジノ体験", "ポーカー��験"],
325
+ "脱出ゲーム": ["リアル脱出", "謎解き"],
326
+ "謎解き": ["クイズラリー", "パズルゲーム"],
327
+
328
+ "翻訳": ["トランスレーション", "通訳"],
329
+ "国際交流": ["インターナショナル交流", "異文化交流"],
330
+ "地域連携": ["地域協働", "地域コラボ"],
331
+
332
+ "表彰": ["アワード", "授与"],
333
+ "審査": ["ジャッジ", "評価"],
334
+ "投票": ["Vote", "人気投票"],
335
+
336
+ "新歓": ["新入生歓迎", "ウェルカム"],
337
+ "定演": ["定期演奏会", "定期公演"],
338
+ "学内": ["キャンパス内", "構内"],
339
+ "学外": ["オフキャンパス", "構外"],
340
+
341
+ "雨具": ["カッパ", "レインコート"],
342
+ "日除け": ["サンシェード", "日よけ"],
343
+ "防寒": ["防寒具", "ホッカイロ"],
344
+
345
+ "受付終了": ["締切", "クローズ"],
346
+ "空席": ["空き", "空き枠"],
347
+ "満席": ["満枠", "ソールドアウト"],
348
+
349
+ "記念品": ["ノベルティ", "粗品"],
350
+ "抽選券": ["くじ券", "抽選チケット"],
351
+ "割引": ["ディスカウント", "値引き"],
352
+ "前売り": ["前売券", "アドバンスチケット"],
353
+ "当日券": ["当券", "ウォークイン"],
354
+
355
+ "音響": ["PA", "サウンド"],
356
+ "照明": ["ライティング", "ステージライト"],
357
+ "舞台": ["ステージ", "プラットフォーム"],
358
+ "幕": ["緞帳", "カーテン"],
359
+ "リハーサル": ["ゲネプロ", "場当たり"],
360
+ "本番": ["ステージ本番", "本公演"],
361
+ "アンコール": ["再演", "Encore"],
362
+
363
+ "司会": ["MC", "進行"],
364
+ "進行表": ["台本", "タイムスケジュール"],
365
+ "台本": ["シナリオ", "進行台本"],
366
+ "連絡網": ["メーリングリスト", "ML"],
367
+ "集合場所": ["集合地点", "待ち合わせ場所"],
368
+ "搬入": ["機材搬入", "荷下ろし"],
369
+ "搬出": ["撤収", "荷積み"],
370
+ "撤収": ["片付け", "クリーンアップ"],
371
+
372
+ "設営": ["セッティング", "セットアップ"],
373
+ "撤去": ["バラシ", "解体"],
374
+ "動線": ["導線", "人の流れ"],
375
+ "前日準備": ["仕込み", "前準備"],
376
+ "打ち合わせ": ["ミーティング", "打合せ"],
377
+
378
+ "スタンプラリー": ["スタンプ集め", "ラリー企画"],
379
+ "クイズラリー": ["クイズ巡り", "クイズ企画"],
380
+ "スカベンジャーハント": ["宝探し", "探索ゲーム"],
381
+ "ミニゲーム": ["プチゲーム", "簡単ゲーム"],
382
+ "フォトコンテスト": ["フォトコン", "写真コンテスト"],
383
+ "絵画コンテスト": ["アートコンテスト", "イラストコン"],
384
+ "コンペ": ["コンペティション", "競技会"],
385
+ "コンテスト": ["大会", "選手権"],
386
+
387
+ "呼び込み": ["客引き", "アナウンス"],
388
+ "売り子": ["販売スタッフ", "呼び込みスタッフ"],
389
+ "会計": ["レジ", "精算"],
390
+ "釣り銭": ["つり銭", "おつり"],
391
+ "両替": ["ブレイクチェンジ", "両替所"],
392
+ "現金": ["キャッシュ", "現金支払い"],
393
+ "キャッシュレス": ["非現金", "電子決済"],
394
+ "QR決済": ["コード決済", "スマホ決済"],
395
+ "クレジットカード": ["クレカ", "カード決済"],
396
+ "PayPay": ["ペイペイ", "QR支払い"],
397
+
398
+ "衛生": ["サニテーション", "清潔"],
399
+ "アレルギー表示": ["アレルゲン表示", "アレルギー情報"],
400
+ "調理許可": ["飲食許可", "営業許可"],
401
+ "火気": ["オープンフレーム", "火器"],
402
+ "消火器": ["消火具", "消火設備"],
403
+ "ガス": ["プロパン", "LPガス"],
404
+ "発電機": ["ジェネレーター", "発電装置"],
405
+
406
+ "申請": ["届け出", "申込"],
407
+ "許可": ["許認可", "承認"],
408
+ "許諾": ["承諾", "ライセンス"],
409
+
410
+ "サウンドチェック": ["音出し", "サウチェク"],
411
+ "モニター": ["返し", "ステージモニター"],
412
+ "バックライン": ["常設機材", "バクライン"],
413
+ "マイク": ["MIC", "マイクロフォン"],
414
+ "ダイナミックマイク": ["ダイナミック", "SM58"],
415
+ "コンデンサーマイク": ["コンデンサー", "コンデンサ"],
416
+ "DI": ["ダイレクトボックス", "ダイレクトインジェクション"],
417
+ "ミキサー": ["卓", "ミキシングコンソール"],
418
+ "コンソール": ["ミキサー卓", "オーディオ卓"],
419
+ "ケーブル": ["配線", "コード"],
420
+ "XLR": ["キャノン", "三芯ケーブル"],
421
+ "TRS": ["フォーン", "ステレオフォーン"],
422
+ "スピーカー": ["SP", "音響スピーカー"],
423
+ "サブウーハー": ["サブウーファー", "サブウーハ"],
424
+
425
+ "スポットライト": ["スポット", "ピンスポ"],
426
+ "PARライト": ["PAR", "パーライト"],
427
+ "ムービングライト": ["ムービング", "ムビング"],
428
+ "DMX": ["DMX512", "ライティング制御"],
429
+ "調光": ["ディマー", "明るさ調整"],
430
+ "照明卓": ["ライティングコンソール", "照明コンソール"],
431
+ "フォグ": ["スモーク", "煙演出"],
432
+ "ヘイズ": ["ヘイザー", "薄煙"],
433
+
434
+ "舞台監督": ["舞監", "ステージマネージャー"],
435
+ "小屋入り": ["仕込み入り", "劇場入り"],
436
+ "バラシ": ["撤去", "解体作業"],
437
+
438
+ "取材": ["プレス取材", "報道取材"],
439
+ "報道": ["ニュース", "メディア"],
440
+ "記者": ["リポーター", "ジャーナリスト"],
441
+
442
+ "避難経路": ["エスケープルート", "避難動線"],
443
+ "立入禁止": ["入場禁止", "進入禁止"],
444
+ "危険物": ["危険な物", "ハザード"],
445
+ "注意喚起": ["アナウンス注意", "注意呼びかけ"],
446
+ "安全管理": ["セーフティマネジメント", "安全対策"],
447
+ "交通整理": ["誘導", "ガードマン"],
448
+
449
+ "車椅子": ["車いす", "車イス"],
450
+ "点字": ["ブライユ", "点字案内"],
451
+ "手話通訳": ["手話", "手話サポート"],
452
+
453
+ "物産展": ["物産ブース", "ご当地物産"],
454
+ "地域PR": ["自治体PR", "地域紹介"],
455
+ "産学連携": ["大学連携", "企業連携"],
456
+ "企業ブース": ["スポンサー出展", "法人出展"],
457
+
458
+ "荒天": ["荒天時", "暴風雨"],
459
+ "小雨決行": ["多少の雨", "雨でも開催"],
460
+ "雨天中止": ["雨天時中止", "悪天候中止"],
461
+ "台風": ["サイクロン", "タイフーン"],
462
+ "熱中症": ["熱射病", "暑熱障害"],
463
+ "猛暑": ["酷暑", "真夏日"],
464
+ "寒波": ["厳寒", "強い寒さ"],
465
+
466
+ "マスク": ["フェイスマスク", "不織布マスク"],
467
+ "消毒": ["アルコール消毒", "手指消毒"],
468
+ "検温": ["体温測定", "サーモチェック"],
469
+
470
+ "同好会": ["同好会サークル", "クラブ"],
471
+ "部活": ["部活動", "クラブ活動"],
472
+ "公認団体": ["大学公認", "公認サークル"],
473
+ "非公認": ["未公認", "非公認サークル"],
474
+ "学友会": ["学生会", "学友"],
475
+
476
+ "提灯": ["ちょうちん", "ランタン"],
477
+ "のぼり": ["幟", "旗"],
478
+ "横断幕": ["バナー", "横断バナー"],
479
+ "看板": ["サイン", "サイネージ"],
480
+ "ポスター": ["掲示ポスター", "張り紙"],
481
+ "チラシ": ["フライヤー", "ビラ"],
482
+ "ビラ配り": ["チラシ配布", "配布活動"],
483
+
484
+ "公式アプリ": ["フェスアプリ", "学祭アプリ"],
485
+ "特設サイト": ["特設ページ", "特設Web"],
486
+
487
+ "ギターボーカル": ["Gt.Vo", "ギタボ"],
488
+ "ベースボーカル": ["Ba.Vo", "ベボ"],
489
+ "ツインギター": ["2ギター", "ツインGt"],
490
+ "ツインボーカル": ["2Vo", "デュオボーカル"],
491
+ "インストバンド": ["インストゥルメンタル", "歌なしバンド"],
492
+
493
+ "合奏発表": ["アンサンブル発表", "演奏発表"],
494
+ "定期公演": ["定演", "定期ライブ"],
495
+ "野外ライブ": ["屋外ライブ", "アウトドアライブ"],
496
+ "室内ライブ": ["屋内ライブ", "インドアライブ"],
497
+
498
+ "新体操": ["リズム体操", "RG"],
499
+ "体操": ["体操競技", "Gymnastics"],
500
+ "チア": ["チアリーディング", "チアダンス"],
501
+ "ヨガ": ["Yoga", "ヨーガ"],
502
+ "ピラティス": ["Pilates", "ボディメイク"],
503
+ "ボクシング": ["Boxing", "拳闘"],
504
+ "キックボクシング": ["キック", "Kickboxing"],
505
+ "少林寺拳法": ["少林寺", "拳法"],
506
+ "相撲": ["Sumo", "すもう"],
507
+ "スケートボード": ["スケボー", "SK8"],
508
+ "ダブルダッチ": ["二重跳びロープ", "DD"],
509
+
510
+ "即売会": ["頒布会", "販売会"],
511
+ "頒布": ["配布", "頒布販売"],
512
+ "委託販売": ["委販", "委託"],
513
+
514
+ "学部紹介": ["学科紹介", "専攻紹介"],
515
+ "研究室公開": ["ラボ公開", "オープンラボ"],
516
+ "オープンキャンパス": ["OC", "学校見学"],
517
+
518
+ "アンプ": ["ギターアンプ", "ベースアンプ"],
519
+ "エフェクター": ["ペダル", "エフェクト"],
520
+ "ストラップ": ["ギターストラップ", "肩紐"],
521
+ "チューナー": ["チューニング", "チューニングメーター"],
522
+ "譜面台": ["楽譜立て", "ミュージックスタンド"],
523
+ "メトロノーム": ["拍子器", "テンポメーター"],
524
+ "カポタスト": ["カポ", "カポタ"],
525
+
526
+ "アンケート": ["サーベイ", "調査票"],
527
+ "意見箱": ["ご意見箱", "フィードバックボックス"],
528
+ "問い合わせ": ["お問い合わせ", "コンタクト"],
529
+
530
+ "学生証": ["IDカード", "学生ID"],
531
+ "身分証": ["身分証明書", "ID"],
532
+ "確認書": ["確認票", "確認レター"],
533
+
534
+ "紛失": ["失くす", "紛失物"],
535
+ "拾得物": ["拾い物", "拾得"],
536
+
537
+ "立て看板": ["立看", "立看板"],
538
+ "仮設テント": ["テント", "仮設ブース"],
539
+ "机": ["テーブル", "長机"],
540
+ "椅子": ["イス", "チェア"],
541
+ "養生": ["養生テープ", "床養生"],
542
+ "コードリール": ["延長ドラム", "延長コード"],
543
+ "延長コード": ["電源タップ", "タップ"],
544
+
545
+ "写真映え": ["フォトジェニック", "映え"],
546
+ "推し活": ["推しごと", "応援活動"],
547
+ "交流会": ["懇親会", "ミートアップ"],
548
+ "打ち上げ": ["打上げ", "アフターパーティー"],
549
+ "差し入れ": ["ドネーション", "差入れ"],
550
+
551
+ "開会式": ["オープニング", "オープニングセレモニー"],
552
+ "閉会式": ["クロージング", "クロージングセレモニー"],
553
+ "表彰式": ["アワードセレモニー", "授賞式"],
554
+
555
+ "MC台本": ["司会台本", "進行台本"],
556
+ "曲順": ["セットリスト", "セトリ"],
557
+ "転換": ["ステージ転換", "セッティング替え"],
558
+
559
+ "搬入口": ["搬入ゲート", "搬入扉"],
560
+ "関係者入口": ["スタッフ入口", "バックステージ入口"],
561
+ "バックステージ": ["舞台裏", "バックステージエリア"],
562
+
563
+ "記録撮影": ["ビデオ撮影", "記念撮影"],
564
+ "配信": ["ライブ配信", "ストリーミング"],
565
+ "録音": ["レコーディング", "音声記録"],
566
+
567
+ "ノベルティ配布": ["粗品配布", "記念品配布"],
568
+ "スタンプカード": ["ポイントカード", "集印帳"],
569
+ "抽選会": ["くじ引き", "抽選イベント"],
570
+
571
+ "雨天備品": ["雨具", "ブルーシート"],
572
+ "日除けテント": ["サンシェード", "日よけテント"],
573
+ "防寒具": ["カイロ", "ブランケット"],
574
+
575
+ "授乳": ["母乳", "ベビーケア"],
576
+ "おむつ": ["ダイパー", "紙おむつ"],
577
+ "多目的室": ["多目的スペース", "ユニバーサルルーム"],
578
+
579
+ "落雷": ["雷", "雷雨"],
580
+ "強風": ["暴風", "突風"],
581
+ "高温": ["猛暑", "酷暑"],
582
+ "低温": ["厳寒", "寒冷"],
583
+
584
+ "清掃ボランティア": ["クリーンボランティア", "清掃スタッフ"],
585
+ "資源回収": ["空き缶回収", "ペットボトル回収"],
586
+ "給水スポット": ["給水所", "ウォーターサーバー"],
587
+
588
+ "地域ボランティア": ["市民ボランティア", "地域スタッフ"],
589
+ "企業協賛": ["スポンサー", "協賛企業"],
590
+ "後援": ["サポート", "バックアップ"],
591
+
592
+ "観覧席": ["客席", "座席"],
593
+ "立見": ["立ち見", "スタンディング"],
594
+ "最前列": ["最前", "フロントロウ"],
595
+
596
+ "撮影禁止": ["写真NG", "動画NG"],
597
+ "飲食禁止": ["飲食NG", "飲食不可"],
598
+ "禁煙": ["ノースモーキング", "喫煙禁止"],
599
+ "喫煙所": ["スモーキングエリア", "喫煙スペース"],
600
+
601
+ "案内看板": ["案内サイン", "ディレクションサイン"],
602
+ "導線案内": ["動線案内", "ルート案内"],
603
+ "整理整頓": ["片付け", "整頓"],
604
+
605
+ "忘れ物": ["落とし物", "遺失物"],
606
+ "呼出": ["呼び出し", "アナウンス呼出"],
607
+ "迷子放送": ["迷子アナウンス", "迷子呼出"]
608
+ }
resources/test_data.csv ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ projectId,organization,reading,title,genre,day1,day2,day3,place,description,prComment,prCommentLong,note,imageName,prImageName1,prImageName2,prImageName3,prImageName4,prImageName5,urlX,urlInstagram,urlOfficialWebsite,urlYoutube,urlOther
2
+ dummy-01,電子工作愛好会,でんしこうさくあいこうかい,光る!電子アート展,展示,true,true,true,工学部2号館,電子工作で作ったアート作品を展示します。,キラキラ光る電子アートの世界へようこそ!部員が丹精込めて作った作品の数々をお楽しみください。初心者向けの体験コーナーもありますよ!,"当会は、電子工作を通じて創造の喜びを分かち合うサークルです。今回の展示では、LEDやセンサーを駆使したインタラクティブなアート作品を多数用意しました。来場者の動きに反応して光や音が変わる作品など、見て触って楽しめる展示を目指しています。ぜひ、不思議な電子アートの世界に足を踏み入れてみてください。",,dummy.jpg,,,,,,,,,,
3
+ dummy-02,空想地図作成サークル,くうそうちずさくせいさーくる,架空都市「ジェミニシティ」のすべて,展示,true,true,false,文学部棟ロビー,我々が創造した架空の都市の地図や設定資料を展示します。,存在しない都市の、あまりにもリアルな歴史と地理。あなたの知的好奇心を刺激する、壮大な空想の世界がここにあります。,"「ジェミニシティ」は、私たちが一年がかりで創り上げた架空の都市です。詳細な地図はもちろん、その歴史、文化、交通網に至るまで、細部にわたって設定を練り上げました。会場では、巨大な手書き地図のほか、住民台帳や市史年表などの資料も展示します。この街に迷い込んでみませんか?",,dummy.jpg,,,,,,,,,,
4
+ dummy-03,世界のボードゲームで遊ぶ会,せかいのぼーどげーむであそぶかい,ボードゲーム体験会,参加型,true,true,true,共通教育棟101教室,世界中の珍しいボードゲームで遊べます。ルールは部員が丁寧に説明します。,ドイツ、フランス、アメリカから集めた選りすぐりのボードゲームをご用意しました。初心者大歓迎!一緒に頭脳戦を楽しみましょう!,ルールが簡単でワイワイ楽しめるパーティーゲームから、じっくり考える戦略ゲームまで、50種類以上のボードゲームを準備しています。時間内は出入り自由、遊び放題です。お一人様でも、お友達とでも、お気軽にお立ち寄りください。,,dummy.jpg,,,,,,,,,,
5
+ dummy-04,謎の中華まん研究会,なぞのちゅうかまんけんきゅうかい,究極の中華まん,飲食,false,true,true,食堂前テント,研究の成果として生まれた、究極に美味しい中華まんを販売します。,肉汁たっぷりのジューシーな中華まんです。一度食べたら忘れられない味を、ぜひご賞味ください!,生地の発酵時間、餡の配合、蒸し時間。すべてを計算し尽くした、我々の研究の集大成がこの「究極の中華まん」です。味は「極・豚まん」と「秘伝・カレーまん」の二種類。熱々をご提供します!,売り切れ次第終了,dummy.jpg,,,,,,,,,,
6
+ dummy-05,大道芸パフォーマンス「ジェスターズ」,だいどうげいぱふぉーまんすじぇすたーず,ジャグリング&マジックショー,パフォーマンス,true,false,true,けやき広場,ジャグリングやマジックなどのパフォーマンスを披露します。,ボールが宙を舞い、トランプが奇跡を起こす!笑いと驚きの30分間。ご家族、ご友人お誘い合わせの上、ぜひご覧ください!,"私たちジェスターズは、ジャグリング、マジック、パントマイムなどを練習しているパフォーマンスサークルです。ステージでは、各メンバーが磨き上げた技を次々と披露します。お客様を巻き込んだ楽しいパフォーマンスも予定していますので、ぜひ手拍子で応援してください!",雨天中止,dummy.jpg,,,,,,,,,,
7
+ dummy-06,空飛ぶほうき研究会,そらとぶほうきけんきゅうかい,VRほうき飛行体験,参加型,true,true,true,体育館裏,VRゴーグルをつけて、空飛ぶほうきに乗る体験ができます。,あなたも魔法使いになれる!VRでリアルな飛行体験。風を感じながら、キャンパスの上空を散歩しよう!,最新のVR技術と、我々が開発した専用のほうき型デバイスを組み合わせることで、本当に空を飛んでいるかのような没入感を実現しました。3分間の短いフライトですが、忘れられない体験になること間違いなしです。,【注意】身長140cm未満の方、乗り物に酔いやすい方はご遠慮ください。,dummy.jpg,,,,,,,,,,
8
+ dummy-07,無重力睡眠研究会,むじゅうりょくすいみんけんきゅうかい,無重力ハンモック体験,その他,false,true,true,中庭,究極のリラックスを。無重力感覚のハンモックで癒やしのひととき。,ゆらゆら揺られて、まるで宇宙にいるみたい。5分間のリラックス体験はいかがですか?日頃の疲れを癒やしに来てください。,このハンモックは、体の圧力が一点に集中しないよう特殊な設計が施されており、まるで無重力空間にいるかのような浮遊感を味わうことができます。木陰で静かな音楽を聴きながら、究極のリラクゼーションをご体験ください。,,dummy.jpg,,,,,,,,,,
9
+ dummy-08,古代文字解読クラブ,こだいもじかいどくくらぶ,古代文字アクセサリー販売,物販,true,true,false,歴史資料館前,ヒエログリフや楔形文字をあしらった手作りアクセサリーを販売します。,古代の神秘をその手に。あなたの名前をヒエログリフにしたキーホルダーが人気です。世界に一つだけのアクセサリーを見つけに来てください。,古代エジプトのヒエログリフ、メソポタミアの楔形文字、マヤ文字など、様々な古代文字をモチーフにしたピアスやネックレス、キーホルダーを販売しています。すべて部員による手作りです。文字の意味を解説したカードもお付けします。,,dummy.jpg,,,,,,,,,,
10
+ dummy-09,即興ミュージカル劇団,そっきょうみゅーじかるげきだん,その場でミュージカル!,パフォーマンス,false,false,true,講堂,お客様から頂いたお題で、その場で即興のミュージカルを創ります。,何が起こるか分からない、一期一会のステージ!あなたのアイデアが、世界でたった一つのミュージカルになります。笑いと感動の渦に巻き込まれに来てください!,開演前にお客様から「場所」「登場人物」「決め台詞」などのお題を募集します。キャストとバンドは、そのお題を元に、打ち合わせなしの完全即興で歌と物語を紡ぎ出します。予測不能な展開をお楽しみください!,撮影・録音禁止,dummy.jpg,,,,,,,,,,
11
+ dummy-10,深海生物同好会,しんかいせいぶつどうこうかい,光る深海生物のレジンアート,物販,true,true,true,理学部棟1階,深海生物をモチーフにした、暗闇で光るレジンアート作品を販売します。,チョウチンアンコウやダイオウグソクムシが、あなたの部屋を怪しく照らす。マニアックな魅力あふれる作品たちです。,部員が一つ一つ手作りした、深海生物のレジン製キーホルダーや置物です。蓄光素材を混ぜ込んでいるため、暗い場所でぼんやりと光ります。デフォルメされた可愛いデザインから、リアルな造形のものまで、様々な作品をご用意してお待ちしております。,,dummy.jpg,,,,,,,,,,
12
+ dummy-11,タイポグラフィ研究会,たいぽぐらふぃけんきゅうかい,文字で魅せるポスター展,展示,true,true,true,デザイン棟ギャラリー,文字のデザインだけで構成されたポスターを展示します。,フォントの力、文字の配置、色の組み合わせ。言葉の意味を超えた、文字そのものの美しさを感じてください。,タイポグラフィとは、文字を読みやすく、美しく配置する技術です。本展示では、部員それぞれが選んだテーマを、タイポグラフィの技術のみで表現したポスター作品を展示しています。あなたの「好き」な文字がきっと見つかります。,,dummy.jpg,,,,,,,,,,
13
+ dummy-12,世界の紅茶を飲む会,せかいのこうちゃをのむかい,世界の紅茶とスコーンの店,飲食,true,true,true,学生会館2階,世界各国の紅茶と、手作りスコーンを提供します。,アッサム、ダージリン、アールグレイ。香り豊かな紅茶で、優雅なひとときを。紅茶に合う自家製スコーンも絶品です。,インド、スリランカ、中国、そして日本。世界中から取り寄せた20種類以上の茶葉の中から、お好きな紅茶をお選びいただけます。紅茶の専門知識を持つ部員が、あなたにぴったりの一杯をおすすめします。,,dummy.jpg,,,,,,,,,,
14
+ dummy-13,ドローンレース同好会,どろーんれーすどうこうかい,第一回ドローンレース選手権,パフォーマンス,false,true,false,グラウンド,部員によるドローンレースの大会です。白熱の空中戦をご覧ください。,目にも留まらぬ速さでコースを駆け抜けるドローン。操縦技術の限界に挑む、手に汗握るレース展開にご期待ください!,特設コースを舞台に、FPV(一人称視点)ドローンによるレースを行いま��。LEDで装飾されたドローンが、立体的なコースを高速で飛び回る様子は圧巻です。未来のスカイスポーツを、ぜひその目でご覧ください。,安全のため、コース内への立ち入りは固く禁じます。,dummy.jpg,,,,,,,,,,
15
+ dummy-14,VR蕎麦打ち体験会,ぶいあーるそばうちたいけんかい,VRであなたも蕎麦職人,参加型,true,true,true,情報科学棟VRラボ,VRで本格的な蕎麦打ちを体験できます。,仮想空間で、こねて、のばして、切る。あなただけの手打ち蕎麦を完成させよう!友達とスコアを競うのも楽しい!,VRコントローラーを使い、蕎麦粉をこねる感触から、麺を切る工程までをリアルに再現しました。ゲーム感覚で楽しみながら、蕎麦打ちの一連の流れを学ぶことができます。高得点者には景品もご用意しています。,,dummy.jpg,,,,,,,,,,
16
+ dummy-15,AIと俳句を作る会,えーあいとはいくをつくるかい,AI vs 人間 俳句バトル,参加型,true,false,true,大講義室,AIが作った俳句と人間が作った俳句、見分けられますか?,人工知能は、人の心を詠むことができるのか。来場者の皆様に審査員となっていただき、AIと人間の俳句対決を行います。,当会が開発した俳句生成AI「松尾」と、俳句歴20年の人間師範が、同じお題で俳句を詠みます。どちらが詠んだ句か隠した状態で皆様に鑑賞・投票していただき、その合計点で勝敗を決めます。皆様の参加をお待ちしております。,,dummy.jpg,,,,,,,,,,
17
+ dummy-16,多肉植物を愛でる会,たにくしょくぶつをめでるかい,ぷにぷに多肉植物の寄せ植え販売,物販,true,true,true,園芸学部温室,可愛い多肉植物の寄せ植えを販売します。,ぷにぷにした見た目に癒やされる、多肉植物はいかがですか?育てやすい種類ばかりなので、初めての方にもおすすめです。,部員が愛情を込めて育てた多肉植物を、おしゃれな鉢に寄せ植えして販売します。お部屋のインテリアにぴったりの一品がきっと見つかります。育て方の相談も、お気軽にどうぞ。,,dummy.jpg,,,,,,,,,,
18
+ dummy-17,鉱物収集サークル,こうぶつしゅうしゅうさーくる,きらめく鉱物の展示・即売会,物販,true,true,true,理学部博物館,国内外の美しい鉱物標本を展示・販売します。,水晶、蛍石、黄鉄鉱。地球が創り出した自然の芸術品。掌に乗る、きらめく小宇宙をあなたのお家に。,部員が全国各地で採集した鉱物や、海外から取り寄せた珍しい鉱物を展示・販売します。鉱物の名前や産地、見どころなどを解説したカードと共に、その魅力をお伝えします。,,dummy.jpg,,,,,,,,,,
19
+ dummy-18,アナログゲーム開発団,あなろぐげーむかいはつだん,自作アナログゲーム試遊会,参加型,true,true,true,サークル棟101,部員が制作したオリジナルのカードゲームやボードゲームで遊べます。,まだ誰も遊んだことのない、世界で初めてのゲーム体験をあなたに。制作者本人によるルール説明付きです!,企画からデザイン、制作まで、すべて部員の手で行ったオリジナルのアナログゲームが勢揃い。あなたのフィードバックが、未来の製品版を作る第一歩になるかもしれません。ぜひ遊びに来てください。,,dummy.jpg,,,,,,,,,,
20
+ dummy-19,歴史的建造物模型部,れきしてきけんぞうぶつもけいぶ,ミニチュア歴史建造物展,展示,true,true,true,図書館1階,歴史的な建物を精密な模型で再現しました。,あの有名なお城や教会が、手のひらサイズに。細部までこだわり抜いた、驚きの再現度をご覧ください。,設計図や古写真を元に、歴史的建造物を1/150スケールで再現しています。素材は木材や紙、プラスチックなど様々。模型を通じて、世界の建築史を巡る旅をお楽しみください。,,dummy.jpg,,,,,,,,,,
21
+ dummy-20,現代神話創作サークル,げんだいしんわそうさくさーくる,我々の神話を聞いてくれ,音楽,false,true,false,野外ステージ,創作した神話の朗読と、テーマソングの演奏を行います。,もし、現代に新たな神話が生まれたなら。壮大なスケールで描かれる、我々のオリジナル神話の発表会です。,現代社会を舞台にした、新たな神々や英雄たちの物語を創作しています。当日は、物語のハイライト部分の朗読と、物語の世界観を表現したオリジナル楽曲のバンド演奏を披露します。,,dummy.jpg,,,,,,,,,,
22
+ dummy-21,プロジェクションマッピング同好会,ぷろじぇくしょんまっぴんぐどうこうかい,校舎が動く!光のショー,パフォーマンス,false,false,true,図書館壁面,図書館の壁面にプロジェクションマッピングを投影します。,見慣れた校舎が、光のアートで生まれ変わる。音楽と映像が織りなす、幻想的な夜をお楽しみください。,閉祭式の後、図書館の壁面をスクリーンに、約10分間のプロジェクションマッピングショーを行います。今年のテーマは「祭り」。ダイナミックな映像にご期待ください。,閉祭式終了後、20:00から開始予定,dummy.jpg,,,,,,,,,,
23
+ dummy-22,世界のスープ研究会,せかいのすーぷけんきゅうかい,旅するスープ屋さん,飲食,true,true,true,国際交流会館前,週替わりならぬ日替わりで、世界各国のスープを提供します。,1日目はロシアのボルシチ、2日目はタイのトムヤムクン、3日目はスペインのガスパチョ。心も体も温まる一杯をどうぞ。,私たちは世界中のスープを研究し、その再現に情熱を燃やしています。各国の食文化が詰まった、本格的な味をお楽しみください。パンもセットで販売します。,,dummy.jpg,,,,,,,,,,
24
+ dummy-23,折り紙ヒコーキ部,おりがみひこーきぶ,誰が一番飛ぶか選手権,参加型,true,true,false,体育館,自分で折った紙ヒコーキの飛距離を競います。,たかが紙ヒコーキ、されど紙ヒコーキ。折り方一つで飛び方が変わる、奥深い世界へようこそ。大人も子供も、本気で遊ぼう!,指定の用紙と折り方テキストを配布します。練習時間内に、よく飛ぶように調整してください。競技は一人一投。最も遠くまで飛ばした方には豪華景品をプレゼント!,,dummy.jpg,,,,,,,,,,
25
+ dummy-24,食品サンプル制作サークル,しょくひんさんぷるせいさくさーくる,本物そっくり!食品サンプル工房,物販,true,true,true,家庭科調理室,本物そっくりの食品サンプルの制作体験と作品販売。,思わず食べちゃいそうになるくらいリアル!あなたも天ぷらやパフェの食品サンプルを作ってみませんか?,プロも使う専用の材料(蝋や樹脂)を使って、食品サンプルの制作体験ができます。スタッフが丁寧にサポートするので、初めての方でも安心です。部員が作ったハイクオリティな作品の販売も行っています。,,dummy.jpg,,,,,,,,,,
26
+ dummy-25,人力発電研究会,じんりきはつでんけんきゅうかい,自転車でスマホを充電しよう,参加型,true,true,true,環境エネルギー棟,自転車を漕いで、自分のスマートフォンを充電できる体験企画です。,運動不足解消にも、エコにもなる。自分の力で電気を作る達成感を味わおう!漕いだ分だけ、スマホが充電されます。,エアロバイクに発電機を接続した、当会オリジナルの充電ステーションです。USBケーブルを接続し、30分間思いっきり漕いでください。フィットネス感覚で、環境問題について考えるきっかけになれば幸いです。,,dummy.jpg,,,,,,,,,,
27
+ dummy-26,競技かるた会,きょうぎかるたかい,百人一首の世界,展示,true,true,false,和室,競技かるたのデモンストレーションと、百人一首の解説展示。,「ちはやふる」で話題の競技かるた。そのスピードと迫力を、目の前で体感しませんか?,畳の上で繰り広げられる、静かなる激闘。部員による競技かるたの模範試合を定時開催します。また、百人一首の歌の意味や背景を解説したパネル展示も行っています。日本の伝統文化の美しさに触れてみてください。,,dummy.jpg,,,,,,,,,,
28
+ dummy-27,錯視アート研究会,さくしあーとけんきゅうかい,脳が騙される!錯視美術館,展示,true,true,true,美術棟展示室,見ていて不思議な錯視・錯覚アート作品を展示しています。,止まっているはずの絵が動いて見える?同じ長さのはずなのに違って見える?あなたの脳が、きっと騙される。不思議なアートの世界をお楽しみください。,部員が制作した、様々な種類の錯視アートを展示しています。なぜそのように見えるのか、という科学的な解説パネルも用意していますので、自由研究のテーマ探しにも役立つかもしれません。,,dummy.jpg,,,,,,,,,,
29
+ dummy-28,燻製同好会,くんせいどうこうかい,大人の燻製おつまみ,飲食,true,true,true,キャンプエリア,チーズやナッツ、ベーコンなどを燻製にして販売します。,桜のチップでじっくり燻した、香り高いおつまみはいかがですか?お酒が好きな大人の方に、ぜひ味わっていただきたい逸品です。,燻製器も部員の手作りです。温度と煙の量を徹底管理し、それぞれの食材が最も美味しくなるように仕上��ました。燻製ならではの深い味わいと香りをお楽しみください。,,dummy.jpg,,,,,,,,,,
30
+ dummy-29,特殊メイク研究会,とくしゅめいくけんきゅうかい,ワンポイント・傷メイク体験,参加型,false,true,true,教室A-1,リアルな傷メイクをワンポイントで体験できます。,ハロウィンで使えること間違いなし!プロが使うような材料で、本物そっくりの切り傷や火傷のメイクを体験してみませんか?,映画やドラマで使われる特殊メイクの技術を、気軽に体験できる企画です。肌に安全な材料を使用し、5分程度でリアルな傷メイクを施します。SNS映えも抜群です!,,dummy.jpg,,,,,,,,,,
31
+ dummy-30,プラネタリウム制作サークル,ぷらねたりうむせいさくさーくる,手作りプラネタリウム上映会,パフォーマンス,true,true,true,視聴覚室,手作りのプラネタリウムで、秋の夜空を解説します。,満点の星空が、あなたを包み込む。部員による生解説で、星座の物語を巡る旅へ出発しましょう。,直径5mのエアドームの中に、我々が自作したピンホール式プラネタリウム投影機を設置しました。市販のプラネタリウムとは一味違う、温かみのある星空をお楽しみいただけます。各回定員20名、約15分間の上映です。,,dummy.jpg,,,,,,,,,,
32
+ dummy-31,書評サークル「ビブリオ」,しょひょうさーくるびぶりお,書評合戦ビブリオバトル,パフォーマンス,false,true,false,図書館ラーニングコモンズ,おすすめ本を持ち寄って、その魅力を5分間で語り合う書評イベント。,あなたもチャンプ本を予想しよう!発表者が5分間で本の魅力を語り、観客が一番読みたくなった本に投票する、知的書評ゲームです。,「人を通して本を知る、本を通して人を知る」をコンセプトにしたイベントです。誰でも観覧可能。あなたの運命の一冊が、ここで見つかるかもしれません。,,dummy.jpg,,,,,,,,,,
33
+ dummy-32,ジオラマ制作同好会,じおらませいさくどうこうかい,箱庭の世界展,展示,true,true,true,工芸室,様々なテーマのジオラマ(箱庭模型)を展示します。,懐かしい昭和の街並み、ファンタジーの世界、未来の都市。小さな箱の中に広がる、無限の物語をご覧ください。,部員がそれぞれの得意なテーマで制作したジオラマ作品を展示します。細部まで作り込まれた、こだわりの世界観が見どころです。思わず写真を撮りたくなるような作品ばかりです。,,dummy.jpg,,,,,,,,,,
34
+ dummy-33,実験クッキング部,じっけんくっきんぐぶ,アルギン酸で掴める水!?,飲食,true,true,false,化学実験室,食べられる化学実験。人工イクラの原理で、ジュースを球状にします。,見た目はまるで宝石。プチっと弾ける不思議な食感のスイーツです。味はぶどう、オレンジ、メロンの3種類。,食品添加物として使われるアルギン酸ナトリウムと乳酸カルシウムの化学反応を利用した、分子ガストロノミーの入門的スイーツです。安全な材料を使っているので、安心してお召し上がりいただけます。,,dummy.jpg,,,,,,,,,,
35
+ dummy-34,ロボット相撲研究会,ろぼっとずもうけんきゅうかい,自律型ロボット相撲大会,パフォーマンス,true,false,false,武道場,自分たちで開発したロボットによる相撲大会です。,プログラムとセンサーだけで動く、自律型ロボットたちの真剣勝負。土俵際での攻防は、手に汗握ること間違いなし!,全日本ロボット相撲大会のルールに準拠した、本格的なロボット相撲です。部員が設計からプログラミングまで、すべてを手掛けた個性豊かなロボットたちが土俵の上でぶつかり合います。,,dummy.jpg,,,,,,,,,,
36
+ dummy-35,ステンドグラス工房,すてんどぐらすこうぼう,光の小箱作りワークショップ,参加型,true,true,true,美術室,ステンドグラスの技法で、小さなランプシェードを作ります。,色とりどりのガラスが織りなす、光のアート。あなただけのオリジナルランプを作ってみませんか?,カット済みのガラス片を、銅テープとはんだごてを使って立体的に組み立てていきます。本格的なステンドグラスの技法を、安全に体験できます。スタッフがマンツーマンで指導しますので、初めての方でも素敵な作品が作れます。,参加費500円,dummy.jpg,,,,,,,,,,
37
+ dummy-36,世界の打楽器アンサンブル,せかいのだ楽器あんさんぶる,世界の打楽器に触れてみよう,音楽,true,true,true,音楽室,カホン、ジャンベ、コンガなど、世界の様々な打楽器を自由に叩けます��,叩けば音が出る、それが打楽器。難しいルールはありません。リズムに乗って、みんなでセッションを楽しみましょう!,普段あまり触れる機会のない、世界中の珍しい打楽器を集めました。見ているだけでも楽しいですが、ぜひ実際に触れて、その音や響きを体感してみてください。部員によるミニコンサートも随時開催します。,,dummy.jpg,,,,,,,,,,
38
+ dummy-37,サンドアート・パフォーマンス,さんどあーとぱふぉーまんす,砂で描く物語,パフォーマンス,false,true,true,小ホール,砂と光で、音楽に合わせて物語を描き出します。,ガラスの上の砂が、次々と形を変えて物語を紡いでいく。儚くも美しい、サンドアートの世界へようこそ。,バックライトで照らされたガラス板の上で、砂を使って絵を描き、それをスクリーンに投影するパフォーマンスです。音楽に合わせて砂の絵が変化していく様子は、まるでアニメーションのよう。幻想的な時間をお届けします。,,dummy.jpg,,,,,,,,,,
39
+ dummy-38,レザークラフトサークル,れざーくらふとさーくる,革のキーホルダー作り,物販,true,true,true,テラス席,本革を使ったキーホルダー作りのワークショップです。,トントン叩いて、自分だけの刻印を。使い込むほど味が出る、本革の魅力に触れてみませんか?,好きな形の革パーツを選び、名前や模様を刻印して、オリジナルのキーホルダーを作ります。簡単な作業なので、小さなお子様でも楽しめます。部員が作った革小物の販売も行っています。,,dummy.jpg,,,,,,,,,,
40
+ dummy-39,マーブリングアート体験会,まーぶりんぐあーとたいけんかい,水に浮かぶ魔法の絵の具,参加型,true,true,false,絵画室,水面に絵の具を垂らして、不思議なマーブル模様を作ります。,二度と同じ模様は作れない、一期一会のアート体験。作った模様は、紙に写し取ってお持ち帰りいただけます。,マーブリングとは、水面に特殊な絵の具を垂らし、その模様を紙などに写し取る技法です。うちわやコースターに模様を写し取る体験ができます。小さなお子様から大人まで、誰でもアーティスト気分を味わえます。,,dummy.jpg,,,,,,,,,,
41
+ dummy-40,巨大からくり装置研究会,きょだいからくりそうちけんきゅうかい,ピタゴラ的装置の展示,展示,true,true,true,物理実験室,ビー玉が転がって、様々な仕掛けが連鎖する巨大からくり装置。,ビー玉の旅を見守ろう。ゴールした時の達成感は格別です。あっと驚く仕掛けが満載!,教室いっぱいに広がる、巨大なからくり装置を展示しています。物理法則を応用した様々なギミックが、次々と連鎖してビー玉をゴールまで運びます。1時間に1回、実際にビー玉を転がす実演を行います。,,dummy.jpg,,,,,,,,,,
42
+ dummy-41,キャンドル作り同好会,きゃんどるづくりどうこうかい,アロマキャンドル販売,物販,true,true,true,中庭テント,手作りのアロマキャンドルを販売しています。,ラベンダー、ローズ、シトラス。優しい香りのキャンドルで、リラックスタイムを演出しませんか?,見た目も可愛いボタニカルキャンドルや、シンプルなソイキャンドルなど、様々な種類のアロマキャンドルを販売しています。火を灯さなくても、置いておくだけでふんわり香ります。プレゼントにもおすすめです。,,dummy.jpg,,,,,,,,,,
43
+ dummy-42,人力飛行機研究会,じんりきひこうきけんきゅうかい,人力飛行機の翼の展示,展示,true,true,false,格納庫,鳥人間コンテストに出場した、人力飛行機の実物大の翼を展示。,全長20メートルを超える、巨大な翼の迫力を間近で。設計の工夫や、素材の軽さをぜひ体感してください。,毎年夏に開催される鳥人間コンテストに向けて、我々が設計・製作した人力飛行機の主翼(片翼)を展示します。部員が常駐しておりますので、設計や製作に関する質問も大歓迎です。,,dummy.jpg,,,,,,,,,,
44
+ dummy-43,ストリート・トライアル部,すとりーととらいあるぶ,自転車の障害物越えパフォーマンス,パフォーマンス,true,false,true,噴水前広場,自転車で障害物を飛び越えたり、バランスを取ったりするパフォーマンス。,まるで自転車が体の一部になったかのよう。重力を無視したかのような、驚異のバイシクル・テクニック!,ストリート・トライアルとは、障害物を乗り越えながらコースを走破する自転車競技です。高い段差を飛び乗ったり、細い一本橋を渡ったりと、アクロバティックな技の数々を披露します。,,dummy.jpg,,,,,,,,,,
45
+ dummy-44,羊毛フェルト手芸部,ようもうふぇるとしゅげいぶ,ふわふわ動物マスコット,物販,true,true,true,手芸室,羊毛フェルトで作った、動物のマスコットを販売します。,手のひらサイズの、ふわふわで可愛い動物たち。一針一針、心を込めて作りました。あなたのお気に入りの子を見つけてください。,羊毛を専用の針で刺し固めて、様々な形を作っていく手芸です。犬や猫、うさぎといった定番の動物から、少しマニアックな動物まで、たくさんのマスコットを用意してお待ちしています。,,dummy.jpg,,,,,,,,,,
46
+ dummy-45,ディベートクラブ,でぃべーとくらぶ,公開ディベート「AIは人間を超えるか」,パフォーマンス,false,true,false,大ホール,「AIは人間を超えるか」をテーマに、公開ディベートを行います。,肯定側と否定側に分かれ、白熱の言葉の応酬。論理と情熱がぶつかり合う、知的なエンターテイメントです。,即興ではなく、入念な準備に基づいた本格的なディベートです。どちらの主張がより説得力があるか、観客の皆様も一緒に考えながらお楽しみください。ディベート終了後には、観客投票も行います。,,dummy.jpg,,,,,,,,,,
47
+ dummy-46,万華鏡制作サークル,まんげきょうせいさくさーくる,オリジナル万華鏡作り,参加型,true,true,true,工作室,自分だけのオリジナル万華鏡を作ることができます。,キラキラ光るビーズやガラスを選んで、世界に一つだけの万華鏡を。覗き込むたびに変わる、美しい模様に癒やされます。,用意されたキットを使い、簡単な手順で万華鏡を作ることができます。中に入れるビーズやオブジェクトは、たくさんの種類の中から自由に選べます。自分用にはもちろん、プレゼントにも最適です。,参加費300円,dummy.jpg,,,,,,,,,,
48
+ dummy-47,環境音楽研究会,かんきょうおんがくけんきゅうかい,癒やしのアンビエント音楽ライブ,音楽,true,true,true,屋上庭園,心地よいアンビエント音楽の生演奏を行います。,賑やかなお祭りの合間に、少しだけ休憩しませんか。風の音や鳥の声と調和する、穏やかな音楽が流れる空間です。,シンセサイザーや自然音を使い、その場の環境に溶け込むような、心地よい音楽を演奏します。読書や休憩など、ご自由にお過ごしください。演奏は一日中、断続的に行っています。,,dummy.jpg,,,,,,,,,,
49
+ dummy-48,特殊印刷工房,とくしゅいんさつこうぼう,活版印刷でメッセージカード作り,参加型,true,true,false,印刷室,昔ながらの活版印刷機で、メッセージカードを印刷する体験です。,一文字ずつ拾って、インクを乗せて、プレスする。デジタルにはない、温かみのある凹凸が魅力です。,金属活字を組んで版を作り、小型の活版印刷機(テキン)を使って、ご自身でカードを印刷していただきます。独特のかすれや凹みなど、一枚一枚異なる風合いが楽しめます。,,dummy.jpg,,,,,,,,,,
50
+ dummy-49,世界の麺料理研究会,せかいのめんりょうりけんきゅうかい,世界のまぜそば祭り,飲食,true,true,true,屋外フードコート,台湾まぜそば、タイ風まぜそば、イタリアンまぜそばを販売します。,汁なし麺の魅力、ここに集結!各国の特色を生かした、オリジナルまぜそばをご賞味あれ。,麺料理の中でも、特に「まぜそば」に特化して研究しています。今回は、部員が開発した自信作の3種類のまぜそばを販売します。追い飯セットもご用意していますので、最後までタレを味わい尽くしてください!,,dummy.jpg,,,,,,,,,,
51
+ dummy-50,影絵劇団「シルエット」,かげえげきだんしるえっと,光と影が紡ぐ物語,パフォーマンス,false,true,true,視聴覚ホール,スクリーンに映し出される、美しい影絵劇。,光と影だけで、こんなにも豊かな世界が表現できる。音楽と共に繰り広げられる、ノスタルジックな物語をお楽しみください。,誰もが知っている昔話を、当劇団オリジナルの脚本と演出で上演します。役者の身体や、細かく作られた人形を使い、繊細な影の動きで物語を表現します。お子様から大人まで楽しめる内容です。,上演時間はウェブサイトをご確認ください,dummy.jpg,,,,,,,,,,
resources/user_dict.csv ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ax,4786,4786,5000,ax,名詞,固有名詞,一般,*,*,*,アックス,ax,*,*,*,*,*
2
+ b.a.s.s.,4786,4786,5000,b.a.s.s.,名詞,固有名詞,一般,*,*,*,バス,b.a.s.s.,*,*,*,*,*
3
+ belinda,4786,4786,5000,belinda,名詞,固有名詞,一般,*,*,*,ベリンダ,belinda,*,*,*,*,*
4
+ cdc,4786,4786,5000,cdc,名詞,固有名詞,一般,*,*,*,シーディーシー,cdc,*,*,*,*,*
5
+ change!!,4786,4786,5000,change!!,名詞,固有名詞,一般,*,*,*,チェンジ,change!!,*,*,*,*,*
6
+ chiよren北天魁,4786,4786,5000,chiよren北天魁,名詞,固有名詞,一般,*,*,*,チヨレンホクテンカイ,chiよren北天魁,*,*,*,*,*
7
+ cisg,4786,4786,5000,cisg,名詞,固有名詞,一般,*,*,*,シーアイエスジー,cisg,*,*,*,*,*
8
+ cmc,4786,4786,5000,cmc,名詞,固有名詞,一般,*,*,*,シーエムシー,cmc,*,*,*,*,*
9
+ c-raft lab,4786,4786,5000,c-raft lab,名詞,固有名詞,一般,*,*,*,クラフトラボ,c-raft lab,*,*,*,*,*
10
+ cuad,4786,4786,5000,cuad,名詞,固有名詞,一般,*,*,*,シーユーエーディー,cuad,*,*,*,*,*
11
+ c-vol,4786,4786,5000,c-vol,名詞,固有名詞,一般,*,*,*,シーボル,c-vol,*,*,*,*,*
12
+ english house student society,4786,4786,5000,english house student society,名詞,固有名詞,一般,*,*,*,イングリッシュハウススチューデントソサエティ,english house student society,*,*,*,*,*
13
+ fabric,4786,4786,5000,fabric,名詞,固有名詞,一般,*,*,*,ファブリック,fabric,*,*,*,*,*
14
+ fc riflie,4786,4786,5000,fc riflie,名詞,固有名詞,一般,*,*,*,エフシーレフリエ,fc riflie,*,*,*,*,*
15
+ fファン,4786,4786,5000,fファン,名詞,固有名詞,一般,*,*,*,エフファン,fファン,*,*,*,*,*
16
+ geo,4786,4786,5000,geo,名詞,固有名詞,一般,*,*,*,ジオ,geo,*,*,*,*,*
17
+ hoop☆star,4786,4786,5000,hoop☆star,名詞,固有名詞,一般,*,*,*,フープスター,hoop☆star,*,*,*,*,*
18
+ las,4786,4786,5000,las,名詞,固有名詞,一般,*,*,*,ラス,las,*,*,*,*,*
19
+ maybe good,4786,4786,5000,maybe good,名詞,固有名詞,一般,*,*,*,メイビーグッド,maybe good,*,*,*,*,*
20
+ 道しるべ,4786,4786,5000,道しるべ,名詞,固有名詞,一般,*,*,*,ミチシルベ,道しるべ,*,*,*,*,*
21
+ orita family club,4786,4786,5000,orita family club,名詞,固有名詞,一般,*,*,*,オリタファミリークラブ,orita family club,*,*,*,*,*
22
+ zoo,4786,4786,5000,zoo,名詞,固有名詞,一般,*,*,*,ズー,zoo,*,*,*,*,*
23
+ ubojchor,4786,4786,5000,ubojchor,名詞,固有名詞,一般,*,*,*,ウボイコール,ubojchor,*,*,*,*,*
24
+ verde,4786,4786,5000,verde,名詞,固有名詞,一般,*,*,*,ベルデ,verde,*,*,*,*,*
25
+ wood stock,4786,4786,5000,wood stock,名詞,固有名詞,一般,*,*,*,ウッドストック,wood stock,*,*,*,*,*
26
+ アイドルマスター,4786,4786,5000,アイドルマスター,名詞,固有名詞,一般,*,*,*,アイドルマスター,アイドルマスター,*,*,*,*,*
27
+ t.o.n.e.,4786,4786,5000,t.o.n.e.,名詞,固有名詞,一般,*,*,*,トーン,t.o.n.e.,*,*,*,*,*
28
+ あまりぃず,4786,4786,5000,あまりぃず,名詞,固有名詞,一般,*,*,*,アマリーズ,あまりぃず,*,*,*,*,*
29
+ アンプラグド,4786,4786,5000,アンプラグド,名詞,固有名詞,一般,*,*,*,アンプラグド,アンプラグド,*,*,*,*,*
30
+ えれちば,4786,4786,5000,えれちば,名詞,固有名詞,一般,*,*,*,エレチバ,えれちば,*,*,*,*,*
31
+ オリエンテーリング,4786,4786,5000,オリエンテーリング,名詞,固有名詞,一般,*,*,*,オリエンテーリング,オリエンテーリング,*,*,*,*,*
32
+ olioli,4786,4786,5000,olioli,名詞,固有名詞,一般,*,*,*,オリオリ,olioli,*,*,*,*,*
33
+ p-ritts,4786,4786,5000,p-ritts,名詞,固有名詞,一般,*,*,*,プリッツ,p-ritts,*,*,*,*,*
34
+ 環境iso,4786,4786,5000,環境iso,名詞,固有名詞,一般,*,*,*,カンキョウアイエスオー,環境iso,*,*,*,*,*
35
+ ミルフィーユ,4786,4786,5000,ミルフィーユ,名詞,固有名詞,一般,*,*,*,ミルフィーユ,ミルフィーユ,*,*,*,*,*
36
+ くたびれもうけ,4786,4786,5000,くたびれもうけ,名詞,固有名詞,一般,*,*,*,クタビレモウケ,くたびれもうけ,*,*,*,*,*
37
+ 劇団個人主義,4786,4786,5000,劇団個人主義,名詞,固有名詞,一般,*,*,*,ゲキダンコジンシュギ,劇団個人主義,*,*,*,*,*
38
+ 劇団nonny,4786,4786,5000,劇団nonny,名詞,固有名詞,一般,*,*,*,ゲキダンノニー,劇団nonny,*,*,*,*,*
39
+ 志剣,4786,4786,5000,志剣,名詞,固有名詞,一般,*,*,*,シケン,志剣,*,*,*,*,*
40
+ 国教,4786,4786,5000,国教,名詞,固有名詞,一般,*,*,*,コッキョウ,国教,*,*,*,*,*
41
+ :drop,4786,4786,5000,:drop,名詞,固有名詞,一般,*,*,*,ドロップ,:drop,*,*,*,*,*
42
+ 小山ゼミ,4786,4786,5000,小山ゼミ,名詞,固有名詞,一般,*,*,*,コヤマゼミ,小山ゼミ,*,*,*,*,*
43
+ コンフローレ,4786,4786,5000,コンフローレ,名詞,固有名詞,一般,*,*,*,コンフローレ,コンフローレ,*,*,*,*,*
44
+ サイエンスプロムナード,4786,4786,5000,サイエンスプロムナード,名詞,固有名詞,一般,*,*,*,サイエンスプロムナード,サイエンスプロムナード,*,*,*,*,*
45
+ トニカ,4786,4786,5000,トニカ,名詞,固有名詞,一般,*,*,*,トニカ,トニカ,*,*,*,*,*
46
+ 紫千会,4786,4786,5000,紫千会,名詞,固有名詞,一般,*,*,*,シセンカイ,紫千会,*,*,*,*,*
47
+ シネマウント・フィルム・パーティー,4786,4786,5000,シネマウント・フィルム・パーティー,名詞,固有名詞,一般,*,*,*,シネマウントフィルムパーティー,シネマウント・フィルム・パーティー,*,*,*,*,*
48
+ 清水ゼミ,4786,4786,5000,清水ゼミ,名詞,固有名詞,一般,*,*,*,シミズゼミ,清水ゼミ,*,*,*,*,*
49
+ possum,4786,4786,5000,possum,名詞,固有名詞,一般,*,*,*,ポッサム,possum,*,*,*,*,*
50
+ 父の樹会,4786,4786,5000,父の樹会,名詞,固有名詞,一般,*,*,*,チチノキカイ,父の樹会,*,*,*,*,*
51
+ 小国,4786,4786,5000,小国,名詞,固有名詞,一般,*,*,*,ショウコク,小国,*,*,*,*,*
52
+ ぼくじる,4786,4786,5000,ぼくじる,名詞,固有名詞,一般,*,*,*,ボクジル,ぼくじる,*,*,*,*,*
53
+ ショコラ,4786,4786,5000,ショコラ,名詞,固有名詞,一般,*,*,*,ショコラ,ショコラ,*,*,*,*,*
54
+ dlc,4786,4786,5000,dlc,名詞,固有名詞,一般,*,*,*,ディーエルシー,dlc,*,*,*,*,*
55
+ yell,4786,4786,5000,yell,名詞,固有名詞,一般,*,*,*,エール,yell,*,*,*,*,*
56
+ belinda,4786,4786,5000,belinda,名詞,固有名詞,一般,*,*,*,ベリンダ,belinda,*,*,*,*,*
57
+ lips,4786,4786,5000,lips,名詞,固有名詞,一般,*,*,*,リップス,lips,*,*,*,*,*
58
+ あらぐさ,4786,4786,5000,あらぐさ,名詞,固有名詞,一般,*,*,*,アラグサ,あらぐさ,*,*,*,*,*
59
+ 地球科学科,4786,4786,5000,地球科学科,名詞,固有名詞,一般,*,*,*,チキュウカガクカ,地球科学科,*,*,*,*,*
60
+ ちのっち,4786,4786,5000,ちのっち,名詞,固有名詞,一般,*,*,*,チノッチ,ちのっち,*,*,*,*,*
61
+ 千葉大祭実行委員会,4786,4786,5000,千葉大祭実行委員会,名詞,固有名詞,一般,*,*,*,チバダイサイジッコウイインカイ,千葉大祭実行委員会,*,*,*,*,*
62
+ ちばねこ,4786,4786,5000,ちばねこ,名詞,固有名詞,一般,*,*,*,チバネコ,ちばねこ,*,*,*,*,*
63
+ ちばポケ,4786,4786,5000,ちばポケ,名詞,固有名詞,一般,*,*,*,チバポケ,ちばポケ,*,*,*,*,*
64
+ ccs,4786,4786,5000,ccs,名詞,固有名詞,一般,*,*,*,シーシーエス,ccs,*,*,*,*,*
65
+ rpurb,4786,4786,5000,rpurb,名詞,固有名詞,一般,*,*,*,ルパーブ,rpurb,*,*,*,*,*
66
+ breakers,4786,4786,5000,breakers,名詞,固有名詞,一般,*,*,*,ブレーカーズ,breakers,*,*,*,*,*
67
+ ショパンの会,4786,4786,5000,ショパンの会,名詞,固有名詞,一般,*,*,*,ショパンノカイ,ショパンの会,*,*,*,*,*
68
+ ひとりぼっちの音楽祭,4786,4786,5000,ひとりぼっちの音楽祭,名詞,固有名詞,一般,*,*,*,ヒトリボッチノオンガクサイ,ひとりぼっちの音楽祭,*,*,*,*,*
69
+ プロセカ,4786,4786,5000,プロセカ,名詞,固有名詞,一般,*,*,*,プロセカ,プロセカ,*,*,*,*,*
70
+ ミストラル,4786,4786,5000,ミストラル,名詞,固有名詞,一般,*,*,*,ミストラル,ミストラル,*,*,*,*,*
71
+ 麦,4786,4786,5000,麦,名詞,固有名詞,一般,*,*,*,ムギ,麦,*,*,*,*,*
72
+ 八木澤研究室,4786,4786,5000,八木澤研究室,名詞,固有名詞,一般,*,*,*,ヤギサワケンキュウシツ,八木澤研究室,*,*,*,*,*
73
+ 葉法会,4786,4786,5000,葉法会,名詞,固有名詞,一般,*,*,*,ヨウホウカイ,葉法会,*,*,*,*,*
74
+ bollon,4786,4786,5000,bollon,名詞,固有名詞,一般,*,*,*,ボロン,bollon,*,*,*,*,*
75
+ ワンダーフォーゲル,4786,4786,5000,ワンダーフォーゲル,名詞,固有名詞,一般,*,*,*,ワンダーフォーゲル,ワンダーフォーゲル,*,*,*,*,*
schemas/__init__.py ADDED
File without changes
schemas/projects.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, Literal
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class Project(BaseModel):
6
+ """団体から提供された企画情報を格納するデータクラス。
7
+ - `projectId`: 企画ID (unique)
8
+ - `organization`: 団体名
9
+ - `title`: 企画名
10
+ - `genre`: 企画ジャンル
11
+ - `day1`: 1日目の実施有無
12
+ - `day2`: 2日目の実施有無
13
+ - `day3`: 3日目の実施有無
14
+ - `place`: 実施場所
15
+ - `description`: 企画内容
16
+ - `prComment`: PRコメント
17
+ - `prCommentLong`: PRコメント(詳細ページ用)
18
+ - `note`: 注意事項
19
+ - `imageName`: PR画像ファイル名
20
+ - `prImageName1`: 任意PR画像ファイル名1
21
+ - `prImageName2`: 任意PR画像ファイル名2
22
+ - `prImageName3`: 任意PR画像ファイル名3
23
+ - `prImageName4`: 任意PR画像ファイル名4
24
+ - `prImageName5`: 任意PR画像ファイル名5
25
+ - `urlX`: URL (X)
26
+ - `urlInstagram`: URL (Instagram)
27
+ - `urlOfficialWebsite`: URL (公式サイト)
28
+ - `urlYoutube`: URL (YouTube)
29
+ - `urlOther`: URL (その他)
30
+ - `reading`: 団体名読み仮名
31
+ """
32
+
33
+ projectId: str
34
+ organization: str
35
+ title: str
36
+ genre: Literal["パフォーマンス", "飲食", "物販", "展示", "参加型", "音楽", "その他"]
37
+ day1: bool
38
+ day2: bool
39
+ day3: bool
40
+ place: str
41
+ description: str
42
+ prComment: str
43
+ prCommentLong: Optional[str]
44
+ note: Optional[str]
45
+ imageName: str
46
+ prImageName1: Optional[str]
47
+ prImageName2: Optional[str]
48
+ prImageName3: Optional[str]
49
+ prImageName4: Optional[str]
50
+ prImageName5: Optional[str]
51
+ urlX: Optional[str]
52
+ urlInstagram: Optional[str]
53
+ urlOfficialWebsite: Optional[str]
54
+ urlYoutube: Optional[str]
55
+ urlOther: Optional[str]
56
+ reading: Optional[str]
57
+
58
+
59
+ class ProjectSummary(BaseModel):
60
+ """/api/projectsのレスポンス用データクラス。企画一覧に表示されるデータ。
61
+ - `projectId`: 企画ID (unique)
62
+ - `organization`: 団体名
63
+ - `title`: 企画名
64
+ - `genre`: 企画ジャンル
65
+ - `day1`: 1日目の実施有無
66
+ - `day2`: 2日目の実施有無
67
+ - `day3`: 3日目の実施有無
68
+ - `place`: 実施場所
69
+ - `description`: 企画内容
70
+ - `prComment`: PRコメント
71
+ - `note`: 注意事項
72
+ - `imageName`: PR画像ファイル名
73
+ """
74
+
75
+ projectId: str
76
+ organization: str
77
+ title: str
78
+ genre: Literal["パフォーマンス", "飲食", "物販", "展示", "参加型", "音楽", "その他"]
79
+ day1: bool
80
+ day2: bool
81
+ day3: bool
82
+ place: str
83
+ description: str
84
+ prComment: str
85
+ note: Optional[str]
86
+ imageName: str
87
+
88
+
89
+ class ProjectDetail(BaseModel):
90
+ """/api/details?projectId=xxxのレスポンス用データクラス。企画詳細ページに表示されるデータ。
91
+ - `projectId`: 企画ID (unique)
92
+ - `organization`: 団体名
93
+ - `title`: 企画名
94
+ - `genre`: 企画ジャンル
95
+ - `day1`: 1日目の実施有無
96
+ - `day2`: 2日目の実施有無
97
+ - `day3`: 3日目の実施有無
98
+ - `place`: 実施場所
99
+ - `description`: 企画内容
100
+ - `prComment`: PRコメント
101
+ - `prCommentLong`: PRコメント(詳細ページ用)
102
+ - `note`: 注意事項
103
+ - `imageName`: PR画像ファイル名
104
+ - `prImageName1`: 任意PR画像ファイル名1
105
+ - `prImageName2`: 任意PR画像ファイル名2
106
+ - `prImageName3`: 任意PR画像ファイル名3
107
+ - `prImageName4`: 任意PR画像ファイル名4
108
+ - `prImageName5`: 任意PR画像ファイル名5
109
+ - `urlX`: URL (X)
110
+ - `urlInstagram`: URL (Instagram)
111
+ - `urlOfficialWebsite`: URL (公式サイト)
112
+ - `urlYoutube`: URL (YouTube)
113
+ - `urlOther`: URL (その他)
114
+ """
115
+
116
+ projectId: str
117
+ organization: str
118
+ title: str
119
+ genre: Literal["パフォーマンス", "飲食", "物販", "展示", "参加型", "音楽", "その他"]
120
+ day1: bool
121
+ day2: bool
122
+ day3: bool
123
+ place: str
124
+ description: str
125
+ prComment: str
126
+ prCommentLong: Optional[str]
127
+ note: Optional[str]
128
+ imageName: str
129
+ prImageName1: Optional[str]
130
+ prImageName2: Optional[str]
131
+ prImageName3: Optional[str]
132
+ prImageName4: Optional[str]
133
+ prImageName5: Optional[str]
134
+ urlX: Optional[str]
135
+ urlInstagram: Optional[str]
136
+ urlOfficialWebsite: Optional[str]
137
+ urlYoutube: Optional[str]
138
+ urlOther: Optional[str]
139
+
140
+
141
+ class ProjectIds(BaseModel):
142
+ """/api/searchのレスポンス用データクラス。
143
+ ## example:
144
+ ```
145
+ {
146
+ "projectIds": ["62000A", "62000B", "62000C"]
147
+ }
148
+ ```
149
+ """
150
+
151
+ projectIds: list[str]
schemas/tf_token.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class TfOfField(BaseModel):
5
+ len: int
6
+ tf: dict[str, int]
7
+
8
+
9
+ class Fields(BaseModel):
10
+ title: TfOfField
11
+ organization: TfOfField
12
+ reading: TfOfField
13
+ description: TfOfField
14
+ prComment: TfOfField
15
+ prCommentLong: TfOfField
16
+
17
+
18
+ class Project(BaseModel):
19
+ projectId: str
20
+ fields: Fields
21
+ tf: dict[str, int]
22
+ tokens: dict[str, int]
scripts/1_build_dict.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import subprocess
2
+ import json
3
+ import importlib.resources
4
+ import os
5
+ import sys
6
+
7
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
8
+
9
+ from utils.logger import setup_logger
10
+ from utils.json import get_file_path_from_config
11
+
12
+ # --- ロギングの設定 ---
13
+ log = setup_logger(__name__)
14
+
15
+
16
+ def main():
17
+ """
18
+ SudachiPyのユーザー辞書をビルドするPythonスクリプト。
19
+
20
+ Reference:
21
+ https://github.com/WorksApplications/SudachiPy/blob/develop/docs/tutorial.md
22
+ """
23
+
24
+ log.info("SudachiPyのユーザー辞書をビルドします。")
25
+
26
+ # 入力用のユーザー辞書CSVファイルのパス
27
+ user_dict_path = get_file_path_from_config(
28
+ "sudachi.user_dict", "resources/user_dict.csv"
29
+ )
30
+ # 出力のユーザー辞書のパス
31
+ output_path = get_file_path_from_config(
32
+ "sudachi.user_dict_generated", "data/generated/user.dic"
33
+ )
34
+
35
+ # sudachi.jsonから辞書の種類を取得
36
+ sudachi_config_path = get_file_path_from_config(
37
+ "sudachi.sudachi_config", "scripts/sudachi.json"
38
+ )
39
+ with open(sudachi_config_path, "r") as f:
40
+ try:
41
+ sudachi_dict = json.load(f)
42
+ sudachi_dict_size = sudachi_dict.get("systemDict", "full")
43
+ sudachi_dict_name = f"sudachidict_{sudachi_dict_size}"
44
+ except FileNotFoundError:
45
+ log.error(f"sudachi.jsonが見つかりません: {sudachi_config_path}")
46
+ sys.exit(1)
47
+
48
+ # sudachidict_<size>パッケージ内のシステム辞書のパスを取得
49
+ try:
50
+ system_dict_ref = importlib.resources.files(sudachi_dict_name).joinpath(
51
+ "resources", "system.dic"
52
+ )
53
+ log.info(f"システム辞書のパス: {system_dict_ref}")
54
+ except ModuleNotFoundError:
55
+ log.error(f"{sudachi_dict_name}パッケージが見つかりません。")
56
+ log.error(f"`pip install {sudachi_dict_name}`を試してください。")
57
+ sys.exit(1)
58
+ except Exception as e:
59
+ log.error(f"予期しないエラーが発生しました: {e}")
60
+ sys.exit(1)
61
+
62
+ # 出力ディレクトリが存在しない場合は作成
63
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
64
+
65
+ with importlib.resources.as_file(system_dict_ref) as system_dict_path:
66
+ command = [
67
+ "sudachipy",
68
+ "ubuild",
69
+ "-o",
70
+ output_path, # 出力ファイルのパス
71
+ "-s",
72
+ str(system_dict_path), # システム辞書のパス
73
+ user_dict_path,
74
+ ]
75
+
76
+ log.info("辞書をビルドします...")
77
+ log.info(f"コマンド: {' '.join(command)}")
78
+
79
+ try:
80
+ # コマンドを実行
81
+ result = subprocess.run(
82
+ command,
83
+ check=True,
84
+ capture_output=True,
85
+ text=True,
86
+ encoding="utf-8",
87
+ )
88
+ log.info("ビルドが正常に完了しました。")
89
+ log.info(f"出力先: {output_path}")
90
+
91
+ if result.stdout:
92
+ log.info(f"--- SudachiPyからのメッセージ ---\n{result.stdout}")
93
+
94
+ except FileNotFoundError:
95
+ log.error("sudachipyコマンドが見つかりません。")
96
+ log.error("SudachiPyがインストールされていることを確認してください。")
97
+ sys.exit(1)
98
+ except subprocess.CalledProcessError as e:
99
+ log.error("辞書のビルド中にエラーが発生しました。")
100
+ log.error(f"エラーコード: {e.returncode}")
101
+ if e.stderr:
102
+ log.error(f"--- 標準エラー出力 ---\n{e.stderr}")
103
+ sys.exit(1)
104
+ except Exception as e:
105
+ log.error(f"予期しないエラーが発生しました: {e}")
106
+ sys.exit(1)
107
+ sys.exit(0)
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
scripts/2_create_projects_data.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import pandas as pd
4
+
5
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
6
+
7
+ from utils.logger import setup_logger
8
+ from utils.json import get_file_path_from_config
9
+ from utils.text_process import remove_invisible_chars
10
+ from utils.json import json_dumps
11
+ import schemas.projects as projects_schema
12
+
13
+ # --- ロギングの設定 ---
14
+ log = setup_logger(__name__)
15
+
16
+
17
+ def main():
18
+ """
19
+ CSVファイルを読み込み、Projectオブジェクトのリストを生成し、
20
+ project.jsonに書き出すスクリプト
21
+ """
22
+
23
+ log.info("企画データの生成を開始します。")
24
+ original_csv_path = get_file_path_from_config(
25
+ "projects.original_csv", "resources/original_projects.csv"
26
+ )
27
+ output_json_path = get_file_path_from_config(
28
+ "projects.output_json", "data/generated/projects.json"
29
+ )
30
+
31
+ # CSVファイルの読み込み
32
+ try:
33
+ df = pd.read_csv(original_csv_path, encoding="utf-8")
34
+ except FileNotFoundError:
35
+ log.error(f"CSVファイルが見つかりません: {original_csv_path}")
36
+ sys.exit(1)
37
+ except pd.errors.ParserError as e:
38
+ log.error(f"CSVファイルの解析中にエラーが発生しました: {e}")
39
+ sys.exit(1)
40
+ except Exception as e:
41
+ log.error(f"予期しないエラーが発生しました: {e}")
42
+ sys.exit(1)
43
+
44
+ # テキスト列に対して不要な文字の除去を行う
45
+ for col in df.select_dtypes(include=["object"]).columns:
46
+ df[col] = df[col].apply(
47
+ lambda x: remove_invisible_chars(x) if isinstance(x, str) else x
48
+ )
49
+
50
+ # 一旦全列 object にしてから欠損値を None に変換
51
+ df = df.astype("object").where(pd.notnull(df), None)
52
+
53
+ # Projectオブジェクトに変換
54
+ # 列名がキーと一致している前提
55
+ projects: list[projects_schema.Project] = [
56
+ projects_schema.Project(**row) for row in df.to_dict(orient="records")
57
+ ]
58
+
59
+ # JSON に書き出し
60
+ json_dumps([item.model_dump() for item in projects], output_json_path)
61
+
62
+ log.info(f"企画データの生成が完了しました: {output_json_path}")
63
+ sys.exit(0)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
scripts/3_build_synonyms_from_sudachi.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ from collections import defaultdict
5
+ from tqdm import tqdm
6
+ import pandas as pd
7
+
8
+ from sudachipy import dictionary, tokenizer
9
+
10
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
11
+
12
+ from utils.logger import setup_logger
13
+ from utils.json import get_file_path_from_config, field_getter, json_dumps
14
+ from utils.io import LineIteratorIO, comment_filtered_lines
15
+ import schemas.projects as projects_schema
16
+
17
+ # --- ロギングの設定 ---
18
+ log = setup_logger(__name__)
19
+
20
+ # --- 設定 ---
21
+ input_file = get_file_path_from_config("projects.projects_json")
22
+ output_file = get_file_path_from_config("sudachi.synonyms_cache")
23
+ sudachi_config_file = get_file_path_from_config("sudachi.sudachi_config")
24
+
25
+ try:
26
+ search_model = field_getter("config/search_model.json")
27
+ target_pos_l1 = search_model("target_pos_l1")
28
+ target_fields = search_model("target_fields")
29
+ ban_list = search_model("synonyms.banlist")
30
+ stopwords_file = get_file_path_from_config("sudachi.stopwords")
31
+ with open(stopwords_file, encoding="utf-8") as f:
32
+ stopwords = set(json.load(f))
33
+ synonyms_file = get_file_path_from_config("sudachi.synonyms")
34
+ except FileNotFoundError as e:
35
+ log.error(f"設定ファイルが見つかりません: {e}")
36
+ sys.exit(1)
37
+ except KeyError as e:
38
+ log.error(f"設定ファイルに必要なキーが見つかりません: {e}")
39
+ sys.exit(1)
40
+ except Exception as e:
41
+ log.error(f"予期しないエラーが発生しました: {e}")
42
+ sys.exit(1)
43
+
44
+
45
+ # --- SudachiPy初期化 ---
46
+ tokenizer_obj = dictionary.Dictionary(config_path=sudachi_config_file).create()
47
+ mode = tokenizer.Tokenizer.SplitMode.A
48
+
49
+
50
+ def tokenize(text: str) -> list[str]:
51
+ """テキストをトークン化し、ストップワードを除去する。"""
52
+ if not text:
53
+ return []
54
+ tokens = []
55
+ for m in tokenizer_obj.tokenize(text, mode):
56
+ if m.normalized_form().strip() == "":
57
+ continue
58
+ pos = m.part_of_speech()
59
+ if pos[0] not in target_pos_l1:
60
+ continue
61
+ base = m.normalized_form().lower()
62
+ if base in stopwords or base in ban_list:
63
+ continue
64
+ tokens.append(base)
65
+ return tokens
66
+
67
+
68
+ def get_corpus_vocab(projects: list[projects_schema.Project]) -> set[str]:
69
+ """プロジェクト全体から語彙セットを構築する"""
70
+ vocab = set()
71
+ log.info("語彙セットを構築中...")
72
+ for project in tqdm(projects):
73
+ for field in target_fields:
74
+ text = getattr(project, field, "")
75
+ if not text:
76
+ continue
77
+ tokens = tokenize(str(text))
78
+ vocab.update(tokens)
79
+ return vocab
80
+
81
+
82
+ def find_and_parse_synonyms_text() -> dict[str, list[str]]:
83
+ """同義語辞書をpandasを使ってパースする"""
84
+ log.info("同義語辞書をパース中...")
85
+ log.info(f"同義語辞書のパス: {synonyms_file}")
86
+
87
+ try:
88
+ # ヘッダーなしのCSVとして読み込む
89
+ # 必要なのはグループID(0)と見出し語(8)
90
+ GROUP_ID_COL_IDX = 0
91
+ TERM_COL_IDX = 8
92
+
93
+ stream = LineIteratorIO(comment_filtered_lines(synonyms_file))
94
+ df = pd.read_csv(
95
+ stream,
96
+ header=None,
97
+ usecols=[GROUP_ID_COL_IDX, TERM_COL_IDX],
98
+ names=["group_id", "term"],
99
+ on_bad_lines="skip",
100
+ encoding="utf-8",
101
+ engine="c",
102
+ sep=",",
103
+ )
104
+
105
+ except Exception as e:
106
+ log.error(f"同義語辞書のパース中にエラーが発生しました: {e}")
107
+ raise e
108
+
109
+ synonym_groups = defaultdict(list)
110
+ # group_idでグループ化し、各グループ内の単語リストを作成
111
+ for group_id, group_df in df.groupby("group_id"):
112
+ terms = group_df["term"].tolist()
113
+ if len(terms) < 2:
114
+ continue
115
+ for i, term in enumerate(terms):
116
+ synonyms = [t for j, t in enumerate(terms) if i != j]
117
+ synonym_groups[term].extend(synonyms)
118
+
119
+ return synonym_groups
120
+
121
+
122
+ def main():
123
+ log.info("同義語キャッシュの生成を開始します")
124
+
125
+ with open(input_file, encoding="utf-8") as f:
126
+ try:
127
+ project_dicts = json.load(f)
128
+ projects = [projects_schema.Project(**item) for item in project_dicts]
129
+ except json.JSONDecodeError as e:
130
+ log.error(f"JSONデコードエラー: {e}")
131
+ sys.exit(1)
132
+ except FileNotFoundError:
133
+ log.error(f"ファイルが見つかりません: {input_file}")
134
+ sys.exit(1)
135
+ except Exception as e:
136
+ log.error(f"予期しないエラーが発生しました: {e}")
137
+ sys.exit(1)
138
+
139
+ # 1. コーパスの語彙を構築
140
+ vocab = get_corpus_vocab(projects)
141
+ log.info(f"コーパスの語彙数: {len(vocab)}")
142
+
143
+ # 2. Sudachiの同義語辞書をパース
144
+ try:
145
+ synonym_groups = find_and_parse_synonyms_text()
146
+ except Exception as e:
147
+ log.error(e)
148
+ sys.exit(1)
149
+
150
+ # 3. コーパスに存在する単語に絞ってキャッシュを作成
151
+ synonym_cache = {}
152
+ log.info("同義語キャッシュを構築中...")
153
+ for term in tqdm(vocab):
154
+ if term in ban_list:
155
+ continue
156
+ if term in synonym_groups:
157
+ valid_synonyms = [
158
+ s for s in synonym_groups[term] if s in vocab and s not in ban_list
159
+ ]
160
+ if valid_synonyms:
161
+ synonym_cache[term] = list(set(valid_synonyms)) # 重複除去
162
+
163
+ # 4. キャッシュを保存
164
+ json_dumps(synonym_cache, output_file)
165
+
166
+ log.info(f"同義語キャッシュの生成が完了しました: {output_file}")
167
+ log.info(f"キャッシュされた単語数: {len(synonym_cache)}")
168
+
169
+
170
+ if __name__ == "__main__":
171
+ main()
scripts/4_prepare_bm25f_meta.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ from collections import defaultdict
5
+ from typing import Dict, List
6
+
7
+ from sudachipy import dictionary, tokenizer
8
+
9
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
10
+
11
+ from utils.logger import setup_logger
12
+ from utils.json import get_file_path_from_config, field_getter, json_dumps
13
+
14
+ log = setup_logger(__name__)
15
+
16
+
17
+ def load_configs():
18
+ search = field_getter("config/search_model.json")
19
+ files = field_getter("config/files.json")
20
+
21
+ target_pos_l1: List[str] = search("target_pos_l1")
22
+ target_fields: List[str] = search("target_fields")
23
+ ban_list: List[str] = search("synonyms.banlist")
24
+
25
+ stopwords_path = files("sudachi.stopwords")
26
+ with open(stopwords_path, encoding="utf-8") as f:
27
+ stopwords = set(json.load(f))
28
+
29
+ sudachi_config_path = files("sudachi.sudachi_config")
30
+
31
+ return target_pos_l1, target_fields, set(ban_list), stopwords, sudachi_config_path
32
+
33
+
34
+ def build_tokenizer(sudachi_config_path: str):
35
+ tok = dictionary.Dictionary(config_path=sudachi_config_path).create()
36
+ mode = tokenizer.Tokenizer.SplitMode.A
37
+ return tok, mode
38
+
39
+
40
+ def tokenize(text: str, tok, mode, target_pos_l1: List[str], ban_list: set, stopwords: set) -> List[str]:
41
+ if not text:
42
+ return []
43
+ out: List[str] = []
44
+ for m in tok.tokenize(text, mode):
45
+ base = m.normalized_form().lower().strip()
46
+ if not base:
47
+ continue
48
+ pos = m.part_of_speech()
49
+ if pos[0] not in target_pos_l1:
50
+ continue
51
+ if base in stopwords or base in ban_list:
52
+ continue
53
+ out.append(base)
54
+ return out
55
+
56
+
57
+ def main():
58
+ log.info("BM25Fメタデータ(bm25_meta.json)を生成します")
59
+ try:
60
+ target_pos_l1, target_fields, ban_list, stopwords, sudachi_config_path = load_configs()
61
+ except Exception as e:
62
+ log.error(f"設定の読み込みに失敗しました: {e}")
63
+ sys.exit(1)
64
+
65
+ projects_path = get_file_path_from_config("projects.projects_json", "data/generated/projects.json")
66
+ output_path = get_file_path_from_config("bm25.bm25_meta", "data/generated/bm25_meta.json")
67
+
68
+ try:
69
+ with open(projects_path, encoding="utf-8") as f:
70
+ projects = json.load(f)
71
+ except Exception as e:
72
+ log.error(f"projects.jsonの読み込みに失敗しました: {e}")
73
+ sys.exit(1)
74
+
75
+ tok, mode = build_tokenizer(sudachi_config_path)
76
+
77
+ N = len(projects)
78
+ df: Dict[str, int] = defaultdict(int)
79
+ field_token_lens_sum: Dict[str, int] = {f: 0 for f in target_fields}
80
+
81
+ log.info(f"ドキュメント数: {N}")
82
+
83
+ for p in projects:
84
+ seen_in_doc = set()
85
+ for field in target_fields:
86
+ text = p.get(field) or ""
87
+ toks = tokenize(str(text), tok, mode, target_pos_l1, ban_list, stopwords)
88
+ field_token_lens_sum[field] += len(toks)
89
+ for t in set(toks):
90
+ if t not in seen_in_doc:
91
+ df[t] += 1
92
+ seen_in_doc.add(t)
93
+
94
+ # IDF 計算(BM25で一般的な +0.5 smoothing と +1 オフセット)
95
+ idf: Dict[str, float] = {}
96
+ for term, dfi in df.items():
97
+ idf_val = max(0.0, ( ( (N - dfi + 0.5) / (dfi + 0.5) ) ))
98
+ # 数値安定化のためlog1p
99
+ import math
100
+
101
+ idf[term] = math.log1p(idf_val)
102
+
103
+ avg_len = {field: (field_token_lens_sum[field] / N if N > 0 else 0.0) for field in target_fields}
104
+
105
+ meta = {
106
+ "N": N,
107
+ "avg_len": avg_len,
108
+ "idf": idf,
109
+ }
110
+
111
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
112
+ json_dumps(meta, output_path)
113
+ log.info(f"bm25_meta.json を出力しました: {output_path}")
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()
scripts/5_prepare_tf_token.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ from collections import Counter
5
+ from typing import Dict, List
6
+
7
+ from sudachipy import dictionary, tokenizer
8
+
9
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
10
+
11
+ from utils.logger import setup_logger
12
+ from utils.json import get_file_path_from_config, field_getter, json_dumps
13
+ import schemas.tf_token as tf_schema
14
+
15
+ log = setup_logger(__name__)
16
+
17
+
18
+ def load_configs():
19
+ search = field_getter("config/search_model.json")
20
+ files = field_getter("config/files.json")
21
+
22
+ target_pos_l1: List[str] = search("target_pos_l1")
23
+ target_fields: List[str] = search("target_fields")
24
+ ban_list: List[str] = search("synonyms.banlist")
25
+
26
+ stopwords_path = files("sudachi.stopwords")
27
+ with open(stopwords_path, encoding="utf-8") as f:
28
+ stopwords = set(json.load(f))
29
+
30
+ sudachi_config_path = files("sudachi.sudachi_config")
31
+
32
+ return target_pos_l1, target_fields, set(ban_list), stopwords, sudachi_config_path
33
+
34
+
35
+ def build_tokenizer(sudachi_config_path: str):
36
+ tok = dictionary.Dictionary(config_path=sudachi_config_path).create()
37
+ mode = tokenizer.Tokenizer.SplitMode.A
38
+ return tok, mode
39
+
40
+
41
+ def tokenize(text: str, tok, mode, target_pos_l1: List[str], ban_list: set, stopwords: set) -> List[str]:
42
+ if not text:
43
+ return []
44
+ out: List[str] = []
45
+ for m in tok.tokenize(text, mode):
46
+ base = m.normalized_form().lower().strip()
47
+ if not base:
48
+ continue
49
+ pos = m.part_of_speech()
50
+ if pos[0] not in target_pos_l1:
51
+ continue
52
+ if base in stopwords or base in ban_list:
53
+ continue
54
+ out.append(base)
55
+ return out
56
+
57
+
58
+ def main():
59
+ log.info("フィールド別TF/トークン(tf_token.json)を生成します")
60
+ try:
61
+ target_pos_l1, target_fields, ban_list, stopwords, sudachi_config_path = load_configs()
62
+ except Exception as e:
63
+ log.error(f"設定の読み込みに失敗しました: {e}")
64
+ sys.exit(1)
65
+
66
+ projects_path = get_file_path_from_config("projects.projects_json", "data/generated/projects.json")
67
+ output_path = get_file_path_from_config("bm25.tf_token", "data/generated/tf_token.json")
68
+
69
+ try:
70
+ with open(projects_path, encoding="utf-8") as f:
71
+ projects = json.load(f)
72
+ except Exception as e:
73
+ log.error(f"projects.jsonの読み込みに失敗しました: {e}")
74
+ sys.exit(1)
75
+
76
+ tok, mode = build_tokenizer(sudachi_config_path)
77
+
78
+ results: List[tf_schema.Project] = []
79
+
80
+ for p in projects:
81
+ project_id = p.get("projectId")
82
+
83
+ # 各フィールドのトークン化とTF
84
+ field_objs: Dict[str, tf_schema.TfOfField] = {}
85
+ doc_tf_counter: Counter = Counter()
86
+ doc_token_set: set = set()
87
+
88
+ for field in target_fields:
89
+ text = p.get(field) or ""
90
+ toks = tokenize(str(text), tok, mode, target_pos_l1, ban_list, stopwords)
91
+ tf = Counter(toks)
92
+ field_objs[field] = tf_schema.TfOfField(len=len(toks), tf=dict(tf))
93
+ doc_tf_counter.update(tf)
94
+ doc_token_set.update(tf.keys())
95
+
96
+ # スキーマ Fields へ詰める(未定義フィールドは長さ0/空dictで埋める)
97
+ def get_field(name: str) -> tf_schema.TfOfField:
98
+ return field_objs.get(name, tf_schema.TfOfField(len=0, tf={}))
99
+
100
+ fields_obj = tf_schema.Fields(
101
+ title=get_field("title"),
102
+ organization=get_field("organization"),
103
+ reading=get_field("reading"),
104
+ description=get_field("description"),
105
+ prComment=get_field("prComment"),
106
+ prCommentLong=get_field("prCommentLong"),
107
+ )
108
+
109
+ project_entry = tf_schema.Project(
110
+ projectId=project_id,
111
+ fields=fields_obj,
112
+ tf=dict(doc_tf_counter),
113
+ # tokens はユニーク語彙の存在フラグ(1)とする
114
+ tokens={t: 1 for t in sorted(doc_token_set)},
115
+ )
116
+ results.append(project_entry)
117
+
118
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
119
+ json_dumps([r.model_dump() for r in results], output_path)
120
+ log.info(f"tf_token.json を出力しました: {output_path}")
121
+
122
+
123
+ if __name__ == "__main__":
124
+ main()
scripts/6_build_word_embeddings.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ from typing import Dict, List, Tuple
5
+
6
+ import numpy as np
7
+
8
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
9
+
10
+ from utils.logger import setup_logger
11
+ from utils.json import field_getter, json_dumps
12
+
13
+ log = setup_logger(__name__)
14
+
15
+
16
+ def load_configs():
17
+ files = field_getter("config/files.json")
18
+
19
+ paths = {
20
+ "tf_token": files("bm25.tf_token"),
21
+ "fasttext_vec": files("embeddings.fasttext_vec"),
22
+ "word_vocab": files("embeddings.word_vocab"),
23
+ "word_vectors": files("embeddings.word_vectors"),
24
+ }
25
+ return paths
26
+
27
+
28
+ def read_vocab_from_tf_token(tf_token_path: str) -> Tuple[List[dict], set[str]]:
29
+ with open(tf_token_path, encoding="utf-8") as f:
30
+ docs = json.load(f)
31
+ vocab: set[str] = set()
32
+ for d in docs:
33
+ # doc全体tfから語彙を得る
34
+ for t in (d.get("tf") or {}).keys():
35
+ vocab.add(t)
36
+ return docs, vocab
37
+
38
+
39
+ def stream_fasttext_vec(
40
+ vec_path: str, vocab: set[str]
41
+ ) -> Tuple[Dict[str, int], np.ndarray]:
42
+ """fastText .vec から、必要語彙のみ抽出してベクトル行列を返す。"""
43
+ token_to_idx: Dict[str, int] = {}
44
+ vectors: List[np.ndarray] = []
45
+
46
+ dim = None
47
+ kept = 0
48
+ with open(vec_path, encoding="utf-8", errors="ignore") as f:
49
+ header = f.readline()
50
+ # ヘッダ行は "<count> <dim>" のことが多い
51
+ try:
52
+ parts = header.strip().split()
53
+ if len(parts) >= 2 and parts[0].isdigit():
54
+ dim = int(parts[1])
55
+ except Exception:
56
+ pass
57
+
58
+ for line in f:
59
+ sp = line.rstrip().split(" ")
60
+ if len(sp) < 2:
61
+ continue
62
+ token = sp[0]
63
+ if token not in vocab:
64
+ continue
65
+ vec_vals = sp[1:]
66
+ if dim is None:
67
+ dim = len(vec_vals)
68
+ if len(vec_vals) != dim:
69
+ continue
70
+ try:
71
+ v = np.asarray([float(x) for x in vec_vals], dtype=np.float32)
72
+ except ValueError:
73
+ continue
74
+ # L2正規化
75
+ norm = np.linalg.norm(v)
76
+ if norm > 0:
77
+ v = v / norm
78
+ token_to_idx[token] = kept
79
+ vectors.append(v)
80
+ kept += 1
81
+
82
+ if dim is None:
83
+ raise RuntimeError(".vec の次元を特定できませんでした")
84
+
85
+ if not vectors:
86
+ log.warning("語彙に一致するベクトルが見つかりませんでした")
87
+ arr = np.zeros((0, dim), dtype=np.float32)
88
+ return token_to_idx, arr
89
+
90
+ arr = np.vstack(vectors).astype(np.float32)
91
+ return token_to_idx, arr
92
+
93
+
94
+ # top-k モードでは文書ベクトルは不要
95
+
96
+
97
+ def main():
98
+ log.info(
99
+ "語彙/文書ベクトル(word_vocab.json, word_vectors.npz, doc_vectors.npy)を生成します"
100
+ )
101
+ try:
102
+ paths = load_configs()
103
+ except Exception as e:
104
+ log.error(f"設定の読み込みに失敗しました: {e}")
105
+ sys.exit(1)
106
+
107
+ tf_token_path = paths["tf_token"]
108
+ fasttext_vec_path = paths["fasttext_vec"]
109
+
110
+ if not os.path.exists(fasttext_vec_path):
111
+ log.warning(f".vec が見つかりません: {fasttext_vec_path}")
112
+ log.warning("Step 6 をスキップします")
113
+ sys.exit(0)
114
+
115
+ try:
116
+ docs, vocab = read_vocab_from_tf_token(tf_token_path)
117
+ except Exception as e:
118
+ log.error(f"tf_token.jsonの読み込みに失敗しました: {e}")
119
+ sys.exit(1)
120
+
121
+ log.info(f"コーパス語彙数: {len(vocab)}")
122
+ token_to_idx, word_vecs = stream_fasttext_vec(fasttext_vec_path, vocab)
123
+ log.info(f"抽出済み語彙ベクトル数: {word_vecs.shape[0]}")
124
+
125
+ # 語彙インデックスの安定化(token_to_idxは追加順次第なのでソート)
126
+ sorted_tokens = sorted(token_to_idx.keys())
127
+ remap = {t: i for i, t in enumerate(sorted_tokens)}
128
+ remapped_vecs = np.zeros_like(word_vecs)
129
+ for t, old_i in token_to_idx.items():
130
+ new_i = remap[t]
131
+ remapped_vecs[new_i] = word_vecs[old_i]
132
+ token_to_idx = remap
133
+ word_vecs = remapped_vecs
134
+
135
+ # 出力
136
+ os.makedirs(os.path.dirname(paths["word_vocab"]), exist_ok=True)
137
+ json_dumps(token_to_idx, paths["word_vocab"]) # 語→index
138
+ # 圧縮npz
139
+ np.savez_compressed(paths["word_vectors"], vectors=word_vecs)
140
+
141
+ log.info(f"word_vocab.json: {paths['word_vocab']}")
142
+ log.info(f"word_vectors.npz: {paths['word_vectors']}")
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
scripts/build_all.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import subprocess
4
+
5
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
6
+
7
+ from utils.logger import setup_logger
8
+ from utils.json import field_getter
9
+
10
+ log = setup_logger(__name__)
11
+
12
+
13
+ def run_step(cmd: list[str], allow_fail: bool = False):
14
+ log.info("==> %s", " ".join(cmd))
15
+ try:
16
+ subprocess.run(cmd, check=True)
17
+ except subprocess.CalledProcessError as e:
18
+ if allow_fail:
19
+ log.warning(f"step失敗を無視します: {cmd} (code={e.returncode})")
20
+ else:
21
+ raise
22
+
23
+
24
+ def main():
25
+ files = field_getter("config/files.json")
26
+
27
+ # Step 1: Sudachi user dict (optional but recommended before tokenization)
28
+ run_step([sys.executable, "scripts/1_build_dict.py"], allow_fail=True)
29
+
30
+ # Step 2: projects.json
31
+ run_step([sys.executable, "scripts/2_create_projects_data.py"])
32
+
33
+ # Step 3: synonyms cache
34
+ run_step([sys.executable, "scripts/3_build_synonyms_from_sudachi.py"]) # idempotent
35
+
36
+ # Step 4: bm25 meta
37
+ run_step([sys.executable, "scripts/4_prepare_bm25f_meta.py"]) # needs projects.json
38
+
39
+ # Step 5: tf_token
40
+ run_step([sys.executable, "scripts/5_prepare_tf_token.py"]) # needs projects.json
41
+
42
+ # Step 6: embeddings (.vec がある場合のみ)
43
+ vec_path = files("embeddings.fasttext_vec")
44
+ if os.path.exists(vec_path):
45
+ run_step([sys.executable, "scripts/6_build_word_embeddings.py"]) # needs tf_token, bm25_meta
46
+ else:
47
+ log.info(".vec が見つからないため Step 6 をスキップします: %s", vec_path)
48
+
49
+ log.info("全ステップ完了")
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
scripts/sudachi.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "systemDict": "full",
3
+ "userDict": [
4
+ "data/generated/user.dic"
5
+ ]
6
+ }
utils/__init__.py ADDED
File without changes
utils/io.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+
3
+
4
+ def comment_filtered_lines(path: str):
5
+ with open(path, encoding="utf-8") as f:
6
+ for line in f:
7
+ if not line.lstrip().startswith("#"):
8
+ yield line
9
+
10
+
11
+ class LineIteratorIO(io.TextIOBase):
12
+ def __init__(self, iterator):
13
+ self._it = iter(iterator)
14
+ self._buf = ""
15
+
16
+ def readable(self):
17
+ return True
18
+
19
+ def read(self, size=-1):
20
+ # size<0 のときはEOFまで貯めて返す
21
+ if size is None or size < 0:
22
+ try:
23
+ for chunk in self._it:
24
+ self._buf += chunk
25
+ except StopIteration:
26
+ pass
27
+ out, self._buf = self._buf, ""
28
+ return out
29
+
30
+ # size 指定ありのときは、必要分だけバッファを満たす
31
+ while len(self._buf) < size:
32
+ try:
33
+ self._buf += next(self._it)
34
+ except StopIteration:
35
+ break
36
+
37
+ out, self._buf = self._buf[:size], self._buf[size:]
38
+ return out
utils/json.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ FILE_CONFIG_PATH = "config/files.json"
6
+
7
+
8
+ def _get_nested(d: dict[str, Any], keys: list[str]) -> Any:
9
+ cur: Any = d
10
+ for k in keys:
11
+ if not isinstance(cur, dict) or k not in cur:
12
+ raise KeyError(k)
13
+ cur = cur[k]
14
+ return cur
15
+
16
+
17
+ def _get_field_from_json(
18
+ path: str | Path, key: str, default: str | None = None, sep: str = "."
19
+ ) -> Any:
20
+ """
21
+ JSONファイルからフィールドを取得する。
22
+ 階層構造がある場合は`sep`で区切って指定する。
23
+
24
+ example: `get_file_path_from_config("projects.original_csv")`
25
+ """
26
+ with open(path, "r", encoding="utf-8") as f:
27
+ try:
28
+ config: dict[str, Any] = json.load(f)
29
+ except json.JSONDecodeError as e:
30
+ raise ValueError(f"JSONデコードエラー: {e}")
31
+ except FileNotFoundError:
32
+ raise ValueError(f"ファイルが見つかりません: {path}")
33
+
34
+ keys = key.split(sep) if sep in key else [key]
35
+
36
+ try:
37
+ value = _get_nested(config, keys)
38
+ except KeyError:
39
+ if default is not None:
40
+ return default
41
+ raise KeyError(f"Key '{key}' not found in {path}")
42
+
43
+ return value
44
+
45
+
46
+ def get_file_path_from_config(
47
+ key: str, default: str | None = None, sep: str = "."
48
+ ) -> str:
49
+ """
50
+ 設定ファイルからファイルのパスを取得する。
51
+ 階層構造がある場合は`sep`で区切って指定する。
52
+
53
+ example: `get_file_path_from_config("projects.original_csv")`
54
+ """
55
+ with open(FILE_CONFIG_PATH, "r", encoding="utf-8") as f:
56
+ config: dict[str, Any] = json.load(f)
57
+
58
+ keys = key.split(sep) if sep in key else [key]
59
+
60
+ try:
61
+ value = _get_nested(config, keys)
62
+ except KeyError:
63
+ if default is not None:
64
+ return default
65
+ raise KeyError(f"Key '{key}' not found in {FILE_CONFIG_PATH}")
66
+
67
+ if isinstance(value, str):
68
+ return value
69
+
70
+ if default is not None:
71
+ return default
72
+ raise TypeError(f"Value at '{key}' is not a string: {type(value).__name__}")
73
+
74
+
75
+ def field_getter(path: str | Path):
76
+ def getter(key: str, default: str | None = None, sep: str = ".") -> Any:
77
+ return _get_field_from_json(path, key, default, sep)
78
+
79
+ return getter
80
+
81
+
82
+ def _write_atomic(path: Path, data: str) -> None:
83
+ temp = path.with_suffix(path.suffix + ".tmp")
84
+ temp.write_bytes(data)
85
+ temp.replace(path)
86
+
87
+
88
+ def json_dumps(obj: Any, path: str | Path) -> None:
89
+ """
90
+ オブジェクトをJSON形式のファイルに書き出す。
91
+ """
92
+ json_bytes = json.dumps(obj, ensure_ascii=False, indent=2).encode("utf-8")
93
+ _write_atomic(Path(path), json_bytes)
utils/logger.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+
4
+
5
+ def setup_logger(name: str = __name__) -> logging.Logger:
6
+ """ロガーを設定して返す"""
7
+ logging.basicConfig(level="INFO", format="%(asctime)s [%(levelname)s] %(message)s")
8
+ log = logging.getLogger(name)
9
+ log.setLevel(os.getenv("LOG_LEVEL", "INFO").upper())
10
+ return log
utils/text_process.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ # U+202a (LRE) やその他の目に見えない制御文字を除去するための正規表現
4
+ # ref: https://www.compart.com/en/unicode/category/Cf
5
+ INVISIBLE_CHARS_PATTERN = re.compile(r"[\u200b-\u200c\u200e-\u200f\u202a-\u202e\ufeff]")
6
+
7
+
8
+ def remove_invisible_chars(text: str) -> str:
9
+ """
10
+ 文字列から目に見えない制御文字やフォーマット文字を除去する。
11
+ """
12
+ if not isinstance(text, str):
13
+ return text
14
+ return INVISIBLE_CHARS_PATTERN.sub("", text)