2

I have a web-service that allows clients to search for articles with query parameters.It works fine if only one parameter is included but fails if I combine search_query and category. This is based on Comfortable_Mexican_Sofa where for_category is found. Even if I remove the order statement i get this error.

error

PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list LINE 1: ...ms_categories"."label" = 'Company News' ORDER BY pg_search_... ^ : SELECT DISTINCT "comfy_cms_pages".* FROM "comfy_cms_pages" INNER JOIN "comfy_cms_categorizations" ON "comfy_cms_categorizations"."categorized_id" = "comfy_cms_pages"."id" AND "comfy_cms_categorizations"."categorized_type" = $1 INNER JOIN "comfy_cms_categories" ON "comfy_cms_categories"."id" = "comfy_cms_categorizations"."category_id" INNER JOIN (SELECT "comfy_cms_pages"."id" AS pg_search_id, (ts_rank((to_tsvector('simple', coalesce("comfy_cms_pages"."content_cache"::text, '')) || to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))), (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':')), 0)) AS rank FROM "comfy_cms_pages" WHERE (((to_tsvector('simple', coalesce("comfy_cms_pages"."content_cache"::text, '')) || to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))) @@ (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':'))))) pg_search_comfy_cms_pages ON "comfy_cms_pages"."id" = pg_search_comfy_cms_pages.pg_search_id WHERE (layout_id = '1' AND is_published = 't') AND "comfy_cms_categories"."label" = 'Company News' ORDER BY pg_search_comfy_cms_pages.rank DESC, "comfy_cms_pages"."id" ASC, "comfy_cms_pages"."created_at" DESC

app/models/article.rb

class Article < Comfy::Cms::Page
  cms_is_categorized
  include PgSearch


  pg_search_scope :search_by_keywords, against: [:content_cache, :label], using: { tsearch: { any_word: true, prefix: true } }

app/commands/search_articles_command.rb

class SearchArticlesCommand
  def initialize(params = {})
    @since = params[:since_date]
    @keys = params[:search_query]
    @category = params[:category]
  end

  def execute
    Article.unscoped do
      query = if @since.present?
                Article.article.since_date(@since)
              else
                Article.published_article
              end
      query = query.for_category(@category) if @category.present?
      query = query.search_by_keywords(@keys) if @keys.present?
      query.where('').order(created_at: :desc)
    end
  end
end

comfortable-mexican-sofa/lib/comfortable_mexican_sofa/extensions/is_categorized.rb

module ComfortableMexicanSofa::IsCategorized

  def self.included(base)
    base.send :extend, ClassMethods
  end

  module ClassMethods
    def cms_is_categorized
      include ComfortableMexicanSofa::IsCategorized::InstanceMethods

      has_many :categorizations,
        :as         => :categorized,
        :class_name => 'Comfy::Cms::Categorization',
        :dependent  => :destroy
      has_many :categories,
        :through    => :categorizations,
        :class_name => 'Comfy::Cms::Category'

      attr_accessor :category_ids

      after_save :sync_categories

      scope :for_category, lambda { |*categories|
        if (categories = [categories].flatten.compact).present?
          self.distinct.
            joins(:categorizations => :category).
            where('comfy_cms_categories.label' => categories)
        end
      }
    end
  end

  module InstanceMethods
    def sync_categories
      (self.category_ids || {}).each do |category_id, flag|
        case flag.to_i
        when 1
          if category = Comfy::Cms::Category.find_by_id(category_id)
            category.categorizations.create(:categorized => self)
          end
        when 0
          self.categorizations.where(:category_id => category_id).destroy_all
        end
      end
    end
  end
end

ActiveRecord::Base.send :include, ComfortableMexicanSofa::IsCategorized

Updated Error

PG::SyntaxError: ERROR:  syntax error at or near "."
LINE 4: ...e = 'Class' AND categorized_id = 'comfy_cms_pages'.'id' AND ...
                                                             ^
: SELECT "comfy_cms_pages".* FROM "comfy_cms_pages" INNER JOIN (SELECT "comfy_cms_pages"."id" AS pg_search_id, (ts_rank((to_tsvector('simple', coalesce("comfy_cms_pages"."content_cache"::text, '')) || to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))), (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':*')), 0)) AS rank FROM "comfy_cms_pages" WHERE (((to_tsvector('simple', coalesce("comfy_cms_pages"."content_cache"::text, '')) || to_tsvector('simple', coalesce("comfy_cms_pages"."label"::text, ''))) @@ (to_tsquery('simple', ''' ' || 'austin' || ' ''' || ':*'))))) pg_search_comfy_cms_pages ON "comfy_cms_pages"."id" = pg_search_comfy_cms_pages.pg_search_id WHERE "comfy_cms_pages"."layout_id" = $1 AND "comfy_cms_pages"."is_published" = $2 AND (
        EXISTS (
          SELECT 1 FROM categorizations
          WHERE categorized_type = 'Class' AND categorized_id = 'comfy_cms_pages'.'id' AND category_id IN (2)
        ))  ORDER BY pg_search_comfy_cms_pages.rank DESC, "comfy_cms_pages"."id" ASC

working solution but not a scope and have to be careful of order its being called

  def self.for_category(_category)
    Comfy::Cms::Categorization.includes(:category).references(:category).select(:categorized).pluck(:categorized_id)
    find(ids)
  end
Antarr Byrd
  • 24,863
  • 33
  • 100
  • 188

2 Answers2

2

I think it's best to override for_category built-in filter of your CMS. Too many joins in that query.

Override for_category like this:

scope :for_category, lambda { |*categories|
  if (categories = [categories].flatten.compact).present?
    self_ids = "{connection.quote_table_name(self.table_name)}.#{connection.quote_column_name(self.primary_key)}"
    self.where(
      "EXISTS (" +
        Comfy::Cms::Categorization.select('1').
          where(categorized_type: self.name).
          where('categorized_id' => self_ids).
          where(category_id: Comfy::Cms::Category.where(label: categories).pluck(:id)).to_sql +
      ")"
    )
  end
}

More on SQL EXISTS usage in Rails you can read in my Rails: SQL EXISTS brief how-to.

More on why you bump into that error you can read in question and answer here.

Specifically, pg_search wants order your results by rank. And for_category wants to select distinct fields of Article only, and doesn't care about search rank. Changing its code to use simple EXISTS instead of complex JOIN query will fix that.

Community
  • 1
  • 1
EugZol
  • 6,476
  • 22
  • 41
  • It give the error undefined method `id' for # – Antarr Byrd Aug 28 '15 at 19:07
  • @AntarrByrd My bad. I've updated my answer, try again please. – EugZol Aug 28 '15 at 19:11
  • It not throws a syntax error at `AND categorized_id = ` – Antarr Byrd Aug 28 '15 at 19:26
  • @AntarrByrd I don't quite understand you. Does it throw some new exception? Could you provide full text of it please? – EugZol Aug 28 '15 at 19:31
  • After changing `categorizations` to `comfy_cms_categorizations` it doesn't throw an error but it return an empty list. And if I do it by category alone it returns everything – Antarr Byrd Aug 28 '15 at 20:45
  • I posted a working solution I have above. Its not a scope however and I have to be careful to call it last when chaining – Antarr Byrd Aug 28 '15 at 20:54
  • @AntarrByrd Your solution doesn't seem to filter anything: it just selects all categories. – EugZol Aug 28 '15 at 21:03
  • @AntarrByrd I've updated my answer - rewritten for more clarity. The problem seems to be in `categorized_type` – please check whether it works if you set `Article` as in my answer edit. – EugZol Aug 28 '15 at 21:05
  • Its actually Comfy::Cms::Page. After changing it from article it still returns everything – Antarr Byrd Aug 28 '15 at 21:19
  • I don't quite get it. You said that it returned an empty list on previous iteration. Now it returns everything instead of only given categories? What does `Comfy::Cms::Categorization.distinct('categorized_type').map(&:categorized_type)` show? – EugZol Aug 28 '15 at 21:28
  • It return everything if I only search by category? If I combine it with search_by_keywords it returns an empty list – Antarr Byrd Aug 28 '15 at 21:49
  • Please clarify – if you use *only* search by category, does it return the right result set, or too much records (not only category you chose)? – EugZol Aug 28 '15 at 21:52
  • It is returning every article I have instead just those with have the given category – Antarr Byrd Aug 28 '15 at 21:57
  • You can try my gem [Where Exists](https://github.com/EugZol/where_exists), which I've released recently. Your scope should work with one line: `self.where_exists(:categories, label: categories)`. – EugZol Aug 30 '15 at 17:55
  • Sadly using the gem doesn't work either. When I search by the category alone nothing (empty relationship) is returned. `scope :for_category, ->(category) { where_exists(:categories, label: Array.wrap(category)) }` . The same if true when using a class method. – Antarr Byrd Aug 31 '15 at 14:35
  • @AntarrByrd Definitely there's something wrong somewhere else. Ensure that you have `Categorization` and `Category` records in the database which point to your `Article`. – EugZol Aug 31 '15 at 14:51
0

I was able to solve this problem by applying reorder to the result of the pg_search result.

search_command.rb

class SearchArticlesCommand
  def initialize(params = {})
    @since = params['since_date']
    @keys = params['search_query']
    @category = params['category']
  end

  def execute
    Article.unscoped do
      query = Article.article
      query = if @since.present?
                query.since_date(@since)
              else
                query.published
              end
      query = query.for_category(@category) if @category.present?
      query = query.search_by_keywords(@keys).reorder('updated_at DESC') if @keys.present?
      query
    end
  end
end

I also overrode for_category (not required)

article.rb

  scope :for_category, (lambda do |category|
    published
      .joins(:categories)
      .group(:id)
      .where('comfy_cms_categories.label' => category)
      .select('comfy_cms_pages.*')
  end)
Antarr Byrd
  • 24,863
  • 33
  • 100
  • 188