From 4d47aeb3d09c752ba0c6ae0c9887ea6030c9adef Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Mon, 15 Dec 2014 15:13:46 -0500 Subject: [PATCH 01/15] Return highlighted snippet of search result text If configuration is set to ``` :using => { :tsearch => {:highlight => true} } ``` then queryies will return records with an additional `pg_highlight` attribute. Currently uses the defaults set by the Postgres function ts_headline, as specified here: http://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-HEADLINE --- lib/pg_search/features/tsearch.rb | 12 ++++++++++ lib/pg_search/scope_options.rb | 10 +++++++- spec/integration/pg_search_spec.rb | 37 +++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index 5e4e321f..bdadf132 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -24,6 +24,10 @@ def rank arel_wrap(tsearch_rank) end + def highlight + arel_wrap(ts_headline) if options[:highlight] + end + private DISALLOWED_TSQUERY_CHARACTERS = /['?\\:]/ @@ -101,6 +105,14 @@ def tsearch_rank "ts_rank((#{tsdocument}), (#{tsquery}), #{normalization})" end + def ts_headline + document = columns_to_use.map do |column| + Arel.sql(normalize(column.to_sql)) + end.join(' || ') + + "ts_headline(#{document}, (#{tsquery}))" + end + def dictionary Compatibility.build_quoted(options[:dictionary] || :simple) end diff --git a/lib/pg_search/scope_options.rb b/lib/pg_search/scope_options.rb index 93f728ae..a22e43c5 100644 --- a/lib/pg_search/scope_options.rb +++ b/lib/pg_search/scope_options.rb @@ -14,7 +14,7 @@ def initialize(config) def apply(scope) scope. - select("#{quoted_table_name}.*, (#{rank}) AS pg_search_rank"). + select("#{quoted_table_name}.*, (#{rank}) AS pg_search_rank" + select_highlight). where(conditions). order("pg_search_rank DESC, #{order_within_rank}"). joins(joins). @@ -86,5 +86,13 @@ def rank feature_for($1).rank.to_sql end end + + def select_highlight + if highlight = feature_for(:tsearch).highlight + ", (#{highlight.to_sql}) AS pg_highlight" + else + "" + end + end end end diff --git a/spec/integration/pg_search_spec.rb b/spec/integration/pg_search_spec.rb index 3cf20abf..034419e6 100644 --- a/spec/integration/pg_search_spec.rb +++ b/spec/integration/pg_search_spec.rb @@ -416,6 +416,40 @@ 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, + :using => { + :tsearch => {:highlight => true} + } + end + + it "adds a #pg_highlight method to each returned model record" do + result = ModelWithPgSearch.search_content("Strip Down").first + + expect(result.pg_highlight).to be_a(String) + end + + it "returns excerpts of text where search match occurred" do + result = ModelWithPgSearch.search_content("Let").first + + expect(result.pg_highlight).to eq("Won't Let You Down") + end + end + + context "with highlight turned off" do + it "does not add a #pg_highlight method to each returned model record" + end + end + describe "ranking" do before do ["Strip Down", "Down", "Down and Out", "Won't Let You Down"].each do |name| @@ -717,7 +751,8 @@ it "should pass the custom configuration down to the specified feature" do stub_feature = double( :conditions => Arel::Nodes::Grouping.new(Arel.sql("1 = 1")), - :rank => Arel::Nodes::Grouping.new(Arel.sql("1.0")) + :rank => Arel::Nodes::Grouping.new(Arel.sql("1.0")), + :highlight => nil ) expect(PgSearch::Features::TSearch).to receive(:new).with(anything, @tsearch_config, anything, anything, anything).at_least(:once).and_return(stub_feature) From 817a58415f1b5b150b765d05c1e7089be50a6a02 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Mon, 15 Dec 2014 15:21:10 -0500 Subject: [PATCH 02/15] Examples to test no addition of unwanted #pg_highlight method --- spec/integration/pg_search_spec.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/spec/integration/pg_search_spec.rb b/spec/integration/pg_search_spec.rb index 034419e6..d62e0da2 100644 --- a/spec/integration/pg_search_spec.rb +++ b/spec/integration/pg_search_spec.rb @@ -446,7 +446,19 @@ end context "with highlight turned off" do - it "does not add a #pg_highlight method to each returned model record" + before do + ModelWithPgSearch.pg_search_scope :search_content, + :against => :content, + :using => { + :tsearch => {:highlight => false} + } + end + + it "does not add a #pg_highlight method to each returned model record" do + result = ModelWithPgSearch.search_content("Strip Down").first + + expect(result).to_not respond_to(:pg_highlight) + end end end From f5d394fd92f993282d6a21d83e80e41158c3d000 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Mon, 15 Dec 2014 15:50:58 -0500 Subject: [PATCH 03/15] Refactor select fields into own methods Improves the "SELECT" clause of the statement by factoring out added fields into their own methods to be added as needed. --- lib/pg_search/features/tsearch.rb | 2 +- lib/pg_search/scope_options.rb | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index bdadf132..28ef09b9 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -25,7 +25,7 @@ def rank end def highlight - arel_wrap(ts_headline) if options[:highlight] + arel_wrap(ts_headline) end private diff --git a/lib/pg_search/scope_options.rb b/lib/pg_search/scope_options.rb index a22e43c5..2cd1b5a1 100644 --- a/lib/pg_search/scope_options.rb +++ b/lib/pg_search/scope_options.rb @@ -14,7 +14,7 @@ def initialize(config) def apply(scope) scope. - select("#{quoted_table_name}.*, (#{rank}) AS pg_search_rank" + select_highlight). + select(fields). where(conditions). order("pg_search_rank DESC, #{order_within_rank}"). joins(joins). @@ -32,6 +32,22 @@ def eager_loading? delegate :connection, :quoted_table_name, :to => :@model + def fields + ["#{quoted_table_name}.*", + pg_search_rank_field, + pg_highlight_field].compact.join(", ") + end + + def pg_search_rank_field + "(#{rank}) AS pg_search_rank" + end + + def pg_highlight_field + if feature_options[:tsearch] && feature_options[:tsearch][:highlight] + "(#{highlight}) AS pg_highlight" + end + end + def conditions config.features.reject do |feature_name, feature_options| feature_options && feature_options[:sort_only] @@ -87,12 +103,8 @@ def rank end end - def select_highlight - if highlight = feature_for(:tsearch).highlight - ", (#{highlight.to_sql}) AS pg_highlight" - else - "" - end + def highlight + feature_for(:tsearch).highlight.to_sql end end end From a7564eec25a35357bbb99b05b61b10d44a94ce23 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Mon, 15 Dec 2014 15:52:34 -0500 Subject: [PATCH 04/15] Encapsulate document parameter of ts_headline in parens Should search within all provided documents --- lib/pg_search/features/tsearch.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index 28ef09b9..e78b49d6 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -110,7 +110,7 @@ def ts_headline Arel.sql(normalize(column.to_sql)) end.join(' || ') - "ts_headline(#{document}, (#{tsquery}))" + "ts_headline((#{document}), (#{tsquery}))" end def dictionary From 8043a88bcac1e329db81050fd7d8d7ce175e7e44 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Mon, 15 Dec 2014 15:53:24 -0500 Subject: [PATCH 05/15] Placeholder unit test for Features::TSearch#highlight --- spec/lib/pg_search/features/tsearch_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/lib/pg_search/features/tsearch_spec.rb b/spec/lib/pg_search/features/tsearch_spec.rb index bc123feb..c113e73e 100644 --- a/spec/lib/pg_search/features/tsearch_spec.rb +++ b/spec/lib/pg_search/features/tsearch_spec.rb @@ -122,4 +122,8 @@ end end end + + describe "#highlight" do + it "returns an expression using the ts_headline() function" + end end From ac77c8b87469db435711311d791fa61dd8a152c7 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Thu, 18 Dec 2014 21:22:02 -0800 Subject: [PATCH 06/15] Unit test for Model#highlight Will generate SQL to run the ts_headline() function with the provided query. --- spec/lib/pg_search/features/tsearch_spec.rb | 24 ++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/spec/lib/pg_search/features/tsearch_spec.rb b/spec/lib/pg_search/features/tsearch_spec.rb index c113e73e..573dfe10 100644 --- a/spec/lib/pg_search/features/tsearch_spec.rb +++ b/spec/lib/pg_search/features/tsearch_spec.rb @@ -124,6 +124,28 @@ end describe "#highlight" do - it "returns an expression using the ts_headline() function" + with_model :Model do + table do |t| + t.string :name + t.text :content + end + end + + it "returns an expression using the ts_headline() function" do + query = "query" + columns = [ + PgSearch::Configuration::Column.new(:name, nil, Model), + PgSearch::Configuration::Column.new(:content, nil, Model), + ] + options = {} + config = double(:config, :ignore => []) + normalizer = PgSearch::Normalizer.new(config) + + feature = described_class.new(query, options, columns, Model, normalizer) + + expect(feature.highlight.to_sql).to eq( + %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' '''))))} + ) + end end end From 72773d996616b8639cacbf5803e886c2ae893d68 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Mon, 22 Dec 2014 14:25:09 -0800 Subject: [PATCH 07/15] Can specify a query delimiter for highlighting Using the ts_search options ```ruby { highlight: { start_sel: "begin", stop_sel: "end" } } ``` will override the default "" and " []) + normalizer = PgSearch::Normalizer.new(config) + + feature = described_class.new(query, options, columns, Model, normalizer) + + expect(feature.highlight.to_sql).to eq( + %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), ''))} + ) + end + + it "allows for custom query delimiters" do + query = "query" + columns = [ + PgSearch::Configuration::Column.new(:name, nil, Model), + PgSearch::Configuration::Column.new(:content, nil, Model), + ] + options = { highlight: { start_sel: "", stop_sel: "" } } config = double(:config, :ignore => []) normalizer = PgSearch::Normalizer.new(config) feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.highlight.to_sql).to eq( - %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' '''))))} + %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = , StopSel = '))} ) end end From 5abee67ac8276e05b250c468b79fb3c26e48f28c Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Sat, 17 Jan 2015 18:20:27 -0500 Subject: [PATCH 08/15] Disable class length rubocop metric Just to make rubocop pass since the class is a tad too long... --- lib/pg_search/features/tsearch.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index 4bb91f89..87c31690 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -1,3 +1,5 @@ +# rubocop:disable Metrics/ClassLength + require "pg_search/compatibility" require "active_support/core_ext/module/delegation" From 3e4e639a0115b30f3ab34818a295ddd9f55dffa8 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Sat, 17 Jan 2015 21:57:47 -0500 Subject: [PATCH 09/15] Raise error if :highlight option is set for PostgreSQL < 9.0 The ts_highlight() command is only available on PostgreSQL versions 9.0 and above. --- lib/pg_search/features/tsearch.rb | 6 ++++++ spec/lib/pg_search/features/tsearch_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index 87c31690..2beaa209 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -14,6 +14,12 @@ def initialize(*args) Sorry, {:using => {:tsearch => {:prefix => true}}} only works in PostgreSQL 8.4 and above.") MESSAGE end + + 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 conditions diff --git a/spec/lib/pg_search/features/tsearch_spec.rb b/spec/lib/pg_search/features/tsearch_spec.rb index 0ba4469c..f08db0fa 100644 --- a/spec/lib/pg_search/features/tsearch_spec.rb +++ b/spec/lib/pg_search/features/tsearch_spec.rb @@ -131,6 +131,26 @@ end end + context "when options[:highlight] is set" do + it "throws an error for PostgreSQL versions < 9.0" do + query = "query" + columns = [ + PgSearch::Configuration::Column.new(:name, nil, Model), + PgSearch::Configuration::Column.new(:content, nil, Model), + ] + options = { highlight: true } + config = double(:config, :ignore => []) + normalizer = PgSearch::Normalizer.new(config) + + connection = double(:connection, :postgresql_version => 80400) + mock_model = double(:model, :connection => connection) + + expect { + described_class.new(query, options, columns, mock_model, normalizer) + }.to raise_error(PgSearch::NotSupportedForPostgresqlVersion) + end + end + it "returns an expression using the ts_headline() function" do query = "query" columns = [ From a4252d82443352b5d1e5d029f0ec67e38c147432 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Sat, 17 Jan 2015 22:02:07 -0500 Subject: [PATCH 10/15] Minor refactor of TSearch#initialize --- lib/pg_search/features/tsearch.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index 2beaa209..b9a48aaa 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -9,13 +9,15 @@ class TSearch < Feature def initialize(*args) super - if options[:prefix] && model.connection.send(:postgresql_version) < 80400 + pg_version = model.connection.send(:postgresql_version) + + if options[:prefix] && pg_version < 80400 raise PgSearch::NotSupportedForPostgresqlVersion.new(<<-MESSAGE.strip_heredoc) Sorry, {:using => {:tsearch => {:prefix => true}}} only works in PostgreSQL 8.4 and above.") MESSAGE end - if options[:highlight] && model.connection.send(:postgresql_version) < 90000 + if options[:highlight] && pg_version < 90000 raise PgSearch::NotSupportedForPostgresqlVersion.new(<<-MESSAGE.strip_heredoc) Sorry, {:using => {:tsearch => {:highlight => true}}} only works in PostgreSQL 9.0 and above.") MESSAGE From d973e9239481bf23fed4d45af1b4ec9d1e388190 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Sat, 17 Jan 2015 22:04:25 -0500 Subject: [PATCH 11/15] Add documentation for :tsearch => :highlight option to README --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index f4c26abf..93cfb629 100644 --- a/README.md +++ b/README.md @@ -717,6 +717,49 @@ 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) + +Setting this attribute to true will return an excerpt of the matching text for the search term in a `pg_highlight` attribute. + +```ruby +class Person < ActiveRecord::Base + include PgSearch + pg_search_scope :search, + :against => :bio, + :using => { + :tsearch => {:highlight => true} + } +end + +Person.create!(:bio => "Born in rural Alberta, where the buffalo roam.") + +first_match = Person.search("Alberta").first +first_match.pg_highlight # => "Born in rural Alberta, where the buffalo roam." +``` + +By default, it will add the delimiters `` and `` around the matched text. You can customize these delimiters with the `:start_sel` and `stop_sel` options. + +```ruby +class Person < ActiveRecord::Base + include PgSearch + pg_search_scope :search, + :against => :bio, + :using => { + :tsearch => { + :highlight => { + :start_sel => "*BEGIN*", + :stop_sel => "*END*" + } + } + } +end + +Person.create!(:bio => "Born in rural Alberta, where the buffalo roam.") + +first_match = Person.search("Alberta").first +first_match.pg_highlight # => "Born in rural *BEGIN*Alberta*END*, where the buffalo roam." +``` + #### :dmetaphone (Double Metaphone soundalike search) [Double Metaphone](http://en.wikipedia.org/wiki/Double_Metaphone) is an From 515e8c5d61e655ee61f40b05ee9b14a5d566b1a0 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Sat, 17 Jan 2015 22:07:41 -0500 Subject: [PATCH 12/15] Add more descriptive contexts to examples for :highlight option --- spec/lib/pg_search/features/tsearch_spec.rb | 56 +++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/spec/lib/pg_search/features/tsearch_spec.rb b/spec/lib/pg_search/features/tsearch_spec.rb index f08db0fa..25397954 100644 --- a/spec/lib/pg_search/features/tsearch_spec.rb +++ b/spec/lib/pg_search/features/tsearch_spec.rb @@ -149,40 +149,42 @@ described_class.new(query, options, columns, mock_model, normalizer) }.to raise_error(PgSearch::NotSupportedForPostgresqlVersion) end - end - it "returns an expression using the ts_headline() function" do - query = "query" - columns = [ - PgSearch::Configuration::Column.new(:name, nil, Model), - PgSearch::Configuration::Column.new(:content, nil, Model), - ] - options = { highlight: true } - config = double(:config, :ignore => []) - normalizer = PgSearch::Normalizer.new(config) + it "returns an expression using the ts_headline() function" do + query = "query" + columns = [ + PgSearch::Configuration::Column.new(:name, nil, Model), + PgSearch::Configuration::Column.new(:content, nil, Model), + ] + options = { highlight: true } + config = double(:config, :ignore => []) + normalizer = PgSearch::Normalizer.new(config) - feature = described_class.new(query, options, columns, Model, normalizer) + feature = described_class.new(query, options, columns, Model, normalizer) - expect(feature.highlight.to_sql).to eq( - %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), ''))} - ) + expect(feature.highlight.to_sql).to eq( + %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), ''))} + ) + end end - it "allows for custom query delimiters" do - query = "query" - columns = [ - PgSearch::Configuration::Column.new(:name, nil, Model), - PgSearch::Configuration::Column.new(:content, nil, Model), - ] - options = { highlight: { start_sel: "", stop_sel: "" } } - config = double(:config, :ignore => []) - normalizer = PgSearch::Normalizer.new(config) + context "when options[:highlight] includes :start_sel and :stop_sel" do + it "allows for custom query delimiters" do + query = "query" + columns = [ + PgSearch::Configuration::Column.new(:name, nil, Model), + PgSearch::Configuration::Column.new(:content, nil, Model), + ] + options = { highlight: { start_sel: "", stop_sel: "" } } + config = double(:config, :ignore => []) + normalizer = PgSearch::Normalizer.new(config) - feature = described_class.new(query, options, columns, Model, normalizer) + feature = described_class.new(query, options, columns, Model, normalizer) - expect(feature.highlight.to_sql).to eq( - %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = , StopSel = '))} - ) + expect(feature.highlight.to_sql).to eq( + %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = , StopSel = '))} + ) + end end end end From b72d37de3e553e2e362c6784be7b992daf74fc32 Mon Sep 17 00:00:00 2001 From: Tanner Welsh Date: Sat, 17 Jan 2015 22:27:20 -0500 Subject: [PATCH 13/15] TSearch#ts_headline uses inherited #document method Don't modify the behavior, because it already exists. --- lib/pg_search/features/tsearch.rb | 4 ---- spec/lib/pg_search/features/tsearch_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index b9a48aaa..b6f78d55 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -116,10 +116,6 @@ def tsearch_rank end def ts_headline - document = columns_to_use.map do |column| - Arel.sql(normalize(column.to_sql)) - end.join(' || ') - "ts_headline((#{document}), (#{tsquery}), '#{ts_headline_options}')" end diff --git a/spec/lib/pg_search/features/tsearch_spec.rb b/spec/lib/pg_search/features/tsearch_spec.rb index 25397954..fd2f218c 100644 --- a/spec/lib/pg_search/features/tsearch_spec.rb +++ b/spec/lib/pg_search/features/tsearch_spec.rb @@ -163,7 +163,7 @@ feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.highlight.to_sql).to eq( - %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), ''))} + %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || ' ' || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), ''))} ) end end @@ -182,7 +182,7 @@ feature = described_class.new(query, options, columns, Model, normalizer) expect(feature.highlight.to_sql).to eq( - %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = , StopSel = '))} + %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || ' ' || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = , StopSel = '))} ) end end From 9b8d956b66d9cba6a248c9c4a88a4c7c838d0f91 Mon Sep 17 00:00:00 2001 From: Shane Thomas Date: Fri, 20 Feb 2015 09:50:07 -0500 Subject: [PATCH 14/15] adds support for MaxFragments --- lib/pg_search/features/tsearch.rb | 1 + spec/lib/pg_search/features/tsearch_spec.rb | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/pg_search/features/tsearch.rb b/lib/pg_search/features/tsearch.rb index b6f78d55..9a9bf3f7 100644 --- a/lib/pg_search/features/tsearch.rb +++ b/lib/pg_search/features/tsearch.rb @@ -154,6 +154,7 @@ def ts_headline_options headline_options = {} headline_options["StartSel"] = options[:highlight][:start_sel] headline_options["StopSel"] = options[:highlight][:stop_sel] + headline_options["MaxFragments"] = options[:highlight][:max_fragments] headline_options.map do |key, value| "#{key} = #{value}" if value diff --git a/spec/lib/pg_search/features/tsearch_spec.rb b/spec/lib/pg_search/features/tsearch_spec.rb index fd2f218c..c2e9b62f 100644 --- a/spec/lib/pg_search/features/tsearch_spec.rb +++ b/spec/lib/pg_search/features/tsearch_spec.rb @@ -185,6 +185,25 @@ %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || ' ' || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = , StopSel = '))} ) end + + it "allows a maximum number of fragments" do + query = "query" + columns = [ + PgSearch::Configuration::Column.new(:name, nil, Model), + PgSearch::Configuration::Column.new(:content, nil, Model), + ] + options = { highlight: { start_sel: "", + stop_sel: "", + max_fragments: 2 } } + config = double(:config, :ignore => []) + normalizer = PgSearch::Normalizer.new(config) + + feature = described_class.new(query, options, columns, Model, normalizer) + + expect(feature.highlight.to_sql).to eq( + %Q{(ts_headline((coalesce(#{Model.quoted_table_name}."name"::text, '') || ' ' || coalesce(#{Model.quoted_table_name}."content"::text, '')), (to_tsquery('simple', ''' ' || 'query' || ' ''')), 'StartSel = , StopSel = , MaxFragments = 2'))} + ) + end end end end From 29dee782056f7f3207af6fddb26c90109555c040 Mon Sep 17 00:00:00 2001 From: Shane Thomas Date: Fri, 20 Feb 2015 10:01:38 -0500 Subject: [PATCH 15/15] mention max_fragments in README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 93cfb629..614f1364 100644 --- a/README.md +++ b/README.md @@ -737,7 +737,7 @@ first_match = Person.search("Alberta").first first_match.pg_highlight # => "Born in rural Alberta, where the buffalo roam." ``` -By default, it will add the delimiters `` and `` around the matched text. You can customize these delimiters with the `:start_sel` and `stop_sel` options. +By default, it will add the delimiters `` and `` 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. ```ruby class Person < ActiveRecord::Base @@ -748,7 +748,8 @@ class Person < ActiveRecord::Base :tsearch => { :highlight => { :start_sel => "*BEGIN*", - :stop_sel => "*END*" + :stop_sel => "*END*", + :max_fragments => 1 } } }