Class: Familia::SortedSet

Inherits:
DataType show all
Defined in:
lib/familia/data_type/types/sorted_set.rb

Instance Attribute Summary collapse

Attributes included from Settings

#current_key_version, #default_expiration, #delim, #encryption_keys, #encryption_personalization, #logical_database, #prefix, #suffix, #transaction_mode

Instance Method Summary collapse

Methods included from Features::Autoloader

autoload_files, included, normalize_to_config_name

Methods included from DataType::Serialization

#deserialize_value, #deserialize_values, #deserialize_values_with_nil, #serialize_value

Methods included from DataType::DatabaseCommands

#current_expiration, #delete!, #echo, #exists?, #expire, #expireat, #move, #persist, #rename, #renamenx, #type

Methods included from DataType::Connection

#dbclient, #dbkey, #direct_access

Methods included from Settings

#configure, #default_suffix, #pipeline_mode, #pipeline_mode=

Methods included from Base

add_feature, #as_json, #expired?, #expires?, find_feature, #generate_id, #to_json, #to_s, #ttl, #update_expiration, #uuid

Constructor Details

This class inherits a constructor from Familia::DataType

Instance Attribute Details

#features_enabledObject (readonly) Originally defined in module Features

Returns the value of attribute features_enabled.

#logical_database(val = nil) ⇒ Object Originally defined in module DataType::ClassMethods

#parentObject Originally defined in module DataType::ClassMethods

Returns the value of attribute parent.

#prefixObject Originally defined in module DataType::ClassMethods

Returns the value of attribute prefix.

#suffixObject Originally defined in module DataType::ClassMethods

Returns the value of attribute suffix.

#uri(val = nil) ⇒ Object Originally defined in module DataType::ClassMethods

Returns the value of attribute uri.

Instance Method Details

#<<(val) ⇒ Integer

Note:

This is a non-standard operation for sorted sets as it doesn't allow specifying a custom score. Use add or []= for more control.

Adds a new element to the sorted set with the current timestamp as the score.

This method provides a convenient way to add elements to the sorted set without explicitly specifying a score. It uses the current Unix timestamp as the score, which effectively sorts elements by their insertion time.

Examples:

sorted_set << "new_element"

Parameters:

  • val (Object)

    The value to be added to the sorted set.

Returns:

  • (Integer)

    Returns 1 if the element is new and added, 0 if the element already existed and the score was updated.



34
35
36
# File 'lib/familia/data_type/types/sorted_set.rb', line 34

def <<(val)
  add(val)
end

#[]=(val, score) ⇒ Object

NOTE: The argument order is the reverse of #add. We do this to more naturally align with how the [] and []= methods are used.

e.g. obj.metrics[VALUE] = SCORE obj.metrics[VALUE] # => SCORE



45
46
47
# File 'lib/familia/data_type/types/sorted_set.rb', line 45

def []=(val, score)
  add val, score
end

#add(val, score = nil, nx: false, xx: false, gt: false, lt: false, ch: false) ⇒ Boolean Also known as: add_element

Note:

GT and LT options do NOT prevent adding new elements, they only affect update behavior for existing elements.

Note:

Default behavior (no options) adds new elements and updates existing ones unconditionally, matching standard Redis ZADD semantics.

Note:

INCR option is not supported. Use the increment method for ZINCRBY operations.

Adds an element to the sorted set with an optional score and ZADD options.

This method supports Redis ZADD options for conditional adds and updates:

  • NX: Only add new elements (don't update existing)
  • XX: Only update existing elements (don't add new)
  • GT: Only update if new score > current score
  • LT: Only update if new score < current score
  • CH: Return changed count (new + updated) instead of just new count

Examples:

Add new element with timestamp

metrics.add('pageview', Time.now.to_f)  #=> true

Preserve original timestamp on subsequent saves

index.add(email, Time.now.to_f, nx: true)  #=> true
index.add(email, Time.now.to_f, nx: true)  #=> false (unchanged)

Update timestamp only for existing entries

index.add(email, Time.now.to_f, xx: true)  #=> false (if doesn't exist)

Only update if new score is higher (leaderboard)

scores.add(player, 1000, gt: true)  #=> true (new entry)
scores.add(player, 1500, gt: true)  #=> false (updated)
scores.add(player, 1200, gt: true)  #=> false (not updated, score lower)

Track total changes for analytics

changed = metrics.add(user, score, ch: true)  #=> true (new or updated)

Combined options: only update existing, only if score increases

index.add(key, new_score, xx: true, gt: true)

Parameters:

  • val (Object)

    The value to add to the sorted set

  • score (Numeric, nil) (defaults to: nil)

    The score for ranking (defaults to current timestamp)

  • nx (Boolean) (defaults to: false)

    Only add new elements, don't update existing (default: false)

  • xx (Boolean) (defaults to: false)

    Only update existing elements, don't add new (default: false)

  • gt (Boolean) (defaults to: false)

    Only update if new score > current score (default: false)

  • lt (Boolean) (defaults to: false)

    Only update if new score < current score (default: false)

  • ch (Boolean) (defaults to: false)

    Return changed count instead of added count (default: false)

Returns:

  • (Boolean)

    Returns the return value from the redis gem's ZADD command. Returns true if element was added or changed (with CH option), false if element score was updated without change tracking or no operation occurred due to option constraints (NX, XX, GT, LT).

Raises:

  • (ArgumentError)

    If mutually exclusive options are specified together (NX+XX, GT+LT, NX+GT, NX+LT)



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/familia/data_type/types/sorted_set.rb', line 103

def add(val, score = nil, nx: false, xx: false, gt: false, lt: false, ch: false)
  score ||= Familia.now

  # Validate mutual exclusivity
  validate_zadd_options!(nx: nx, xx: xx, gt: gt, lt: lt)

  # Build options hash for redis gem
  opts = {}
  opts[:nx] = true if nx
  opts[:xx] = true if xx
  opts[:gt] = true if gt
  opts[:lt] = true if lt
  opts[:ch] = true if ch

  # Pass options to ZADD
  ret = if opts.empty?
    dbclient.zadd(dbkey, score, serialize_value(val))
  else
    dbclient.zadd(dbkey, score, serialize_value(val), **opts)
  end

  update_expiration
  ret
end

#at(idx) ⇒ Object



295
296
297
# File 'lib/familia/data_type/types/sorted_set.rb', line 295

def at(idx)
  range(idx, idx).first
end

#collectObject



185
186
187
# File 'lib/familia/data_type/types/sorted_set.rb', line 185

def collect(&)
  members.collect(&)
end

#collectrawObject



201
202
203
# File 'lib/familia/data_type/types/sorted_set.rb', line 201

def collectraw(&)
  membersraw.collect(&)
end

#decrement(val, by = 1) ⇒ Object Also known as: decr, decrby



275
276
277
# File 'lib/familia/data_type/types/sorted_set.rb', line 275

def decrement(val, by = 1)
  increment val, -by
end

#eachObject



177
178
179
# File 'lib/familia/data_type/types/sorted_set.rb', line 177

def each(&)
  members.each(&)
end

#each_with_indexObject



181
182
183
# File 'lib/familia/data_type/types/sorted_set.rb', line 181

def each_with_index(&)
  members.each_with_index(&)
end

#eachrawObject



193
194
195
# File 'lib/familia/data_type/types/sorted_set.rb', line 193

def eachraw(&)
  membersraw.each(&)
end

#eachraw_with_indexObject



197
198
199
# File 'lib/familia/data_type/types/sorted_set.rb', line 197

def eachraw_with_index(&)
  membersraw.each_with_index(&)
end

#element_countInteger Also known as: size, length, count

Returns the number of elements in the sorted set

Returns:

  • (Integer)

    number of elements



7
8
9
# File 'lib/familia/data_type/types/sorted_set.rb', line 7

def element_count
  dbclient.zcard dbkey
end

#empty?Boolean

Returns:

  • (Boolean)


14
15
16
# File 'lib/familia/data_type/types/sorted_set.rb', line 14

def empty?
  element_count.zero?
end

#firstObject

Return the first element in the list. Redis: ZRANGE(0)



300
301
302
# File 'lib/familia/data_type/types/sorted_set.rb', line 300

def first
  at(0)
end

#increment(val, by = 1) ⇒ Object Also known as: incr, incrby



269
270
271
# File 'lib/familia/data_type/types/sorted_set.rb', line 269

def increment(val, by = 1)
  dbclient.zincrby(dbkey, by, val).to_i
end

#lastObject

Return the last element in the list. Redis: ZRANGE(-1)



305
306
307
# File 'lib/familia/data_type/types/sorted_set.rb', line 305

def last
  at(-1)
end

#member?(val) ⇒ Boolean Also known as: include?

Returns:

  • (Boolean)


135
136
137
138
# File 'lib/familia/data_type/types/sorted_set.rb', line 135

def member?(val)
  Familia.trace :MEMBER, nil, "#{val}<#{val.class}>" if Familia.debug?
  !rank(val).nil?
end

#members(count = -1,, opts = {}) ⇒ Object Also known as: to_a, all



153
154
155
156
157
# File 'lib/familia/data_type/types/sorted_set.rb', line 153

def members(count = -1, opts = {})
  count -= 1 if count.positive?
  elements = membersraw count, opts
  deserialize_values(*elements)
end

#membersraw(count = -1,, opts = {}) ⇒ Object



161
162
163
164
# File 'lib/familia/data_type/types/sorted_set.rb', line 161

def membersraw(count = -1, opts = {})
  count -= 1 if count.positive?
  rangeraw 0, count, opts
end

#range(sidx, eidx, opts = {}) ⇒ Object



209
210
211
212
213
# File 'lib/familia/data_type/types/sorted_set.rb', line 209

def range(sidx, eidx, opts = {})
  echo :range, Familia.pretty_stack(limit: 1) if Familia.debug
  elements = rangeraw(sidx, eidx, opts)
  deserialize_values(*elements)
end

#rangebyscore(sscore, escore, opts = {}) ⇒ Object

e.g. obj.metrics.rangebyscore (now-12.hours), now, :limit => [0, 10]



237
238
239
240
241
# File 'lib/familia/data_type/types/sorted_set.rb', line 237

def rangebyscore(sscore, escore, opts = {})
  echo :rangebyscore, Familia.pretty_stack(limit: 1) if Familia.debug
  elements = rangebyscoreraw(sscore, escore, opts)
  deserialize_values(*elements)
end

#rangebyscoreraw(sscore, escore, opts = {}) ⇒ Object



243
244
245
246
# File 'lib/familia/data_type/types/sorted_set.rb', line 243

def rangebyscoreraw(sscore, escore, opts = {})
  echo :rangebyscoreraw, Familia.pretty_stack(limit: 1) if Familia.debug
  dbclient.zrangebyscore(dbkey, sscore, escore, **opts)
end

#rangeraw(sidx, eidx, opts = {}) ⇒ Object



215
216
217
218
219
220
221
222
223
224
# File 'lib/familia/data_type/types/sorted_set.rb', line 215

def rangeraw(sidx, eidx, opts = {})
  # NOTE: :withscores (no underscore) is the correct naming for the
  # redis-4.x gem. We pass :withscores through explicitly b/c
  # dbclient.zrange et al only accept that one optional argument.
  # Passing `opts`` through leads to an ArgumentError:
  #
  #   sorted_sets.rb:374:in `zrevrange': wrong number of arguments (given 4, expected 3) (ArgumentError)
  #
  dbclient.zrange(dbkey, sidx, eidx, **opts)
end

#rank(v) ⇒ Object

rank of member +v+ when ordered lowest to highest (starts at 0)



142
143
144
145
# File 'lib/familia/data_type/types/sorted_set.rb', line 142

def rank(v)
  ret = dbclient.zrank dbkey, serialize_value(v, strict_values: false)
  ret&.to_i
end

#remove_element(value) ⇒ Integer Also known as: remove

Removes a member from the sorted set

Parameters:

  • value

    The value to remove from the sorted set

Returns:

  • (Integer)

    The number of members that were removed (0 or 1)



284
285
286
287
288
289
290
291
292
# File 'lib/familia/data_type/types/sorted_set.rb', line 284

def remove_element(value)
  Familia.trace :REMOVE_ELEMENT, nil, "#{value}<#{value.class}>" if Familia.debug?
  # We use `strict_values: false` here to allow for the deletion of values
  # that are in the sorted set. If it's a horreum object, the value is
  # the identifier and not a serialized version of the object. So either
  # the value exists in the sorted set or it doesn't -- we don't need to
  # raise an error if it's not found.
  dbclient.zrem dbkey, serialize_value(value, strict_values: false)
end

#remrangebyrank(srank, erank) ⇒ Object



261
262
263
# File 'lib/familia/data_type/types/sorted_set.rb', line 261

def remrangebyrank(srank, erank)
  dbclient.zremrangebyrank dbkey, srank, erank
end

#remrangebyscore(sscore, escore) ⇒ Object



265
266
267
# File 'lib/familia/data_type/types/sorted_set.rb', line 265

def remrangebyscore(sscore, escore)
  dbclient.zremrangebyscore dbkey, sscore, escore
end

#revmembers(count = -1,, opts = {}) ⇒ Object



166
167
168
169
170
# File 'lib/familia/data_type/types/sorted_set.rb', line 166

def revmembers(count = -1, opts = {})
  count -= 1 if count.positive?
  elements = revmembersraw count, opts
  deserialize_values(*elements)
end

#revmembersraw(count = -1,, opts = {}) ⇒ Object



172
173
174
175
# File 'lib/familia/data_type/types/sorted_set.rb', line 172

def revmembersraw(count = -1, opts = {})
  count -= 1 if count.positive?
  revrangeraw 0, count, opts
end

#revrange(sidx, eidx, opts = {}) ⇒ Object



226
227
228
229
230
# File 'lib/familia/data_type/types/sorted_set.rb', line 226

def revrange(sidx, eidx, opts = {})
  echo :revrange, Familia.pretty_stack(limit: 1) if Familia.debug
  elements = revrangeraw(sidx, eidx, opts)
  deserialize_values(*elements)
end

#revrangebyscore(sscore, escore, opts = {}) ⇒ Object

e.g. obj.metrics.revrangebyscore (now-12.hours), now, :limit => [0, 10]



249
250
251
252
253
# File 'lib/familia/data_type/types/sorted_set.rb', line 249

def revrangebyscore(sscore, escore, opts = {})
  echo :revrangebyscore, Familia.pretty_stack(limit: 1) if Familia.debug
  elements = revrangebyscoreraw(sscore, escore, opts)
  deserialize_values(*elements)
end

#revrangebyscoreraw(sscore, escore, opts = {}) ⇒ Object



255
256
257
258
259
# File 'lib/familia/data_type/types/sorted_set.rb', line 255

def revrangebyscoreraw(sscore, escore, opts = {})
  echo :revrangebyscoreraw, Familia.pretty_stack(limit: 1) if Familia.debug
  opts[:with_scores] = true if opts[:withscores]
  dbclient.zrevrangebyscore(dbkey, sscore, escore, opts)
end

#revrangeraw(sidx, eidx, opts = {}) ⇒ Object



232
233
234
# File 'lib/familia/data_type/types/sorted_set.rb', line 232

def revrangeraw(sidx, eidx, opts = {})
  dbclient.zrevrange(dbkey, sidx, eidx, **opts)
end

#revrank(v) ⇒ Object

rank of member +v+ when ordered highest to lowest (starts at 0)



148
149
150
151
# File 'lib/familia/data_type/types/sorted_set.rb', line 148

def revrank(v)
  ret = dbclient.zrevrank dbkey, serialize_value(v, strict_values: false)
  ret&.to_i
end

#score(val) ⇒ Object Also known as: []



129
130
131
132
# File 'lib/familia/data_type/types/sorted_set.rb', line 129

def score(val)
  ret = dbclient.zscore dbkey, serialize_value(val, strict_values: false)
  ret&.to_f
end

#selectObject



189
190
191
# File 'lib/familia/data_type/types/sorted_set.rb', line 189

def select(&)
  members.select(&)
end

#selectrawObject



205
206
207
# File 'lib/familia/data_type/types/sorted_set.rb', line 205

def selectraw(&)
  membersraw.select(&)
end