circle-search-26-api / scripts /5_prepare_tf_token.py
yukikase's picture
feat: サークル検索API実装(BM25F + fastText対応)
568bbca
Raw
History Blame Contribute Delete
4.27 kB
import os
import sys
import json
from collections import Counter
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
from schemas import tf_token
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("フィールド別TF/トークン(tf_token.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.tf_token", "data/generated/tf_token.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)
results: List[tf_token.Circle] = []
for c in circles:
circle_id = c.get("circleId")
# 各フィールドのトークン化とTF
field_objs: Dict[str, tf_token.TfOfField] = {}
doc_tf_counter: Counter = Counter()
doc_token_set: set = set()
for field in target_fields:
text = c.get(field) or ""
toks = tokenize(str(text), tok, mode, target_pos_l1, ban_list, stopwords)
tf = Counter(toks)
field_objs[field] = tf_token.TfOfField(len=len(toks), tf=dict(tf))
doc_tf_counter.update(tf)
doc_token_set.update(tf.keys())
# スキーマ Fields へ詰める(未定義フィールドは長さ0/空dictで埋める)
def get_field(name: str) -> tf_token.TfOfField:
return field_objs.get(name, tf_token.TfOfField(len=0, tf={}))
fields_obj = tf_token.Fields(
name=get_field("name"),
circleName=get_field("circleName"),
circleNameKana=get_field("circleNameKana"),
description=get_field("description"),
prSummary=get_field("prSummary"),
prDetail=get_field("prDetail"),
)
circle_entry = tf_token.Circle(
circleId=circle_id,
fields=fields_obj,
tf=dict(doc_tf_counter),
# tokens はユニーク語彙の存在フラグ(1)とする
tokens={t: 1 for t in sorted(doc_token_set)},
)
results.append(circle_entry)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
json_dumps([r.model_dump() for r in results], output_path)
log.info(f"tf_token.json を出力しました: {output_path}")
if __name__ == "__main__":
main()