module Jekyll::Algolia::Indexer

def self.delete_records_by_id(index, ids)

Does nothing in dry run mode

ids - Array of objectIDs to delete
index - Algolia Index to target

Public: Delete records whose objectIDs are passed
def self.delete_records_by_id(index, ids)
  return if ids.empty?
  Logger.log("I:Deleting #{ids.length} records")
  return if Configurator.dry_run?
  begin
    index.delete_objects!(ids)
  rescue StandardError => error
    ErrorHandler.stop(error)
  end
end

def self.index(index_name)

index_name - String name of the index

Public: Returns an Algolia Index object from an index name
def self.index(index_name)
  ::Algolia::Index.new(index_name)
end

def self.init

User Agent and give an easy access to the main index
This call will instanciate the Algolia API client, set the custom

Public: Init the module
def self.init
  ::Algolia.init(
    application_id: Configurator.application_id,
    api_key: Configurator.api_key
  )
  set_user_agent
end

def self.local_object_ids(records)

records - Array of all local records

Public: Returns an array of the local objectIDs
def self.local_object_ids(records)
  records.map { |record| record[:objectID] }.compact.sort
end

def self.remote_object_ids(index)

processed, but makes debugging easier when comparing arrays is needed.
The returned array is sorted. It won't have any impact on the way it is

index - Algolia Index to target

Public: Returns an array of all the objectIDs in the index
def self.remote_object_ids(index)
  list = []
  begin
    index.browse(attributesToRetrieve: 'objectID') do |hit|
      list << hit['objectID']
    end
  rescue StandardError
    # The index might not exist if it's the first time we use the plugin
    # so we'll consider that it means there are no records there
    return []
  end
  list.sort
end

def self.remote_settings(index)

index - The Algolia Index

Public: Get the settings of the remote index
def self.remote_settings(index)
  index.get_settings
rescue StandardError => error
  ErrorHandler.stop(error)
end

def self.rename_index(old_name, new_name)

Does nothing in dry run mode

new_name - New name of the index
old_name - Current name of the index

Public: Rename an index
def self.rename_index(old_name, new_name)
  Logger.verbose("I:Renaming `#{old_name}` to `#{new_name}`")
  return if Configurator.dry_run?
  begin
    ::Algolia.move_index(old_name, new_name)
  rescue StandardError => error
    ErrorHandler.stop(error, new_name: new_name)
  end
end

def self.run(records)

records - Records to push

Public: Push all records to Algolia and configure the index
def self.run(records)
  init
  record_count = records.length
  # Indexing zero record is surely a misconfiguration
  if record_count.zero?
    files_to_exclude = Configurator.algolia('files_to_exclude').join(', ')
    Logger.known_message(
      'no_records_found',
      'files_to_exclude' => files_to_exclude,
      'nodes_to_index' => Configurator.algolia('nodes_to_index')
    )
    exit 1
  end
  indexing_mode = Configurator.indexing_mode
  Logger.verbose("I:Indexing mode: #{indexing_mode}")
  case indexing_mode
  when 'diff'
    run_diff_mode(records)
  when 'atomic'
    run_atomic_mode(records)
  end
end

def self.run_atomic_mode(records)

state.
consume more operations, but will never leave the index in a transient
people are always searching into a fully configured index. It will
For the end-user, it will make all the changes in one go, making sure
configure it, and then overwrite the previous index with this new one.
The `atomic` indexing mode will push all records to a brand new index,

records - Array of records to push

Public: Index content following the `atomic` indexing mode
def self.run_atomic_mode(records)
  index_name = Configurator.index_name
  index = index(index_name)
  index_tmp_name = "#{Configurator.index_name}_tmp"
  index_tmp = index(index_tmp_name)
  Logger.verbose("I:Using `#{index_tmp_name}` as temporary index")
  # Copying original settings to the new index
  remote_settings = remote_settings(index)
  new_settings = remote_settings.merge(Configurator.settings)
  update_settings(index_tmp, new_settings)
  # Pushing everthing to a brand new index
  update_records(index_tmp, records)
  # Renaming the new index in place of the old
  rename_index(index_tmp_name, index_name)
  Logger.log('I:✔ Indexing complete')
end

def self.run_diff_mode(records)

of all records in the index, but it will consume less operations.
updated. It will be a bit slower as it will first need to get the list
remove old content from it. It won't touch records that haven't been
The `diff` indexing mode will only push new content to the index and

records - Array of local records

Public: Index content following the `diff` indexing mode
def self.run_diff_mode(records)
  index = index(Configurator.index_name)
  # Update settings
  update_settings(index, Configurator.settings)
  # Getting list of objectID in remote and locally
  remote_ids = remote_object_ids(index)
  local_ids = local_object_ids(records)
  old_records_ids = remote_ids - local_ids
  new_records_ids = local_ids - remote_ids
  if old_records_ids.empty? && new_records_ids.empty?
    Logger.log('I:Nothing to index. Your content is already up to date.')
    return
  end
  Logger.log('I:Pushing records to Algolia...')
  # Delete remote records that are no longer available locally
  delete_records_by_id(index, old_records_ids)
  # Add only records that are not yet already in the remote
  new_records = records.select do |record|
    new_records_ids.include?(record[:objectID])
  end
  update_records(index, new_records)
  Logger.log('I:✔ Indexing complete')
end

def self.set_user_agent

are explicit in defining it to help debug from the dashboard.
each integration version is pinned to a specific API client version, we
every API client should follow the "Algolia for YYY" pattern. Even if
Every integrations should follow the "YYY Integration" pattern, and

Public: Set the User-Agent to send to the API
def self.set_user_agent
  user_agent = [
    "Jekyll Integration (#{VERSION})",
    "Algolia for Ruby (#{::Algolia::VERSION})",
    "Jekyll (#{::Jekyll::VERSION})",
    "Ruby (#{RUBY_VERSION})"
  ].join('; ')
  ::Algolia.set_extra_header('User-Agent', user_agent)
end

def self.update_records(index, records)

Does nothing in dry run mode

content will change its objectID as well.
should be updated but this case should never happen as changing a record
New records will be automatically added. Technically existing records

records - Array of records to update
index - Algolia Index to update

Public: Update records of the specified index
def self.update_records(index, records)
  batch_size = Configurator.algolia('indexing_batch_size')
  records.each_slice(batch_size) do |batch|
    Logger.log("I:Pushing #{batch.size} records")
    next if Configurator.dry_run?
    begin
      index.add_objects!(batch)
    rescue StandardError => error
      ErrorHandler.stop(error, records: records)
    end
  end
end

def self.update_settings(index, settings)

Does nothing in dry run mode

settings - The hash of settings to pass to the index
index - The Algolia Index

Public: Update settings of the index
def self.update_settings(index, settings)
  Logger.verbose('I:Updating settings')
  return if Configurator.dry_run?
  begin
    index.set_settings(settings)
  rescue StandardError => error
    ErrorHandler.stop(error, settings: settings)
  end
end