Class: Vivlio::Starter::CLI::IndexCommands::IndexCandidateExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/vivlio/starter/cli/index/index_candidate_extractor.rb

Overview

索引候補語自動抽出クラス

Constant Summary collapse

<<~BANNER
  # ================================================================
  # 索引候補リスト(config/index_candidates.yml)
  # ================================================================
  # vs index:candidate(内部コマンド)によって自動生成される索引用語候補です。
  #
  # 使い方:
  #   1. contexts で示される章・抜粋を参照し、索引に載せたい語を確認する
  #   2. enabled を true/false に切り替えて採用可否を管理する
  #   3. 採用する語は原稿に [用語|読み] でマークアップ、または index_glossary_terms.yml へ登録する
  #   4. 読みが誤っている場合は index_glossary_terms.yml に正しい yomi を追加してから再生成する
  # ================================================================
BANNER
DEFINITION_PATTERNS =

定義パターン(「〜とは」「〜について」など)

[
  /(.{2,20})とは[、,]?[^。]*(?:である|です|を意味|を指|という)/,
  /(.{2,20})(?:について|に関して)(?:は|の)/,
  /(.{2,20})(?:を|が)(?:定義|説明|解説)/,
  /「(.{2,20})」(?:とは|は|について)/,
  /(.{2,20})(?:の概念|の定義|の意味)/
].freeze
TECHNICAL_TERM_PATTERNS =

専門用語パターン(カタカナ語、英字語など)

[
  /[ァ-ヶー]{3,}/, # カタカナ3文字以上
  /[A-Z][a-zA-Z]{2,}/, # 英語の単語
  /[A-Z]{2,}/ # 略語(HTML, CSS など)
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeIndexCandidateExtractor

Returns a new instance of IndexCandidateExtractor.



65
66
67
68
69
70
71
# File 'lib/vivlio/starter/cli/index/index_candidate_extractor.rb', line 65

def initialize
  @documents = {}
  @term_scores = Hash.new(0.0)
  @term_contexts = Hash.new { |h, k| h[k] = [] }
  @yomi_inferrer = YomiInferrer.new
  @context_width = load_context_width
end

Instance Attribute Details

#documentsObject (readonly)

Returns the value of attribute documents.



58
59
60
# File 'lib/vivlio/starter/cli/index/index_candidate_extractor.rb', line 58

def documents
  @documents
end

#term_contextsObject (readonly)

Returns the value of attribute term_contexts.



58
59
60
# File 'lib/vivlio/starter/cli/index/index_candidate_extractor.rb', line 58

def term_contexts
  @term_contexts
end

#term_scoresObject (readonly)

Returns the value of attribute term_scores.



58
59
60
# File 'lib/vivlio/starter/cli/index/index_candidate_extractor.rb', line 58

def term_scores
  @term_scores
end

Instance Method Details

#all_candidatesObject

全ての候補語を取得



61
62
63
# File 'lib/vivlio/starter/cli/index/index_candidate_extractor.rb', line 61

def all_candidates
  @term_scores.keys
end

#export_candidates!(output_file = 'config/index_candidates.yml', threshold = 150) ⇒ Object

索引候補を YAML ファイルに出力

Parameters:

  • output_file (String) (defaults to: 'config/index_candidates.yml')

    出力ファイルパス

  • threshold (Integer) (defaults to: 150)

    スコア閾値(この値以上の候補のみ出力)



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/vivlio/starter/cli/index/index_candidate_extractor.rb', line 105

def export_candidates!(output_file = 'config/index_candidates.yml', threshold = 150)
  FileUtils.mkdir_p(File.dirname(output_file))

  candidates = @term_scores
               .select { |_, score| score >= threshold }
               .sort_by { |_, score| -score }
               .map do |term, score|
    yomi = @yomi_inferrer.available? ? @yomi_inferrer.infer(term) : term
    contexts = @term_contexts[term]
               .uniq { |ctx| [ctx[:chapter], ctx[:context]] }
               .first(3)

    {
      'term' => term,
      'yomi' => yomi,
      'score' => score.round(1),
      'contexts' => contexts,
      'enabled' => true
    }
  end

  data = {
    'generated_at' => Time.now.iso8601,
    'threshold' => threshold,
    'total_candidates' => candidates.size,
    'candidates' => candidates
  }

  yaml = data.to_yaml(line_width: -1)
  yaml_with_spacing = yaml
                      .sub("candidates:\n- term:", "candidates:\n\n- term:")
                      .gsub("\n- term:", "\n\n- term:")

  File.write(output_file, "#{BANNER}#{yaml_with_spacing}", encoding: 'utf-8')
  Common.log_success("索引候補を #{output_file} に出力しました")
rescue StandardError => e
  Common.log_error("索引候補の出力に失敗しました: #{e.message}")
end

#extract_from_chapters!(chapters) ⇒ Object

全章を解析して索引候補を抽出

Parameters:

  • chapters (Array<String>)

    対象章のファイル名リスト



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
# File 'lib/vivlio/starter/cli/index/index_candidate_extractor.rb', line 75

def extract_from_chapters!(chapters)
  Common.log_action('索引候補の自動抽出を開始します...')

  # ドキュメントを読み込み (contents/ 配下のみ)
  chapters.each do |chapter|
    md_file = File.join(Common::CONTENTS_DIR, "#{chapter}.md")

    unless File.exist?(md_file)
      Common.log_warn("索引候補抽出: contents/ に #{chapter}.md が見つからないためスキップします")
      next
    end

    content = File.read(md_file, encoding: 'utf-8')
    @documents[chapter] = content
  end

  # 各種抽出を実行
  extract_definition_patterns!
  extract_technical_terms!
  extract_noun_sequences! if @yomi_inferrer.available?

  # TF-IDF スコアリング
  calculate_tfidf_scores!

  Common.log_success("#{@term_scores.size} 件の候補語を抽出しました")
end