# -*- coding: utf-8 -*- # # @file # @brief # @author ongaeshi # @date 2012/05/29 require 'kconv' require 'milkode/common/util' module Milkode class DocumentTable attr_reader :table def self.define_schema begin Groonga::Schema.define do |schema| schema.create_table("documents", :type => :hash) do |table| table.string("path") table.string("package") table.string("restpath") table.text("content") table.time("timestamp") table.string("suffix") end schema.create_table("terms", :type => :patricia_trie, :key_normalize => true, :default_tokenizer => "TokenBigramSplitSymbolAlphaDigit") do |table| table.index("documents.path", :with_position => true) table.index("documents.package", :with_position => true) table.index("documents.restpath", :with_position => true) table.index("documents.content", :with_position => true) table.index("documents.suffix", :with_position => true) end end rescue Groonga::Schema::ColumnCreationWithDifferentOptions puts < :hash) do |table| table.string("path") table.string("package") table.string("restpath") table.text("content") table.time("timestamp") table.text("suffix") end schema.create_table("terms", :type => :patricia_trie, :key_normalize => true, :default_tokenizer => "TokenBigramSplitSymbolAlphaDigit") do |table| table.index("documents.path", :with_position => true) table.index("documents.package", :with_position => true) table.index("documents.restpath", :with_position => true) table.index("documents.content", :with_position => true) table.index("documents.suffix", :with_position => true) end end end end # レコードをまとめて削除する # 過去の方法 # 検索結果にマッチしたレコード等をまとめて削除 # 削除前にインデックスを削除し、削除後にインデックスを再度追加してい # 大量のレコードを削除する場合に高速に動作する # # 現在の方法 # 上記の方法がかえって遅くなったので元に戻す # 普通に速くなった気がする # def remove_records(records, &block) # Groonga::Schema.define do |schema| # schema.change_table("terms") do |table| # table.remove_index("documents.path") # table.remove_index("documents.package") # table.remove_index("documents.restpath") # table.remove_index("documents.content") # table.remove_index("documents.suffix") # end # end records.each do |record| yield record if block record.key.delete end # Groonga::Schema.define do |schema| # schema.change_table("terms") do |table| # table.index("documents.path", :with_position => true) # table.index("documents.package", :with_position => true) # table.index("documents.restpath", :with_position => true) # table.index("documents.content", :with_position => true) # table.index("documents.suffix", :with_position => true) # end # end end def initialize(table) @table = table end def size @table.size end # 指定ファイルをテーブルに追加 # # @param package_dir パッケージディレクトリ -> '/path/to/Package' # @param restpath パッケージディレクトリ以下のパス名 -> 'src/Foo.hpp' # @param package_name パッケージ名(未指定の場合は Fie.basename(package_dir) ) # # @retval :newfile 新規追加 # @retval :update 更新 # @retval nil タイムスタンプ比較により更新無し # def add(package_dir, restpath, package_name = nil) filename = File.join(package_dir, restpath) # フルパスの作成 filename = File.expand_path(filename) # 絶対パスに変換 path = Util.filename_to_utf8(filename) # データベースに格納する時のファイル名はutf8 package = package_name || File.basename(package_dir) package = Util.filename_to_utf8(package) restpath = Util.filename_to_utf8(restpath) suffix = File.extname(path).sub('.', "") timestamp = Util.truncate_nsec(File.mtime(filename)) # OSへの問い合わせは変換前のファイル名で record = @table[path] unless record # 新規追加 @table.add(path, :path => path, :package => package, :restpath => restpath, :content => load_content(filename), :timestamp => timestamp, :suffix => suffix) return :newfile else if (record.timestamp < timestamp) # 更新 record.package = package record.restpath = restpath record.content = load_content(filename) record.timestamp = timestamp record.suffix = suffix return :update else # タイムスタンプ比較により更新無し return nil end end end def remove(name) @table[name].delete end def remove_match_path(path, &block) remove_records(search(:paths => [path]), &block) end def remove_all(&block) remove_records(@table.select, &block) end # shortpathの一致するレコードを取得 def find_shortpath(shortpath) package, restpath = Util.divide_shortpath(shortpath) result = @table.select { |record| (record.package == package) & (record.restpath == restpath) } return result.records[0] end # 指定パス以下のファイルを全て取得 def find_shortpath_below(shortpath) if (shortpath.nil? || shortpath.empty?) @table.select.records else package, restpath = Util.divide_shortpath(shortpath) if (restpath.nil? || restpath.empty?) @table.select { |record| record.package == package }.to_a else @table.select { |record| (record.package == package) & (record.restpath =~ restpath)}.to_a end end end # 実体の存在しないデータを削除 def cleanup self.each do |r| unless File.exist? r.path yield r if block_given? # p r.restpath remove(r.path) end end end # 詳細検索 # # @param options 検索オプション、ハッシュで指定 # :patterns => マッチする行 # :keywords => 検索キーワード # :paths => ファイルパス(AND) # :packages => パッケージ名(OR) # :strict_packages => 厳密なパッケージ名(OR) # :restpaths => 短縮パス(AND) # :suffixs => 拡張子 # :offset => オフセット(default = 0) # :limit => 表示リミット(default = -1) def search_with_match(options) patterns = options[:patterns] || [] keywords = options[:keywords] || [] packages = options[:packages] || [] strict_packages = options[:strict_packages] || [] paths = options[:paths] || [] restpaths = options[:restpaths] || [] suffixs = options[:suffixs] || [] fpath_or_packages = options[:fpath_or_packages] || [] offset = options[:offset] || 0 limit = options[:limit] || -1 result = @table.select do |record| expression = nil # マッチする行 patterns.each do |word| sub_expression = record.content =~ word if expression.nil? expression = sub_expression else expression &= sub_expression end end # キーワード(絞り込むための手がかり) keywords.each do |word| sub_expression = record.content =~ word sub_expression |= record.restpath =~ word sub_expression |= record.package =~ word if expression.nil? expression = sub_expression else expression &= sub_expression end end # パッケージ(OR) pe = package_expression(record, packages) if (pe) if expression.nil? expression = pe else expression &= pe end end # 厳密なパッケージ(OR) pe = strict_packages_expression(record, strict_packages) if (pe) if expression.nil? expression = pe else expression &= pe end end # ファイルパス paths.each do |word| sub_expression = record.path =~ word if expression.nil? expression = sub_expression else expression &= sub_expression end end # 短縮パス restpaths.each do |word| sub_expression = record.restpath =~ word if expression.nil? expression = sub_expression else expression &= sub_expression end end # 拡張子(OR) se = suffix_expression(record, suffixs) if (se) if expression.nil? expression = se else expression &= se end end # ファイル名かパッケージ名 fpath_or_packages.each do |word| sub_expression = record.restpath =~ word sub_expression |= record.package =~ word if expression.nil? expression = sub_expression else expression &= sub_expression end end # 検索式 expression end # スコアとタイムスタンプでソート # records = result.sort([{:key => "_score", :order => "descending"}, # {:key => "timestamp", :order => "descending"}], # :offset => offset, # :limit => limit) # ファイル名でソート records = Util.groonga_table_sort(result, [{:key => "package", :order => "ascending"}, {:key => "restpath", :order => "ascending"}], :offset => offset, :limit => limit) # 検索結果のレコード(limitの影響を受ける), 総マッチ数(limitの影響を受けない), result(Groonga::Hash) return records, result.size, result end # マッチしたレコードのみを返す def search(options) records, match_total = search_with_match(options) records end def self.drilldown(result, column, num = nil) drilled = result.group(column).map {|record| [record.n_sub_records, record.key]}.sort_by {|a| a[0]}.reverse num ? drilled[0, num] : drilled end def select_all_sort_by_shortpath(offset = 0, limit = -1) result = @table.select # @todo ここが速度低下の原因?と思ったけど、ここは全て選択の部分だけか・・・ # 2010/10/29 ongaeshi # 本当はこのようにgroongaAPIでソートしたいのだが上手くいかなかった # # ファイル名順にソート # records = table.sort([{:key => "shortpath", :order => "descending"}], # :offset => offset, # :limit => limit) # ソート if (limit != -1) records = result.records.sort_by{|record| DocumentRecord::shortpath(record).downcase }[offset, limit] else records = result.records.sort_by{|record| DocumentRecord::shortpath(record).downcase }[offset..limit] end return records, result.size end # 指定されたパッケージのクリーンアップ def cleanup_package_name(package_name, ignore_checker = nil) # クリーンアップ対象のファイルを検索 result = @table.select { |record| record.package == package_name } # 存在しない&無視ファイルの削除 result.each do |r| if !File.exist?(r.path) || (ignore_checker && ignore_checker.ignore?("/#{r.restpath}")) yield r if block_given? # p r.restpath remove(r.path) end end end def each @table.select.each do |r| yield r end end def dump self.each do |r| p [r.path, r.package, r.restpath, r.content, r.timestamp, r.suffix] end end def to_a @table.to_a end def package_records(name) search(:strict_packages => [name]) end private def load_content(filename) Util.load_content($stdout, filename) end def package_expression(record, packages) sub = nil packages.each do |word| e = record.package =~ word if sub.nil? sub = e else sub |= e end end sub end def strict_packages_expression(record, packages) sub = nil packages.each do |word| e = record.package == word if sub.nil? sub = e else sub |= e end end sub end def suffix_expression(record, suffixs) sub = nil suffixs.each do |word| e = record.suffix == word if sub.nil? sub = e else sub |= e end end sub end end end