Module: Familia::Features::Relationships::Querying::ClassMethods

Defined in:
lib/familia/features/relationships/querying.rb

Overview

Querying::ClassMethods

Instance Method Summary collapse

Instance Method Details

#collection_statistics(collections) ⇒ Hash

Get collection statistics

Parameters:

  • collections (Array<Hash>)

    Collection configurations

Returns:

  • (Hash)

    Statistics about the collections



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/familia/features/relationships/querying.rb', line 210

def collection_statistics(collections)
  stats = {
    total_collections: collections.length,
    collection_sizes: {},
    total_unique_members: 0,
    total_members: 0,
    score_ranges: {}
  }

  all_members = Set.new

  collections.each do |collection|
    key = build_collection_key(collection)
    collection_name = format_collection(collection)

    size = dbclient.zcard(key)
    stats[:collection_sizes][collection_name] = size
    stats[:total_members] += size

    next unless size.positive?

    # Get score range
    min_score = dbclient.zrange(key, 0, 0, with_scores: true).first&.last
    max_score = dbclient.zrange(key, -1, -1, with_scores: true).first&.last

    stats[:score_ranges][collection_name] = {
      min: min_score,
      max: max_score,
      min_decoded: min_score ? decode_score(min_score) : nil,
      max_decoded: max_score ? decode_score(max_score) : nil
    }

    # Track unique members
    members = dbclient.zrange(key, 0, -1)
    all_members.merge(members)
  end

  stats[:total_unique_members] = all_members.size
  stats[:overlap_ratio] = if stats[:total_members].positive?
                            (stats[:total_members] - stats[:total_unique_members]).to_f / stats[:total_members]
                          else
                            0
                          end

  stats
end

#difference_collections(base_collection, exclude_collections = [], min_permission: nil, ttl: 300) ⇒ Familia::SortedSet

Difference of collections (items in first collection but not in others)

Parameters:

  • base_collection (Hash)

    Base collection configuration

  • exclude_collections (Array<Hash>) (defaults to: [])

    Collections to exclude

  • min_permission (Symbol) (defaults to: nil)

    Minimum required permission

  • ttl (Integer) (defaults to: 300)

    TTL for result set in seconds

Returns:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/familia/features/relationships/querying.rb', line 79

def difference_collections(base_collection, exclude_collections = [], min_permission: nil, ttl: 300)
  temp_key = create_temp_key("difference_#{name.downcase}", ttl)

  base_key = build_collection_key(base_collection)
  exclude_keys = build_collection_keys(exclude_collections)

  # Apply permission filtering if needed
  if min_permission
    base_key = filter_key_by_permission(base_key, min_permission, "#{temp_key}_base")
    exclude_keys = filter_keys_by_permission(exclude_keys, min_permission, temp_key)
  end

  # Start with base collection
  dbclient.zunionstore(temp_key, [base_key])

  # Remove elements from exclude collections
  exclude_keys.each do |exclude_key|
    members_to_remove = dbclient.zrange(exclude_key, 0, -1)
    dbclient.zrem(temp_key, members_to_remove) if members_to_remove.any?
  end

  dbclient.expire(temp_key, ttl)

  Familia::SortedSet.new(nil, dbkey: temp_key, logical_database: logical_database)
end

#intersection_collections(collections, min_permission: nil, ttl: 300, aggregate: :sum) ⇒ Familia::SortedSet

Intersection of multiple collections (items present in ALL collections)

Parameters:

  • collections (Array<Hash>)

    Collection configurations

  • min_permission (Symbol) (defaults to: nil)

    Minimum required permission

  • ttl (Integer) (defaults to: 300)

    TTL for result set in seconds

Returns:



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/familia/features/relationships/querying.rb', line 55

def intersection_collections(collections, min_permission: nil, ttl: 300, aggregate: :sum)
  return empty_result_set if collections.empty?

  temp_key = create_temp_key("intersection_#{name.downcase}", ttl)
  source_keys = build_collection_keys(collections)

  # Apply permission filtering if needed
  source_keys = filter_keys_by_permission(source_keys, min_permission, temp_key) if min_permission

  return empty_result_set if source_keys.empty?

  dbclient.zinterstore(temp_key, source_keys, aggregate: aggregate)
  dbclient.expire(temp_key, ttl)

  Familia::SortedSet.new(nil, dbkey: temp_key, logical_database: logical_database)
end

#query_collections(collections, filters = {}, ttl: 300) ⇒ Familia::SortedSet

Query collections with complex filters

Examples:

Complex query

Domain.query_collections([
  { owner: customer, collection: :domains },
  { owner: team, collection: :domains }
], {
  min_permission: :write,
  score_range: [1.week.ago.to_i, Time.now.to_i],
  limit: 50,
  operation: :union
})

Parameters:

  • collections (Array<Hash>)

    Collection configurations

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

    Query filters

  • ttl (Integer) (defaults to: 300)

    TTL for result set in seconds

Returns:



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/familia/features/relationships/querying.rb', line 158

def query_collections(collections, filters = {}, ttl: 300)
  return empty_result_set if collections.empty?

  operation = filters[:operation] || :union
  min_permission = filters[:min_permission]
  score_range = filters[:score_range]
  limit = filters[:limit]
  offset = filters[:offset] || 0

  temp_key = create_temp_key("query_#{name.downcase}", ttl)
  source_keys = build_collection_keys(collections)

  # Apply permission filtering
  source_keys = filter_keys_by_permission(source_keys, min_permission, temp_key) if min_permission

  return empty_result_set if source_keys.empty?

  # Perform set operation
  case operation
  when :union
    dbclient.zunionstore(temp_key, source_keys)
  when :intersection
    dbclient.zinterstore(temp_key, source_keys)
  end

  # Apply score range filtering
  if score_range
    min_score, max_score = score_range
    # Remove elements outside the score range
    dbclient.zremrangebyscore(temp_key, '-inf', "(#{min_score}")
    dbclient.zremrangebyscore(temp_key, "(#{max_score}", '+inf')
  end

  # Apply limit
  if limit
    total_count = dbclient.zcard(temp_key)
    if total_count > offset + limit
      # Keep only the requested range
      dbclient.zremrangebyrank(temp_key, offset + limit, -1)
    end
    dbclient.zremrangebyrank(temp_key, 0, offset - 1) if offset.positive?
  end

  dbclient.expire(temp_key, ttl)

  Familia::SortedSet.new(nil, dbkey: temp_key, logical_database: logical_database)
end

#shared_members(collections, min_shared: 1, ttl: 300) ⇒ Hash

Find collections with shared members

Parameters:

  • collections (Array<Hash>)

    Collection configurations

  • min_shared (Integer) (defaults to: 1)

    Minimum number of shared members

  • ttl (Integer) (defaults to: 300)

    TTL for result set in seconds

Returns:

  • (Hash)

    Map of collection pairs to shared member counts



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/familia/features/relationships/querying.rb', line 111

def shared_members(collections, min_shared: 1, ttl: 300)
  return {} if collections.length < 2

  shared_results = {}
  collections.map { |c| build_collection_key(c) }

  # Compare each pair of collections
  collections.combination(2).each do |coll1, coll2|
    key1 = build_collection_key(coll1)
    key2 = build_collection_key(coll2)

    temp_key = create_temp_key("shared_#{SecureRandom.hex(4)}", ttl)

    # Use intersection to find shared members
    shared_count = dbclient.zinterstore(temp_key, [key1, key2])

    if shared_count >= min_shared
      shared_members_list = dbclient.zrange(temp_key, 0, -1, with_scores: true)
      shared_results["#{format_collection(coll1)}#{format_collection(coll2)}"] = {
        count: shared_count,
        members: shared_members_list
      }
    end

    dbclient.del(temp_key)
  end

  shared_results
end

#union_collections(collections, min_permission: nil, ttl: 300, aggregate: :sum) ⇒ Familia::SortedSet

Union of multiple collections (accessible items across multiple sources)

Examples:

Union of accessible domains

Domain.union_collections([
  { owner: customer, collection: :domains },
  { owner: team, collection: :domains },
  { owner: org, collection: :all_domains }
], min_permission: :read, ttl: 300)

Parameters:

  • collections (Array<Hash>)

    Collection configurations

  • min_permission (Symbol) (defaults to: nil)

    Minimum required permission

  • ttl (Integer) (defaults to: 300)

    TTL for result set in seconds

Returns:



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/familia/features/relationships/querying.rb', line 32

def union_collections(collections, min_permission: nil, ttl: 300, aggregate: :sum)
  return empty_result_set if collections.empty?

  temp_key = create_temp_key("union_#{name.downcase}", ttl)
  source_keys = build_collection_keys(collections)

  # Apply permission filtering if needed
  source_keys = filter_keys_by_permission(source_keys, min_permission, temp_key) if min_permission

  return empty_result_set if source_keys.empty?

  dbclient.zunionstore(temp_key, source_keys, aggregate: aggregate)
  dbclient.expire(temp_key, ttl)

  Familia::SortedSet.new(nil, dbkey: temp_key, logical_database: logical_database)
end