soulmate_rails 0.2.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []