FIX: Improve search in encrypted topics (#140)

FIX: Improve search in encrypted topics (#140)

Filtering has been improved to ignore empty words and order the results by the number of matched words in query relative to the result of the length (i.e. closer matches will be at the top of the list).

Encrypted results that were returned by the server will be decrypted and saved into the cache too.

diff --git a/assets/javascripts/discourse/initializers/add-search-results.js b/assets/javascripts/discourse/initializers/add-search-results.js
index d006b2b..d7423d7 100644
--- a/assets/javascripts/discourse/initializers/add-search-results.js
+++ b/assets/javascripts/discourse/initializers/add-search-results.js
@@ -15,54 +15,106 @@ import { Promise } from "rsvp";
 
 const CACHE_KEY = "discourse-encrypt-cache";
 
-function addCacheItem(session, type, item) {
+function getCache(session) {
   let cache = session.get(CACHE_KEY);
   if (!cache) {
     session.set(CACHE_KEY, (cache = {}));
   }
+  return cache;
+}
 
+function addObjectToCache(cache, type, object) {
   if (!cache[type]) {
     cache[type] = [];
-  } else if (item.id) {
-    cache[type] = cache[type].filter((i) => i.id !== item.id);
   }
-  cache[type].push(item);
+  cache[type][object.id] = object;
 }
 
-function getOrFetchCache(session) {
-  const cache = session.get(CACHE_KEY);
-  if (cache) {
-    return Promise.resolve(cache);
-  }
+function addPostToCache(cache, post) {
+  post.topic_title_headline = null;
+  addObjectToCache(cache, "posts", post);
+}
 
-  return ajax("/encrypt/posts")
-    .then((result) => {
-      const promises = [];
+function addTopicToCache(cache, topic) {
+  putTopicKey(topic.id, topic.topic_key);
+  putTopicTitle(topic.id, topic.encrypted_title);
+  return getTopicTitle(topic.id).then((title) => {
+    topic.title = title;
+    topic.fancy_title = `${iconHTML("user-secret")} ${title}`;
+    topic.excerpt = null;
+    addObjectToCache(cache, "topics", topic);
+    return topic;
+  });
+}
 
-      result.posts.forEach((post) => {
-        post.topic_title_headline = null;
-        addCacheItem(session, "posts", post);
-      });
+function loadCache(cache) {
+  return ajax("/encrypt/posts").then((result) => {
+    result.posts.forEach((post) => addPostToCache(cache, post));
+    const promises = result.topics.map((topic) =>
+      addTopicToCache(cache, topic).catch(() => {})
+    );
+    return Promise.all(promises);
+  });
+}
 
-      result.topics.forEach((topic) => {
-        putTopicKey(topic.id, topic.topic_key);
-        putTopicTitle(topic.id, topic.encrypted_title);
-
-        promises.push(
-          getTopicTitle(topic.id)
-            .then((title) => {
-              topic.title = title;
-              topic.fancy_title = `${iconHTML("user-secret")} ${title}`;
-              topic.excerpt = null;
-              addCacheItem(session, "topics", topic);
-            })
-            .catch(() => {})
-        );
-      });
+function addEncryptedSearchResultsFromCache(cache, results) {
+  const terms = results.grouped_search_result.term
+    .toLowerCase()
+    .trim()
+    .split(/\s+/);
+
+  // Add to results encrypted topics that have matching titles
+  const existentTopicIds = new Set(results.topics.map((topic) => topic.id));
+  const topics = {};
+  Object.values(cache.topics || {}).forEach((topic) => {
+    if (existentTopicIds.has(topic.id)) {
+      return;
+    }
+
+    const matchedWordsCount = terms.filter((term) =>
+      topic.title.toLowerCase().includes(term)
+    ).length;
+    if (matchedWordsCount === 0) {
+      return;
+    }
 
-      return Promise.all(promises);
-    })
-    .then(() => session.get(CACHE_KEY));
+    topics[topic.id] = topic = Topic.create(topic);
+    results.topics.unshift(topic);
+
+    topic.set(
+      "searchPriority",
+      matchedWordsCount +
+        matchedWordsCount / topic.title.trim().split(/\s+/).length
+    );
+  });
+
+  // Add associated posts for each new topic
+  Object.values(cache.posts || {}).forEach((post) => {
+    if (post.post_number !== 1 || !topics[post.topic_id]) {
+      return;
+    }
+
+    post = Post.create(post);
+    post.setProperties({
+      topic: topics[post.topic_id],
+      blurb: I18n.t("encrypt.encrypted_post"),
+    });
+
+    results.posts.unshift(post);
+    results.grouped_search_result.post_ids.unshift(post.id);
+  });
+
+  // Move new encrypted results to top because they might be more relevant
+  results.posts.sort(
+    (a, b) => (b.topic.searchPriority || 0) - (a.topic.searchPriority || 0)
+  );
+
+  // Reset topic_title_headline for encrypted results
+  results.posts.map((post) => {
+    if (cache.topics[post.topic_id]) {
+      post.set("topic_title_headline", "");
+    }
+  });
 }
 
 export default {
@@ -77,60 +129,34 @@ export default {
     const session = container.lookup("session:main");
     withPluginApi("0.11.3", (api) => {
       api.addSearchResultsCallback((results) => {
-        if (
-          results?.grouped_search_result?.type_filter !== "private_messages"
-        ) {
-          return Promise.resolve(results);
-        }
+        const cache = getCache(session);
+        const promises = [];
 
-        return getOrFetchCache(session).then((cache) => {
-          const topics = {};
-          if (cache.topics) {
-            const words = results.grouped_search_result.term
-              .toLowerCase()
-              .split(/\s+/);
-
-            cache.topics.forEach((topic) => {
-              const topicObj = results.topics.find((t) => topic.id === t.id);
-              if (topicObj) {
-                topicObj.setProperties(topic);
-              } else if (
-                words.some((word) => topic.title.toLowerCase().includes(word))
-              ) {
-                topic = Topic.create(topic);
-                results.topics.unshift(topic);
-                topics[topic.id] = topic;
-              }
-            });
-
-            // Reset topic_title_headline
-            const encryptedTopicIds = new Set(cache.topics.map((t) => t.id));
-            results.posts.map((post) => {
-              if (encryptedTopicIds.has(post.topic_id)) {
-                post.set("topic_title_headline", "");
-              }
-            });
+        // Decrypt existing topics and cache them
+        results.topics.forEach((topic) => {
+          if (topic.topic_key) {
+            promises.push(addTopicToCache(cache, topic).catch(() => {}));
           }
+        });
 
-          if (cache.posts) {
-            cache.posts.forEach((post) => {
-              if (post.post_number !== 1 || !topics[post.topic_id]) {
-                return;
-              }
-
-              post = Post.create(post);
-              post.setProperties({
-                topic: topics[post.topic_id],
-                blurb: I18n.t("encrypt.encrypted_post"),
-              });
-
-              results.posts.unshift(post);
-              results.grouped_search_result.post_ids.unshift(post.id);
-            });
+        // Search for more encrypted topics
+        if (
+          results?.grouped_search_result?.type_filter === "private_messages"
+        ) {
+          let cachePromise = Promise.resolve();
+          if (!cache.loaded) {
+            cachePromise = loadCache(cache);
+            cache.loaded = true;
           }
 
-          return results;
-        });
+          promises.push(
+            cachePromise.then(() =>
+              addEncryptedSearchResultsFromCache(cache, results)
+            )
+          );
+        }
+
+        return Promise.all(promises).then(() => results);
       });
     });
   },

GitHub sha: 6f86946e8606060830f6df849805f3e5214d337c

This commit appears in #140 which was approved by eviltrout. It was merged by udan11.