123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- module CanSearch
- extend ActiveSupport::Concern
- included do
- =begin
- This function provides the possibility to add model specific sql extensions
- for the searches in the DB. E.g. role or group specific conditions
- in user model.
- see e.g. also app/models/user/search.rb
- =end
- scope :search_sql_extension, ->(_params) {}
- =begin
- This function defines the sql search query for the text fields which are searched in. By default
- it is all string columns but can be modified.
- see e.g. also app/models/ticket/search.rb
- =end
- scope :search_sql_query_extension, lambda { |params|
- return if params[:query].blank?
- search_columns = columns.select { |row| row.type == :string && !row.try(:array) }.map(&:name)
- return if search_columns.blank?
- where_or_cis(search_columns, "%#{SqlHelper.quote_like(params[:query].to_s.downcase)}%")
- }
- # Scope to specific IDs if they're given in params.
- # Usually those IDs are pre-filled in .search_params_pre method.
- scope :search_sql_ids, lambda { |params|
- where(id: params[:ids]) if params[:ids].present?
- }
- end
- class_methods do
- =begin
- This defines the default search sort by for the search function.
- =end
- def search_default_sort_by
- 'updated_at'
- end
- =begin
- This defines the default search order by for the search function.
- =end
- def search_default_order_by
- 'desc'
- end
- =begin
- This function can be used to fix parameters for the model
- e.g. is used to restrict the result set of organization searches
- to only return customer organizations in case of a customer user
- see e.g. also app/models/organization/search.rb
- =end
- def search_params_pre(params)
- # optional
- end
- =begin
- This function provides the possibility to add model specific query extensions
- for the searches in the elasticsearch. E.g. role or group specific conditions
- in user model.
- see e.g. also app/models/user/search.rb
- =end
- def search_query_extension(params)
- # optional
- end
- =begin
- search objects via search index
- result = Model.search(
- current_user: User.find(123),
- query: 'search something',
- limit: 15,
- offset: 100,
- )
- returns
- result = [obj1, obj2, obj3]
- search objects via search index with total count
- result = Model.search(
- current_user: User.find(123),
- query: 'search something',
- limit: 15,
- offset: 100,
- with_total_count: true
- )
- returns
- result = {
- object_ids: [1,2,3],
- count: 3,
- }
- search objects via search index with ONLY total count
- result = Model.search(
- current_user: User.find(123),
- query: 'search something',
- limit: 15,
- offset: 100,
- only_total_count: true
- )
- returns
- result = {
- count: 3,
- }
- search objects via search index
- result = Model.search(
- current_user: User.find(123),
- query: 'search something',
- limit: 15,
- offset: 100,
- full: false,
- )
- returns
- result = [1,2,3]
- search objects via database
- result = Group.search(
- current_user: User.find(123),
- query: 'some query', # query or condition is required
- condition: {
- 'groups.id' => {
- operator: 'is',
- value: [1,2,3],
- },
- },
- limit: 15,
- offset: 100,
- # sort single column
- sort_by: 'created_at',
- order_by: 'asc',
- # sort multiple columns
- sort_by: [ 'created_at', 'updated_at' ],
- order_by: [ 'asc', 'desc' ],
- full: false,
- )
- returns
- result = [1,2,3]
- =end
- def search(params)
- # It's possible to search objects that don't have .search_preferences method.
- # However, if .search_preferences exist and return falsey value, search is not authorized in a given context!
- # Thus we need to check if method exist instead of using try()!
- return if defined?(search_preferences) && !search_preferences(params[:current_user])
- params = search_build_params(params)
- # try search index backend
- # we only search in elastic search when we have a query present
- # else we try to use the database result, since it is more up to date
- object_ids, object_count = if SearchIndexBackend.enabled? && included_modules.include?(HasSearchIndexBackend) && params[:query].present?
- search_es(params)
- else
- search_sql(params)
- end
- search_result(params, object_ids, object_count)
- end
- def search_result(params, object_ids, object_count)
- if params[:only_total_count].present?
- {
- total_count: object_count,
- }
- elsif params[:with_total_count].present?
- if params[:full].present?
- return {
- objects: object_ids.map { |id| lookup(id: id) },
- total_count: object_count
- }
- end
- {
- object_ids: object_ids,
- total_count: object_count
- }
- elsif params[:full].present?
- object_ids.map { |id| lookup(id: id) }
- else
- object_ids
- end
- end
- def search_build_params(params)
- search_params_pre(params)
- sql_helper = ::SqlHelper.new(object: self)
- params[:condition] ||= {}
- params[:limit] ||= 50
- params[:query] = params[:query]&.delete('*')
- params[:offset] = params[:offset].presence || params[:from].presence || 0
- params[:full] = !params.key?(:full) || ActiveModel::Type::Boolean.new.cast(params[:full])
- params[:sort_by] = sql_helper.get_sort_by(params, search_default_sort_by)
- params[:order_by] = sql_helper.get_order_by(params, search_default_order_by)
- params
- end
- def search_es(params)
- result = SearchIndexBackend.search_by_index(
- params[:query],
- to_s,
- params.merge(query_extension: search_query_extension(params), with_total_count: true)
- )
- if params[:only_total_count].blank?
- object_ids = result&.dig(:object_metadata)&.pluck(:id) || []
- end
- object_count = result&.dig(:total_count) || 0
- [object_ids, object_count]
- end
- def search_sql(params)
- scope = search_sql_base(params)
- objects_order_sql = sql_helper.get_order(params[:sort_by], params[:order_by], "#{table_name}.updated_at DESC")
- objects_scope = scope
- .reorder(Arel.sql(objects_order_sql))
- .offset(params[:offset])
- .limit(params[:limit])
- .group(:id)
- if params[:only_total_count].blank?
- object_ids = objects_scope.pluck(:id)
- end
- object_count = scope.count("DISTINCT #{table_name}.id")
- [object_ids, object_count]
- end
- def search_sql_base(params)
- query_condition, bind_condition, tables = selector2sql(params[:condition])
- scope = params[:scope].present? ? params[:scope].new(params[:current_user]).resolve : all
- scope
- .joins(tables).where(query_condition, *bind_condition)
- .search_sql_extension(params)
- .search_sql_query_extension(params)
- .search_sql_ids(params)
- end
- def sql_helper
- @sql_helper ||= ::SqlHelper.new(object: self)
- end
- end
- end
|