From ece1ff77d6b2bde578d3bdaad45589589d96902d Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 4 Sep 2023 17:20:35 +0200
Subject: [PATCH] Add `in:library` syntax to search (#26760)

Co-authored-by: Claire <claire.github-309c@sitedethib.com>
---
 app/lib/search_query_transformer.rb       | 88 ++++++++++++++++++++---
 app/services/statuses_search_service.rb   | 37 +---------
 spec/lib/search_query_transformer_spec.rb | 33 ++++-----
 3 files changed, 98 insertions(+), 60 deletions(-)

diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb
index af3964fd3..2dc10830d 100644
--- a/app/lib/search_query_transformer.rb
+++ b/app/lib/search_query_transformer.rb
@@ -9,23 +9,90 @@ class SearchQueryTransformer < Parslet::Transform
     before
     after
     during
+    in
   ).freeze
 
   class Query
-    attr_reader :must_not_clauses, :must_clauses, :filter_clauses
+    def initialize(clauses, options = {})
+      raise ArgumentError if options[:current_account].nil?
 
-    def initialize(clauses)
-      grouped = clauses.compact.chunk(&:operator).to_h
-      @must_not_clauses = grouped.fetch(:must_not, [])
-      @must_clauses = grouped.fetch(:must, [])
-      @filter_clauses = grouped.fetch(:filter, [])
+      @clauses = clauses
+      @options = options
+
+      flags_from_clauses!
     end
 
-    def apply(search)
+    def request
+      search = Chewy::Search::Request.new(*indexes).filter(default_filter)
+
       must_clauses.each { |clause| search = search.query.must(clause.to_query) }
       must_not_clauses.each { |clause| search = search.query.must_not(clause.to_query) }
       filter_clauses.each { |clause| search = search.filter(**clause.to_query) }
-      search.query.minimum_should_match(1)
+
+      search
+    end
+
+    private
+
+    def clauses_by_operator
+      @clauses_by_operator ||= @clauses.compact.chunk(&:operator).to_h
+    end
+
+    def flags_from_clauses!
+      @flags = clauses_by_operator.fetch(:flag, []).to_h { |clause| [clause.prefix, clause.term] }
+    end
+
+    def must_clauses
+      clauses_by_operator.fetch(:must, [])
+    end
+
+    def must_not_clauses
+      clauses_by_operator.fetch(:must_not, [])
+    end
+
+    def filter_clauses
+      clauses_by_operator.fetch(:filter, [])
+    end
+
+    def indexes
+      case @flags['in']
+      when 'library'
+        [StatusesIndex]
+      else
+        [PublicStatusesIndex, StatusesIndex]
+      end
+    end
+
+    def default_filter
+      {
+        bool: {
+          should: [
+            {
+              term: {
+                _index: PublicStatusesIndex.index_name,
+              },
+            },
+            {
+              bool: {
+                must: [
+                  {
+                    term: {
+                      _index: StatusesIndex.index_name,
+                    },
+                  },
+                  {
+                    term: {
+                      searchable_by: @options[:current_account].id,
+                    },
+                  },
+                ],
+              },
+            },
+          ],
+
+          minimum_should_match: 1,
+        },
+      }
     end
   end
 
@@ -108,6 +175,9 @@ class SearchQueryTransformer < Parslet::Transform
         @filter = :created_at
         @type = :range
         @term = { gte: term, lte: term, time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
+      when 'in'
+        @operator = :flag
+        @term = term
       else
         raise "Unknown prefix: #{prefix}"
       end
@@ -176,6 +246,6 @@ class SearchQueryTransformer < Parslet::Transform
   end
 
   rule(query: sequence(:clauses)) do
-    Query.new(clauses)
+    Query.new(clauses, current_account: current_account)
   end
 end
diff --git a/app/services/statuses_search_service.rb b/app/services/statuses_search_service.rb
index 2317a2a1a..e4b38a9da 100644
--- a/app/services/statuses_search_service.rb
+++ b/app/services/statuses_search_service.rb
@@ -14,20 +14,8 @@ class StatusesSearchService < BaseService
   private
 
   def status_search_results
-    definition = parsed_query.apply(
-      Chewy::Search::Request.new(StatusesIndex, PublicStatusesIndex).filter(
-        bool: {
-          should: [
-            publicly_searchable,
-            non_publicly_searchable,
-          ],
-
-          minimum_should_match: 1,
-        }
-      )
-    )
-
-    results             = definition.collapse(field: :id).order(id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
+    request             = parsed_query.request
+    results             = request.collapse(field: :id).order(id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
     account_ids         = results.map(&:account_id)
     account_domains     = results.map(&:account_domain)
     preloaded_relations = @account.relations_map(account_ids, account_domains)
@@ -37,27 +25,6 @@ class StatusesSearchService < BaseService
     []
   end
 
-  def publicly_searchable
-    {
-      term: { _index: PublicStatusesIndex.index_name },
-    }
-  end
-
-  def non_publicly_searchable
-    {
-      bool: {
-        must: [
-          {
-            term: { _index: StatusesIndex.index_name },
-          },
-          {
-            term: { searchable_by: @account.id },
-          },
-        ],
-      },
-    }
-  end
-
   def parsed_query
     SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query), current_account: @account)
   end
diff --git a/spec/lib/search_query_transformer_spec.rb b/spec/lib/search_query_transformer_spec.rb
index 17f06d283..4b949b1b8 100644
--- a/spec/lib/search_query_transformer_spec.rb
+++ b/spec/lib/search_query_transformer_spec.rb
@@ -3,17 +3,18 @@
 require 'rails_helper'
 
 describe SearchQueryTransformer do
-  subject { described_class.new.apply(parser, current_account: nil) }
+  subject { described_class.new.apply(parser, current_account: account) }
 
+  let(:account) { Fabricate(:account) }
   let(:parser) { SearchQueryParser.new.parse(query) }
 
   context 'with "hello world"' do
     let(:query) { 'hello world' }
 
     it 'transforms clauses' do
-      expect(subject.must_clauses.map(&:term)).to match_array %w(hello world)
-      expect(subject.must_not_clauses).to be_empty
-      expect(subject.filter_clauses).to be_empty
+      expect(subject.send(:must_clauses).map(&:term)).to match_array %w(hello world)
+      expect(subject.send(:must_not_clauses)).to be_empty
+      expect(subject.send(:filter_clauses)).to be_empty
     end
   end
 
@@ -21,9 +22,9 @@ describe SearchQueryTransformer do
     let(:query) { 'hello -world' }
 
     it 'transforms clauses' do
-      expect(subject.must_clauses.map(&:term)).to match_array %w(hello)
-      expect(subject.must_not_clauses.map(&:term)).to match_array %w(world)
-      expect(subject.filter_clauses).to be_empty
+      expect(subject.send(:must_clauses).map(&:term)).to match_array %w(hello)
+      expect(subject.send(:must_not_clauses).map(&:term)).to match_array %w(world)
+      expect(subject.send(:filter_clauses)).to be_empty
     end
   end
 
@@ -31,9 +32,9 @@ describe SearchQueryTransformer do
     let(:query) { 'hello is:reply' }
 
     it 'transforms clauses' do
-      expect(subject.must_clauses.map(&:term)).to match_array %w(hello)
-      expect(subject.must_not_clauses).to be_empty
-      expect(subject.filter_clauses.map(&:term)).to match_array %w(reply)
+      expect(subject.send(:must_clauses).map(&:term)).to match_array %w(hello)
+      expect(subject.send(:must_not_clauses)).to be_empty
+      expect(subject.send(:filter_clauses).map(&:term)).to match_array %w(reply)
     end
   end
 
@@ -41,9 +42,9 @@ describe SearchQueryTransformer do
     let(:query) { 'foo: bar' }
 
     it 'transforms clauses' do
-      expect(subject.must_clauses.map(&:term)).to match_array %w(foo bar)
-      expect(subject.must_not_clauses).to be_empty
-      expect(subject.filter_clauses).to be_empty
+      expect(subject.send(:must_clauses).map(&:term)).to match_array %w(foo bar)
+      expect(subject.send(:must_not_clauses)).to be_empty
+      expect(subject.send(:filter_clauses)).to be_empty
     end
   end
 
@@ -51,9 +52,9 @@ describe SearchQueryTransformer do
     let(:query) { 'foo:bar' }
 
     it 'transforms clauses' do
-      expect(subject.must_clauses.map(&:term)).to contain_exactly('foo bar')
-      expect(subject.must_not_clauses).to be_empty
-      expect(subject.filter_clauses).to be_empty
+      expect(subject.send(:must_clauses).map(&:term)).to contain_exactly('foo bar')
+      expect(subject.send(:must_not_clauses)).to be_empty
+      expect(subject.send(:filter_clauses)).to be_empty
     end
   end
 end