1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
# frozen_string_literal: true
module Mongo
module SearchIndex
# A class representing a view of search indexes.
class View
include Enumerable
include Retryable
include Collection::Helpers
# @return [ Mongo::Collection ] the collection this view belongs to
attr_reader :collection
# @return [ nil | String ] the index id to query
attr_reader :requested_index_id
# @return [ nil | String ] the index name to query
attr_reader :requested_index_name
# @return [ Hash ] the options hash to use for the aggregate command
# when querying the available indexes.
attr_reader :aggregate_options
# Create the new search index view.
#
# @param [ Collection ] collection The collection.
# @param [ Hash ] options The options that configure the behavior of the view.
#
# @option options [ String ] :id The specific index id to query (optional)
# @option options [ String ] :name The name of the specific index to query (optional)
# @option options [ Hash ] :aggregate The options hash to send to the
# aggregate command when querying the available indexes.
def initialize(collection, options = {})
@collection = collection
@requested_index_id = options[:id]
@requested_index_name = options[:name]
@aggregate_options = options[:aggregate] || {}
return if @aggregate_options.is_a?(Hash)
raise ArgumentError, "The :aggregate option must be a Hash (got a #{@aggregate_options.class})"
end
# Create a single search index with the given definition. If the name is
# provided, the new index will be given that name.
#
# @param [ Hash ] definition The definition of the search index.
# @param [ nil | String ] name The name to give the new search index.
#
# @return [ String ] the name of the new search index.
def create_one(definition, name: nil, type: 'search')
create_many([ { name: name, definition: definition, type: type } ]).first
end
# Create multiple search indexes with a single command.
#
# @param [ Array<Hash> ] indexes The description of the indexes to
# create. Each element of the list must be a hash with a definition
# key, and an optional name key.
#
# @return [ Array<String> ] the names of the new search indexes.
def create_many(indexes)
spec = spec_with(indexes: indexes.map { |v| validate_search_index!(v) })
result = Operation::CreateSearchIndexes.new(spec).execute(next_primary, context: execution_context)
result.first['indexesCreated'].map { |idx| idx['name'] }
end
# Drop the search index with the given id, or name. One or the other must
# be specified, but not both.
#
# @param [ String ] id the id of the index to drop
# @param [ String ] name the name of the index to drop
#
# @return [ Mongo::Operation::Result | false ] the result of the
# operation, or false if the given index does not exist.
def drop_one(id: nil, name: nil)
validate_id_or_name!(id, name)
spec = spec_with(index_id: id, index_name: name)
op = Operation::DropSearchIndex.new(spec)
# per the spec:
# Drivers MUST suppress NamespaceNotFound errors for the
# ``dropSearchIndex`` helper. Drop operations should be idempotent.
do_drop(op, nil, execution_context)
end
# Iterate over the search indexes.
#
# @param [ Proc ] block if given, each search index will be yieleded to
# the block.
#
# @return [ self | Enumerator ] if a block is given, self is returned.
# Otherwise, an enumerator will be returned.
def each(&block)
@result ||= begin
spec = {}.tap do |s|
s[:id] = requested_index_id if requested_index_id
s[:name] = requested_index_name if requested_index_name
end
collection.with(read_concern: {}).aggregate(
[ { '$listSearchIndexes' => spec } ],
aggregate_options
)
end
return @result.to_enum unless block
@result.each(&block)
self
end
# Update the search index with the given id or name. One or the other
# must be provided, but not both.
#
# @param [ Hash ] definition the definition to replace the given search
# index with.
# @param [ nil | String ] id the id of the search index to update
# @param [ nil | String ] name the name of the search index to update
#
# @return [ Mongo::Operation::Result ] the result of the operation
def update_one(definition, id: nil, name: nil)
validate_id_or_name!(id, name)
spec = spec_with(index_id: id, index_name: name, index: definition)
Operation::UpdateSearchIndex.new(spec).execute(next_primary, context: execution_context)
end
# The following methods are to make the view act more like an array,
# without having to explicitly make it an array...
# Queries whether the search index enumerable is empty.
#
# @return [ true | false ] whether the enumerable is empty or not.
def empty?
count.zero?
end
private
# A helper method for building the specification document with certain
# values pre-populated.
#
# @param [ Hash ] extras the values to put into the specification
#
# @return [ Hash ] the specification document
def spec_with(extras)
{
coll_name: collection.name,
db_name: collection.database.name,
}.merge(extras)
end
# A helper method for retrieving the primary server from the cluster.
#
# @return [ Mongo::Server ] the server to use
def next_primary(ping = nil, session = nil)
collection.cluster.next_primary(ping, session)
end
# A helper method for constructing a new operation context for executing
# an operation.
#
# @return [ Mongo::Operation::Context ] the operation context
def execution_context
Operation::Context.new(client: collection.client)
end
# Validates the given id and name, ensuring that exactly one of them
# is non-nil.
#
# @param [ nil | String ] id the id to validate
# @param [ nil | String ] name the name to validate
#
# @raise [ ArgumentError ] if neither or both arguments are nil
def validate_id_or_name!(id, name)
return unless (id.nil? && name.nil?) || (!id.nil? && !name.nil?)
raise ArgumentError, 'exactly one of id or name must be specified'
end
# Validates the given search index document, ensuring that it has no
# extra keys, and that the name and definition are valid.
#
# @param [ Hash ] doc the document to validate
#
# @raise [ ArgumentError ] if the document is invalid.
def validate_search_index!(doc)
validate_search_index_keys!(doc.keys)
validate_search_index_name!(doc[:name] || doc['name'])
validate_search_index_definition!(doc[:definition] || doc['definition'])
doc
end
# Validates the keys of a search index document, ensuring that
# they are all valid.
#
# @param [ Array<String | Hash> ] keys the keys of a search index document
#
# @raise [ ArgumentError ] if the list contains any invalid keys
def validate_search_index_keys!(keys)
extras = keys - [ 'name', 'definition', 'type', :name, :definition, :type ]
raise ArgumentError, "invalid keys in search index creation: #{extras.inspect}" if extras.any?
end
# Validates the name of a search index, ensuring that it is either a
# String or nil.
#
# @param [ nil | String ] name the name of a search index
#
# @raise [ ArgumentError ] if the name is not valid
def validate_search_index_name!(name)
return if name.nil? || name.is_a?(String)
raise ArgumentError, "search index name must be nil or a string (got #{name.inspect})"
end
# Validates the definition of a search index.
#
# @param [ Hash ] definition the definition of a search index
#
# @raise [ ArgumentError ] if the definition is not valid
def validate_search_index_definition!(definition)
return if definition.is_a?(Hash)
raise ArgumentError, "search index definition must be a Hash (got #{definition.inspect})"
end
end
end
end
|