FEATURE: Use PG `ts_headline` for highlighting topic title in search.

FEATURE: Use PG ts_headline for highlighting topic title in search.

diff --git a/app/assets/javascripts/discourse/app/lib/topic-fancy-title.js b/app/assets/javascripts/discourse/app/lib/topic-fancy-title.js
new file mode 100644
index 0000000..1da4dda
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/topic-fancy-title.js
@@ -0,0 +1,18 @@
+import Site from "discourse/models/site";
+import { censor } from "pretty-text/censored-words";
+import { emojiUnescape } from "discourse/lib/text";
+import { isRTL } from "discourse/lib/text-direction";
+
+export function fancyTitle(title, supportMixedTextDirection) {
+  let fancyTitle = censor(
+    emojiUnescape(title) || "",
+    Site.currentProp("censored_regexp")
+  );
+
+  if (supportMixedTextDirection) {
+    const titleDir = isRTL(title) ? "rtl" : "ltr";
+    return `<span dir="${titleDir}">${fancyTitle}</span>`;
+  }
+
+  return fancyTitle;
+}
diff --git a/app/assets/javascripts/discourse/app/models/bookmark.js b/app/assets/javascripts/discourse/app/models/bookmark.js
index 1fb5472..51da517 100644
--- a/app/assets/javascripts/discourse/app/models/bookmark.js
+++ b/app/assets/javascripts/discourse/app/models/bookmark.js
@@ -2,10 +2,7 @@ import getURL from "discourse-common/lib/get-url";
 import I18n from "I18n";
 import Category from "discourse/models/category";
 import User from "discourse/models/user";
-import { isRTL } from "discourse/lib/text-direction";
-import { censor } from "pretty-text/censored-words";
-import { emojiUnescape } from "discourse/lib/text";
-import Site from "discourse/models/site";
+import { fancyTitle } from "discourse/lib/topic-fancy-title";
 import { longDate } from "discourse/lib/formatter";
 import { none } from "@ember/object/computed";
 import { computed } from "@ember/object";
@@ -78,16 +75,7 @@ const Bookmark = RestModel.extend({
 
   @discourseComputed("title")
   fancyTitle(title) {
-    let fancyTitle = censor(
-      emojiUnescape(title) || "",
-      Site.currentProp("censored_regexp")
-    );
-
-    if (this.siteSettings.support_mixed_text_direction) {
-      const titleDir = isRTL(title) ? "rtl" : "ltr";
-      return `<span dir="${titleDir}">${fancyTitle}</span>`;
-    }
-    return fancyTitle;
+    return fancyTitle(title, this.siteSettings.support_mixed_text_direction);
   },
 
   @discourseComputed("created_at")
diff --git a/app/assets/javascripts/discourse/app/models/post.js b/app/assets/javascripts/discourse/app/models/post.js
index 6c211a8..f45a966 100644
--- a/app/assets/javascripts/discourse/app/models/post.js
+++ b/app/assets/javascripts/discourse/app/models/post.js
@@ -16,6 +16,7 @@ import { Promise } from "rsvp";
 import Site from "discourse/models/site";
 import User from "discourse/models/user";
 import showModal from "discourse/lib/show-modal";
+import { fancyTitle } from "discourse/lib/topic-fancy-title";
 
 const Post = RestModel.extend({
   @discourseComputed("url")
@@ -102,6 +103,19 @@ const Post = RestModel.extend({
     );
   },
 
+  @discourseComputed(
+    "siteSettings.use_pg_headlines_for_excerpt",
+    "topic_title_headline"
+  )
+  useTopicTitleHeadline(enabled, title) {
+    return enabled && title;
+  },
+
+  @discourseComputed("topic_title_headline")
+  topicTitleHead(title) {
+    return fancyTitle(title, this.siteSettings.support_mixed_text_direction);
+  },
+
   afterUpdate(res) {
     if (res.category) {
       this.site.updateCategory(res.category);
diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js
index 5815893..e3e6b01 100644
--- a/app/assets/javascripts/discourse/app/models/topic.js
+++ b/app/assets/javascripts/discourse/app/models/topic.js
@@ -7,13 +7,12 @@ import { flushMap } from "discourse/models/store";
 import RestModel from "discourse/models/rest";
 import { propertyEqual, fmt } from "discourse/lib/computed";
 import { longDate } from "discourse/lib/formatter";
-import { isRTL } from "discourse/lib/text-direction";
 import ActionSummary from "discourse/models/action-summary";
 import { popupAjaxError } from "discourse/lib/ajax-error";
-import { censor } from "pretty-text/censored-words";
 import { emojiUnescape } from "discourse/lib/text";
 import PreloadStore from "discourse/lib/preload-store";
 import { userPath } from "discourse/lib/url";
+import { fancyTitle } from "discourse/lib/topic-fancy-title";
 import discourseComputed, {
   observes,
   on
@@ -119,16 +118,7 @@ const Topic = RestModel.extend({
 
   @discourseComputed("fancy_title")
   fancyTitle(title) {
-    let fancyTitle = censor(
-      emojiUnescape(title) || "",
-      Site.currentProp("censored_regexp")
-    );
-
-    if (this.siteSettings.support_mixed_text_direction) {
-      const titleDir = isRTL(title) ? "rtl" : "ltr";
-      return `<span dir="${titleDir}">${fancyTitle}</span>`;
-    }
-    return fancyTitle;
+    return fancyTitle(title, this.siteSettings.support_mixed_text_direction);
   },
 
   // returns createdAt if there's no bumped date
diff --git a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs
index dedf5be..3f1657c 100644
--- a/app/assets/javascripts/discourse/app/templates/full-page-search.hbs
+++ b/app/assets/javascripts/discourse/app/templates/full-page-search.hbs
@@ -88,7 +88,15 @@
 
                   <a href={{result.url}} {{action "logClick" result.topic_id}} class="search-link">
                     {{topic-status topic=result.topic disableActions=true showPrivateMessageIcon=true}}
-                    <span class="topic-title">{{#highlight-search highlight=q}}{{html-safe result.topic.fancyTitle}}{{/highlight-search}}</span>
+                    <span class="topic-title">
+                      {{#if result.useTopicTitleHeadline}}
+                        {{html-safe result.topicTitleHead}}
+                      {{else}}
+                        {{#highlight-search highlight=q}}
+                          {{html-safe result.topic.fancyTitle}}
+                        {{/highlight-search}}
+                      {{/if}}
+                    </span>
                   </a>
 
                   <div class="search-category">
diff --git a/app/serializers/search_post_serializer.rb b/app/serializers/search_post_serializer.rb
index 0759dac..bca2f7e 100644
--- a/app/serializers/search_post_serializer.rb
+++ b/app/serializers/search_post_serializer.rb
@@ -3,7 +3,19 @@
 class SearchPostSerializer < BasicPostSerializer
   has_one :topic, serializer: SearchTopicListItemSerializer
 
-  attributes :like_count, :blurb, :post_number
+  attributes :like_count, :blurb, :post_number, :topic_title_headline
+
+  def include_topic_title_headline?
+    if SiteSetting.use_pg_headlines_for_excerpt
+      object.topic_title_headline.present?
+    else
+      false
+    end
+  end
+
+  def topic_title_headline
+    object.topic_title_headline
+  end
 
   def blurb
     options[:result].blurb(object)
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 47d8d60..a325968 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -1796,6 +1796,7 @@ search:
   use_pg_headlines_for_excerpt:
     default: false
     hidden: true
+    client: true
   search_ranking_normalization:
     default: "0"
     hidden: true
diff --git a/lib/search.rb b/lib/search.rb
index d8f2e40..fe19f17 100644
--- a/lib/search.rb
+++ b/lib/search.rb
@@ -1166,10 +1166,15 @@ class Search
 
   def posts_scope(default_scope = Post.all)
     if SiteSetting.use_pg_headlines_for_excerpt
+      search_term = @term.present? ? PG::Connection.escape_string(@term) : nil
+      ts_config = default_ts_config
+
       default_scope
         .joins("INNER JOIN post_search_data pd ON pd.post_id = posts.id")
+        .joins("INNER JOIN topics t1 ON t1.id = posts.topic_id")
         .select(

[... diff too long, it was truncated ...]

GitHub sha: e60c74d3