FEATURE: Update to latest algolia gem (#33)

FEATURE: Update to latest algolia gem (#33)

This commit includes several bug fixes and improvements. The indexing process is more efficient and uses less Algolia resources.

Security-wise, the admin API key is no longer visible to the client. Records that are no longer visible will also be deleted from the index. It used to use system user’s guardian which meant that everything was indexed. The new default is to use the guardian of the anonymous user.

The code was reorganized and logic for each model was moved in their own class. Tests were added for indexers.

Co-authored-by: Dan Ungureanu dan@ungureanu.me

diff --git a/app/jobs/regular/update_algolia_post.rb b/app/jobs/regular/update_algolia_post.rb
deleted file mode 100644
index 68909aa..0000000
--- a/app/jobs/regular/update_algolia_post.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Jobs
-  class UpdateAlgoliaPost < ::Jobs::Base
-    def execute(args)
-      DiscourseAlgolia::AlgoliaHelper.index_post(args[:post_id], args[:discourse_event])
-    end
-  end
-end
diff --git a/app/jobs/regular/update_algolia_tags.rb b/app/jobs/regular/update_algolia_tags.rb
deleted file mode 100644
index 5fff6ca..0000000
--- a/app/jobs/regular/update_algolia_tags.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Jobs
-  class UpdateAlgoliaTags < ::Jobs::Base
-    def execute(args)
-      DiscourseAlgolia::AlgoliaHelper.index_tags(args[:tags], args[:discourse_event])
-    end
-  end
-end
diff --git a/app/jobs/regular/update_algolia_topic.rb b/app/jobs/regular/update_algolia_topic.rb
deleted file mode 100644
index 21a0ed9..0000000
--- a/app/jobs/regular/update_algolia_topic.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Jobs
-  class UpdateAlgoliaTopic < ::Jobs::Base
-    def execute(args)
-      DiscourseAlgolia::AlgoliaHelper.index_topic(args[:topic_id], args[:discourse_event])
-    end
-  end
-end
diff --git a/app/jobs/regular/update_algolia_user.rb b/app/jobs/regular/update_algolia_user.rb
deleted file mode 100644
index e02e533..0000000
--- a/app/jobs/regular/update_algolia_user.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Jobs
-  class UpdateAlgoliaUser < ::Jobs::Base
-    def execute(args)
-      DiscourseAlgolia::AlgoliaHelper.index_user(args[:user_id], args[:discourse_event])
-    end
-  end
-end
diff --git a/app/jobs/scheduled/update_indexes.rb b/app/jobs/scheduled/update_indexes.rb
new file mode 100644
index 0000000..240c29b
--- /dev/null
+++ b/app/jobs/scheduled/update_indexes.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Jobs
+  class UpdateIndexes < ::Jobs::Scheduled
+    every 5.minutes
+
+    def execute(args)
+      DiscourseAlgolia.process!
+    end
+  end
+end
diff --git a/assets/javascripts/discourse/initializers/discourse-algolia.js.es6 b/assets/javascripts/discourse/initializers/discourse-algolia.js.es6
index 8b3322a..c39836e 100644
--- a/assets/javascripts/discourse/initializers/discourse-algolia.js.es6
+++ b/assets/javascripts/discourse/initializers/discourse-algolia.js.es6
@@ -1,12 +1,236 @@
+import { schedule } from "@ember/runloop";
+import { withPluginApi } from "discourse/lib/plugin-api";
+import DiscourseURL from "discourse/lib/url";
 import I18n from "I18n";
 import { h } from "virtual-dom";
-import DiscourseURL from "discourse/lib/url";
-import { withPluginApi } from "discourse/lib/plugin-api";
-import discourseAutocomplete from "./discourse-autocomplete";
-import { schedule } from "@ember/runloop";
+
+/* global algoliasearch */
+/* global autocomplete */
+
+function initializeAutocomplete(options) {
+  const client = algoliasearch(
+    options.algoliaApplicationId,
+    options.algoliaSearchApiKey
+  );
+
+  const postsIndex = client.initIndex("discourse-posts");
+  const tagsIndex = client.initIndex("discourse-tags");
+  const usersIndex = client.initIndex("discourse-users");
+
+  const hitsPerPage = 4;
+
+  // When Algolia Answers is enabled, use a different endpoint
+  const postsSourceFallback = autocomplete.sources.hits(postsIndex, {
+    hitsPerPage,
+  });
+
+  const postsSource = !options.algoliaAnswersEnabled
+    ? postsSourceFallback
+    : function (query, callback) {
+        const data = {
+          query: query,
+          queryLanguages: ["en"],
+          attributesForPrediction: ["content"],
+          nbHits: hitsPerPage,
+        };
+
+        fetch(
+          `https://${options.algoliaApplicationId}-dsn.algolia.net/1/answers/${postsIndex.indexName}/prediction`,
+          {
+            method: "POST",
+            headers: {
+              "X-Algolia-Application-Id": options.algoliaApplicationId,
+              "X-Algolia-API-Key": options.algoliaSearchApiKey,
+            },
+            body: JSON.stringify(data),
+          }
+        )
+          .then((response) => response.json())
+          .then((res) => {
+            if (!res.hits) {
+              throw new Error(`Invalid response: ${res.message}`);
+            } else {
+              res.hits.forEach((hit) => {
+                if ("_answer" in hit && "extract" in hit["_answer"]) {
+                  hit["_snippetResult"]["content"]["value"] =
+                    hit["_answer"]["extract"];
+                }
+              });
+              callback(res.hits);
+            }
+          })
+          .catch((err) => {
+            // eslint-disable-next-line no-console
+            console.error("[Algolia Answers]", err);
+            return postsSourceFallback(query, callback);
+          });
+      };
+
+  return autocomplete(
+    "#search-box",
+    {
+      openOnFocus: true,
+      hint: false,
+      debug: options.debug,
+      templates: {
+        dropdownMenu: `
+          <div class="left-container">
+            <div class="aa-dataset-posts" />
+          </div>
+          <div class="right-container">
+            <span class="aa-dataset-users" />
+            <span class="aa-dataset-tags" />
+          </div>
+        `,
+        footer: `
+          <div class="aa-footer">
+            <div class="left-container">
+              <a class="advanced-search" href="/search">${I18n.t(
+                "discourse_algolia.advanced_search"
+              )}</a>
+            </div>
+            <div class="right-container">
+              <a target="_blank" class="algolia-logo" href="https://algolia.com/"
+                title="${I18n.t("discourse_algolia.powered_by")}"></a>
+            </div>
+          </div>
+        `,
+      },
+    },
+    [
+      {
+        source: autocomplete.sources.hits(usersIndex, {
+          hitsPerPage,
+        }),
+        name: "users",
+        displayKey: "users",
+        templates: {
+          empty: "",
+          suggestion(hit) {
+            return `
+              <div class='hit-user-left'>
+                <img class="hit-user-avatar" src="${
+                  options.imageBaseURL
+                }${hit.avatar_template.replace("{size}", 50)}" />
+              </div>
+              <div class='hit-user-right'>
+                <div class="hit-user-username-holder">
+                  <span class="hit-user-username">
+                    @${hit._highlightResult.username.value}
+                  </span>
+                  <span class="hit-user-custom-ranking" title="${I18n.t(
+                    "discourse_algolia.user_likes"
+                  )}">
+                    ${
+                      hit.likes_received > 0
+                        ? `<span class="hit-user-like-heart">❤</span> ${hit.likes_received}`
+                        : ""
+                    }
+                  </span>
+                </div>
+                <div class="hit-user-name">
+                  ${autocomplete.escapeHighlightedString(
+                    hit._highlightResult.name
+                      ? hit._highlightResult.name.value
+                      : hit.name
+                      ? hit.name
+                      : hit.username
+                  )}
+                </div>
+              </div>
+            `;
+          },
+        },
+      },
+      {
+        source: autocomplete.sources.hits(tagsIndex, {
+          hitsPerPage,
+        }),
+        name: "tags",
+        displayKey: "tags",
+        templates: {
+          empty: "",
+          suggestion: function (hit) {
+            return `
+            <div class='hit-tag'>
+              <span class="hit-tag-name">#${autocomplete.escapeHighlightedString(
+                hit._highlightResult.name
+                  ? hit._highlightResult.name.value

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

GitHub sha: 9a1f08ff4735a33764cb2e53432ba9f359c5b697

This commit appears in #33 which was approved by CvX. It was merged by udan11.