Skip to content

Commit

Permalink
Move search-related operations to subquery
Browse files Browse the repository at this point in the history
  • Loading branch information
nertzy committed Feb 22, 2015
1 parent fcf4480 commit eb730f7
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 33 deletions.
27 changes: 26 additions & 1 deletion lib/pg_search.rb
Expand Up @@ -15,7 +15,6 @@

module PgSearch
extend ActiveSupport::Concern
include Compatibility::ActiveRecord3 if ActiveRecord::VERSION::MAJOR == 3

mattr_accessor :multisearch_options
self.multisearch_options = {}
Expand Down Expand Up @@ -75,7 +74,33 @@ def multisearch_enabled?
end
end

def method_missing(symbol, *args)
case symbol
when :pg_search_rank
raise PgSearchRankNotSelected.new unless respond_to?(:pg_search_rank)
read_attribute(:pg_search_rank).to_f
else
super
end
end

def respond_to_missing?(symbol, *args)
case symbol
when :pg_search_rank
attributes.key?(:pg_search_rank)
else
super
end
end


class NotSupportedForPostgresqlVersion < StandardError; end

class PgSearchRankNotSelected < StandardError
def message
"You must chain .with_pg_search_rank after the pg_search_scope to access the pg_search_rank attribute on returned records"
end
end
end

require "pg_search/document"
Expand Down
6 changes: 0 additions & 6 deletions lib/pg_search/compatibility.rb
@@ -1,11 +1,5 @@
module PgSearch
module Compatibility
module ActiveRecord3
def pg_search_rank
read_attribute(:pg_search_rank).to_f
end
end

def self.build_quoted(string)
if defined?(Arel::Nodes::Quoted)
Arel::Nodes.build_quoted(string)
Expand Down
41 changes: 31 additions & 10 deletions lib/pg_search/scope_options.rb
Expand Up @@ -4,7 +4,7 @@

module PgSearch
class ScopeOptions
attr_reader :config, :feature_options
attr_reader :config, :feature_options, :model

def initialize(config)
@config = config
Expand All @@ -13,12 +13,11 @@ def initialize(config)
end

def apply(scope)
scope.
select("#{quoted_table_name}.*, (#{rank}) AS pg_search_rank").
where(conditions).
order("pg_search_rank DESC, #{order_within_rank}").
joins(joins).
extend(DisableEagerLoading)
scope
.joins(rank_join)
.order("pg_search.rank DESC, #{order_within_rank}")
.extend(DisableEagerLoading)
.extend(WithPgSearchRank)
end

# workaround for https://github.com/Casecommons/pg_search/issues/14
Expand All @@ -28,9 +27,27 @@ def eager_loading?
end
end

module WithPgSearchRank
def with_pg_search_rank
scope = self
scope = scope.select("*") unless scope.select_values.any?
scope.select("pg_search.rank AS pg_search_rank")
end
end

private

delegate :connection, :quoted_table_name, :to => :@model
delegate :connection, :quoted_table_name, :to => :model

def subquery
model
.select("#{primary_key} AS id")
.select("#{rank} AS rank")
.joins(subquery_join)
.where(conditions)
.limit(nil)
.offset(nil)
end

def conditions
config.features.reject do |feature_name, feature_options|
Expand All @@ -47,10 +64,10 @@ def order_within_rank
end

def primary_key
"#{quoted_table_name}.#{connection.quote_column_name(@model.primary_key)}"
"#{quoted_table_name}.#{connection.quote_column_name(model.primary_key)}"
end

def joins
def subquery_join
if config.associations.any?
config.associations.map do |association|
association.join(primary_key)
Expand Down Expand Up @@ -86,5 +103,9 @@ def rank
feature_for($1).rank.to_sql
end
end

def rank_join
"INNER JOIN (#{subquery.to_sql}) pg_search ON #{primary_key} = pg_search.id"
end
end
end
2 changes: 1 addition & 1 deletion spec/integration/associations_spec.rb
Expand Up @@ -300,7 +300,7 @@

results = ModelWithAssociation.with_associated('foo bar')

expect(results.to_sql.scan("INNER JOIN").length).to eq(1)
expect(results.to_sql.scan("INNER JOIN #{AssociatedModel.quoted_table_name}").length).to eq(1)
included.each { |object| expect(results).to include(object) }
excluded.each { |object| expect(results).not_to include(object) }
end
Expand Down
61 changes: 46 additions & 15 deletions spec/integration/pg_search_spec.rb
Expand Up @@ -211,7 +211,7 @@
loser = ModelWithPgSearch.create!(:content => 'foo')
winner = ModelWithPgSearch.create!(:content => 'foo foo')

results = ModelWithPgSearch.search_content("foo")
results = ModelWithPgSearch.search_content("foo").with_pg_search_rank
expect(results[0].pg_search_rank).to be > results[1].pg_search_rank
expect(results).to eq([winner, loser])
end
Expand Down Expand Up @@ -426,7 +426,7 @@
it "adds a #pg_search_rank method to each returned model record" do
ModelWithPgSearch.pg_search_scope :search_content, :against => :content

result = ModelWithPgSearch.search_content("Strip Down").first
result = ModelWithPgSearch.search_content("Strip Down").with_pg_search_rank.first

expect(result.pg_search_rank).to be_a(Float)
end
Expand All @@ -441,7 +441,7 @@
end

it "ranks the results for documents with less text higher" do
results = ModelWithPgSearch.search_content_with_normalization("down")
results = ModelWithPgSearch.search_content_with_normalization("down").with_pg_search_rank

expect(results.map(&:content)).to eq(["Down", "Strip Down", "Down and Out", "Won't Let You Down"])
expect(results.first.pg_search_rank).to be > results.last.pg_search_rank
Expand All @@ -456,7 +456,7 @@
end

it "ranks the results equally" do
results = ModelWithPgSearch.search_content_without_normalization("down")
results = ModelWithPgSearch.search_content_without_normalization("down").with_pg_search_rank

expect(results.map(&:content)).to eq(["Strip Down", "Down", "Down and Out", "Won't Let You Down"])
expect(results.first.pg_search_rank).to eq(results.last.pg_search_rank)
Expand All @@ -474,7 +474,7 @@
loser = ModelWithPgSearch.create!(:title => 'bar', :content => 'foo')
winner = ModelWithPgSearch.create!(:title => 'foo', :content => 'bar')

results = ModelWithPgSearch.search_weighted_by_array_of_arrays('foo')
results = ModelWithPgSearch.search_weighted_by_array_of_arrays('foo').with_pg_search_rank
expect(results[0].pg_search_rank).to be > results[1].pg_search_rank
expect(results).to eq([winner, loser])
end
Expand All @@ -490,7 +490,7 @@
loser = ModelWithPgSearch.create!(:title => 'bar', :content => 'foo')
winner = ModelWithPgSearch.create!(:title => 'foo', :content => 'bar')

results = ModelWithPgSearch.search_weighted_by_hash('foo')
results = ModelWithPgSearch.search_weighted_by_hash('foo').with_pg_search_rank
expect(results[0].pg_search_rank).to be > results[1].pg_search_rank
expect(results).to eq([winner, loser])
end
Expand All @@ -506,7 +506,7 @@
loser = ModelWithPgSearch.create!(:title => 'bar', :content => 'foo')
winner = ModelWithPgSearch.create!(:title => 'foo', :content => 'bar')

results = ModelWithPgSearch.search_weighted('foo')
results = ModelWithPgSearch.search_weighted('foo').with_pg_search_rank
expect(results[0].pg_search_rank).to be > results[1].pg_search_rank
expect(results).to eq([winner, loser])
end
Expand Down Expand Up @@ -910,15 +910,24 @@

it "should return records with a rank attribute equal to the :ranked_by expression" do
ModelWithPgSearch.create!(:content => 'foo', :importance => 10)
results = ModelWithPgSearch.search_content_with_importance_as_rank("foo")
results = ModelWithPgSearch.search_content_with_importance_as_rank("foo").with_pg_search_rank
expect(results.first.pg_search_rank).to eq(10)
end

it "should substitute :tsearch with the tsearch rank expression in the :ranked_by expression" do
ModelWithPgSearch.create!(:content => 'foo', :importance => 10)

tsearch_rank = ModelWithPgSearch.search_content_with_default_rank("foo").first.pg_search_rank
multiplied_rank = ModelWithPgSearch.search_content_with_importance_as_rank_multiplier("foo").first.pg_search_rank
tsearch_result =
ModelWithPgSearch.search_content_with_default_rank("foo").with_pg_search_rank.first

tsearch_rank = tsearch_result.pg_search_rank

multiplied_result =
ModelWithPgSearch.search_content_with_importance_as_rank_multiplier("foo")
.with_pg_search_rank
.first

multiplied_rank = multiplied_result.pg_search_rank

expect(multiplied_rank).to be_within(0.001).of(tsearch_rank * 10)
end
Expand All @@ -936,17 +945,39 @@

%w[tsearch trigram dmetaphone].each do |feature|
context "using the #{feature} ranking algorithm" do
it "should return results with a rank" do
scope_name = :"search_content_ranked_by_#{feature}"

let(:scope_name) { :"search_content_ranked_by_#{feature}" }
before do
ModelWithPgSearch.pg_search_scope scope_name,
:against => :content,
:ranked_by => ":#{feature}"

ModelWithPgSearch.create!(:content => 'foo')
end

context "when .with_pg_search_rank is chained after" do
specify "its results respond to #pg_search_rank" do
result = ModelWithPgSearch.send(scope_name, 'foo').with_pg_search_rank.first
expect(result).to respond_to(:pg_search_rank)
end

it "returns the rank when #pg_search_rank is called on a result" do
results = ModelWithPgSearch.send(scope_name, 'foo').with_pg_search_rank
expect(results.first.pg_search_rank).to be_a Float
end
end

results = ModelWithPgSearch.send(scope_name, 'foo')
expect(results.first.pg_search_rank).to be_a Float
context "when .with_pg_search_rank is not chained after" do
specify "its results do not respond to #pg_search_rank" do
result = ModelWithPgSearch.send(scope_name, 'foo').first
expect(result).not_to respond_to(:pg_search_rank)
end

it "raises PgSearch::PgSearchRankNotSelected when #pg_search_rank is called on a result" do
result = ModelWithPgSearch.send(scope_name, 'foo').first
expect {
result.pg_search_rank
}.to raise_exception(PgSearch::PgSearchRankNotSelected)
end
end
end
end
Expand Down

0 comments on commit eb730f7

Please sign in to comment.