From d75e3ca82b9bcb363c51a62a2f853241e1aa9fd8 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 12 Sep 2023 16:09:28 +1000 Subject: [PATCH] FEATURE: include tag and category context in search (#217) Previous to this we just included title/body.. tags and category structure can be very critical for decision making. --- lib/modules/ai_bot/commands/search_command.rb | 18 +++++++- .../ai_bot/commands/read_command_spec.rb | 18 ++++---- .../ai_bot/commands/search_command_spec.rb | 42 ++++++++++++++++--- 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/lib/modules/ai_bot/commands/search_command.rb b/lib/modules/ai_bot/commands/search_command.rb index 4df65b87..69a534e7 100644 --- a/lib/modules/ai_bot/commands/search_command.rb +++ b/lib/modules/ai_bot/commands/search_command.rb @@ -154,17 +154,33 @@ module DiscourseAi::AiBot::Commands end @last_num_results = posts.length + # this is the general pattern from core + # if there are millions of hidden tags it may fail + hidden_tags = nil if posts.blank? { args: search_args, rows: [], instruction: "nothing was found, expand your search" } else format_results(posts, args: search_args) do |post| - { + category_names = [ + post.topic.category&.parent_category&.name, + post.topic.category&.name, + ].compact.join(" > ") + row = { title: post.topic.title, url: Discourse.base_path + post.url, excerpt: post.excerpt, created: post.created_at, + category: category_names, } + + if SiteSetting.tagging_enabled + hidden_tags ||= DiscourseTagging.hidden_tag_names + # using map over pluck to avoid n+1 (assuming caller preloading) + tags = post.topic.tags.map(&:name) - hidden_tags + row[:tags] = tags.join(", ") if tags.present? + end + row end end end diff --git a/spec/lib/modules/ai_bot/commands/read_command_spec.rb b/spec/lib/modules/ai_bot/commands/read_command_spec.rb index 47cc4661..dc100bc0 100644 --- a/spec/lib/modules/ai_bot/commands/read_command_spec.rb +++ b/spec/lib/modules/ai_bot/commands/read_command_spec.rb @@ -9,19 +9,19 @@ RSpec.describe DiscourseAi::AiBot::Commands::ReadCommand do fab!(:tag_funny) { Fabricate(:tag, name: "funny") } fab!(:tag_sad) { Fabricate(:tag, name: "sad") } fab!(:tag_hidden) { Fabricate(:tag, name: "hidden") } - fab!(:staff_tag_group) { Fabricate(:tag_group, name: "Staff only", tag_names: ["hidden"]) } + fab!(:staff_tag_group) do + tag_group = Fabricate.build(:tag_group, name: "Staff only", tag_names: ["hidden"]) + + tag_group.permissions = [ + [Group::AUTO_GROUPS[:staff], TagGroupPermission.permission_types[:full]], + ] + tag_group.save! + tag_group + end fab!(:topic_with_tags) do Fabricate(:topic, category: category, tags: [tag_funny, tag_sad, tag_hidden]) end - let(:staff) { Group::AUTO_GROUPS[:staff] } - let(:full) { TagGroupPermission.permission_types[:full] } - - before do - staff_tag_group.permissions = [[staff, full]] - staff_tag_group.save! - end - describe "#process" do it "can read a topic" do topic_id = topic_with_tags.id diff --git a/spec/lib/modules/ai_bot/commands/search_command_spec.rb b/spec/lib/modules/ai_bot/commands/search_command_spec.rb index 4071c0b0..28d50265 100644 --- a/spec/lib/modules/ai_bot/commands/search_command_spec.rb +++ b/spec/lib/modules/ai_bot/commands/search_command_spec.rb @@ -9,9 +9,28 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do before { SearchIndexer.enable } after { SearchIndexer.disable } + fab!(:parent_category) { Fabricate(:category, name: "animals") } + fab!(:category) { Fabricate(:category, parent_category: parent_category, name: "amazing-cat") } + + fab!(:tag_funny) { Fabricate(:tag, name: "funny") } + fab!(:tag_sad) { Fabricate(:tag, name: "sad") } + fab!(:tag_hidden) { Fabricate(:tag, name: "hidden") } + fab!(:staff_tag_group) do + tag_group = Fabricate.build(:tag_group, name: "Staff only", tag_names: ["hidden"]) + + tag_group.permissions = [ + [Group::AUTO_GROUPS[:staff], TagGroupPermission.permission_types[:full]], + ] + tag_group.save! + tag_group + end + fab!(:topic_with_tags) do + Fabricate(:topic, category: category, tags: [tag_funny, tag_sad, tag_hidden]) + end + describe "#process" do it "can handle no results" do - post1 = Fabricate(:post) + post1 = Fabricate(:post, topic: topic_with_tags) search = described_class.new(bot_user: bot_user, post: post1, args: nil) results = search.process(query: "order:fake ABDDCDCEDGDG") @@ -42,7 +61,7 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do hyde_embedding, ) - post1 = Fabricate(:post) + post1 = Fabricate(:post, topic: topic_with_tags) search = described_class.new(bot_user: bot_user, post: post1, args: nil) DiscourseAi::Embeddings::VectorRepresentations::AllMpnetBaseV2 @@ -60,7 +79,7 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do it "supports subfolder properly" do Discourse.stubs(:base_path).returns("/subfolder") - post1 = Fabricate(:post) + post1 = Fabricate(:post, topic: topic_with_tags) search = described_class.new(bot_user: bot_user, post: post1, args: nil) @@ -68,8 +87,22 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do expect(results[:rows].to_s).to include("/subfolder" + post1.url) end + it "returns category and tags" do + post1 = Fabricate(:post, topic: topic_with_tags) + search = described_class.new(bot_user: bot_user, post: post1, args: nil) + results = search.process(user: post1.user.username) + + row = results[:rows].first + category = row[results[:column_names].index("category")] + + expect(category).to eq("animals > amazing-cat") + + tags = row[results[:column_names].index("tags")] + expect(tags).to eq("funny, sad") + end + it "can handle limits" do - post1 = Fabricate(:post) + post1 = Fabricate(:post, topic: topic_with_tags) _post2 = Fabricate(:post, user: post1.user) _post3 = Fabricate(:post, user: post1.user) @@ -78,7 +111,6 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do results = search.process(limit: 2, user: post1.user.username) - expect(results[:column_names].length).to eq(4) expect(results[:rows].length).to eq(2) # just searching for everything