soulmate_rails 0.2.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ # rcov generated
2
+ coverage
3
+
4
+ # rdoc generated
5
+ rdoc
6
+
7
+ # yard generated
8
+ doc
9
+ .yardoc
10
+
11
+ # bundler
12
+ .bundle
13
+
14
+ # jeweler generated
15
+ pkg
16
+
17
+ test/db/*.rdb
18
+
19
+ Gemfile.lock
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.3@soulmate_rails --create
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Eric Waller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,93 @@
1
+ # Soulmate Rails
2
+
3
+ Soulmate Rails is a rails plugin that helps to solve the common problem of
4
+ auto-completion in rails intuitively. It extends the soulmate gem <a
5
+ href="http://github.com/seatgeek/soulmate">Soulmate</a> to make it easily
6
+ pluggable into a rails project.
7
+
8
+ ## Getting Started
9
+
10
+ ### Installation :
11
+
12
+ ```sh
13
+ $ gem install soulmate_rails
14
+ ```
15
+
16
+ ### Usage :
17
+
18
+ Following is an example of how one can use Soulmate Rails for enabling backend
19
+ autocompletion using redis.
20
+
21
+ ```ruby
22
+ class User < ActiveRecord::Base
23
+ autocomplete :first_name, :score => :calculate_score
24
+ autocomplete :last_name, :score => :id
25
+
26
+ def calculate_score
27
+ # Some magic calculation returning a number.
28
+ end
29
+ end
30
+
31
+ 1.9.3p385 :001 > User.create(:first_name => 'First1', :last_name => 'Last1')
32
+ 1.9.3p385 :002 > User.create(:first_name => 'First2', :last_name => 'Last2')
33
+ 1.9.3p385 :003 > User.create(:first_name => 'First3', :last_name => 'Last3')
34
+ 1.9.3p385 :004 > User.search_by_first_name('firs')
35
+ => [#<User:0x000000014bb1e8 @new_record=false,
36
+ @attributes={"first_name"=>"First3", "last_name"=>"Last3" "id"=>3},
37
+ @changed_attributes={}>, #<User:0x000000014bb1e9 @new_record=false,
38
+ @attributes={"first_name"=>"First2", "last_name"=>"Last2" "id"=>2},
39
+ @changed_attributes={}>, #<User:0x000000014bb1ea @new_record=false,
40
+ @attributes={"first_name"=>"First1", "last_name"=>"Last1" "id"=>1},
41
+ @changed_attributes={}>]
42
+ 1.9.3p385 :005 > User.search_by_last_name('last1')
43
+ => [#<User:0x000000014bb1e8 @new_record=false,
44
+ @attributes={"first_name"=>"First3", "last_name"=>"Last3" "id"=>3},
45
+ @changed_attributes={}>]
46
+ ```
47
+
48
+ The `autocomplete` method takes 2 arguments :
49
+
50
+ * attribute name to use for autocompletion.
51
+ * options that determine how autocompletion works for indexing.
52
+
53
+ Methods added by autocomplete :
54
+
55
+ * Class Methods
56
+ * `search_by(attribute, term, options={})` - Generic method to search by
57
+ an attribute for which an autocomplete was defined.
58
+ * `search_by_#{attribute}(term, options={})` - Specific methods for each
59
+ attribute autocomplete was defined for.
60
+ * Instance Methods
61
+ * `update_index_for(attribute, options={})`
62
+ * `update_index_for_#{attribute}` - used in an `after_save` callback to
63
+ update index for searching.
64
+ * `remove_index_for(attribute)`
65
+ * `remove_index_for_#{attribute}` - used in a `before_destroy` callback to
66
+ remove index for searching. Hence you should use `destroy` as opposed to
67
+ `delete` to ensure the callbacks are invoked appropriately by rails and
68
+ soulmate updates the index.
69
+
70
+ Options you can provide to `autocomplete` :
71
+
72
+ * `:score` : This is required. Soulmate uses it for sorting the results (in
73
+ reverse order, i.e. higher score first). This can be the name of a function
74
+ or can also be the name of another attribute with integer values.
75
+ * `:aliases` : This is optional. Soulmate uses this as aliases for the term
76
+ field and uses it for searching as well. This can be an array of values or
77
+ a method name which returns an array of values.
78
+
79
+ ## Contributing
80
+ ### Reporting an Issue :
81
+ * Use <a href="http://github.com/dhruvasagar/soulmate_rails/issues">Github
82
+ Issue Tracker</a> to report issues.
83
+
84
+ ### Contributing to code :
85
+ * Fork it.
86
+ * Commit your changes ( git commit ).
87
+ * Push to github ( git push ).
88
+ * Open a Pull Request.
89
+
90
+ ## License
91
+ Soulmate Rails is released under the MIT License
92
+
93
+ <!-- vim: set tw=80 colorcolumn=80 -->
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ require File.join(File.expand_path('../lib', __FILE__), 'soulmate_rails', 'version')
5
+
6
+ RSpec::Core::RakeTask.new
7
+ task :default => :spec
8
+
9
+ desc 'Builds the gem'
10
+ task :build do
11
+ sh 'gem build soulmate_rails.gemspec'
12
+ Dir.mkdir('pkg') unless File.directory?('pkg')
13
+ sh "mv soulmate_rails-#{SoulmateRails::VERSION}.gem pkg/"
14
+ end
15
+
16
+ desc 'Builds and Installs the gem'
17
+ task :install => :build do
18
+ sh "gem install pkg/soulmate_rails-#{SoulmateRails::VERSION}.gem"
19
+ end
20
+
21
+ desc 'Open an irb session preloaded with this library'
22
+ task :console do
23
+ sh 'irb -rubygems -I lib -r soulmate_rails.rb'
24
+ end
@@ -0,0 +1,23 @@
1
+ module Soulmate
2
+ class Base
3
+ include Helpers
4
+
5
+ attr_accessor :type
6
+
7
+ def initialize(type)
8
+ @type = normalize(type)
9
+ end
10
+
11
+ def base
12
+ "soulmate-index:#{type}"
13
+ end
14
+
15
+ def database
16
+ "soulmate-data:#{type}"
17
+ end
18
+
19
+ def cachebase
20
+ "soulmate-cache:#{type}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # coding: utf-8
2
+
3
+ module Soulmate
4
+ module Helpers
5
+
6
+ def prefixes_for_phrase(phrase)
7
+ words = normalize(phrase).split(' ').reject do |w|
8
+ Soulmate.stop_words.include?(w)
9
+ end
10
+ words.map do |w|
11
+ (MIN_COMPLETE-1..(w.length-1)).map{ |l| w[0..l] }
12
+ end.flatten.uniq
13
+ end
14
+
15
+ def normalize(str)
16
+ # Letter, Mark, Number, Connector_Punctuation (Chinese, Japanese, etc.)
17
+ str.downcase.gsub(/[^\p{Word}\ ]/i, '').strip
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,63 @@
1
+ module Soulmate
2
+ class Loader < Base
3
+
4
+ def load(items)
5
+ # delete the sorted sets for this type
6
+ phrases = Soulmate.redis.smembers(base)
7
+ Soulmate.redis.pipelined do
8
+ phrases.each do |p|
9
+ Soulmate.redis.del("#{base}:#{p}")
10
+ end
11
+ Soulmate.redis.del(base)
12
+ end
13
+
14
+ # Redis can continue serving cached requests for this type while the reload is
15
+ # occuring. Some requests may be cached incorrectly as empty set (for requests
16
+ # which come in after the above delete, but before the loading completes). But
17
+ # everything will work itself out as soon as the cache expires again.
18
+
19
+ # delete the data stored for this type
20
+ Soulmate.redis.del(database)
21
+
22
+ items.each_with_index do |item, i|
23
+ add(item, :skip_duplicate_check => true)
24
+ end
25
+ end
26
+
27
+ # "id", "term", "score", "aliases", "data"
28
+ def add(item, opts = {})
29
+ opts = { :skip_duplicate_check => false }.merge(opts)
30
+ raise ArgumentError, 'Item must have an id AND a term' unless item["id"] && item["term"]
31
+
32
+ # kill any old items with this id
33
+ remove("id" => item["id"]) unless opts[:skip_duplicate_check]
34
+
35
+ Soulmate.redis.pipelined do
36
+ # store the raw data in a separate key to reduce memory usage
37
+ Soulmate.redis.hset(database, item["id"], MultiJson.encode(item))
38
+ phrase = ([item["term"]] + (item["aliases"] || [])).join(' ')
39
+ prefixes_for_phrase(phrase).each do |p|
40
+ Soulmate.redis.sadd(base, p) # remember this prefix in a master set
41
+ Soulmate.redis.zadd("#{base}:#{p}", item["score"], item["id"]) # store the id of this term in the index
42
+ end
43
+ end
44
+ end
45
+
46
+ # remove only cares about an item's id, but for consistency takes an object
47
+ def remove(item)
48
+ prev_item = Soulmate.redis.hget(database, item["id"])
49
+ if prev_item
50
+ prev_item = MultiJson.decode(prev_item)
51
+ # undo the operations done in add
52
+ Soulmate.redis.pipelined do
53
+ Soulmate.redis.hdel(database, prev_item["id"])
54
+ phrase = ([prev_item["term"]] + (prev_item["aliases"] || [])).join(' ')
55
+ prefixes_for_phrase(phrase).each do |p|
56
+ Soulmate.redis.srem(base, p)
57
+ Soulmate.redis.zrem("#{base}:#{p}", prev_item["id"])
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,31 @@
1
+ module Soulmate
2
+ class Matcher < Base
3
+
4
+ def matches_for_term(term, options = {})
5
+ options = { :limit => 5, :cache => true }.merge(options)
6
+
7
+ words = normalize(term).split(' ').reject do |w|
8
+ w.size < MIN_COMPLETE or Soulmate.stop_words.include?(w)
9
+ end.sort
10
+
11
+ return [] if words.empty?
12
+
13
+ cachekey = "#{cachebase}:" + words.join('|')
14
+
15
+ if !options[:cache] || !Soulmate.redis.exists(cachekey)
16
+ interkeys = words.map { |w| "#{base}:#{w}" }
17
+ Soulmate.redis.zinterstore(cachekey, interkeys)
18
+ Soulmate.redis.expire(cachekey, 10 * 60) # expire after 10 minutes
19
+ end
20
+
21
+ ids = Soulmate.redis.zrevrange(cachekey, 0, options[:limit] - 1)
22
+ if ids.size > 0
23
+ results = Soulmate.redis.hmget(database, *ids)
24
+ results = results.reject{ |r| r.nil? } # handle cached results for ids which have since been deleted
25
+ results.map { |r| MultiJson.decode(r) }
26
+ else
27
+ []
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ require 'uri'
2
+ require 'redis'
3
+ require 'multi_json'
4
+
5
+ require 'active_support/concern'
6
+
7
+ require 'soulmate/helpers'
8
+ require 'soulmate/base'
9
+ require 'soulmate/matcher'
10
+ require 'soulmate/loader'
11
+
12
+ require 'soulmate_rails/model_additions'
13
+ require 'soulmate_rails/railtie' if defined? Rails
14
+
15
+ module Soulmate
16
+
17
+ extend self
18
+
19
+ MIN_COMPLETE = 2
20
+ DEFAULT_STOP_WORDS = ["vs", "at", "the"]
21
+
22
+ def redis=(server)
23
+ if server.is_a?(String)
24
+ @redis = nil
25
+ @redis_url = server
26
+ else
27
+ @redis = server
28
+ end
29
+
30
+ redis
31
+ end
32
+
33
+ def redis
34
+ @redis ||= (
35
+ url = URI(@redis_url || ENV["REDIS_URL"] || "redis://127.0.0.1:6379/0")
36
+
37
+ ::Redis.new({
38
+ :host => url.host,
39
+ :port => url.port,
40
+ :db => url.path[1..-1],
41
+ :password => url.password
42
+ })
43
+ )
44
+ end
45
+
46
+ def stop_words
47
+ @stop_words ||= DEFAULT_STOP_WORDS
48
+ end
49
+
50
+ def stop_words=(arr)
51
+ @stop_words = Array(arr).flatten
52
+ end
53
+
54
+ end
@@ -0,0 +1,61 @@
1
+ module SoulmateRails
2
+ module ModelAdditions
3
+ extend ActiveSupport::Concern
4
+
5
+ def update_index_for(attribute, options={})
6
+ loader = instance_variable_get("@#{loader_for(attribute)}") || instance_variable_set("@#{loader_for(attribute)}", Soulmate::Loader.new(loader_for(attribute)))
7
+ item = {
8
+ 'id' => "#{attribute}_#{self.id}",
9
+ 'term' => send(attribute),
10
+ 'score' => ( respond_to?(options[:score]) ? send(options[:score]) : options[:score] )
11
+ }.merge( options[:aliases] ? ( respond_to?(options[:aliases]) ? send(options[:aliases]) : options[:aliases] ) : {} )
12
+ # NOTE: Not supporting :data for now, will find a better way to use this later.
13
+ # .merge( options[:data] ? ( respond_to?(options[:data]) ? send(options[:data]) : options[:data] ) : {} )
14
+ loader.add(item, options)
15
+ end
16
+
17
+ def remove_index_for(attribute)
18
+ loader = instance_variable_get("@#{loader_for(attribute)}") || instance_variable_set("@#{loader_for(attribute)}", Soulmate::Loader.new(loader_for(attribute)))
19
+ loader.remove('id' => "#{attribute}_#{self.id}")
20
+ end
21
+
22
+ def loader_for(attribute)
23
+ "#{self.class.normalized_class_name}_#{attribute}"
24
+ end
25
+
26
+ module ClassMethods
27
+ def autocomplete(attribute, options={})
28
+ define_method "update_index_for_#{attribute}" do
29
+ update_index_for(attribute, options)
30
+ end
31
+ after_save "update_index_for_#{attribute}"
32
+
33
+ define_method "remove_index_for_#{attribute}" do
34
+ remove_index_for(attribute)
35
+ end
36
+ before_destroy "remove_index_for_#{attribute}"
37
+
38
+ define_singleton_method "search_by_#{attribute}" do |term, *opts|
39
+ opts = opts.empty? ? {} : opts.first
40
+ search_by(attribute, term, opts)
41
+ end
42
+ end
43
+
44
+ def search_by(attribute, term, options={})
45
+ matcher = instance_variable_get("@#{matcher_for(attribute)}") || instance_variable_set("@#{matcher_for(attribute)}", Soulmate::Matcher.new(matcher_for(attribute)))
46
+ matches = matcher.matches_for_term(term, options)
47
+ matches = matches.map do |match|
48
+ find(match['id'].split('_')[-1].to_i) rescue nil
49
+ end.compact
50
+ end
51
+
52
+ def matcher_for(attribute)
53
+ "#{normalized_class_name}_#{attribute}"
54
+ end
55
+
56
+ def normalized_class_name
57
+ self.name.gsub('::', '_').downcase
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,15 @@
1
+ module SoulmateRails
2
+ class Railtie < Rails::Railtie
3
+ initializer 'soulmate_rails.model_additions' do
4
+ ActiveSupport.on_load :active_record do
5
+ include ModelAdditions
6
+ end
7
+ end
8
+
9
+ initializer 'soulmate_rails.set_configs' do |app|
10
+ options = app.config.soulmate_rails
11
+
12
+ Soulmate.redis = options[:redis] if options[:redis]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ module SoulmateRails
2
+ MAJOR = 0
3
+ MINOR = 2
4
+ PATCH = 0
5
+ STATUS = 'alpha'
6
+
7
+ VERSION = [MAJOR, MINOR, PATCH, STATUS].compact.join('.')
8
+ end
@@ -0,0 +1,33 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ $:.push File.expand_path("../lib", __FILE__)
7
+ require "soulmate_rails/version"
8
+
9
+ Gem::Specification.new do |s|
10
+ s.name = "soulmate_rails"
11
+ s.email = ["dhruva.sagar@gmail.com"]
12
+ s.version = SoulmateRails::VERSION
13
+ s.authors = ["Dhruva Sagar"]
14
+ s.homepage = "http://github.com/dhruvasagar/soulmate_rails"
15
+ s.summary = "Redis-backed autocompletion engine for Rails"
16
+ s.description = "Soulmate Rails is a tool to help solve the common problem of developing a fast autocomplete feature for Rails. It uses Redis's sorted sets to build an index of partial words and corresponding top matches."
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map {|f| File.basename(f)}
21
+
22
+ s.licenses = ["MIT"]
23
+ s.require_paths = ["lib"]
24
+
25
+ s.add_runtime_dependency 'redis', '>= 2.0'
26
+ s.add_runtime_dependency 'multi_json', '>= 1.0'
27
+ s.add_runtime_dependency 'activesupport', '>= 0'
28
+
29
+ s.add_development_dependency 'rake', '>= 0'
30
+ s.add_development_dependency 'rspec', '>= 0'
31
+ s.add_development_dependency 'supermodel', '>= 0'
32
+ s.add_development_dependency 'mock_redis', '>= 0'
33
+ end
@@ -0,0 +1,8 @@
1
+ dir ./spec/db
2
+ pidfile ./redis.pid
3
+ port 6380
4
+ timeout 300
5
+ loglevel debug
6
+ logfile stdout
7
+ databases 16
8
+ daemonize yes
@@ -0,0 +1,4 @@
1
+ vs
2
+ at
3
+ the
4
+ to
@@ -0,0 +1,7 @@
1
+ {"id":1,"term":"Dodger Stadium","score":84,"data":{"url":"\/dodger-stadium-tickets\/","subtitle":"Los Angeles, CA"},"aliases":["Chavez Ravine"]}
2
+ {"id":28,"term":"Angel Stadium","score":90,"data":{"url":"\/angel-stadium-tickets\/","subtitle":"Anaheim, CA"},"aliases":["Edison International Field of Anaheim"]}
3
+ {"id":30,"term":"Chase Field ","score":80,"data":{"url":"\/chase-field-tickets\/","subtitle":"Phoenix, AZ"},"aliases":["Bank One Ballpark", "Bank One Stadium"]}
4
+ {"id":29,"term":"Sun Life Stadium","score":75,"data":{"url":"\/sun-life-stadium-tickets\/","subtitle":"Miami, FL"},"aliases":["Dolphins Stadium","Land Shark Stadium"]}
5
+ {"id":2,"term":"Turner Field","score":50,"data":{"url":"\/turner-field-tickets\/","subtitle":"Atlanta, GA"}}
6
+ {"id":3,"term":"Citi Field","score":92,"data":{"url":"\/citi-field-tickets\/","subtitle":"Atlanta, GA"},"aliases":["Shea Stadium"]}
7
+ {"id":8,"term":"中国佛山 李小龙","score":94,"data":{"url":"\/Bruce Lee\/","subtitle":"Chinese Foshan"},"aliases":["Li XiaoLong"]}
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ module Soulmate
4
+ describe Loader do
5
+ before :each do
6
+ @loader = Loader.new('venues')
7
+ end
8
+
9
+ context 'add' do
10
+ it 'should successfully add acceptable value' do
11
+ expect {
12
+ @loader.add({"id" => 11,"term" => "Dodger Stadium","score" => 84,"data" => {"url" => "\/dodger-stadium-tickets\/","subtitle" => "Los Angeles, CA"},"aliases" => ["Chavez Ravine"]})
13
+ }.to_not raise_error
14
+ end
15
+
16
+ context 'invalid item' do
17
+ it 'should raise ArgumentError if id is missing' do
18
+ expect {
19
+ @loader.add({"term" => "Dodger Stadium","score" => 84,"data" => {"url" => "\/dodger-stadium-tickets\/","subtitle" => "Los Angeles, CA"},"aliases" => ["Chavez Ravine"]})
20
+ }.to raise_error
21
+ end
22
+
23
+ it 'should raise ArgumentError if term is missing' do
24
+ expect {
25
+ @loader.add({"id" => 11, "score" => 84,"data" => {"url" => "\/dodger-stadium-tickets\/","subtitle" => "Los Angeles, CA"},"aliases" => ["Chavez Ravine"]})
26
+ }.to raise_error(ArgumentError)
27
+ end
28
+ end
29
+ end
30
+
31
+ context 'load' do
32
+ before :each do
33
+ @items = []
34
+ venues = File.open(TestRoot + '/samples/venues.json', 'r')
35
+ venues.each_line do |venue|
36
+ @items << MultiJson.decode(venue)
37
+ end
38
+ @items_loaded = @loader.load(@items)
39
+ end
40
+
41
+ it 'should load values' do
42
+ @items_loaded.size.should eq(7)
43
+ end
44
+ end
45
+
46
+ context 'integration' do
47
+ before :each do
48
+ @matcher = Matcher.new('venues')
49
+ end
50
+
51
+ it 'should successfully remove the item' do
52
+ @loader.load([])
53
+ results = @matcher.matches_for_term('te', :cache => false)
54
+ results.size.should eq(0)
55
+
56
+ @loader.add('id' => 1, 'term' => 'Testing this', 'score' => 10)
57
+ results = @matcher.matches_for_term('te', :cache => false)
58
+ results.size.should eq(1)
59
+
60
+ @loader.remove('id' => 1)
61
+ results = @matcher.matches_for_term('te', :cache => false)
62
+ results.size.should eq(0)
63
+ end
64
+
65
+ it 'should successfully update items' do
66
+ @loader.load([])
67
+ @loader.add("id" => 1, "term" => "Testing this", "score" => 10)
68
+ @loader.add("id" => 2, "term" => "Another Term", "score" => 9)
69
+ @loader.add("id" => 3, "term" => "Something different", "score" => 5)
70
+
71
+ results = @matcher.matches_for_term('te', :cache => false)
72
+ results.size.should eq(2)
73
+ results.first['term'].should eq('Testing this')
74
+ results.first['score'].should eq(10)
75
+
76
+ @loader.add("id" => 1, "term" => "Updated", "score" => 5)
77
+ results = @matcher.matches_for_term('te', :cache => false)
78
+ results.size.should eq(1)
79
+ results.first['term'].should eq('Another Term')
80
+ results.first['score'].should eq(9)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Soulmate
6
+ describe Matcher do
7
+ before :each do
8
+ items = []
9
+ loader = Loader.new('venues')
10
+ venues = File.open(TestRoot + '/samples/venues.json', 'r')
11
+ venues.each_line do |venue|
12
+ items << MultiJson.decode(venue)
13
+ end
14
+ loader.load(items)
15
+
16
+ @matcher = Matcher.new('venues')
17
+ end
18
+
19
+ it 'should successfully return matches for given term' do
20
+ results = @matcher.matches_for_term('stad')
21
+ results.size.should eq(5)
22
+ results.first['term'].should eq('Citi Field')
23
+ end
24
+
25
+ it 'should successfully return matches with aliases' do
26
+ results = @matcher.matches_for_term('land shark stadium')
27
+ results.size.should eq(1)
28
+ results.first['term'].should eq('Sun Life Stadium')
29
+ end
30
+
31
+ it 'should successfully return matches with chinese' do
32
+ results = @matcher.matches_for_term('中国')
33
+ results.size.should eq(1)
34
+ results.first['term'].should eq('中国佛山 李小龙')
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ module SoulmateRails
4
+ class User < SuperModel::Base
5
+ include ModelAdditions
6
+
7
+ autocomplete :name, :score => :id
8
+ end
9
+
10
+ describe ModelAdditions do
11
+ context 'single autocomplete' do
12
+ before :each do
13
+ @user = User.create(:name => 'Dhruva Sagar')
14
+ end
15
+
16
+ it 'should successfully search by name' do
17
+ users = User.search_by_name('dhruv')
18
+ user = users.first
19
+ user.should eq(@user)
20
+ end
21
+ end
22
+
23
+ context 'multiple autocompletes' do
24
+ before :each do
25
+ # Define another autocomplete for country
26
+ User.autocomplete(:country, :score => :id)
27
+ @user = User.create(:name => 'Dhruva Sagar', :country => 'India')
28
+ end
29
+
30
+ it 'should successfully search by name as well as country' do
31
+ users = User.search_by_name('dhr')
32
+ user = users.first
33
+ user.should eq(@user)
34
+
35
+ users = User.search_by_country('ind')
36
+ user = users.first
37
+ user.should eq(@user)
38
+ end
39
+ end
40
+
41
+ after :each do
42
+ User.destroy_all
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ require 'supermodel'
2
+ require 'mock_redis'
3
+ require 'soulmate_rails'
4
+
5
+ TestRoot = File.expand_path(File.dirname(__FILE__))
6
+
7
+ RSpec.configure do |config|
8
+ config.order = 'random'
9
+ config.color_enabled = true
10
+
11
+ config.before(:each) do
12
+ redis = MockRedis.new
13
+ redis.flushdb
14
+ Soulmate.redis = redis
15
+ end
16
+ end
17
+
metadata ADDED
@@ -0,0 +1,186 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: soulmate_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0.alpha
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Dhruva Sagar
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '2.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: multi_json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '1.0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: activesupport
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: supermodel
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: mock_redis
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Soulmate Rails is a tool to help solve the common problem of developing
127
+ a fast autocomplete feature for Rails. It uses Redis's sorted sets to build an index
128
+ of partial words and corresponding top matches.
129
+ email:
130
+ - dhruva.sagar@gmail.com
131
+ executables: []
132
+ extensions: []
133
+ extra_rdoc_files: []
134
+ files:
135
+ - .document
136
+ - .gitignore
137
+ - .rvmrc
138
+ - Gemfile
139
+ - LICENSE.txt
140
+ - README.markdown
141
+ - Rakefile
142
+ - lib/soulmate/base.rb
143
+ - lib/soulmate/helpers.rb
144
+ - lib/soulmate/loader.rb
145
+ - lib/soulmate/matcher.rb
146
+ - lib/soulmate_rails.rb
147
+ - lib/soulmate_rails/model_additions.rb
148
+ - lib/soulmate_rails/railtie.rb
149
+ - lib/soulmate_rails/version.rb
150
+ - soulmate_rails.gemspec
151
+ - spec/config/test.conf
152
+ - spec/samples/stop-words.txt
153
+ - spec/samples/venues.json
154
+ - spec/soulmate/loader_spec.rb
155
+ - spec/soulmate/matcher_spec.rb
156
+ - spec/soulmate_rails/model_additions_spec.rb
157
+ - spec/spec_helper.rb
158
+ homepage: http://github.com/dhruvasagar/soulmate_rails
159
+ licenses:
160
+ - MIT
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ none: false
167
+ requirements:
168
+ - - ! '>='
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ segments:
172
+ - 0
173
+ hash: -2339712200945415298
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ none: false
176
+ requirements:
177
+ - - ! '>'
178
+ - !ruby/object:Gem::Version
179
+ version: 1.3.1
180
+ requirements: []
181
+ rubyforge_project:
182
+ rubygems_version: 1.8.25
183
+ signing_key:
184
+ specification_version: 3
185
+ summary: Redis-backed autocompletion engine for Rails
186
+ test_files: []