module SimpleRecommender::Recommendable

Constants

AssociationMetadata
DEFAULT_N_RESULTS
SIMILARITY_KEY

Public Instance Methods

association_metadata(reflection) click to toggle source

Returns database metadata about an association based on its type, used in constructing a similarity query based on that association

# File lib/simple_recommender/recommendable.rb, line 25
def association_metadata(reflection)
  case reflection
  when ActiveRecord::Reflection::HasAndBelongsToManyReflection
    AssociationMetadata.new(
      reflection.join_table,
      reflection.foreign_key,
      reflection.association_foreign_key
    )
  when ActiveRecord::Reflection::ThroughReflection
    AssociationMetadata.new(
      reflection.through_reflection.table_name,
      reflection.through_reflection.foreign_key,
      reflection.association_foreign_key
    )
  else
    raise ArgumentError, "Association '#{reflection.name}' is not a supported type"
  end
end
similar_query(association_name:, n_results:) click to toggle source

Returns a Postgres query that can be executed to return similar items. Reflects on the association to get relevant table names, and then uses Postgres's integer array intersection/union operators to efficiently compute a Jaccard similarity metric between this item and all other items in the table.

# File lib/simple_recommender/recommendable.rb, line 49
      def similar_query(association_name:, n_results:)
        reflection = self.class.reflect_on_association(association_name)

        if reflection.nil?
          raise ArgumentError, "Could not find association #{association_name}"
        end

        metadata = association_metadata(reflection)
        join_table = metadata[:join_table]
        fkey = metadata[:foreign_key]
        assoc_fkey = metadata[:association_foreign_key]
        this_table = self.class.table_name

        <<-SQL
          WITH similar_items AS (
            SELECT
              t2.#{fkey},
              (# (array_agg(DISTINCT t1.#{assoc_fkey}) & array_agg(DISTINCT t2.#{assoc_fkey})))::float/
              (# (array_agg(DISTINCT t1.#{assoc_fkey}) | array_agg(DISTINCT t2.#{assoc_fkey})))::float as similarity
            FROM #{join_table} AS t1, #{join_table} AS t2
            WHERE t1.#{fkey} = #{id} and t2.#{fkey} != #{id}
            GROUP BY t2.#{fkey}
            ORDER BY similarity DESC
            LIMIT #{n_results}
          )
          SELECT #{this_table}.*, similarity
          FROM similar_items
          JOIN #{this_table} ON #{this_table}.id = similar_items.#{fkey}
          ORDER BY similarity DESC;
        SQL
      end