FEATURE: show tags in search results

This commit is contained in:
Neil Lalonde 2017-08-25 11:52:18 -04:00
parent cdcc5d6174
commit 2c56f8df7c
14 changed files with 162 additions and 2 deletions

View File

@ -18,6 +18,7 @@ export function translateResults(results, opts) {
if (!results.users) { results.users = []; }
if (!results.posts) { results.posts = []; }
if (!results.categories) { results.categories = []; }
if (!results.tags) { results.tags = []; }
const topicMap = {};
results.topics = results.topics.map(function(topic){
@ -44,12 +45,17 @@ export function translateResults(results, opts) {
return Category.list().findBy('id', category.id);
}).compact();
results.tags = results.tags.map(function(tag){
let tagName = Handlebars.Utils.escapeExpression(tag.name);
return Ember.Object.create({ id: tagName, url: Discourse.getURL("/tags/" + tagName) });
}).compact();
const r = results.grouped_search_result;
results.resultTypes = [];
// TODO: consider refactoring front end to take a better structure
if (r) {
[['topic','posts'],['user','users'],['category','categories']].forEach(function(pair){
[['topic','posts'],['user','users'],['category','categories'],['tag','tags']].forEach(function(pair){
const type = pair[0], name = pair[1];
if (results[name].length > 0) {
var result = {

View File

@ -91,6 +91,15 @@ createSearchResult({
}
});
createSearchResult({
type: 'tag',
linkField: 'url',
builder(t) {
const tag = Handlebars.Utils.escapeExpression(t.get('id'));
return h('a', { attributes: { href: t.get('url') }, className: `tag-${tag} discourse-tag ${Discourse.SiteSettings.tag_style}`}, tag);
}
});
createWidget('search-menu-results', {
tagName: 'div.results',

View File

@ -8,6 +8,7 @@ module Jobs
rebuild_problem_posts
rebuild_problem_categories
rebuild_problem_users
rebuild_problem_tags
end
def rebuild_problem_categories(limit = 500)
@ -47,6 +48,15 @@ module Jobs
end
end
def rebuild_problem_tags(limit = 10000)
tag_ids = load_problem_tag_ids(limit)
tag_ids.each do |id|
tag = Tag.find_by(id: id)
SearchIndexer.index(tag, force: true) if tag
end
end
private
def load_problem_post_ids(limit)
@ -83,5 +93,13 @@ module Jobs
.limit(limit)
.pluck(:id)
end
def load_problem_tag_ids(limit)
Tag.joins(:tag_search_data)
.where('tag_search_data.locale != ?
OR tag_search_data.version != ?', SiteSetting.default_locale, Search::INDEX_VERSION)
.limit(limit)
.pluck(:id)
end
end
end

View File

@ -1,4 +1,6 @@
class Tag < ActiveRecord::Base
include Searchable
validates :name, presence: true, uniqueness: true
has_many :tag_users # notification settings
@ -12,6 +14,8 @@ class Tag < ActiveRecord::Base
has_many :tag_group_memberships
has_many :tag_groups, through: :tag_group_memberships
after_save :index_search
COUNT_ARG = "topics.id"
# Apply more activerecord filters to the tags_by_count_query, and then
@ -56,6 +60,10 @@ class Tag < ActiveRecord::Base
def full_url
"#{Discourse.base_url}/tags/#{self.name}"
end
def index_search
SearchIndexer.index(self)
end
end
# == Schema Information

View File

@ -0,0 +1,3 @@
class TagSearchData < ActiveRecord::Base
include HasSearchData
end

View File

@ -2,6 +2,7 @@ class GroupedSearchResultSerializer < ApplicationSerializer
has_many :posts, serializer: SearchPostSerializer
has_many :users, serializer: SearchResultUserSerializer
has_many :categories, serializer: BasicCategorySerializer
has_many :tags, serializer: TagSerializer
attributes :more_posts, :more_users, :more_categories, :term, :search_log_id, :more_full_page_results
def search_log_id
@ -12,4 +13,8 @@ class GroupedSearchResultSerializer < ApplicationSerializer
search_log_id.present?
end
def include_tags?
SiteSetting.tagging_enabled
end
end

View File

@ -0,0 +1,3 @@
class TagSerializer < ApplicationSerializer
attributes :id, :name
end

View File

@ -84,6 +84,10 @@ class SearchIndexer
update_index('category', category_id, name)
end
def self.update_tags_index(tag_id, name)
update_index('tag', tag_id, name)
end
def self.index(obj, force: false)
return if @disabled
@ -115,6 +119,10 @@ class SearchIndexer
if obj.class == Category && (obj.name_changed? || force)
SearchIndexer.update_categories_index(obj.id, obj.name)
end
if obj.class == Tag && (obj.name_changed? || force)
SearchIndexer.update_tags_index(obj.id, obj.name)
end
end
class HtmlScrubber < Nokogiri::XML::SAX::Document

View File

@ -0,0 +1,16 @@
class CreateTagSearchData < ActiveRecord::Migration
def up
create_table :tag_search_data, primary_key: :tag_id do |t|
t.tsvector "search_data"
t.text "raw_data"
t.text "locale"
t.integer "version", default: 0
end
execute 'create index idx_search_tag on tag_search_data using gin(search_data)'
end
def down
execute 'drop index idx_search_tag'
drop_table :tag_search_data
end
end

View File

@ -18,7 +18,7 @@ class Search
end
def self.facets
%w(topic category user private_messages)
%w(topic category user private_messages tags)
end
def self.ts_config(locale = SiteSetting.default_locale)
@ -553,6 +553,7 @@ class Search
unless @search_context
user_search if @term.present?
category_search if @term.present?
tags_search if @term.present?
end
topic_search
end
@ -631,6 +632,20 @@ class Search
end
end
def tags_search
return unless SiteSetting.tagging_enabled
tags = Tag.includes(:tag_search_data)
.where("tag_search_data.search_data @@ #{ts_query}")
.references(:tag_search_data)
.order("name asc")
.limit(limit)
tags.each do |tag|
@results.add(tag)
end
end
def posts_query(limit, opts = nil)
opts ||= {}
posts = Post.where(post_type: Topic.visible_post_types(@guardian.user))

View File

@ -14,6 +14,7 @@ class Search
:posts,
:categories,
:users,
:tags,
:more_posts,
:more_categories,
:more_users,
@ -34,6 +35,7 @@ class Search
@posts = []
@categories = []
@users = []
@tags = []
end
def find_user_data(guardian)

View File

@ -375,6 +375,51 @@ describe Search do
end
context 'tags' do
def search
Search.execute(tag.name)
end
let!(:tag) { Fabricate(:tag) }
let(:tag_group) { Fabricate(:tag_group) }
let(:category) { Fabricate(:category) }
context 'tagging is disabled' do
before { SiteSetting.tagging_enabled = false }
it 'does not include tags' do
expect(search.tags).to_not be_present
end
end
context 'tagging is enabled' do
before { SiteSetting.tagging_enabled = true }
it 'returns the tag in the result' do
expect(search.tags).to eq([tag])
end
it 'shows staff tags' do
staff_tag = Fabricate(:tag, name: "#{tag.name}9")
SiteSetting.staff_tags = "#{staff_tag.name}"
expect(Search.execute(tag.name, guardian: Guardian.new(Fabricate(:admin))).tags).to contain_exactly(tag, staff_tag)
expect(search.tags).to contain_exactly(tag, staff_tag)
end
it 'includes category-restricted tags' do
category_tag = Fabricate(:tag, name: "#{tag.name}9")
tag_group.tags = [category_tag]
category.set_permissions(admins: :full)
category.allowed_tag_groups = [tag_group.name]
category.save!
expect(Search.execute(tag.name, guardian: Guardian.new(Fabricate(:admin))).tags).to contain_exactly(tag, category_tag)
expect(search.tags).to contain_exactly(tag, category_tag)
end
end
end
context 'type_filter' do
let!(:user) { Fabricate(:user, username: 'amazing', email: 'amazing@amazing.com') }

View File

@ -25,6 +25,18 @@ QUnit.test("search", (assert) => {
});
});
QUnit.test("search for a tag", (assert) => {
visit("/");
click('#search-button');
fillIn('#search-term', 'evil');
keyEvent('#search-term', 'keyup', 16);
andThen(() => {
assert.ok(exists('.search-menu .results ul li'), 'it shows results');
});
});
QUnit.test("search scope checkbox", assert => {
visit("/c/bug");
click('#search-button');

View File

@ -108,6 +108,16 @@ export default function() {
id: 1234
}]
});
} else if (request.queryParams.q === 'evil') {
return response({
posts: [{
id: 1234
}],
tags: [{
id: 6,
name: 'eviltrout'
}]
});
}
return response({});