Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,34 @@ one_close = Person.create!(:name => 'leigh heinz')
Person.search('ash hines') # => [exact, one_exact_one_close, one_exact]
```

##### :highlight (PostgreSQL 9.0 and newer only)

Adding .with_pg_search_highlight after the pg_search_scope you can access to
`pg_highlight` attribute for each object.


```ruby
class Person < ActiveRecord::Base
include PgSearch
pg_search_scope :search,
:against => :bio,
:using => {
:tsearch => {
:start_sel => '<b>',
:stop_sel => '</b>'
}
}
end

Person.create!(:bio => "Born in rural Alberta, where the buffalo roam.")

first_match = Person.search("Alberta").with_pg_search_highlight.first
first_match.pg_search_highlight # => "Born in rural <b>Alberta</b>, where the buffalo roam."
```

By default, it will add the delimiters `<b>` and `</b>` around the matched text. You can customize these delimiters and the number of fragments returned with the `:start_sel`, `stop_sel`, and `max_fragments` options.


#### :dmetaphone (Double Metaphone soundalike search)

[Double Metaphone](http://en.wikipedia.org/wiki/Double_Metaphone) is an
Expand Down
13 changes: 12 additions & 1 deletion lib/pg_search.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ def method_missing(symbol, *args)
when :pg_search_rank
raise PgSearchRankNotSelected.new unless respond_to?(:pg_search_rank)
read_attribute(:pg_search_rank).to_f
when :pg_search_highlight
raise PgSearchHighlightNotSelected.new unless respond_to?(:pg_search_highlight)
read_attribute(:pg_search_highlight)
else
super
end
Expand All @@ -82,6 +85,8 @@ def respond_to_missing?(symbol, *args)
case symbol
when :pg_search_rank
attributes.key?(:pg_search_rank)
when :pg_search_highlight
attributes.key?(:pg_search_highlight)
else
super
end
Expand All @@ -90,9 +95,15 @@ def respond_to_missing?(symbol, *args)
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" # rubocop:disable Metrics/LineLength
end
end

class PgSearchHighlightNotSelected < StandardError
# rubocop:disable Metrics/LineLength
def message
"You must chain .with_pg_search_rank after the pg_search_scope to access the pg_search_rank attribute on returned records"
"You must chain .with_pg_search_highlight after the pg_search_scope to access the pg_search_highlight attribute on returned records"
end
end
end
Expand Down
51 changes: 43 additions & 8 deletions lib/pg_search/features/tsearch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@

module PgSearch
module Features
class TSearch < Feature
class TSearch < Feature # rubocop:disable Metrics/ClassLength
def self.valid_options
super + [:dictionary, :prefix, :negation, :any_word, :normalization, :tsvector_column]
super + [:dictionary, :prefix, :negation, :any_word, :normalization, :tsvector_column, :highlight]
end

def initialize(*args)
super

if options[:prefix] && model.connection.send(:postgresql_version) < 80400
raise PgSearch::NotSupportedForPostgresqlVersion.new(<<-MESSAGE.strip_heredoc)
Sorry, {:using => {:tsearch => {:prefix => true}}} only works in PostgreSQL 8.4 and above.")
MESSAGE
end
checks_for_highlight
checks_for_prefix
end

def conditions
Expand All @@ -28,8 +24,47 @@ def rank
arel_wrap(tsearch_rank)
end

def highlight
arel_wrap(ts_headline)
end

private

def checks_for_prefix
if options[:prefix] && model.connection.send(:postgresql_version) < 80400
raise PgSearch::NotSupportedForPostgresqlVersion.new(<<-MESSAGE.strip_heredoc)
Sorry, {:using => {:tsearch => {:prefix => true}}} only works in PostgreSQL 8.4 and above.")
MESSAGE
end
end

def checks_for_highlight
if options[:highlight] && model.connection.send(:postgresql_version) < 90000
raise PgSearch::NotSupportedForPostgresqlVersion.new(<<-MESSAGE.strip_heredoc)
Sorry, {:using => {:tsearch => {:highlight => true}}} only works in PostgreSQL 9.0 and above.")
MESSAGE
end
end

def ts_headline
"ts_headline((#{document}), (#{tsquery}), '#{ts_headline_options}')"
end

def ts_headline_options
return nil unless options[:highlight].is_a?(Hash)

headline_options = map_headline_options
headline_options.map{|key, value| "#{key} = #{value}" if value }.compact.join(", ")
end

def map_headline_options
{
"StartSel" => options[:highlight][:start_sel],
"StopSel" => options[:highlight][:stop_sel],
"MaxFragments" => options[:highlight][:max_fragments]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of allowing other options?

From the docs:

  • FragmentDelimiter
  • HighlightAll
  • ShortWord
  • MaxWords, MinWords

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is good idea, also I think that the options could be passed in the query and overwrite the default configuration.

end

DISALLOWED_TSQUERY_CHARACTERS = /['?\\:]/

def tsquery_for_term(unsanitized_term) # rubocop:disable Metrics/AbcSize
Expand Down
27 changes: 27 additions & 0 deletions lib/pg_search/scope_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def apply(scope)
.order("#{rank_table_alias}.rank DESC, #{order_within_rank}")
.extend(DisableEagerLoading)
.extend(WithPgSearchRank)
.extend(WithPgSearchHighlight[feature_for(:tsearch)])
end

# workaround for https://github.com/Casecommons/pg_search/issues/14
Expand All @@ -30,6 +31,32 @@ def eager_loading?
end
end

module WithPgSearchHighlight
def self.[](tsearch)
Module.new do
include WithPgSearchHighlight
define_method(:tsearch) { tsearch }
end
end

def tsearch
raise TypeError.new("You need to instantiate this module with []")
end

def with_pg_search_highlight
scope = self
scope.select(pg_search_highlight_field)
end

def pg_search_highlight_field
"(#{highlight}) AS pg_search_highlight, #{table_name}.*"
end

def highlight
tsearch.highlight.to_sql
end
end

module WithPgSearchRank
def with_pg_search_rank
scope = self
Expand Down
27 changes: 27 additions & 0 deletions spec/integration/pg_search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,33 @@
end
end

describe "highlighting" do
before do
["Strip Down", "Down", "Down and Out", "Won't Let You Down"].each do |name|
ModelWithPgSearch.create! :content => name
end
end

context "with highlight turned on" do
before do
ModelWithPgSearch.pg_search_scope :search_content,
:against => :content
end

it "adds a #pg_search_highlight method to each returned model record" do
result = ModelWithPgSearch.search_content("Strip Down").with_pg_search_highlight.first

expect(result.pg_search_highlight).to be_a(String)
end

it "returns excerpts of text where search match occurred" do
result = ModelWithPgSearch.search_content("Let").with_pg_search_highlight.first

expect(result.pg_search_highlight).to eq("Won't <b>Let</b> You Down")
end
end
end

describe "ranking" do
before do
["Strip Down", "Down", "Down and Out", "Won't Let You Down"].each do |name|
Expand Down