ion 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,56 @@
1
+ # An index
2
+ #
3
+ # You can subclass me by reimplementing #index, #deindex and #search.
4
+ #
5
+ class Ion::Index
6
+ attr_reader :name
7
+ attr_reader :options
8
+
9
+ def initialize(name, options, args={}, &blk)
10
+ @name = name
11
+ @options = options
12
+ @lambda = blk if block_given?
13
+ @lambda ||= Proc.new { self.send(name) }
14
+ end
15
+
16
+ # Indexes a record
17
+ def index(record)
18
+ end
19
+
20
+ def self.deindex(record)
21
+ end
22
+
23
+ # Completely obliterates traces of a record from the indices
24
+ def self.del(record)
25
+ self.deindex record
26
+ references_key(record).del
27
+ end
28
+
29
+ # Returns a key (set) of results
30
+ def search(what, args={})
31
+ end
32
+
33
+ protected
34
+
35
+ # Returns the value for a certain record
36
+ def value_for(record)
37
+ record.instance_eval &@lambda
38
+ end
39
+
40
+ # Returns the index key
41
+ # Example: Ion:Album:text:title
42
+ def index_key
43
+ @type ||= self.class.name.split(':').last.downcase
44
+ @options.key[@type][self.name]
45
+ end
46
+
47
+ # Ion:Album:references:1:text
48
+ def self.references_key(record)
49
+ type = self.name.split(':').last.downcase
50
+ record.class.ion.key[:references][record.id][type]
51
+ end
52
+
53
+ def references_key(record)
54
+ self.class.references_key record
55
+ end
56
+ end
@@ -0,0 +1,18 @@
1
+ module Ion
2
+ module Indices
3
+ autoload :Text, "#{Ion::PREFIX}/indices/text"
4
+ autoload :Number, "#{Ion::PREFIX}/indices/number"
5
+ autoload :Metaphone, "#{Ion::PREFIX}/indices/metaphone"
6
+ autoload :Sort, "#{Ion::PREFIX}/indices/sort"
7
+
8
+ def self.names
9
+ [ :text, :number, :metaphone, :sort ]
10
+ end
11
+
12
+ def self.get(name)
13
+ name = Stringer.classify(name).to_sym
14
+ raise InvalidIndexType unless const_defined?(name)
15
+ const_get(name)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ module Ion
2
+ class Indices::Metaphone < Indices::Text
3
+ def index_words(str)
4
+ words = ::Text::Metaphone.metaphone(str).strip.split(' ')
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ module Ion
2
+ class Indices::Number < Indices::Text
3
+ MARGIN = 0.0001
4
+
5
+ def index(record)
6
+ value = value_for(record).to_f
7
+ refs = references_key(record)
8
+
9
+ index_key.zadd value, record.id
10
+ refs.sadd index_key
11
+ end
12
+
13
+ def search(what, args={})
14
+ key = Ion.volatile_key
15
+ key.zunionstore [index_key] #copy
16
+
17
+ if what.is_a?(Hash)
18
+ # Strip away the upper/lower limits.
19
+ key.zremrangebyscore '-inf', what[:gt] if what[:gt]
20
+ key.zremrangebyscore what[:lt], '+inf' if what[:lt]
21
+ key.zremrangebyscore '-inf', (what[:min].to_f-MARGIN) if what[:min]
22
+ key.zremrangebyscore (what[:max].to_f+MARGIN), '+inf' if what[:max]
23
+ else
24
+ key.zremrangebyscore '-inf', what.to_f-MARGIN
25
+ key.zremrangebyscore what.to_f+MARGIN, '+inf'
26
+ end
27
+
28
+ key
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ module Ion
2
+ class Indices::Sort < Ion::Index
3
+ def index(record)
4
+ value = value_for(record)
5
+ value = transform(value)
6
+
7
+ key_for(record).hset name, value
8
+ end
9
+
10
+ # The function that the string passes thru before going to the db.
11
+ def transform(value)
12
+ str = value.to_s.downcase.strip
13
+ str = str[4..-1] if str[0..3] == "the " # Remove articles from sorting
14
+ str
15
+ end
16
+
17
+ def self.deindex(record)
18
+ key_for(record).del
19
+ end
20
+
21
+ def spec
22
+ # Ion:sort:Album:*->title
23
+ @spec ||= self.class.key[@options.model.name]["*->#{name}"]
24
+ end
25
+
26
+ protected
27
+ def self.key_for(record)
28
+ # Ion:sort:Album:1
29
+ key[record.class.name][record.id]
30
+ end
31
+
32
+ def self.key
33
+ # Ion:sort
34
+ Ion.key[:sort]
35
+ end
36
+
37
+ def key_for(record)
38
+ self.class.key_for(record)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ module Ion
2
+ class Indices::Text < Index
3
+ def index_words(str)
4
+ Stringer.keywords str
5
+ end
6
+
7
+ def search_words(str)
8
+ index_words str
9
+ end
10
+
11
+ def index(record)
12
+ super
13
+ words = index_words(value_for(record).to_s)
14
+ refs = references_key(record)
15
+
16
+ words.each do |word|
17
+ k = index_key[word]
18
+ refs.sadd k
19
+ k.zadd 1, record.id
20
+ end
21
+ end
22
+
23
+ def self.deindex(record)
24
+ super
25
+ refs = references_key(record)
26
+ refs.smembers.each do |key|
27
+ Ion.redis.zrem(key, record.id)
28
+ Ion.redis.del(key) if Ion.redis.zrange(key,0,0).nil?
29
+ end
30
+ end
31
+
32
+ def search(what, args={})
33
+ super
34
+ words = search_words(what)
35
+ keys = words.map { |word| index_key[word] }
36
+
37
+ w = keys.map { 0 }; w[0] = 1
38
+ Ion.intersect keys, weights: w
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ class Ion::Options
2
+ attr_reader :model
3
+
4
+ def initialize(model)
5
+ @model = model
6
+ @indices = Hash.new { |h, k| h[k] = Hash.new }
7
+ end
8
+
9
+ def search(&blk)
10
+ Ion::Search.new(self, &blk)
11
+ end
12
+
13
+ def key
14
+ @key ||= Ion.key[model.name] #=> 'Ion:Person'
15
+ end
16
+
17
+ # Returns a certain index.
18
+ # @example
19
+ # @options.index(:text, :title) #=> <#Ion::Indices::Text>
20
+ def index(type, name)
21
+ @indices[type][name]
22
+ end
23
+
24
+ # Returns all indices.
25
+ def indices
26
+ @indices.values.map(&:values).flatten
27
+ end
28
+
29
+ def index_types
30
+ indices.map(&:class).uniq
31
+ end
32
+
33
+ protected
34
+ # Creates the shortcuts `text :foo` => `field :text, :foo`
35
+ Ion::Indices.names.each do |type|
36
+ define_method(type) do |id, options={}, &blk|
37
+ field type, id, options, &blk
38
+ end
39
+ end
40
+
41
+ def field(type, id, options={}, &blk)
42
+ index_type = Ion::Indices.get(type)
43
+ @indices[type][id.to_sym] = index_type.new(id, self, options, &blk)
44
+ end
45
+ end
46
+
@@ -0,0 +1,163 @@
1
+ class Ion::Scope
2
+ include Ion::Helpers
3
+
4
+ attr_writer :key
5
+
6
+ def initialize(search, args={}, &blk)
7
+ @search = search
8
+ @gate = args[:gate] || :all
9
+ @score = args[:score] || 1.0
10
+ @type = :z # or :l
11
+
12
+ yieldie(&blk) and done if block_given?
13
+
14
+ raise Ion::Error unless [:all, :any].include?(@gate)
15
+ end
16
+
17
+ # Returns a unique hash of what the scope contains.
18
+ def search_hash
19
+ @search_hash ||= [[@gate, @score]]
20
+ end
21
+
22
+ def any_of(&blk)
23
+ scopes << subscope(:gate => :any, &blk)
24
+ end
25
+
26
+ def all_of(&blk)
27
+ scopes << subscope(:gate => :all, &blk)
28
+ end
29
+
30
+ def boost(amount=1.0, &blk)
31
+ boosts << [Ion::Scope.new(@search, :gate => :all, &blk), amount]
32
+ end
33
+
34
+ def score(score, &blk)
35
+ scopes << subscope(:score => score, &blk)
36
+ end
37
+
38
+ def key
39
+ @key ||= Ion.volatile_key
40
+ end
41
+
42
+ def sort_by(what)
43
+ index = @search.options.index(:sort, what)
44
+ key.sort by: index.spec, order: "ASC ALPHA", store: key
45
+ end
46
+
47
+ # Only when done
48
+ def count
49
+ return key.zcard if key.type == "zset"
50
+ return key.llen if key.type == "list"
51
+ 0
52
+ end
53
+
54
+ # Defines the shortcuts `text :foo 'xyz'` => `search :text, :foo, 'xyz'`
55
+ Ion::Indices.names.each do |type|
56
+ define_method(type) do |field, what, args={}|
57
+ search type, field, what, args
58
+ end
59
+ end
60
+
61
+ # Searches a given field.
62
+ # @example
63
+ # class Album
64
+ # ion { text :name }
65
+ # end
66
+ #
67
+ # Album.ion.search {
68
+ # search :text, :name, "Emotional Technology"
69
+ # text :name, "Emotional Technology" # same
70
+ # }
71
+ def search(type, field, what, args={})
72
+ subkey = options.index(type, field).search(what, args)
73
+ temp_keys << subkey
74
+ subkeys << subkey
75
+ search_hash << [type,field,what,args]
76
+ end
77
+
78
+ def options
79
+ @search.options
80
+ end
81
+
82
+ def ids(range)
83
+ from, to = range.first, range.last
84
+ to -= 1 if range.exclude_end?
85
+
86
+ type = key.type
87
+ results = if type == "zset"
88
+ key.zrevrange(from, to)
89
+ elsif type == "list"
90
+ key.lrange(from, to)
91
+ else
92
+ Array.new
93
+ end
94
+
95
+ expire and results
96
+ end
97
+
98
+ protected
99
+ # List of scopes to be cleaned up after. (Array of Scope instances)
100
+ def scopes() @scopes ||= Array.new end
101
+
102
+ # List of keys that contain results to be combined.
103
+ def subkeys() @subkeys ||= Array.new end
104
+
105
+ # List of keys (like search keys) to be cleaned up after.
106
+ def temp_keys() @temp_keys ||= Array.new end
107
+
108
+ # List of boost scopes -- [Scope, amount] tuples
109
+ def boosts() @boosts ||= Array.new end
110
+
111
+ # Called by #run after doing an instance_eval of DSL stuff.
112
+ # This consolidates the keys into self.key.
113
+ def done
114
+ if subkeys.size == 1
115
+ self.key = subkeys.first
116
+ elsif subkeys.size > 1
117
+ combine subkeys
118
+ end
119
+
120
+ # Adjust scores accordingly
121
+ self.key = rescore(key, @score)
122
+
123
+ boost_keys = boosts.map do |(scope, amount)|
124
+ inter = Ion.volatile_key
125
+ inter.zinterstore([key, scope.key], :weights => [0, amount])
126
+ temp_keys << inter
127
+ inter
128
+ end
129
+
130
+ key.zunionstore [key, *boost_keys]
131
+ end
132
+
133
+ def rescore(key, score)
134
+ return key if score == 1.0
135
+ dest = key.include?('~') ? key : Ion.volatile_key
136
+ dest.zunionstore([key], weights: score)
137
+ dest
138
+ end
139
+
140
+ # Sets the TTL for the temp keys.
141
+ def expire
142
+ scopes.each { |s| s.send :expire }
143
+ Ion.expire key, *temp_keys
144
+ end
145
+
146
+ def combine(subkeys)
147
+ if @gate == :all
148
+ key.zinterstore subkeys
149
+ elsif @gate == :any
150
+ key.zunionstore subkeys
151
+ end
152
+ end
153
+
154
+ # Used by all_of and any_of
155
+ def subscope(args={}, &blk)
156
+ opts = { :gate => @gate, :score => @score }
157
+ scope = Ion::Scope.new(@search, opts.merge(args), &blk)
158
+
159
+ subkeys << scope.key
160
+ search_hash << scope.search_hash
161
+ scope
162
+ end
163
+ end
@@ -0,0 +1,83 @@
1
+ class Ion::Search
2
+ include Enumerable
3
+
4
+ attr_reader :options
5
+ attr_reader :scope
6
+
7
+ def initialize(options, &blk)
8
+ @options = options
9
+ @scope = Ion::Scope.new(self, &blk)
10
+ end
11
+
12
+ # Returns the model.
13
+ # @example
14
+ #
15
+ # search = Album.ion.search { ... }
16
+ # assert search.model == Album
17
+ #
18
+ def model
19
+ @options.model
20
+ end
21
+
22
+ # Returns a unique hash for the search.
23
+ def search_hash
24
+ require 'digest'
25
+ Digest::MD5.hexdigest @scope.search_hash.inspect
26
+ end
27
+
28
+ def range(args=nil)
29
+ @range = if args == :all
30
+ nil
31
+ elsif args.is_a?(Range)
32
+ args
33
+ elsif !args.is_a?(Hash)
34
+ @range
35
+ elsif args[:from] && args[:limit]
36
+ ((args[:from]-1)..(args[:from]-1 + args[:limit]-1))
37
+ elsif args[:page] && args[:limit]
38
+ (((args[:page]-1)*args[:limit])..((args[:page])*args[:limit]))
39
+ elsif args[:from] && args[:to]
40
+ ((args[:from]-1)..(args[:to]-1))
41
+ elsif args[:from]
42
+ ((args[:from]-1)..-1)
43
+ else
44
+ @range
45
+ end || (0..-1)
46
+ end
47
+
48
+ def to_a
49
+ ids.map &model
50
+ end
51
+
52
+ def each(&blk)
53
+ to_a.each &blk
54
+ end
55
+
56
+ def size
57
+ @scope.count
58
+ end
59
+
60
+ def yieldie(&blk)
61
+ @scope.yieldie &blk
62
+ end
63
+
64
+ def sort_by(what)
65
+ @scope.sort_by what
66
+ end
67
+
68
+ def ids
69
+ @scope.ids range
70
+ end
71
+
72
+ def key
73
+ @scope.key
74
+ end
75
+
76
+ # Searching
77
+
78
+ protected
79
+ # Interal: called when the `Model.ion.search { }` block is done
80
+ def done
81
+ @scope.send :done
82
+ end
83
+ end