Class: Familia::SortedSet

Inherits:
DataType show all
Includes:
DataType::CollectionBase
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_hkdf_salt, #encryption_hkdf_salt_history, #encryption_keys, #encryption_personalization, #encryption_personalization_history, #logical_database, #prefix, #raise_on_unsaved_parent_write, #schema_path, #schema_validator, #schemas, #strict_write_order, #suffix, #transaction_mode

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DataType::CollectionBase

#collection_type?, #each_record

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, #strip_legacy_json_encoding

Methods included from DataType::DatabaseCommands

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

Methods included from DataType::Connection

#dbclient, #dbkey, #uri

Methods included from Connection::Behavior

#connect, #create_dbclient, #multi, #normalize_uri, #pipeline, #pipelined, #transaction, #uri=, #url, #url=

Methods included from Settings

#configure, #default_suffix, #dirty_write_warnings, #dirty_write_warnings=, #pipelined_mode, #pipelined_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.

Class Method Details

.supports_max_length?Boolean

SortedSet implements :max_length (capped collection, issue #351): a cap of N retains the N HIGHEST-scoring members — trim is ZREMRANGEBYRANK key 0 -(N+1), removing from the low-score end. When scores are timestamps (the #add default) this keeps the newest N; with arbitrary scores it keeps the top-N by score, not by recency.

All member-creating paths trim: #add (covers << and []=), #update /

merge! (bulk ZADD), and #increment / #decrement (ZINCRBY creates the

member if absent). The trim is issued unconditionally after the write — an under-cap trim is an O(log N) no-op, and it is harmless when an NX/XX/GT/LT constraint skipped the write. NOT capped (external writers beware): unionstore/interstore/diffstore destination keys and restore.

Returns:

  • (Boolean)


21
22
23
# File 'lib/familia/data_type/types/sorted_set.rb', line 21

def self.supports_max_length?
  true
end

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.



54
55
56
# File 'lib/familia/data_type/types/sorted_set.rb', line 54

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



65
66
67
# File 'lib/familia/data_type/types/sorted_set.rb', line 65

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.

Note:

This method executes a Redis ZADD immediately, unlike scalar field setters which are deferred until save. If the parent object has unsaved scalar field changes, consider calling save first to avoid split-brain state.

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', Familia.now.to_f)  #=> true

Preserve original timestamp on subsequent saves

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

Update timestamp only for existing entries

index.add(email, Familia.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)



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/familia/data_type/types/sorted_set.rb', line 127

def add(val, score = nil, nx: false, xx: false, gt: false, lt: false, ch: false)
  warn_if_dirty!
  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 = capped_zadd_write do |conn|
    if opts.empty?
      conn.zadd(dbkey, score, serialize_value(val))
    else
      conn.zadd(dbkey, score, serialize_value(val), **opts)
    end
  end

  update_expiration
  ret
end

#at(idx) ⇒ Object



455
456
457
# File 'lib/familia/data_type/types/sorted_set.rb', line 455

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

#collectrawObject



334
335
336
# File 'lib/familia/data_type/types/sorted_set.rb', line 334

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

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



437
438
439
# File 'lib/familia/data_type/types/sorted_set.rb', line 437

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

#diff(*other_sets, withscores: false) ⇒ Array

Returns the difference between this sorted set and other sorted sets.

Examples:

Difference of two sorted sets

zset.diff(other_zset)  #=> ["unique_member"]

Parameters:

  • other_sets (Array<SortedSet, String>)

    Other sorted sets or key names

  • withscores (Boolean) (defaults to: false)

    Whether to include scores in result

Returns:

  • (Array)

    Members in this set but not in other sets



779
780
781
782
783
784
785
786
787
788
789
# File 'lib/familia/data_type/types/sorted_set.rb', line 779

def diff(*other_sets, withscores: false)
  keys = [dbkey] + resolve_set_keys(other_sets)

  result = if withscores
    dbclient.zdiff(*keys, withscores: true)
  else
    dbclient.zdiff(*keys)
  end

  process_set_operation_result(result, withscores: withscores)
end

#diffstore(destination, *other_sets) ⇒ Integer

Stores the difference of sorted sets into a destination key.

Parameters:

  • destination (String)

    Destination key name

  • other_sets (Array<SortedSet, String>)

    Other sorted sets or key names

Returns:

  • (Integer)

    Number of elements in the resulting sorted set



797
798
799
800
801
802
# File 'lib/familia/data_type/types/sorted_set.rb', line 797

def diffstore(destination, *other_sets)
  keys = [dbkey] + resolve_set_keys(other_sets)
  result = dbclient.zdiffstore(destination, keys)
  update_expiration
  result
end

#each(since: nil, batch_size: 100, **kwargs) {|member| ... } ⇒ Enumerator, self

Note:

The until: parameter uses Ruby keyword syntax. Since until is a reserved word, it's accessed via **kwargs internally.

Note:

Score-cursor pagination: bounded queries paginate using the last seen score as the next exclusive minimum. This is O(n) total work, unlike offset-based LIMIT which is O(n²). However, members with identical scores may be skipped between pages. Use high-precision floats (e.g., Familia.now) for scores to avoid collisions.

Iterates over members of the sorted set.

When called with score bounds (since/until), uses ZRANGEBYSCORE for efficient range queries. Otherwise uses ZSCAN for memory-efficient iteration over large sets.

Examples:

Iterate all members

scores.each { |member| puts member }

Iterate members within time range

scores.each(since: 1.hour.ago, until: Time.now) { |m| process(m) }

Parameters:

  • since (Numeric, Time, nil) (defaults to: nil)

    Minimum score (inclusive). Time objects are converted to float timestamps.

  • until (Numeric, Time, nil)

    Maximum score (inclusive). Time objects are converted to float timestamps. Use kwargs syntax: until: value.

  • batch_size (Integer) (defaults to: 100)

    Number of elements to fetch per ZSCAN iteration (only used when no score bounds provided)

Yields:

  • (member)

    Each deserialized member

Returns:

  • (Enumerator, self)

    Returns Enumerator if no block given, self otherwise

Raises:

  • (ArgumentError)


283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/familia/data_type/types/sorted_set.rb', line 283

def each(since: nil, batch_size: 100, **kwargs, &block)
  until_score = kwargs.delete(:until)
  raise ArgumentError, "unknown keyword(s): #{kwargs.keys.join(', ')}" if kwargs.any?

  return to_enum(:each, since: since, until: until_score, batch_size: batch_size) unless block

  # Convert Time objects to numeric scores
  since_score = since.is_a?(Time) ? since.to_f : since
  until_score_val = until_score.is_a?(Time) ? until_score.to_f : until_score

  if since_score || until_score_val
    # Score-cursor pagination: track last score, use exclusive bound for next page
    min = since_score || '-inf'
    max = until_score_val || '+inf'
    loop do
      # with_scores returns nested pairs: [["a", 1.0], ["b", 2.0]]
      pairs = rangebyscoreraw(min, max, limit: [0, batch_size], with_scores: true)
      break if pairs.empty?

      pairs.each do |raw_member, score|
        yield deserialize_value(raw_member)
        min = "(#{score}" # exclusive bound for next iteration
      end

      break if pairs.size < batch_size
    end
  else
    # Use ZSCAN for unbounded iteration (memory-efficient)
    cursor = 0
    loop do
      new_cursor, pairs = scan(cursor, count: batch_size)
      # ZSCAN yields an Array of [member, score] pairs (not a Hash), so
      # Hash#each_key would raise NoMethodError here. The cop cannot see
      # that and its autocorrect breaks this loop — keep it disabled.
      pairs.each { |member, _score| block.call(member) } # rubocop:disable Style/HashEachMethods
      cursor = new_cursor
      break if cursor.zero?
    end
  end

  self
end

#eachrawObject



326
327
328
# File 'lib/familia/data_type/types/sorted_set.rb', line 326

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

#eachraw_with_indexObject



330
331
332
# File 'lib/familia/data_type/types/sorted_set.rb', line 330

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



27
28
29
# File 'lib/familia/data_type/types/sorted_set.rb', line 27

def element_count
  dbclient.zcard dbkey
end

#empty?Boolean

Returns:

  • (Boolean)


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

def empty?
  element_count.zero?
end

#enforce_max_length!Integer, Redis::Future

Trims an already-oversized sorted set down to its :max_length cap, keeping the max_length highest-scoring members — the same trim every capped write applies (ZREMRANGEBYRANK 0 -(max_length+1)).

Capped writes enforce the cap going forward, but nothing runs at definition time: adding max_length: to a live collection leaves the existing overage in place until the next write. Call this once, e.g. as a migration step, to enforce the cap immediately.

Returns:

  • (Integer, Redis::Future)

    number of members removed (0 when already within cap); inside a caller transaction or pipeline, the removal count's future instead

Raises:



408
409
410
411
412
413
414
# File 'lib/familia/data_type/types/sorted_set.rb', line 408

def enforce_max_length!
  max = require_max_length!
  warn_if_dirty!
  removed = dbclient.zremrangebyrank(dbkey, 0, -(max + 1))
  update_expiration
  removed
end

#firstObject

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



460
461
462
# File 'lib/familia/data_type/types/sorted_set.rb', line 460

def first
  at(0)
end

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



423
424
425
426
427
428
429
430
431
432
433
# File 'lib/familia/data_type/types/sorted_set.rb', line 423

def increment(val, by = 1)
  warn_if_dirty!
  # ZINCRBY creates the member if absent, so it is a capped path too.
  ret = capped_zadd_write { |conn| conn.zincrby(dbkey, by, serialize_value(val)) }
  # Inside a transaction or pipeline the write is a Redis::Future, which
  # cannot be coerced until the block commits — pass it through untouched
  # (same contract as #add). See Familia.positive? for the same idiom.
  ret = ret.to_f unless ret.is_a?(Redis::Future)
  update_expiration
  ret
end

#inter(*other_sets, weights: nil, aggregate: nil, withscores: false) ⇒ Array

Returns the intersection of this sorted set with other sorted sets.

Examples:

Intersection of two sorted sets

zset.inter(other_zset)  #=> ["common_member"]

Parameters:

  • other_sets (Array<SortedSet, String>)

    Other sorted sets or key names

  • weights (Array<Numeric>, nil) (defaults to: nil)

    Multiplication factors for each set's scores

  • aggregate (Symbol, nil) (defaults to: nil)

    How to aggregate scores (:sum, :min, :max)

Returns:

  • (Array)

    Array of members (or [member, score] pairs with withscores)



608
609
610
611
612
613
614
# File 'lib/familia/data_type/types/sorted_set.rb', line 608

def inter(*other_sets, weights: nil, aggregate: nil, withscores: false)
  keys = [dbkey] + resolve_set_keys(other_sets)
  opts = build_set_operation_opts(weights: weights, aggregate: aggregate, withscores: withscores)

  result = dbclient.zinter(*keys, **opts)
  process_set_operation_result(result, withscores: withscores)
end

#interstore(destination, *other_sets, weights: nil, aggregate: nil) ⇒ Integer

Stores the intersection of sorted sets into a destination key.

Parameters:

  • destination (String)

    Destination key name

  • other_sets (Array<SortedSet, String>)

    Other sorted sets or key names

  • weights (Array<Numeric>, nil) (defaults to: nil)

    Multiplication factors for each set's scores

  • aggregate (Symbol, nil) (defaults to: nil)

    How to aggregate scores (:sum, :min, :max)

Returns:

  • (Integer)

    Number of elements in the resulting sorted set



761
762
763
764
765
766
767
768
# File 'lib/familia/data_type/types/sorted_set.rb', line 761

def interstore(destination, *other_sets, weights: nil, aggregate: nil)
  keys = [dbkey] + resolve_set_keys(other_sets)
  opts = build_set_operation_opts(weights: weights, aggregate: aggregate)

  result = dbclient.zinterstore(destination, keys, **opts)
  update_expiration
  result
end

#lastObject

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



465
466
467
# File 'lib/familia/data_type/types/sorted_set.rb', line 465

def last
  at(-1)
end

#lexcount(min, max) ⇒ Integer

Counts members in a lexicographical range.

Parameters:

  • min (String)

    Minimum lex value

  • max (String)

    Maximum lex value

Returns:

  • (Integer)

    Number of members in the range



665
666
667
# File 'lib/familia/data_type/types/sorted_set.rb', line 665

def lexcount(min, max)
  dbclient.zlexcount(dbkey, min, max)
end

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

Returns:

  • (Boolean)


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

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



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

def members(count = -1, opts = {})
  # NOTE: count math (positive count -> end index) is handled once by
  # membersraw. Do not decrement here too, or members(n) returns n-1.
  elements = membersraw count, opts
  deserialize_values(*elements)
end

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



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

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

#mscore(*members) ⇒ Array<Float, nil>

Gets scores for multiple members at once.

Examples:

Get scores for multiple members

zset.mscore('member1', 'member2', 'member3')  #=> [1.0, 2.0, nil]

Parameters:

  • members (Array<Object>)

    Members to get scores for

Returns:

  • (Array<Float, nil>)

    Scores for each member (nil if member doesn't exist)



566
567
568
569
570
571
572
# File 'lib/familia/data_type/types/sorted_set.rb', line 566

def mscore(*members)
  return [] if members.empty?

  serialized = members.map { |m| serialize_value(m) }
  result = dbclient.zmscore(dbkey, *serialized)
  result.map { |s| s&.to_f }
end

#popmax(count = 1) ⇒ Array?

Removes and returns the member(s) with the highest score(s).

Examples:

Pop single highest-scoring member

zset.popmax  #=> ["member1", 100.0]

Pop multiple highest-scoring members

zset.popmax(3)  #=> [["member3", 100.0], ["member2", 90.0], ["member1", 80.0]]

Parameters:

  • count (Integer) (defaults to: 1)

    Number of members to pop (default: 1)

Returns:

  • (Array, nil)

    Array of [member, score] pairs, or single pair if count=1, or nil if set is empty



517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/familia/data_type/types/sorted_set.rb', line 517

def popmax(count = 1)
  warn_if_dirty!
  # Normalize explicit nil to the default so the structural dispatch below
  # behaves identically whether the arg is omitted or passed as nil.
  # redis-rb treats nil as count <= 1 and returns a flat pair.
  count = 1 if count.nil?
  result = dbclient.zpopmax(dbkey, count)
  return nil if result.nil? || result.empty?

  update_expiration

  # redis-rb returns a flat [member, score] pair when count <= 1, and a
  # nested [[member, score], ...] array when count > 1. Normalize by
  # inspecting the result's structure rather than relying on count alone,
  # so that a redis-rb version change or a member that serializes to an
  # array-like string cannot mislead the dispatch.
  if count == 1
    pair = result.first.is_a?(Array) ? result.first : result
    [deserialize_value(pair[0]), pair[1].to_f]
  else
    result.map { |member, score| [deserialize_value(member), score.to_f] }
  end
end

#popmin(count = 1) ⇒ Array?

Removes and returns the member(s) with the lowest score(s).

Examples:

Pop single lowest-scoring member

zset.popmin  #=> ["member1", 1.0]

Pop multiple lowest-scoring members

zset.popmin(3)  #=> [["member1", 1.0], ["member2", 2.0], ["member3", 3.0]]

Parameters:

  • count (Integer) (defaults to: 1)

    Number of members to pop (default: 1)

Returns:

  • (Array, nil)

    Array of [member, score] pairs, or single pair if count=1, or nil if set is empty



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/familia/data_type/types/sorted_set.rb', line 481

def popmin(count = 1)
  warn_if_dirty!
  # Normalize explicit nil to the default so the structural dispatch below
  # behaves identically whether the arg is omitted or passed as nil.
  # redis-rb treats nil as count <= 1 and returns a flat pair.
  count = 1 if count.nil?
  result = dbclient.zpopmin(dbkey, count)
  return nil if result.nil? || result.empty?

  update_expiration

  # redis-rb returns a flat [member, score] pair when count <= 1, and a
  # nested [[member, score], ...] array when count > 1. Normalize by
  # inspecting the result's structure rather than relying on count alone,
  # so that a redis-rb version change or a member that serializes to an
  # array-like string cannot mislead the dispatch.
  if count == 1
    pair = result.first.is_a?(Array) ? result.first : result
    [deserialize_value(pair[0]), pair[1].to_f]
  else
    result.map { |member, score| [deserialize_value(member), score.to_f] }
  end
end

#randmember(count = nil, withscores: false) ⇒ Object, ...

Returns random member(s) from the sorted set.

Examples:

Get single random member

zset.randmember  #=> "member1"

Get 3 random members

zset.randmember(3)  #=> ["member1", "member2", "member3"]

Get random member with score

zset.randmember(1, withscores: true)  #=> [["member1", 1.0]]

Parameters:

  • count (Integer, nil) (defaults to: nil)

    Number of members to return (nil for single member)

  • withscores (Boolean) (defaults to: false)

    Whether to include scores in result

Returns:

  • (Object, Array, nil)

    Random member(s), or nil if set is empty



684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
# File 'lib/familia/data_type/types/sorted_set.rb', line 684

def randmember(count = nil, withscores: false)
  if count.nil?
    result = dbclient.zrandmember(dbkey)
    return nil if result.nil?

    deserialize_value(result)
  else
    result = if withscores
      dbclient.zrandmember(dbkey, count, withscores: true)
    else
      dbclient.zrandmember(dbkey, count)
    end

    return [] if result.nil? || result.empty?

    if withscores
      result.map { |member, score| [deserialize_value(member), score.to_f] }
    else
      deserialize_values(*result)
    end
  end
end

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



342
343
344
345
346
# File 'lib/familia/data_type/types/sorted_set.rb', line 342

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

#rangebylex(min, max, limit: nil) ⇒ Array

Returns members in a lexicographical range (requires all members have same score).

Examples:

Get members between 'a' and 'z' (inclusive)

zset.rangebylex('[a', '[z')  #=> ["apple", "banana", "cherry"]

Get first 10 members starting with 'a'

zset.rangebylex('[a', '(b', limit: [0, 10])

Parameters:

  • min (String)

    Minimum lex value (use '-' for unbounded, '[' or '(' prefix for inclusive/exclusive)

  • max (String)

    Maximum lex value (use '+' for unbounded, '[' or '(' prefix for inclusive/exclusive)

  • limit (Array<Integer>, nil) (defaults to: nil)

    [offset, count] for pagination

Returns:

  • (Array)

    Members in the lexicographical range



629
630
631
632
# File 'lib/familia/data_type/types/sorted_set.rb', line 629

def rangebylex(min, max, limit: nil)
  result = dbclient.zrangebylex(dbkey, min, max, limit: limit)
  deserialize_values(*result)
end

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

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



364
365
366
367
368
# File 'lib/familia/data_type/types/sorted_set.rb', line 364

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



370
371
372
373
# File 'lib/familia/data_type/types/sorted_set.rb', line 370

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



348
349
350
351
# File 'lib/familia/data_type/types/sorted_set.rb', line 348

def rangeraw(sidx, eidx, opts = {})
  # NOTE: Redis 5.x gem uses :with_scores (with underscore)
  dbclient.zrange(dbkey, sidx, eidx, **opts)
end

#rank(v) ⇒ Object

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



216
217
218
219
# File 'lib/familia/data_type/types/sorted_set.rb', line 216

def rank(v)
  ret = dbclient.zrank dbkey, serialize_value(v)
  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)



446
447
448
449
450
451
452
# File 'lib/familia/data_type/types/sorted_set.rb', line 446

def remove_element(value)
  warn_if_dirty!
  Familia.trace :REMOVE_ELEMENT, nil, "#{value}<#{value.class}>" if Familia.debug?
  ret = dbclient.zrem dbkey, serialize_value(value)
  update_expiration
  ret
end

#remrangebylex(min, max) ⇒ Integer

Removes members in a lexicographical range.

Parameters:

  • min (String)

    Minimum lex value

  • max (String)

    Maximum lex value

Returns:

  • (Integer)

    Number of members removed



652
653
654
655
656
657
# File 'lib/familia/data_type/types/sorted_set.rb', line 652

def remrangebylex(min, max)
  warn_if_dirty!
  result = dbclient.zremrangebylex(dbkey, min, max)
  update_expiration
  result
end

#remrangebyrank(srank, erank) ⇒ Object



388
389
390
391
392
393
# File 'lib/familia/data_type/types/sorted_set.rb', line 388

def remrangebyrank(srank, erank)
  warn_if_dirty!
  ret = dbclient.zremrangebyrank dbkey, srank, erank
  update_expiration
  ret
end

#remrangebyscore(sscore, escore) ⇒ Object



416
417
418
419
420
421
# File 'lib/familia/data_type/types/sorted_set.rb', line 416

def remrangebyscore(sscore, escore)
  warn_if_dirty!
  ret = dbclient.zremrangebyscore dbkey, sscore, escore
  update_expiration
  ret
end

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



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

def revmembers(count = -1, opts = {})
  # See #members: revmembersraw already converts a positive count to the
  # correct end index; decrementing here as well would drop one element.
  elements = revmembersraw count, opts
  deserialize_values(*elements)
end

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



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

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

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



353
354
355
356
357
# File 'lib/familia/data_type/types/sorted_set.rb', line 353

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

#revrangebylex(max, min, limit: nil) ⇒ Array

Returns members in reverse lexicographical range.

Parameters:

  • max (String)

    Maximum lex value (use '+' for unbounded)

  • min (String)

    Minimum lex value (use '-' for unbounded)

  • limit (Array<Integer>, nil) (defaults to: nil)

    [offset, count] for pagination

Returns:

  • (Array)

    Members in reverse lexicographical range



641
642
643
644
# File 'lib/familia/data_type/types/sorted_set.rb', line 641

def revrangebylex(max, min, limit: nil)
  result = dbclient.zrevrangebylex(dbkey, max, min, limit: limit)
  deserialize_values(*result)
end

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

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



376
377
378
379
380
# File 'lib/familia/data_type/types/sorted_set.rb', line 376

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



382
383
384
385
386
# File 'lib/familia/data_type/types/sorted_set.rb', line 382

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



359
360
361
# File 'lib/familia/data_type/types/sorted_set.rb', line 359

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)



222
223
224
225
# File 'lib/familia/data_type/types/sorted_set.rb', line 222

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

#scan(cursor = 0, match: nil, count: nil) ⇒ Array

Iterates over members using cursor-based scanning.

Examples:

Scan all members

cursor = 0
loop do
  cursor, members = zset.scan(cursor)
  members.each { |member, score| puts "#{member}: #{score}" }
  break if cursor == 0
end

Scan with pattern matching

cursor, members = zset.scan(0, match: 'user:*', count: 100)

Parameters:

  • cursor (Integer) (defaults to: 0)

    Cursor position (0 to start)

  • match (String, nil) (defaults to: nil)

    Pattern to match member names

  • count (Integer, nil) (defaults to: nil)

    Hint for number of elements to return per call

Returns:

  • (Array)

    [new_cursor, [[member, score], ...]]



725
726
727
728
729
730
731
732
733
734
# File 'lib/familia/data_type/types/sorted_set.rb', line 725

def scan(cursor = 0, match: nil, count: nil)
  opts = {}
  opts[:match] = match if match
  opts[:count] = count if count

  new_cursor, result = dbclient.zscan(dbkey, cursor, **opts)

  members = result.map { |member, score| [deserialize_value(member), score.to_f] }
  [new_cursor.to_i, members]
end

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



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

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

#score_count(min, max) ⇒ Integer Also known as: zcount

Counts members within a score range.

Examples:

Count members with scores between 10 and 100

zset.score_count(10, 100)  #=> 5

Count members with scores up to 50

zset.score_count('-inf', 50)  #=> 3

Parameters:

  • min (Numeric, String)

    Minimum score (use '-inf' for unbounded)

  • max (Numeric, String)

    Maximum score (use '+inf' for unbounded)

Returns:

  • (Integer)

    Number of members with scores in the range



553
554
555
# File 'lib/familia/data_type/types/sorted_set.rb', line 553

def score_count(min, max)
  dbclient.zcount(dbkey, min, max)
end

#selectrawObject



338
339
340
# File 'lib/familia/data_type/types/sorted_set.rb', line 338

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

#union(*other_sets, weights: nil, aggregate: nil, withscores: false) ⇒ Array

Returns the union of this sorted set with other sorted sets.

Examples:

Union of two sorted sets

zset.union(other_zset)  #=> ["member1", "member2", "member3"]

Union with weighted scores

zset.union(other_zset, weights: [1, 2])

Union with score aggregation

zset.union(other_zset, aggregate: :max)

Parameters:

  • other_sets (Array<SortedSet, String>)

    Other sorted sets or key names

  • weights (Array<Numeric>, nil) (defaults to: nil)

    Multiplication factors for each set's scores

  • aggregate (Symbol, nil) (defaults to: nil)

    How to aggregate scores (:sum, :min, :max)

Returns:

  • (Array)

    Array of members (or [member, score] pairs with withscores)



590
591
592
593
594
595
596
# File 'lib/familia/data_type/types/sorted_set.rb', line 590

def union(*other_sets, weights: nil, aggregate: nil, withscores: false)
  keys = [dbkey] + resolve_set_keys(other_sets)
  opts = build_set_operation_opts(weights: weights, aggregate: aggregate, withscores: withscores)

  result = dbclient.zunion(*keys, **opts)
  process_set_operation_result(result, withscores: withscores)
end

#unionstore(destination, *other_sets, weights: nil, aggregate: nil) ⇒ Integer

Stores the union of sorted sets into a destination key.

Parameters:

  • destination (String)

    Destination key name

  • other_sets (Array<SortedSet, String>)

    Other sorted sets or key names

  • weights (Array<Numeric>, nil) (defaults to: nil)

    Multiplication factors for each set's scores

  • aggregate (Symbol, nil) (defaults to: nil)

    How to aggregate scores (:sum, :min, :max)

Returns:

  • (Integer)

    Number of elements in the resulting sorted set



744
745
746
747
748
749
750
751
# File 'lib/familia/data_type/types/sorted_set.rb', line 744

def unionstore(destination, *other_sets, weights: nil, aggregate: nil)
  keys = [dbkey] + resolve_set_keys(other_sets)
  opts = build_set_operation_opts(weights: weights, aggregate: aggregate)

  result = dbclient.zunionstore(destination, keys, **opts)
  update_expiration
  result
end

#update(hsh = {}) ⇒ Integer Also known as: merge!

Note:

Unlike single-value #add, scores are required: this bulk path does not default a missing score to Familia.now. A non-Numeric score (e.g. nil) raises ArgumentError rather than surfacing a low-level client error.

Note:

Like #add, this executes immediately (not deferred) and cascades expiration. Empty input is a no-op returning 0.

Bulk-adds or updates multiple members in a single ZADD.

Mirrors HashKey#update/merge! -- the established Familia pattern for bulk-setting keyed collections. A sorted set is member => score, the same pair shape as HashKey's field => value, so it takes a Hash rather than the variadic splat used by the value-only UnsortedSet/ListKey.

Issues exactly one ZADD instead of one round-trip per member, which is what makes populating a large sorted set fast.

Examples:

board.update("alice" => 1000, "bob" => 850)  #=> 2
board.merge!("alice" => 1200)                 #=> 0 (score updated)

Parameters:

  • hsh (Hash) (defaults to: {})

    Mapping of member => score (Object => Numeric)

Returns:

  • (Integer)

    Number of new members added (members whose score was merely updated are not counted), per redis-rb's bulk ZADD return value

Raises:

  • (ArgumentError)

    If the argument is not a Hash, or any score is not Numeric



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/familia/data_type/types/sorted_set.rb', line 183

def update(hsh = {})
  warn_if_dirty!
  raise ArgumentError, 'Argument to bulk add must be a hash' unless hsh.is_a?(Hash)
  return 0 if hsh.empty?

  pairs = hsh.map do |member, score|
    unless score.is_a?(Numeric)
      raise ArgumentError, "SortedSet#update score for #{member.inspect} must be Numeric, got #{score.class}"
    end

    [score, serialize_value(member)]
  end
  # Trim is mandatory here: a bulk populate can blow past the cap in a
  # single ZADD, not just by one member.
  ret = capped_zadd_write { |conn| conn.zadd(dbkey, pairs) }
  update_expiration
  ret
end