202 lines
6.6 KiB
Ruby

# frozen_string_literal: true
require 'algoliasearch'
module Jekyll
module Algolia
# Module to push records to Algolia and configure the index
module Indexer
include Jekyll::Algolia
# Public: Init the module
#
# This call will instanciate the Algolia API client, set the custom
# User Agent and give an easy access to the main index
def self.init
::Algolia.init(
application_id: Configurator.application_id,
api_key: Configurator.api_key
)
set_user_agent
end
# Public: Set the User-Agent to send to the API
#
# Every integrations should follow the "YYY Integration" pattern, and
# every API client should follow the "Algolia for YYY" pattern. Even if
# each integration version is pinned to a specific API client version, we
# are explicit in defining it to help debug from the dashboard.
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
# Public: Returns an Algolia Index object from an index name
#
# index_name - String name of the index
def self.index(index_name)
::Algolia::Index.new(index_name)
end
# Public: Check if an index exists
#
# index_name - Name of the index
#
# Note: there is no API endpoint to do that, so we try to get the settings
# instead, which will fail if the index does not exist
def self.index?(index_name)
index(index_name).get_settings
return true
rescue StandardError
return false
end
# Public: Returns an array of all the objectIDs in the index
#
# index - Algolia Index to target
#
# The returned array is sorted. It won't have any impact on the way it is
# processed, but makes debugging easier when comparing arrays is needed.
def self.remote_object_ids(index)
list = []
Logger.verbose(
"I:Inspecting existing records in index #{index.name}..."
)
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
# Public: Returns an array of the local objectIDs
#
# records - Array of all local records
def self.local_object_ids(records)
records.map { |record| record[:objectID] }.compact.sort
end
# Public: Update settings of the index
#
# index - The Algolia Index
#
# Does nothing in dry run mode
# Settings will only be updated in the first push, and if custom settings
# are defined in _config.yml. Otherwise, they are left untouched, allowing
# users to configure them through their dashboard.
def self.update_settings(index)
has_custom_settings = !Configurator.algolia('settings').nil?
index_exists = index?(index.name)
# No need to update the settings if the index is already configured and
# the user did not specify custom settings
return if index_exists && !has_custom_settings
Logger.verbose('I:Updating settings')
return if Configurator.dry_run?
settings = Configurator.settings
begin
index.set_settings!(settings)
rescue StandardError => error
ErrorHandler.stop(error, settings: settings)
end
end
# Public: Update records of the index
#
# index_name - The Algolia index
# old_records_ids - Ids of records to delete from the index
# new_records - Records to add to the index
#
# Note: All operations will be done in one batch, assuring an atomic
# update
# Does nothing in dry run mode
def self.update_records(index_name, old_records_ids, new_records)
# Stop if nothing to change
if old_records_ids.empty? && new_records.empty?
Logger.log('I:Nothing to index. Your content is already up to date.')
return
end
Logger.log("I:Updating records in index #{index_name}...")
Logger.log("I:Records to delete: #{old_records_ids.length}")
Logger.log("I:Records to add: #{new_records.length}")
return if Configurator.dry_run?
operations = new_records.map do |new_record|
{ action: 'addObject', indexName: index_name, body: new_record }
end
old_records_ids.each do |object_id|
operations << {
action: 'deleteObject', indexName: index_name,
body: { objectID: object_id }
}
end
# Run the batches in slices if they are too large
batch_size = Configurator.algolia('indexing_batch_size')
operations.each_slice(batch_size) do |slice|
begin
::Algolia.batch!(slice)
rescue StandardError => error
records = slice.map do |record|
record[:body]
end
ErrorHandler.stop(error, records: records)
end
end
end
# Public: Push all records to Algolia and configure the index
#
# records - Records to push
def self.run(records)
init
# Indexing zero record is surely a misconfiguration
if records.length.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
index_name = Configurator.index_name
index = index(index_name)
# Update settings
update_settings(index)
# Getting list of objectID in remote and locally
remote_ids = remote_object_ids(index)
local_ids = local_object_ids(records)
# Getting list of what to add and what to delete
old_records_ids = remote_ids - local_ids
new_records_ids = local_ids - remote_ids
new_records = records.select do |record|
new_records_ids.include?(record[:objectID])
end
update_records(index_name, old_records_ids, new_records)
Logger.log('I:✔ Indexing complete')
end
end
end
end