module Gummi module DbLayer module Document extend ActiveSupport::Concern included do include Virtus.model include Gummi::DbLayer::Document::Attributes end # ––––––––––––– # Class Methods # ––––––––––––– module ClassMethods # –––––––––––––––––––––– # Public Persistence API # –––––––––––––––––––––– # Public: Checks if specified document exists # # id - The document id # # Examples # # DB::Person.exists?('123') # # => true # # Returns true or false def exists?(id) client.exists with_options(id: id) end def create(*args) instance = new(*args) instance.create ? instance : nil end # Public: Get document using the (realtime) get api # # id - The document id # # parent_id - The parent id (required if getting a child document) # # Examples # # DB::Rating.get('123', parent_id: '456') # # Returns the document if found, nil if not found def get(id, parent_id: nil) raise ArgumentError, "parent_id is required for getting child documents" if parent_document_type && parent_id.blank? options = with_options({ id: id, fields: %w{ _source _parent } }) options.merge!(parent: parent_id) if parent_id response = ActiveSupport::Notifications.instrument "search.elasticsearch", name: "Document#get", search: options do Hashie::Mash.new client.get options end hit_to_document response rescue ::Elasticsearch::Transport::Transport::Errors::NotFound nil end alias :find :get def delete(id, parent_id: nil) raise ArgumentError, "parent_id is required for deleting child documents" if parent_document_type && parent_id.blank? options = with_options(id: id) options.merge! parent: parent_id if parent_id response = ActiveSupport::Notifications.instrument "search.elasticsearch", name: "Document#delete!", search: options do Hashie::Mash.new client.delete options end rescue ::Elasticsearch::Transport::Transport::Errors::NotFound nil end def update(id, attributes) return unless id.present? if parent_id_attribute_name && parent_id = self.send(parent_id_attribute_name) options = { parent: parent_id } else options = {} end client.update options.merge(index: index.name, type: document_type, id: id, body: { doc: attributes }) true rescue ::Elasticsearch::Transport::Transport::Errors::NotFound nil end def delete_children_by_query(parent_id, children_query) parent_id_query = { term: { _parent: parent_id } } query = { query: { bool: { must: [parent_id_query, children_query] } } } delete_by_query query end def delete_by_query(query) options = { index: index.name, type: document_type, body: query } Hashie::Mash.new client.delete_by_query options end def with_options(options = {}) {index: index.name, type: document_type}.merge options end # ––––––––––––––––––––– # Public Conversion API # ––––––––––––––––––––– def hits_to_documents(hits) documents = [hits].flatten.map { |hit| hit_to_document(hit) } documents.length > 1 ? documents : documents.first end def hit_to_document(hit) attributes = { id: hit._id, version: hit._version } attributes.merge!(parent_id_attribute_name => hit.fields._parent) if hit.fields && hit.fields._parent attributes.merge! hit._source if hit._source self.new attributes end # –––––––––––––––––––––––––––––– # Index and Document definitions # –––––––––––––––––––––––––––––– def index(*args) @index = args.first unless args.empty? @index || Gummi::DbLayer::DefaultIndex end def document_type(*args) @document_type = args.first.to_sym unless args.empty? @document_type || name.demodulize.underscore.to_sym end def parent(model) parent_document_type model.document_type end def parent_document_type(*args) @parent_document_type = args.first unless args.empty? parent_id_attribute_name "#{@parent_document_type}_id".to_sym if @parent_document_type && !parent_id_attribute_name @parent_document_type end def parent_id_attribute_name(*args) unless args.empty? @parent_id_attribute_name = args.first attr_accessor @parent_id_attribute_name end @parent_id_attribute_name end # –––––––––––––––– # Public Index API # –––––––––––––––– def sync_mapping! client.indices.put_mapping creation_options end def creation_options result = { index: index.name, type: document_type, body: { document_type => { properties: mapping, } } } result[:body][document_type].merge!(_parent: { type: parent_document_type }) if parent_document_type.present? result end # ––––––––––––––––– # Public Search API # ––––––––––––––––– def new_filtered_search(options = {}) Gummi::DbLayer::Document::Search::Filtered.new default_search_options.merge(options) end def new_raw_search(options = {}) Gummi::DbLayer::Document::Search::Raw.new default_search_options.merge(options) end def default_search_options { document_class: self, index: index.name, type: document_type, } end # ––––––––––––––––––––––––––– # Internal Backend Connection # ––––––––––––––––––––––––––– def client Gummi.client end private # –––––––––––––––––––––––– # Internal Persistence API # –––––––––––––––––––––––– end # –––––––––––––––– # Instance Methods # –––––––––––––––– attr_accessor :id attr_accessor :version # –––––––––––––––––––––– # Public Persistence API # –––––––––––––––––––––– def create(method = :create) attributes = self.attributes if parent_id_attribute_name && parent_id = self.send(parent_id_attribute_name) options = { parent: parent_id } else options = {} end raise ArgumentError unless [:create, :index].include?(method) opts = options.merge(index: index.name, type: document_type, id: self.id, body: attributes) response = ActiveSupport::Notifications.instrument "search.elasticsearch", name: "Document#create(#{method})", search: opts do Hashie::Mash.new client.send(method, opts) end if response.created self.id = response._id self.version = response._version true else false end end def overwrite create :index end private # –––––––––––––––––––– # Class method proxies # –––––––––––––––––––– def index self.class.index end def document_type self.class.document_type end def parent_id_attribute_name self.class.parent_id_attribute_name end def client self.class.client end end end end