ktsn-ud commited on
Commit
f446d43
·
1 Parent(s): 20bc01d

codex生成: 4以降のscript全て

Browse files
config/files.json CHANGED
@@ -10,5 +10,16 @@
10
  "projects": {
11
  "original_csv": "resources/test_data.csv",
12
  "projects_json": "data/generated/projects.json"
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
- }
 
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 CHANGED
@@ -1,30 +1,56 @@
1
  {
2
- "target_pos_l1": [
3
- "名詞", "動詞"
4
- ],
5
- "target_fields": [
6
- "title",
7
- "organization",
8
- "description",
9
- "prComment",
10
- "prCommentLong",
11
- "reading"
12
- ],
13
- "synonyms": {
14
- "enable": true,
15
- "sources": {
16
- "sudachi": true,
17
- "custom_json": "resources/synonyms_custom.json"
18
- },
19
- "limits": {
20
- "max_expansions_per_term": 4,
21
- "max_query_variants": 5,
22
- "min_char_len": 2
23
- },
24
- "banlist": [
25
- "",
26
- "会",
27
- "サークル"
28
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
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": false,
47
+ "mode": "soft",
48
+ "alpha": 0.7
49
+ },
50
+ "query_subword": {
51
+ "enable": true,
52
+ "path": "resources/embeddings/cc.ja.300.bin",
53
+ "oov_weight": 0.8,
54
+ "cache_size": 50000
55
+ }
56
+ }
docs/files.md CHANGED
@@ -2,9 +2,16 @@
2
  - `cc.ja.300.bin`: OOV(Out of Vocabrary)時に使用 更新不要
3
  - `generated/`: 毎年更新
4
  - `projects.json`: API返却用データ
 
5
  - `bm25_meta.json`: BM25Fメタデータ
 
6
  - `tf_token.json`: フィールド別TF/トークン
 
7
  - `synonyms_cache.json`: Sudachi同義語キャッシュ
 
8
  - `word_vocab.json`
 
9
  - `word_vectors.npz`
 
10
  - `doc_vectors.npy`
 
 
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`で生成
16
  - `doc_vectors.npy`
17
+ - `scripts/6_build_word_embeddings.py`で生成
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,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ import math
5
+ from typing import Dict, List, Tuple
6
+
7
+ import numpy as np
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
+ files = field_getter("config/files.json")
19
+ search = field_getter("config/search_model.json")
20
+
21
+ paths = {
22
+ "tf_token": files("bm25.tf_token"),
23
+ "bm25_meta": files("bm25.bm25_meta"),
24
+ "fasttext_vec": files("embeddings.fasttext_vec"),
25
+ "word_vocab": files("embeddings.word_vocab"),
26
+ "word_vectors": files("embeddings.word_vectors"),
27
+ "doc_vectors": files("embeddings.doc_vectors"),
28
+ }
29
+ field_weights: Dict[str, float] = search("bm25f.field_weights")
30
+ target_fields: List[str] = search("target_fields")
31
+
32
+ return paths, field_weights, target_fields
33
+
34
+
35
+ def read_vocab_from_tf_token(tf_token_path: str) -> Tuple[List[dict], set[str]]:
36
+ with open(tf_token_path, encoding="utf-8") as f:
37
+ docs = json.load(f)
38
+ vocab: set[str] = set()
39
+ for d in docs:
40
+ # doc全体tfから語彙を得る
41
+ for t in (d.get("tf") or {}).keys():
42
+ vocab.add(t)
43
+ return docs, vocab
44
+
45
+
46
+ def stream_fasttext_vec(vec_path: str, vocab: set[str]) -> Tuple[Dict[str, int], np.ndarray]:
47
+ """fastText .vec から、必要語彙のみ抽出してベクトル行列を返す。"""
48
+ token_to_idx: Dict[str, int] = {}
49
+ vectors: List[np.ndarray] = []
50
+
51
+ dim = None
52
+ kept = 0
53
+ with open(vec_path, encoding="utf-8", errors="ignore") as f:
54
+ header = f.readline()
55
+ # ヘッダ行は "<count> <dim>" のことが多い
56
+ try:
57
+ parts = header.strip().split()
58
+ if len(parts) >= 2 and parts[0].isdigit():
59
+ dim = int(parts[1])
60
+ except Exception:
61
+ pass
62
+
63
+ for line in f:
64
+ sp = line.rstrip().split(" ")
65
+ if len(sp) < 2:
66
+ continue
67
+ token = sp[0]
68
+ if token not in vocab:
69
+ continue
70
+ vec_vals = sp[1:]
71
+ if dim is None:
72
+ dim = len(vec_vals)
73
+ if len(vec_vals) != dim:
74
+ continue
75
+ try:
76
+ v = np.asarray([float(x) for x in vec_vals], dtype=np.float32)
77
+ except ValueError:
78
+ continue
79
+ # L2正規化
80
+ norm = np.linalg.norm(v)
81
+ if norm > 0:
82
+ v = v / norm
83
+ token_to_idx[token] = kept
84
+ vectors.append(v)
85
+ kept += 1
86
+
87
+ if dim is None:
88
+ raise RuntimeError(".vec の次元を特定できませんでした")
89
+
90
+ if not vectors:
91
+ log.warning("語彙に一致するベクトルが見つかりませんでした")
92
+ arr = np.zeros((0, dim), dtype=np.float32)
93
+ return token_to_idx, arr
94
+
95
+ arr = np.vstack(vectors).astype(np.float32)
96
+ return token_to_idx, arr
97
+
98
+
99
+ def build_doc_vectors(
100
+ docs: List[dict],
101
+ token_to_idx: Dict[str, int],
102
+ word_vecs: np.ndarray,
103
+ idf: Dict[str, float],
104
+ field_weights: Dict[str, float],
105
+ target_fields: List[str],
106
+ ) -> np.ndarray:
107
+ dim = word_vecs.shape[1] if word_vecs.size > 0 else 300
108
+ doc_mat = np.zeros((len(docs), dim), dtype=np.float32)
109
+
110
+ for i, d in enumerate(docs):
111
+ accum = np.zeros((dim,), dtype=np.float32)
112
+ w_sum = 0.0
113
+
114
+ fields = (d.get("fields") or {})
115
+ for field in target_fields:
116
+ field_obj = fields.get(field) or {}
117
+ tf = field_obj.get("tf") or {}
118
+ f_weight = float(field_weights.get(field, 1.0))
119
+ if f_weight <= 0:
120
+ continue
121
+ for t, cnt in tf.items():
122
+ idx = token_to_idx.get(t)
123
+ if idx is None:
124
+ continue
125
+ idf_t = float(idf.get(t, 0.0))
126
+ w = f_weight * idf_t * float(cnt)
127
+ if w <= 0:
128
+ continue
129
+ accum += word_vecs[idx] * w
130
+ w_sum += w
131
+
132
+ if w_sum > 0:
133
+ vec = accum / w_sum
134
+ # L2正規化
135
+ n = np.linalg.norm(vec)
136
+ if n > 0:
137
+ vec = vec / n
138
+ doc_mat[i] = vec.astype(np.float32)
139
+ else:
140
+ # ベクトルなしの場合はゼロベクトル
141
+ doc_mat[i] = np.zeros((dim,), dtype=np.float32)
142
+
143
+ return doc_mat
144
+
145
+
146
+ def main():
147
+ log.info("語彙/文書ベクトル(word_vocab.json, word_vectors.npz, doc_vectors.npy)を生成します")
148
+ try:
149
+ paths, field_weights, target_fields = load_configs()
150
+ except Exception as e:
151
+ log.error(f"設定の読み込みに失敗しました: {e}")
152
+ sys.exit(1)
153
+
154
+ tf_token_path = paths["tf_token"]
155
+ bm25_meta_path = paths["bm25_meta"]
156
+ fasttext_vec_path = paths["fasttext_vec"]
157
+
158
+ if not os.path.exists(fasttext_vec_path):
159
+ log.warning(f".vec が見つかりません: {fasttext_vec_path}")
160
+ log.warning("Step 6 をスキップします")
161
+ sys.exit(0)
162
+
163
+ try:
164
+ docs, vocab = read_vocab_from_tf_token(tf_token_path)
165
+ except Exception as e:
166
+ log.error(f"tf_token.jsonの読み込みに失敗しました: {e}")
167
+ sys.exit(1)
168
+
169
+ try:
170
+ with open(bm25_meta_path, encoding="utf-8") as f:
171
+ bm25_meta = json.load(f)
172
+ idf = bm25_meta.get("idf", {})
173
+ except Exception as e:
174
+ log.error(f"bm25_meta.jsonの読み込みに失敗しました: {e}")
175
+ sys.exit(1)
176
+
177
+ log.info(f"コーパス語彙数: {len(vocab)}")
178
+ token_to_idx, word_vecs = stream_fasttext_vec(fasttext_vec_path, vocab)
179
+ log.info(f"抽出済み語彙ベクトル数: {word_vecs.shape[0]}")
180
+
181
+ # 語彙インデックスの安定化(token_to_idxは追加順次第なのでソート)
182
+ sorted_tokens = sorted(token_to_idx.keys())
183
+ remap = {t: i for i, t in enumerate(sorted_tokens)}
184
+ remapped_vecs = np.zeros_like(word_vecs)
185
+ for t, old_i in token_to_idx.items():
186
+ new_i = remap[t]
187
+ remapped_vecs[new_i] = word_vecs[old_i]
188
+ token_to_idx = remap
189
+ word_vecs = remapped_vecs
190
+
191
+ # 文書ベクトル
192
+ doc_mat = build_doc_vectors(docs, token_to_idx, word_vecs, idf, field_weights, target_fields)
193
+
194
+ # 出力
195
+ os.makedirs(os.path.dirname(paths["word_vocab"]), exist_ok=True)
196
+ json_dumps(token_to_idx, paths["word_vocab"]) # 語→index
197
+ # 圧縮npz
198
+ np.savez_compressed(paths["word_vectors"], vectors=word_vecs)
199
+ np.save(paths["doc_vectors"], doc_mat)
200
+
201
+ log.info(f"word_vocab.json: {paths['word_vocab']}")
202
+ log.info(f"word_vectors.npz: {paths['word_vectors']}")
203
+ log.info(f"doc_vectors.npy: {paths['doc_vectors']}")
204
+
205
+
206
+ if __name__ == "__main__":
207
+ main()
208
+
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()