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