Module: Models::HybridSearchable
- Extended by:
- ActiveSupport::Concern
- Included in:
- Image, Publication, Video
- Defined in:
- app/concerns/models/hybrid_searchable.rb
Overview
Shared Reciprocal Rank Fusion (RRF) helper for hybrid search scopes.
Combines two ranked ID lists (e.g. AI embedding + keyword search) into a
single ordered ActiveRecord::Relation using RRF scoring:
score(id) = Σ 1/(k + rank) for each list that contains the id
Items appearing in both lists score highest; items in only one list
still appear but rank lower.
Class Method Summary collapse
-
.ranked_ids(relation) ⇒ Array<Integer>
Pluck ordered ids for RRF, stripping any eager-load values the caller chained on (e.g. a controller's .includes before .ransack).
-
.rrf_ranked_relation(ai_ids, keyword_ids, limit:, k: 60) ⇒ ActiveRecord::Relation
Build a relation filtered to the union of two ranked ID lists, ordered by Reciprocal Rank Fusion score (highest first).
Class Method Details
.ranked_ids(relation) ⇒ Array<Integer>
Pluck ordered ids for RRF, stripping any eager-load values the caller
chained on (e.g. a controller's .includes before .ransack). ids on a
relation carrying includes wraps the query in GROUP BY , which
raises PG::GroupingError when the scope orders by a joined subquery
column (best_match.min_distance, pg_search rank) — and eager loading is
dead weight for an id pluck anyway.
35 36 37 |
# File 'app/concerns/models/hybrid_searchable.rb', line 35 def ranked_ids(relation) relation.unscope(:includes, :eager_load, :preload).ids end |
.rrf_ranked_relation(ai_ids, keyword_ids, limit:, k: 60) ⇒ ActiveRecord::Relation
Build a relation filtered to the union of two ranked ID lists,
ordered by Reciprocal Rank Fusion score (highest first).
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'app/concerns/models/hybrid_searchable.rb', line 47 def rrf_ranked_relation(ai_ids, keyword_ids, limit:, k: 60) combined_ids = (ai_ids | keyword_ids) return none if combined_ids.empty? ai_rank = ai_ids.each_with_index.to_h kw_rank = keyword_ids.each_with_index.to_h scored = combined_ids.map do |id| rrf = 0.0 rrf += 1.0 / (k + ai_rank[id]) if ai_rank.key?(id) rrf += 1.0 / (k + kw_rank[id]) if kw_rank.key?(id) [id, rrf] end.sort_by { |_, s| -s } ordered_ids = scored.map { |id, _| id.to_i }.first(limit) where(id: ordered_ids).order( Arel.sql( "CASE #{table_name}.id " + ordered_ids.each_with_index.map { |id, i| "WHEN #{id} THEN #{i}" }.join(' ') + " ELSE #{ordered_ids.size} END" ) ) end |