Redisで入力補完機能を作ってみる


## 目的

普段作っているGreenというサービスにて、企業名の入力補完を実装したい。
ただMySQLで実装するのは、性能的にもシステム設計的にも微妙すぎるので、[webpayさんのブログ](http://engineering.webpay.co.jp/2014/09/29/redis-inverted-index/)を参考にredisで実装してみました。

Redisの説明は流石に不要だと思うので、割愛させて頂きますw

## 実装方法

### 概要

- js側で入力をフックにapiにリクエストを飛ばす
- api側でredisに入れたデータを追加する

### コード

```ruby

class FluctuatedCompany
  REDIS_KEY = "fluctuated_company_names_inverted_index_v2"
                                                                                                                                                                                               
  def initialize
    @redis = Redis.new()
  end 

  def search(query)
    found = []
    next_cursor = 0 
    begin
      next_cursor_resp, resp = @redis.hscan(REDIS_KEY, next_cursor, 'MATCH', '*%s*' % query, 'COUNT', 100)
      next_cursor = next_cursor_resp.to_i
      resp.each_slice(2) { |pair| found += decode(pair[1]) }
    end while next_cursor > 0 
    found.uniq
  end 

  def add(company_id, *fluctuated_names)
    fluctuated_names.reject{ |fluctuated_name| fluctuated_name.blank? }.each do |fluctuated_name|
      ids = decode(@redis.hget(REDIS_KEY, [fluctuated_name]))
      ids << company_id
      @redis.hset(REDIS_KEY, fluctuated_name, ids.uniq.join(','))
    end 
  end 

  private
  def decode(value)
    (value || '').split(',').map(&:to_i)
  end 

  def encode(ids)
    ids.join(',')
  end 
end

```


```javascript

$(function() {
  // 以下企業名のオートコンプリート
  var suggestTimerID;
  var suggestBoxManager = new SuggestBoxManager({
    inputSelector: ".js-user_company_name_input",
    listSelector: ".js-suggest_company_box",
    eventSelector: ".js-user_company_name_box"
  }); 
  suggestBoxManager.generate();

  $(document).on('keyup paste', 'input.js-user_company_name_input', function() {
    var _self = $(this);
    var index = $('.js-user_company_name_input').index(_self);
    var suggestBox = suggestBoxManager.findByIndex(index);
    var suggestClearTimer = clearTimeout(suggestTimerID);
    
    suggestTimerID = setTimeout(function() {
      suggestBox.show();
      suggestBox.suggest(_self.val(), {
        done: function(data) {
          suggestBox.reset();
          for(var i = 0, len = data.companies.length; i < len; i++) {
            suggestBox.dom.append('※htmlを追加');
          }

          // リストに対してのclickイベントをbind
          $('.js-suggest_company_list').click(function() {
            $(this).closest('td').find('.js-user_company_id').val($(this).data('id'));
            suggestBox.inputDom.val($(this).data('name')).change();
            suggestBox.hide();
          }); 
        }   
      }); 
    },300);

  }); 
});

var SuggestBoxManager = function(customOptions) {                                                                                                                                              
  this.container = [];
  this.options = $.extend({}, {
                   inputSelector: '.js-user_company_name_input',
                   listSelector: ".js-suggest_company_box",
                   eventSelector: ".js-user_company_name_box"
                 }, customOptions);
}

SuggestBoxManager.prototype = {
  add: function(suggestBox) {
    this.container.push(suggestBox);
  },
  generate: function(){
    var _self = this;
    var target = $(_self.options.eventSelector);
    target.each( function(){
      var suggestBox = new SuggestBox($(this).find(_self.options.listSelector), $(this).find(_self.options.inputSelector));
      _self.add(suggestBox);
    });
  },
  regenere: function(){
    this.generate();
  },
  findByIndex: function(index){
     return this.container[index];
  }
}

var SuggestBox = function(dom, inputDom, customOptions){
  if (customOptions === undefined) customOptions = {};
  this.dom = dom;
  this.inputDom = inputDom;
  this.options = $.extend({}, {
                   resourceUrl: '/apis/companies.json'
                 }, customOptions);
}

SuggestBox.prototype = {
  show: function() {
    this.dom.show();
  },
  hide: function() {
    this.dom.hide();
  },
  reset: function() {
    this.dom.html('');
  },
  suggest: function(resourceName, callback) {
    var callback = (callback === undefined) ? {} : callback;
    $.ajax({
      method: "GET",
      data: { resource_name: resourceName },
      url : this.options.resourceUrl,
      context: this,
      beforeSend: function(){
        if(callback.beforeSend) callback.beforeSend();
      }
    }).done(function(data, textStatus, jqXHR){
      if(callback.done) callback.done(data, textStatus, jqXHR);                                                                                                                                
    }).fail(function(jqXHR, textStatus, errorThrown){
      if(callback.fail) callback.fail(jqXHR, textStatus, errorThrown);
    }).always(function(xhr,status){
      if(callback.always) callback.always(xhr,status);
    });
  }
}
```

## 準備

addメソッドを使って入力補完に使いたい候補データを流し込んで下さい。

## 結果

UIは下記の通りです。いい感じに仕上がりました。
さすがredis良い仕事をしてくれますね！

![gif_m.gif](https://qiita-image-store.s3.amazonaws.com/0/5365/1d1fab44-8796-00df-0624-6f7f1184fb0e.gif "gif_m.gif")