LoginSignup
19
11

More than 5 years have passed since last update.

Algolia Searchをつかって東京都府中市の粗大ゴミシール料金検索をつくりました

Last updated at Posted at 2019-02-24

概要

アプリ

こちらから使えます。(もし使えなかったら、それはalgoliaの使用上限に達したからです。あしからず。)
https://fuchu-gomi-sticker.herokuapp.com

ソース

動作イメージ

4b67298239906ceb53d5aa9d4e98f8a4.gif

解説

概要

  • モデル作成と府中市の粗大ゴミシール料金表データのインポート
  • algoliaを使ってサーバーサイドで検索
  • algoliaを使ってフロントエンドでオートコンプリート
  • Bulmaを使って飾り付け

モデル作成と府中市の粗大ゴミシール料金表データのインポート

府中市が公開している粗大ゴミシールのCSVは下記の様になっています。
基本はこれを取り込みます。
image.png

Trashモデルをつくります。カラム構造は以下の通りです。

db/migrate/20190223071022_create_trashes.rb
class CreateTrashes < ActiveRecord::Migration[6.0]
  def change
    create_table :trashes do |t|
      t.string :name # 品目
      t.string :name_furi # 品目のふりがな(ひらがな)
      t.integer :disposal_cost # 粗大ゴミシールの料金
      t.boolean :disposable, default: true # 引き取ってもらえるかかどうか(今回は説明しません)
      t.string :import_file # データcsvファイル名(今回は説明しません)
      t.string :memo # メモ(今回は説明しません)
      t.date :updated_on # データ更新日(府中市HPに書かれている日付です。)
      t.timestamps
    end
  end
end

imortはrake taskで行います。

lib/tasks/db.rake
namespace :db do
  desc '府中市のcsvをインポート'
  # https://www.city.fuchu.tokyo.jp/kurashi/gomirisaikuru/dashikata/sodaigomidasikata.html
  # docker-compose exec web rake db:import['tmp/gojuon3012.csv','2019-01-05']
  task :import, %w[filename updated_on] => :environment do |_task, args|
    raise if args['filename'].nil? || args['updated_on'].nil?

    date = Date.parse(args['updated_on']).to_date
    Trash.import_csv(args['filename'], date) # データを取り込む
    Trash.all.each(&:update_name_furi) # フリガナをふる
  end
end

impor rakdeで使っている、importするメソッドをTrashにはやします。importは acitiverecord-import を使います。
配布されているCSVのエンコードcp932であり、UTF-8ではないので変換してあげてから取り込んでいます。

app/models/trash.rb
require 'csv'
class Trash < ApplicationRecord
  def self.import_csv(file, date)
    list = []
    ActiveRecord::Base.transaction do
      CSV.foreach(file, encoding: 'cp932', headers: true) do |row|
        import_file = File.basename(file)
        list << Trash.new(trash_params(row).merge(import_file: import_file, updated_on: date))
      end
      import list # activerecord-import のメソッド
    end
  end

  def self.trash_params(row)
    {
      name: row['品目'],
      disposal_cost: row['料金'] == '不可' ? nil : row['料金'].sub(',', ''), # csvを見ると 不可書かれているデータがあったことからその場合の変換を追加。
      disposable: row['料金'] == '不可'# csvを見ると 不可書かれているデータがあったことからその場合の変換を追加。
    }
  end
end

ただし、このまま取り込むと、ひらがなの検索ができないので、使い勝手がいまひとつ。なので、漢字をひらがなに変換して別カラム name_furi に取り込みます。
Trash.all.each(&:update_name_furi)
変換はrubyfuriを使いました。使うためにはYahoo Developer APIの入手が必要なので事前に取得して .env に記述してください。

app/models/trash.rb
  def update_name_furi(force: false)
    return if name_furi.present? || !force

    rubyfuri = Rubyfuri::Client.new(ENV['YAHOO_JAPAN_DEVELOPER_CLIENT_ID'])
    update(name_furi: rubyfuri.furu(name))
  end

これでデータの準備は完了です。

algoliaを使ってサーバーサイドで検索

Algolia API入出

まずalgoliaでアカウント作成しAPIを入手し、.env に記述してください。
Screenshot from Gyazo

algoliasearch-railsインストール

gem algoliasearch-railsをつかいます。設定はこのgemのREADMEを元に行います

algoliasearch-rails設定

APIキーを記述します。本アプリではAPIキーはdotenvを使っているので ENV[XXXX]でよびだしています。直接かいてもいいです。
algoliaはkaminariとwill_paginateにも対応しています。 pagination_backend にお使いのgemを記述してください。

config/initializers/algoliasearch.rb
AlgoliaSearch.configuration = {
    application_id: ENV['ALGOLIA_APPLICATION_ID'],
    api_key: ENV['ALGOLIA_ADMIN_API_KEY'],
    connect_timeout: 2,
    receive_timeout: 30,
    send_timeout: 30,
    batch_timeout: 120,
    search_timeout: 5,
    pagination_backend: :kaminari # 使用しているページネーションのgemを記載
}

TrashモデルにAlgoliaを使うためのコードを追加します。

app/models/trash.rb
class Trash < ApplicationRecord
  include AlgoliaSearch
  algoliasearch do
    attributes :name, :name_furi, :disposal_cost # インデックスしたいカラムを書く
    searchableAttributes %w[name name_furi] # 実際検索対象となるカラムを書く
    tags do # タグをつけたい場合は書く
      t = [disposable ? '粗大ゴミ可' : '粗大ゴミ不可']
      t << '無料' if disposal_cost&.zero?
      t
    end
    customRanking ['desc(disposal_cost)'] # 検索結果の重みが同順位の場合更にソートしたい場合は書く(今回はシールの値段が高い順にならべました)
  end

インデックスを作成するためのコマンドを実行します。

rails c
> Trash.reindex

これで検索の準備は完了です。

Algoliaを使って検索するには、以下の通りです。 Tarsh.search でもよいのですが、 もしransackを入れている場合ぶつかりますので、 algolia_search がおすすめです。
paginationのページ番号は page: にかいてください。

app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    @trashes = Trash.algolia_search(params[:query], hitsPerPage: 10, page: params[:page])
  end
end

あとは、普通のrailsと同じです。

ここまでのgithubのコードはこちらのリリースです。

algoliaを使ってフロントエンドでオートコンプリート

サーバーサイドで検索しましたので、今度はフロントエンドから検索して、オートコンプリートを実現します。

基本的なやり方は algolia/autocomplete.jsをご覧下さい。railsで使うためのインストールを解説します。(別にCDNを使ってライブラリを使う場合は適当に読み飛ばしてください。)

必要なlibraryをインストールします。

yarn add autocomplete.js
yarn add algoliasearch

呼び出すためのjsファイルを追加します。

app/javascript/packs/suggestionForm.js
const algoliasearch = require('algoliasearch'); // 呼び出す
const autocomplete = require('autocomplete.js'); // 呼び出す
const algolia_application_id = 'ALGOLIA_APPLICATION_ID'; // API KEY
const algolia_search_only_api_key = 'ALGOLIA_SEARCH_ONLY_API_KEY'; // API KEY

const client = algoliasearch(algolia_application_id, algolia_search_only_api_key);
const index = client.initIndex('Trash');
autocomplete('#query', {hint: false}, [ // id=queryの inputフォームを対象
  {
    source: autocomplete.sources.hits(index, {hitsPerPage: 5}), // 何件表示するか
    displayKey: function (suggestion) {
      return suggestion.name 
    },
    templates: {
      suggestion: function (suggestion) {
        return `${suggestion._highlightResult.name.value} / ${suggestion._highlightResult.name_furi.value}`; // 候補表示する文字列
      },
      empty: '検索キーワードを見直してください' // 検索結果がない時の表示
    }
  }
]).on('autocomplete:selected', function (event, suggestion, dataset, context) {
  console.log(event, suggestion, dataset, context);
  if (context.selectionMethod === 'click') {
    window.location.assign(`/?query=${suggestion.name}`); // 候補をクリックしたときの動作。今回は候補名をもとにGETリクエストを送ります。
    return;
  }
});

上記のjsファイルを呼び出します。

app/views/home/index.html.slim
// inputフォームのidがquery。もしこれが異なる場合は上記の #queryを置き換えてください。
      = f.text_field :query, value: params[:query], class: 'input', autocomplete: 'off' 
// 省略
= javascript_pack_tag 'suggestionForm'

これで、autocompleteで表示されるようになります。

このままだと文字が重なって見にくいので、autocomplete.jsで紹介されているcssを追加しておきます。

app/assets/stylesheets/algolia.css
.algolia-autocomplete {
    width: 100%;
}
.algolia-autocomplete .aa-input, .algolia-autocomplete .aa-hint {
    width: 100%;
}
.algolia-autocomplete .aa-hint {
    color: #999;
}
.algolia-autocomplete .aa-dropdown-menu {
    width: 100%;
    background-color: #fff;
    border: 1px solid #999;
    border-top: none;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion {
    cursor: pointer;
    padding: 5px 4px;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
    background-color: #B2D7FF;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion em {
    font-weight: bold;
    font-style: normal;
}

ここまでのリリースはこちらを参照ください。

Bulmaを使って飾り付け

Bulmaをインストールします。

yarn add bulma

application.cssをapplication.scss にリネームしてBulmaをimporとする記述を追加する。

app/assets/stylesheets/application.scss
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */
@import "bulma/bulma"

あとは、以下のような感じで書きます。

app/views/home/index.html.slim
h2.title 東京都府中市 粗大ごみの料金検索
div
  p 府中市が公開している「粗大ごみの料金表」より該当品のシール代金を表示します
  p 粗大ゴミとして廃棄したい物品の名前を入力して検索してください。
  p シールの購入方法はこちらの
  p = link_to '府中市粗大ごみの出し方', 'https://www.city.fuchu.tokyo.jp/kurashi/gomirisaikuru/dashikata/sodaigomidasikata.html'
  p からお願いします。
= form_with url: root_path, method: :get, local: true, autocomplete: 'off' do |f|
  .field.has-addons
    .control.is-expanded
      = f.text_field :query, value: params[:query], class: 'input', autocomplete: 'off'
    .control
      = f.submit '検索', class: 'button is-info'

.container
  div.columns.is-multiline
    - @trashes.each do |trash|
      div.column.is-6.is-12-mobile
        .card
          header.card-header
            p.card-header-title = trash.name
          .card-content
            .content
              p = "#{trash.disposal_cost} 円"
          footer.card-footer
            .card-footer-item
              = trash.updated_on


= paginate @trashes

= javascript_pack_tag 'suggestionForm'

以上。

今後

19
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
11