ion 0.0.1

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.
@@ -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