ktsn-ud commited on
Commit
41e01be
·
1 Parent(s): d9e5322

codex生成: topkのみに

Browse files
api/search/engine.py CHANGED
@@ -23,7 +23,6 @@ class SearchConfig:
23
  syn_limits: Dict[str, int]
24
  banlist: List[str]
25
  word_sim_enable: bool
26
- word_sim_mode: str
27
  word_sim_alpha: float
28
  word_sim_topk_k: int
29
  query_subword_enable: bool
@@ -76,7 +75,6 @@ class SearchEngine:
76
  # Vectors
77
  self.word_vocab: Dict[str, int] = {}
78
  self.word_vectors: Optional[np.ndarray] = None
79
- self.doc_vectors: Optional[np.ndarray] = None
80
  self.ft_model = None
81
 
82
  # ----- Init / Load -----
@@ -95,7 +93,6 @@ class SearchEngine:
95
  syn_limits=search("synonyms.limits"),
96
  banlist=search("synonyms.banlist"),
97
  word_sim_enable=bool(search("word_sim.enable")),
98
- word_sim_mode=(search("word_sim.mode") or "soft").lower(),
99
  word_sim_alpha=float(search("word_sim.alpha")),
100
  word_sim_topk_k=int(search("word_sim.topk_k", 3)),
101
  query_subword_enable=bool(search("query_subword.enable")),
@@ -167,12 +164,7 @@ class SearchEngine:
167
  except Exception as e:
168
  log.warning(f"word vectors not ready: {e}")
169
 
170
- try:
171
- doc_vec_path = files("embeddings.doc_vectors")
172
- if os.path.exists(doc_vec_path):
173
- self.doc_vectors = np.load(doc_vec_path)
174
- except Exception as e:
175
- log.warning(f"doc vectors not ready: {e}")
176
 
177
  # fastText OOV
178
  if self.cfg.query_subword_enable and self.cfg.query_subword_path and os.path.exists(self.cfg.query_subword_path):
@@ -286,7 +278,7 @@ class SearchEngine:
286
  def _word_sim_scores(self, terms: List[str]) -> Optional[np.ndarray]:
287
  if not self.cfg.word_sim_enable:
288
  return None
289
- if self.word_vectors is None or self.doc_vectors is None or self.doc_vectors.size == 0:
290
  return None
291
 
292
  # Build per-term vectors with weights (IDF; OOV down-weighted)
@@ -310,71 +302,50 @@ class SearchEngine:
310
  V = np.stack(vecs).astype(np.float32) # T x D
311
  W = np.asarray(weights, dtype=np.float32) # T
312
 
313
- # Mode: average query vector then cosine
314
- if self.cfg.word_sim_mode in ("avg", "soft"):
315
- q = np.average(V, axis=0, weights=W)
316
- n = np.linalg.norm(q)
317
- if n > 0:
318
- q = q / n
319
- if not np.all(np.isfinite(q)):
320
- return None
321
- sims = self.doc_vectors @ q.astype(np.float32)
322
- return sims
323
-
324
- # Mode: top-k pooling over term-term cosine contributions (query terms x document terms)
325
- if self.cfg.word_sim_mode == "topk":
326
- k = max(1, int(self.cfg.word_sim_topk_k))
327
- n_docs = len(self.tf_token_docs)
328
- sims_all = np.zeros((n_docs,), dtype=np.float32)
329
- # Pre-shape
330
- Vq = V # Tq x D (already float32, L2-normalized token vectors)
331
- Wq = W # Tq
332
- for i, d in enumerate(self.tf_token_docs):
333
- # collect unique doc tokens across target fields
334
- fields = d.get("fields") or {}
335
- doc_terms = set()
336
- for fname in self.cfg.target_fields:
337
- fobj = (fields.get(fname) or {})
338
- tf = (fobj.get("tf") or {})
339
- doc_terms.update(tf.keys())
340
- if not doc_terms:
341
- sims_all[i] = 0.0
342
- continue
343
- # build matrix of doc term vectors
344
- Vd_list = []
345
- for t in doc_terms:
346
- idx = self.word_vocab.get(t)
347
- if idx is None:
348
- continue
349
- Vd_list.append(self.word_vectors[idx])
350
- if not Vd_list:
351
- sims_all[i] = 0.0
352
- continue
353
- Vd = np.stack(Vd_list).astype(np.float32) # Td x D
354
- # pairwise cosine (since vectors are normalized)
355
- M = Vd @ Vq.T # Td x Tq
356
- # weight by query term importance and clip negatives
357
- if Wq.size:
358
- M = M * Wq[None, :]
359
- M = np.maximum(M, 0.0)
360
- # take global top-k across all pairs
361
- Td, Tq = M.shape
362
- total = Td * Tq
363
- kk = min(k, total) if total > 0 else 0
364
- if kk == 0:
365
- sims_all[i] = 0.0
366
  continue
367
- flat = M.reshape(-1)
368
- if kk == total:
369
- top_vals = flat
370
- else:
371
- idxk = np.argpartition(flat, -kk)[-kk:]
372
- top_vals = flat[idxk]
373
- sims_all[i] = float(top_vals.mean()) if top_vals.size else 0.0
374
- return sims_all
375
-
376
- # Fallback: disable if unknown mode
377
- return None
 
 
 
 
 
 
 
 
 
 
 
 
378
 
379
  # ----- Public API -----
380
  def search(
 
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
  query_subword_enable: bool
 
75
  # Vectors
76
  self.word_vocab: Dict[str, int] = {}
77
  self.word_vectors: Optional[np.ndarray] = None
 
78
  self.ft_model = None
79
 
80
  # ----- Init / Load -----
 
93
  syn_limits=search("synonyms.limits"),
94
  banlist=search("synonyms.banlist"),
95
  word_sim_enable=bool(search("word_sim.enable")),
 
96
  word_sim_alpha=float(search("word_sim.alpha")),
97
  word_sim_topk_k=int(search("word_sim.topk_k", 3)),
98
  query_subword_enable=bool(search("query_subword.enable")),
 
164
  except Exception as e:
165
  log.warning(f"word vectors not ready: {e}")
166
 
167
+ # doc_vectors.npy は topk 方式では不要
 
 
 
 
 
168
 
169
  # fastText OOV
170
  if self.cfg.query_subword_enable and self.cfg.query_subword_path and os.path.exists(self.cfg.query_subword_path):
 
278
  def _word_sim_scores(self, terms: List[str]) -> Optional[np.ndarray]:
279
  if not self.cfg.word_sim_enable:
280
  return None
281
+ if self.word_vectors is None:
282
  return None
283
 
284
  # Build per-term vectors with weights (IDF; OOV down-weighted)
 
302
  V = np.stack(vecs).astype(np.float32) # T x D
303
  W = np.asarray(weights, dtype=np.float32) # T
304
 
305
+ # top-k pooling over term-term cosine contributions (query terms x document terms)
306
+ k = max(1, int(self.cfg.word_sim_topk_k))
307
+ n_docs = len(self.tf_token_docs)
308
+ sims_all = np.zeros((n_docs,), dtype=np.float32)
309
+ Vq = V # Tq x D (normalized)
310
+ Wq = W # Tq
311
+ for i, d in enumerate(self.tf_token_docs):
312
+ fields = d.get("fields") or {}
313
+ doc_terms = set()
314
+ for fname in self.cfg.target_fields:
315
+ fobj = (fields.get(fname) or {})
316
+ tf = (fobj.get("tf") or {})
317
+ doc_terms.update(tf.keys())
318
+ if not doc_terms:
319
+ sims_all[i] = 0.0
320
+ continue
321
+ Vd_list = []
322
+ for t in doc_terms:
323
+ idx = self.word_vocab.get(t)
324
+ if idx is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  continue
326
+ Vd_list.append(self.word_vectors[idx])
327
+ if not Vd_list:
328
+ sims_all[i] = 0.0
329
+ continue
330
+ Vd = np.stack(Vd_list).astype(np.float32) # Td x D
331
+ M = Vd @ Vq.T # Td x Tq
332
+ if Wq.size:
333
+ M = M * Wq[None, :]
334
+ M = np.maximum(M, 0.0)
335
+ Td, Tq = M.shape
336
+ total = Td * Tq
337
+ kk = min(k, total) if total > 0 else 0
338
+ if kk == 0:
339
+ sims_all[i] = 0.0
340
+ continue
341
+ flat = M.reshape(-1)
342
+ if kk == total:
343
+ top_vals = flat
344
+ else:
345
+ idxk = np.argpartition(flat, -kk)[-kk:]
346
+ top_vals = flat[idxk]
347
+ sims_all[i] = float(top_vals.mean()) if top_vals.size else 0.0
348
+ return sims_all
349
 
350
  # ----- Public API -----
351
  def search(
docs/files.md CHANGED
@@ -13,5 +13,3 @@
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`で生成
 
13
  - `scripts/6_build_word_embeddings.py`で生成
14
  - `word_vectors.npz`
15
  - `scripts/6_build_word_embeddings.py`で生成
 
 
scripts/6_build_word_embeddings.py CHANGED
@@ -20,16 +20,11 @@ def load_configs():
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]]:
@@ -96,63 +91,18 @@ def stream_fasttext_vec(vec_path: str, vocab: set[str]) -> Tuple[Dict[str, int],
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):
@@ -166,14 +116,6 @@ def main():
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]}")
@@ -188,21 +130,15 @@ def main():
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
-
 
20
 
21
  paths = {
22
  "tf_token": files("bm25.tf_token"),
 
23
  "fasttext_vec": files("embeddings.fasttext_vec"),
24
  "word_vocab": files("embeddings.word_vocab"),
25
  "word_vectors": files("embeddings.word_vectors"),
 
26
  }
27
+ return paths
 
 
 
28
 
29
 
30
  def read_vocab_from_tf_token(tf_token_path: str) -> Tuple[List[dict], set[str]]:
 
91
  return token_to_idx, arr
92
 
93
 
94
+ # top-k モードでは文書ベクトルは不要
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
 
97
  def main():
98
  log.info("語彙/文書ベクトル(word_vocab.json, word_vectors.npz, doc_vectors.npy)を生成します")
99
  try:
100
+ paths = load_configs()
101
  except Exception as e:
102
  log.error(f"設定の読み込みに失敗しました: {e}")
103
  sys.exit(1)
104
 
105
  tf_token_path = paths["tf_token"]
 
106
  fasttext_vec_path = paths["fasttext_vec"]
107
 
108
  if not os.path.exists(fasttext_vec_path):
 
116
  log.error(f"tf_token.jsonの読み込みに失敗しました: {e}")
117
  sys.exit(1)
118
 
 
 
 
 
 
 
 
 
119
  log.info(f"コーパス語彙数: {len(vocab)}")
120
  token_to_idx, word_vecs = stream_fasttext_vec(fasttext_vec_path, vocab)
121
  log.info(f"抽出済み語彙ベクトル数: {word_vecs.shape[0]}")
 
130
  token_to_idx = remap
131
  word_vecs = remapped_vecs
132
 
 
 
 
133
  # 出力
134
  os.makedirs(os.path.dirname(paths["word_vocab"]), exist_ok=True)
135
  json_dumps(token_to_idx, paths["word_vocab"]) # 語→index
136
  # 圧縮npz
137
  np.savez_compressed(paths["word_vectors"], vectors=word_vecs)
 
138
 
139
  log.info(f"word_vocab.json: {paths['word_vocab']}")
140
  log.info(f"word_vectors.npz: {paths['word_vectors']}")
 
141
 
142
 
143
  if __name__ == "__main__":
144
  main()