Spaces:
Sleeping
Sleeping
| import os | |
| import sys | |
| import json | |
| import math | |
| from collections import defaultdict | |
| from typing import Dict, List | |
| from sudachipy import dictionary, tokenizer | |
| sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) | |
| from utils.logger import setup_logger | |
| from utils.json import get_file_path_from_config, field_getter, json_dumps | |
| log = setup_logger(__name__) | |
| def load_configs(): | |
| search = field_getter("config/search_model.json") | |
| files = field_getter("config/files.json") | |
| target_pos_l1: List[str] = search("target_pos_l1") | |
| target_fields: List[str] = search("target_fields") | |
| ban_list: List[str] = search("synonyms.banlist") | |
| stopwords_path = files("sudachi.stopwords") | |
| with open(stopwords_path, encoding="utf-8") as f: | |
| stopwords = set(json.load(f)) | |
| sudachi_config_path = files("sudachi.sudachi_config") | |
| return target_pos_l1, target_fields, set(ban_list), stopwords, sudachi_config_path | |
| def build_tokenizer(sudachi_config_path: str): | |
| tok = dictionary.Dictionary(config_path=sudachi_config_path).create() | |
| mode = tokenizer.Tokenizer.SplitMode.A | |
| return tok, mode | |
| def tokenize( | |
| text: str, tok, mode, target_pos_l1: List[str], ban_list: set, stopwords: set | |
| ) -> List[str]: | |
| if not text: | |
| return [] | |
| out: List[str] = [] | |
| for m in tok.tokenize(text, mode): | |
| base = m.normalized_form().lower().strip() | |
| if not base: | |
| continue | |
| pos = m.part_of_speech() | |
| if pos[0] not in target_pos_l1: | |
| continue | |
| if base in stopwords or base in ban_list: | |
| continue | |
| out.append(base) | |
| return out | |
| def main(): | |
| log.info("BM25Fメタデータ(bm25_meta.json)を生成します") | |
| try: | |
| target_pos_l1, target_fields, ban_list, stopwords, sudachi_config_path = ( | |
| load_configs() | |
| ) | |
| except Exception as e: | |
| log.error(f"設定の読み込みに失敗しました: {e}") | |
| sys.exit(1) | |
| circles_path = get_file_path_from_config( | |
| "circles.circles_json", "data/generated/circles.json" | |
| ) | |
| output_path = get_file_path_from_config( | |
| "bm25.bm25_meta", "data/generated/bm25_meta.json" | |
| ) | |
| try: | |
| with open(circles_path, encoding="utf-8") as f: | |
| circles = json.load(f) | |
| except Exception as e: | |
| log.error(f"circles.jsonの読み込みに失敗しました: {e}") | |
| sys.exit(1) | |
| tok, mode = build_tokenizer(sudachi_config_path) | |
| N = len(circles) | |
| df: Dict[str, int] = defaultdict(int) | |
| field_token_lens_sum: Dict[str, int] = {f: 0 for f in target_fields} | |
| log.info(f"ドキュメント数: {N}") | |
| for c in circles: | |
| seen_in_doc = set() | |
| for field in target_fields: | |
| text = c.get(field) or "" | |
| if not text: | |
| continue | |
| toks = tokenize(str(text), tok, mode, target_pos_l1, ban_list, stopwords) | |
| field_token_lens_sum[field] += len(toks) | |
| for t in set(toks): | |
| if t not in seen_in_doc: | |
| df[t] += 1 | |
| seen_in_doc.add(t) | |
| # IDF 計算(BM25で一般的な +0.5 smoothing と +1 オフセット) | |
| idf: Dict[str, float] = {} | |
| for term, dfi in df.items(): | |
| idf_val = max(0.0, ((N - dfi + 0.5) / (dfi + 0.5))) | |
| # 数値安定化のためlog1p | |
| idf[term] = math.log1p(idf_val) | |
| avg_len = { | |
| field: (field_token_lens_sum[field] / N if N > 0 else 0.0) | |
| for field in target_fields | |
| } | |
| meta = { | |
| "N": N, | |
| "avg_len": avg_len, | |
| "idf": idf, | |
| } | |
| os.makedirs(os.path.dirname(output_path), exist_ok=True) | |
| json_dumps(meta, output_path) | |
| log.info(f"bm25_meta.json を出力しました: {output_path}") | |
| if __name__ == "__main__": | |
| main() | |