検索機能の問題...。

検索機能を追加したい。softwarebookの時も簡単な検索機能を実装したが、それは特定の列を検索することを前提に、テキスト検索に限定していた。今回は、もうちょっと実用的なものにしたい...。リスト表示の列タイトルごとに、キーワードや数値の範囲で絞り込むような機能を持たせようと考えた。イメージするのは、表計算ソフトの「フィルタ」のような感じ。気楽に始めてみたが、これがなかなかの難題になってしまった。(ここ数日の悩みであり、課題です。)

表示されている全ての列を対象にフィルタ機能を持たせるには...

現在のリスト表示される列タイトルは以下の通り。

ファイル コメント 編集フラグ サイズ アップロード日時 ファイル修正日時 管理部門 担当者
    • ファイル    :CSVファイル名が文字列で保存されている。
    • コメント    :メモ的な要素の文字列。
    • 編集フラグ   :「編集可能」「表示のみ」の状態がtrue、falseの真偽値として保存されている。
    • サイズ     :ファイルサイズが数値として保存されている。
    • アップロード日時:CSVファイルをアップロードした日時
    • ファイル修正日時:CSVファイルを修正した日時
    • 管理部門    :管理部門のidを保存している。(多:多関連で部門名を表示している)
    • 担当者     :管理部門のidを保存している。(多:多関連で部門名を表示している)


それぞれの列は、上記のような意味合い。このように、文字列、真偽値、数値、日時、idコード、と多様なデータ形式で保存されている。検索する時は、単純に文字列であれば、フリー入力したテキストをLIKE演算子でそのまま検索してしまえば良い。ところが、文字列以外の時は同じようにやってしまうと問題が出てくる...。

  • 数値であれば大小の比較をしたい。
  • 真偽値なのに「編集可能」で検索しても何も見つからない。
  • 日付は入力方法と比較方法が単純ではない。
  • idコードもリスト表示されている文字列で検索してもダメだ。

そのため、それぞれのデータ形式に合わせて検索方法を用意する必要がある。さらに、検索方法が違えば、条件を指定するのに最適な入力方法も変わってくる。例えば、文字列や数値はテキストボックスで良いが、真偽値やidコードならセレクトボックスで選択した方がいいはず。このようなフィルター検索を実現するために、最初はif文で条件を判定して実装してみたが...。

ifだけでは、いずれ破綻する...。

深く考えずに、フリーワードでのフィルターから始まり、数値の比較フィルター、真偽値やidコードの選択フィルターと、その都度条件を追加していくと、ハイ、以下のような混迷コントローラーの出来上がり...。これでも一応動くことは動くが、ダメですね。あと1週間したら自分でも分からなくなる。(パラメータを調整する順番が重要だったりする。)今後、列の追加やフィルター条件を追加したい時にも、悩むだろうし、自信を持って対応できない。

app/controllers/csvs_controller.rb
...(途中省略)...
  def set_pagination
  # 配列の設定
    @date_input_sample = '<span id="date_input_sample">(入力例: 2007/4/1 , 4/1 , 6:10)</span>'
    @no_select = ['', '']
    @column_titles = %w{file_name file_comment editable file_size created_at file_updated_at management_section_id user_id}
    @compare = [%w{...と等しい =}, %w{...と等しくない <>}, %w{...より大きい >}, %w{...以上 >=}, %w{...より小さい <}, %w{...以下 <=}, ['空白である', 'blank'],  ['空白以外', 'not_blank']] 
    @like_op = [%w{...を含む 3}, %w{...から始まる 2}, %w{...で終わる 1}, %w{...と一致する 0}, %w{空白である 4}, %w{空白以外 5}]
    
  # パラメーターの読み取り
    @filter_column = params[:filter_column] || @column_titles[0]
    filter_item = params[:filter_item] || '%'
    filter_item = '%' if filter_item.empty?
    filter_compare = params[:filter_compare] || 'LIKE'
    filter_like_op = params[:filter_like_op].to_i
    
  # 条件によってパラメーターを調整
    # params[:filter_like_op]が「nil」であっても大丈夫。(nil.to_i => 0)
    op = [[], ['%', ''], ['', '%'], ['%', '%'], [], []][filter_like_op]
    filter_item = ''   if filter_like_op == 4 || filter_compare == 'blank'
    filter_item = '_%' if filter_like_op == 5 || filter_compare == 'not_blank'
    filter_compare = 'LIKE' if filter_item == '%'
    filter_compare = 'LIKE' if /blank$/ =~ filter_compare   
    # NULLも含めて検索するSQL。NULLはデータが存在しない項目「,,」これは空文字列「,"",」
    option = " OR #{@filter_column} IS NULL" if filter_item == '%' || filter_item == '' || filter_compare == '<>'
    
  # paginateの実行
    @conditions = ["#{@filter_column} #{filter_compare} ?#{option}", 
                   "#{op[0]}#{filter_item}#{op[1]}"]
    @next_direction = (params[:sort_direction] == 'asc') ? 'desc' : 'asc'
    order = "#{params[:sort_field] || 'id'} #{@next_direction}"
    @csv_pages, @csvs = paginate :csvs, :per_page => 10, 
                                 :conditions => @conditions, 
                                 :order => order
  end

  def list
    set_pagination
    render :action => 'list'
  end
...(途中省略)...

日時は奥が深い...。

入力の問題

日時を入力する時に、Railsにはselect_datetimeというメソッドがあるが、自分の好みとしてはあまり好きではない。セレクトボックスで、年、月、日、時、分、秒を選択するのは面倒だ。テキストボックスにダイレクトに「4/17」と入力すれば、2007/4/17でフィルターされるようにしたい。4/17ならTime.parse('4/17')で簡単に実現できるが、以下のような入力も許容するようにしたい。

入力値 取得する日時
2007、2007/ →2007/01/01 00:00:00
4、4/ →2007/04/01 00:00:00
2006/4、2006/4/ →2006/04/01 00:00:00
14: →現在日付 14:00:00
4/17 14: →2007/04/17 14:00:00
期間の問題

さらに厄介な問題がある。以下のようなリストがあったとして...

ファイル名 アップロード日時
200701損益 2007/4/16 11:25:37
200702損益 2007/4/17 09:15:46
200703損益 2007/4/17 13:01:12
200704損益 2007/4/17 15:19:58
200705損益 2007/4/18 20:45:22
  • 「アップロード日時が4/17以下」というフィルタを実行した時に、何の工夫もないと「200701損益」1つしか表示されない。
  • しかし、人情としては、「200701損益〜200704損益」4つが表示されることを期待する。(「4/17より小さい」というフィルタの時に「200701損益」1つが表示されるのは納得できる。)
  • 同様に「アップロード日時が4/17と等しい」というフィルタを実行した時にはどうなるか?普通は何も表示されない。
  • ところが、人情としては、「200702損益〜200704損益」3つが表示されることを期待する。


なぜ、このような食い違いが起こるのか?それは、人情は4/17と入力した時に、4/17 00:00:00〜23:59:59.999...という期間として認識しているからだ。だから日時フィルターを実装する時には期間として認識するように工夫しなくてはならない...。

入力値 取得する日時
2007、2007/ →2007/01/01 00:00:00〜2007/12/31 23:59:59
4、4/ →2007/04/01 00:00:00〜2007/04/30 23:59:59
2006/4、2006/4/ →2006/04/01 00:00:00〜2006/04/30 23:59:59
14: →現在日付 14:00:00〜現在日付 14:59:59
4/17 14: →2007/04/17 14:00:00〜2007/04/17 14:59:59
4/17 14:12 →2007/04/17 14:12:00〜2007/04/17 14:12:59
4/17 →2007/04/17 00:00:00〜2007/04/17 23:59:59
      • 秒未満は無視することにした。


今日は、問題ばかりが浮き彫りになって終わってしまった...。