diff --git a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 index 0bc886a8178..356a2b688ec 100644 --- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 +++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 @@ -77,7 +77,8 @@ export default Em.Component.extend({ likes: false, private: false, seen: false - } + }, + all_tags: false }, status: '', min_post_count: '', @@ -230,13 +231,15 @@ export default Em.Component.extend({ const match = this.filterBlocks(REGEXP_TAGS_PREFIX); const tags = this.get('searchedTerms.tags'); + const contain_all_tags = this.get('searchedTerms.special.all_tags'); if (match.length !== 0) { - const existingInput = _.isArray(tags) ? tags.join(',') : tags; + const join_char = contain_all_tags ? '+' : ','; + const existingInput = _.isArray(tags) ? tags.join(join_char) : tags; const userInput = match[0].replace(REGEXP_TAGS_REPLACE, ''); if (existingInput !== userInput) { - this.set('searchedTerms.tags', (userInput.length !== 0) ? userInput.split(',') : []); + this.set('searchedTerms.tags', (userInput.length !== 0) ? userInput.split(join_char) : []); } } else if (tags.length !== 0) { this.set('searchedTerms.tags', []); @@ -365,14 +368,16 @@ export default Em.Component.extend({ } }, - @observes('searchedTerms.tags') + @observes('searchedTerms.tags', 'searchedTerms.special.all_tags') updateSearchTermForTags() { const match = this.filterBlocks(REGEXP_TAGS_PREFIX); const tagFilter = this.get('searchedTerms.tags'); let searchTerm = this.get('searchTerm') || ''; + const contain_all_tags = this.get('searchedTerms.special.all_tags'); if (tagFilter && tagFilter.length !== 0) { - const tags = tagFilter.join(','); + const join_char = contain_all_tags ? '+' : ','; + const tags = tagFilter.join(join_char); if (match.length !== 0) { searchTerm = searchTerm.replace(match[0], `tags:${tags}`); diff --git a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs index 71a9c1b6198..e3ac643ec64 100644 --- a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs +++ b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs @@ -41,6 +41,9 @@
{{tag-chooser tags=searchedTerms.tags blacklist=searchedTerms.tags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true" width="70%"}} +
+ +
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 80e21f6e072..55961d2f7bb 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1338,6 +1338,7 @@ en: seen: I've read unseen: I've not read wiki: are wiki + all_tags: Contains all tags statuses: label: Where topics open: are open diff --git a/lib/search.rb b/lib/search.rb index 5df9de3ce97..377de504d82 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -447,15 +447,27 @@ class Search end end - advanced_filter(/tags?:([a-zA-Z0-9,\-_]+)/) do |posts, match| - tags = match.split(",") + advanced_filter(/tags?:([a-zA-Z0-9,\-_+]+)/) do |posts, match| + if match.include?('+') + tags = match.split('+') - posts.where("topics.id IN ( + posts.where("topics.id IN ( + SELECT tt.topic_id + FROM topic_tags tt, tags + WHERE tt.tag_id = tags.id + GROUP BY tt.topic_id + HAVING to_tsvector(#{query_locale}, array_to_string(array_agg(tags.name), ' ')) @@ to_tsquery(#{query_locale}, ?) + )", tags.join('&')) + else + tags = match.split(",") + + posts.where("topics.id IN ( SELECT DISTINCT(tt.topic_id) FROM topic_tags tt, tags WHERE tt.tag_id = tags.id AND tags.name in (?) )", tags) + end end private diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index ef88b3e4a99..5ab747be538 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -706,15 +706,35 @@ describe Search do expect(Search.execute('this is a test #beta').posts.size).to eq(0) end - it "can find with tag" do - topic1 = Fabricate(:topic, title: 'Could not, would not, on a boat') - topic1.tags = [Fabricate(:tag, name: 'eggs'), Fabricate(:tag, name: 'ham')] - Fabricate(:post, topic: topic1) - post2 = Fabricate(:post, topic: topic1, raw: "It probably doesn't help that they're green...") + context 'tags' do + let(:tag1) { Fabricate(:tag, name: 'lunch') } + let(:tag2) { Fabricate(:tag, name: 'eggs') } + let(:topic1) { Fabricate(:topic, tags: [tag2, Fabricate(:tag)]) } + let(:topic2) { Fabricate(:topic, tags: [tag2]) } + let(:topic3) { Fabricate(:topic, tags: [tag1, tag2]) } + let!(:post1) { Fabricate(:post, topic: topic1)} + let!(:post2) { Fabricate(:post, topic: topic2)} + let!(:post3) { Fabricate(:post, topic: topic3)} - expect(Search.execute('green tags:eggs').posts.map(&:id)).to eq([post2.id]) - expect(Search.execute('green tags:plants').posts.size).to eq(0) + it 'can find posts with tag' do + post4 = Fabricate(:post, topic: topic3, raw: "It probably doesn't help that they're green...") + + expect(Search.execute('green tags:eggs').posts.map(&:id)).to eq([post4.id]) + expect(Search.execute('tags:plants').posts.size).to eq(0) + end + + it 'can find posts with any tag from multiple tags' do + Fabricate(:post) + + expect(Search.execute('tags:eggs,lunch').posts.map(&:id).sort).to eq([post1.id, post2.id, post3.id].sort) + end + + it 'can find posts which contains all provided tags' do + expect(Search.execute('tags:lunch+eggs').posts.map(&:id)).to eq([post3.id]) + expect(Search.execute('tags:eggs+lunch').posts.map(&:id)).to eq([post3.id]) + end end + end it 'can parse complex strings using ts_query helper' do