Class: Redis::Commands::Search::Index
- Inherits:
-
Object
- Object
- Redis::Commands::Search::Index
- Defined in:
- lib/redis/commands/modules/search/index.rb
Overview
High-level handle to a search index: a Schema, an optional key prefix and a Redis
connection bundled together. It is the most ergonomic entry point for HASH/JSON document
workflows — it remembers the prefix (so document ids round-trip to the logical id passed to
#add), validates field values against the schema, and accepts a Query, a query string,
or a block in #search. Obtain one via Redis#create_index or Index.create.
Instance Attribute Summary collapse
- #name ⇒ String? readonly
- #prefix ⇒ String? readonly
Class Method Summary collapse
-
.create(redis, name, schema, storage_type, prefix: nil, stopwords: nil, skip_initial_scan: false, **options) ⇒ Index
Create the index on the server (runs
FT.CREATE) and return a handle to it. -
.key_prefix(prefix, definition) ⇒ Object
The single literal key prefix the Index should manage, or nil when it can't be determined unambiguously (no prefix, or a definition with several prefixes).
-
.resolve_storage_type(storage_type, definition) ⇒ Object
The effective storage type, mirroring FT.CREATE: a definition's own
index_typewins when set, otherwise fall back to thestorage_typekeyword.
Instance Method Summary collapse
-
#add(doc_id, **fields) ⇒ Integer, String
Add (or replace) a document under the key
"<prefix><doc_id>"(ordoc_idwhen no prefix is set). -
#aggregate(query, *args) ⇒ AggregateResult
Run an aggregation pipeline against the index (delegates to
FT.AGGREGATE). -
#alter(field_or_args) ⇒ String
Add a field to the index schema (delegates to
FT.ALTER). -
#drop(delete_documents: false) ⇒ String
Drop the index (delegates to
FT.DROPINDEX). -
#explain(query) ⇒ String
Return the execution plan for a query without running it (delegates to
FT.EXPLAIN). -
#hybrid_search(query:, combine_method: nil, post_processing: nil, params_substitution: nil, timeout: nil, cursor: nil) ⇒ Search::HybridResult
Run a hybrid (lexical + vector) search against this index (delegates to
FT.HYBRID). -
#info ⇒ Hash
Return information and statistics about the index (delegates to
FT.INFO). -
#initialize(redis, name, schema, storage_type, prefix: nil, stopwords: nil) ⇒ Index
constructor
A new instance of Index.
-
#profile(*args) ⇒ Array
Profile the execution of a query (delegates to
FT.PROFILE). -
#search(query = nil, query_params: nil, params: nil, dialect: nil, nocontent: nil, verbatim: nil, no_stopwords: nil, with_scores: nil, with_payloads: nil, slop: nil, in_order: nil, language: nil, return_fields: nil, summarize: nil, highlight: nil, sort_by: nil, asc: nil, scorer: nil, explain_score: nil, &block) ⇒ SearchResult
Search the index.
-
#spellcheck(query, distance: nil, include: nil, exclude: nil) ⇒ Hash{String=>Array<Hash>}
Perform spelling correction over a query against the index (delegates to
FT.SPELLCHECK). -
#syndump ⇒ Hash{String=>Array<String>}
Dump the synonym groups of the index (delegates to
FT.SYNDUMP). -
#synupdate(group_id, *terms, skip_initial_scan: false) ⇒ String
Add or update a synonym group on the index (delegates to
FT.SYNUPDATE). -
#tagvals(field_name) ⇒ Array<String>
List the distinct values of a TAG field (delegates to
FT.TAGVALS).
Constructor Details
#initialize(redis, name, schema, storage_type, prefix: nil, stopwords: nil) ⇒ Index
Returns a new instance of Index.
23 24 25 26 27 28 29 30 |
# File 'lib/redis/commands/modules/search/index.rb', line 23 def initialize(redis, name, schema, storage_type, prefix: nil, stopwords: nil) @redis = redis @name = name @prefix = prefix @schema = schema @storage_type = storage_type @stopwords = stopwords end |
Instance Attribute Details
#name ⇒ String? (readonly)
15 16 17 |
# File 'lib/redis/commands/modules/search/index.rb', line 15 def name @name end |
#prefix ⇒ String? (readonly)
15 16 17 |
# File 'lib/redis/commands/modules/search/index.rb', line 15 def prefix @prefix end |
Class Method Details
.create(redis, name, schema, storage_type, prefix: nil, stopwords: nil, skip_initial_scan: false, **options) ⇒ Index
Create the index on the server (runs FT.CREATE) and return a handle to it.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/redis/commands/modules/search/index.rb', line 46 def self.create( redis, name, schema, storage_type, prefix: nil, stopwords: nil, skip_initial_scan: false, ** ) raise ArgumentError, "Invalid schema" unless schema.is_a?(Schema) redis.ft_create( name, schema, storage_type, prefix: prefix, stopwords: stopwords, skip_initial_scan: skip_initial_scan, ** ) # The Index stores the *literal* key prefix it prepends to (and strips from) document # ids. A definition (which FT.CREATE prefers over the +prefix:+ keyword) carries its # prefixes verbatim, e.g. "bicycle:"; the +prefix:+ keyword form appends the ":". It also # records the resolved storage type (HASH/JSON) so #add writes documents the right way. new( redis, name, schema, resolve_storage_type(storage_type, [:definition]), prefix: key_prefix(prefix, [:definition]), stopwords: stopwords ) end |
.key_prefix(prefix, definition) ⇒ Object
The single literal key prefix the Index should manage, or nil when it can't be determined unambiguously (no prefix, or a definition with several prefixes).
69 70 71 72 73 74 75 |
# File 'lib/redis/commands/modules/search/index.rb', line 69 def self.key_prefix(prefix, definition) if definition.is_a?(IndexDefinition) && definition.prefixes.size == 1 definition.prefixes.first elsif prefix "#{prefix}:" end end |
.resolve_storage_type(storage_type, definition) ⇒ Object
The effective storage type, mirroring FT.CREATE: a definition's own index_type wins
when set, otherwise fall back to the storage_type keyword. This keeps #add's write
path (hset vs json_set) consistent with the ON clause, so a definition without a type
plus storage_type: :json still indexes as JSON.
81 82 83 84 |
# File 'lib/redis/commands/modules/search/index.rb', line 81 def self.resolve_storage_type(storage_type, definition) type = definition&.index_type || storage_type type.to_s.casecmp?(IndexType::JSON) ? IndexType::JSON : IndexType::HASH end |
Instance Method Details
#add(doc_id, **fields) ⇒ Integer, String
Add (or replace) a document under the key "<prefix><doc_id>" (or doc_id when no
prefix is set). The document is written to match the index's storage type: a HASH
(+HSET+) for a HASH index, or a JSON document at the root path (+JSON.SET+) for a JSON
index — so the index actually picks it up either way.
For a JSON index the given fields are written verbatim as the JSON document at the
root path (+$+), so each key lands at a top-level attribute. This works when the schema
indexes root-level JSONPaths (+$.brand AS brand+, so add("1", brand: "x") stores
{"brand":"x"} which $.brand matches). It does not build nested structure: a field
over a nested path (+$.user.name AS name+) is not satisfied by a flat name: "..."
write — pass the full document shape instead (+add("1", user: { name: "Ann" })+).
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/redis/commands/modules/search/index.rb', line 105 def add(doc_id, **fields) key = @prefix ? "#{@prefix}#{doc_id}" : doc_id # Validate fields fields.each do |field_name, value| field = @schema.field(field_name) if field.is_a?(NumericField) && !value.is_a?(Numeric) raise Redis::CommandError, "Invalid value for numeric field '#{field_name}': #{value}" end end begin json? ? @redis.json_set(key, "$", fields) : @redis.hset(key, fields) rescue Redis::CommandError => error raise Redis::CommandError, "Error adding document: #{error.}" end end |
#aggregate(query, *args) ⇒ AggregateResult
Run an aggregation pipeline against the index (delegates to FT.AGGREGATE).
259 260 261 |
# File 'lib/redis/commands/modules/search/index.rb', line 259 def aggregate(query, *args) @redis.ft_aggregate(@name, query, *args) end |
#alter(field_or_args) ⇒ String
Add a field to the index schema (delegates to FT.ALTER).
294 295 296 |
# File 'lib/redis/commands/modules/search/index.rb', line 294 def alter(field_or_args) @redis.ft_alter(@name, field_or_args) end |
#drop(delete_documents: false) ⇒ String
Drop the index (delegates to FT.DROPINDEX).
249 250 251 |
# File 'lib/redis/commands/modules/search/index.rb', line 249 def drop(delete_documents: false) @redis.ft_dropindex(@name, delete_documents: delete_documents) end |
#explain(query) ⇒ String
Return the execution plan for a query without running it (delegates to FT.EXPLAIN).
285 286 287 |
# File 'lib/redis/commands/modules/search/index.rb', line 285 def explain(query) @redis.ft_explain(@name, query) end |
#hybrid_search(query:, combine_method: nil, post_processing: nil, params_substitution: nil, timeout: nil, cursor: nil) ⇒ Search::HybridResult
Run a hybrid (lexical + vector) search against this index (delegates to FT.HYBRID).
272 273 274 275 276 277 278 279 |
# File 'lib/redis/commands/modules/search/index.rb', line 272 def hybrid_search(query:, combine_method: nil, post_processing: nil, params_substitution: nil, timeout: nil, cursor: nil) @redis.ft_hybrid_search( @name, query: query, combine_method: combine_method, post_processing: post_processing, params_substitution: params_substitution, timeout: timeout, cursor: cursor ) end |
#info ⇒ Hash
Return information and statistics about the index (delegates to FT.INFO).
241 242 243 |
# File 'lib/redis/commands/modules/search/index.rb', line 241 def info @redis.ft_info(@name) end |
#profile(*args) ⇒ Array
Profile the execution of a query (delegates to FT.PROFILE).
338 339 340 |
# File 'lib/redis/commands/modules/search/index.rb', line 338 def profile(*args) @redis.ft_profile(@name, *args) end |
#search(query = nil, query_params: nil, params: nil, dialect: nil, nocontent: nil, verbatim: nil, no_stopwords: nil, with_scores: nil, with_payloads: nil, slop: nil, in_order: nil, language: nil, return_fields: nil, summarize: nil, highlight: nil, sort_by: nil, asc: nil, scorer: nil, explain_score: nil, &block) ⇒ SearchResult
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 233 234 235 236 |
# File 'lib/redis/commands/modules/search/index.rb', line 157 def search(query = nil, query_params: nil, params: nil, dialect: nil, nocontent: nil, verbatim: nil, no_stopwords: nil, with_scores: nil, with_payloads: nil, slop: nil, in_order: nil, language: nil, return_fields: nil, summarize: nil, highlight: nil, sort_by: nil, asc: nil, scorer: nil, explain_score: nil, &block) if block_given? query = Query.build(&block) elsif query.is_a?(String) query = Query.new(query) end raise ArgumentError, "Invalid query" unless query.is_a?(Query) query_string = query.to_redis_args.first sort_order = asc == false ? "DESC" : "ASC" sortby = sort_by ? [sort_by, sort_order] : query.[:sortby] limit_ids = query.limit_ids_value # INKEYS needs full Redis keys. Prepend the index prefix to logical ids (mirroring # #add), but leave ids that already carry it alone so callers passing full keys don't # get a doubled prefix that matches nothing. if limit_ids && @prefix limit_ids = limit_ids.map { |id| id.to_s.start_with?(@prefix) ? id : "#{@prefix}#{id}" } end # An empty RETURN list is not a meaningful per-call value (it would just omit RETURN and # return all fields), so treat it as "unset" and fall back to the Query's RETURN list. return_fields = nil if return_fields.is_a?(Array) && return_fields.empty? # Build a fresh options hash per call. A per-call keyword argument wins when explicitly # given (including +false+, to turn a flag off); +nil+ falls back to whatever the Query # was built with. The Query is never mutated, so reusing one Query across searches never # leaks options between calls and per-call flags can both enable and disable. pick = ->(call_value, query_value) { call_value.nil? ? query_value : call_value } = { filter: query.filters, geo_filter: query.geo_filters, limit_ids: limit_ids, limit: query.[:limit], sortby: sortby, dialect: pick.call(dialect, query.[:dialect]), return: pick.call(return_fields, query.return_fields), decode_fields: query.return_fields_decode, highlight: pick.call(highlight, query.), summarize: pick.call(summarize, query.), verbatim: pick.call(verbatim, query.verbatim_value), no_stopwords: pick.call(no_stopwords, query.no_stopwords_value), no_content: pick.call(nocontent, query.no_content_value), with_scores: pick.call(with_scores, query.[:withscores]), scorer: pick.call(scorer, query.[:scorer]), explain_score: pick.call(explain_score, query.[:explainscore]), language: pick.call(language, query.language_value), with_payloads: pick.call(with_payloads, query.with_payloads_value), slop: pick.call(slop, query.slop_value), in_order: pick.call(in_order, query.in_order_value), timeout: query.timeout_value, limit_fields: query.limit_fields_value, expander: query. } substitutions = query_params || params [:params] = substitutions if substitutions result = @redis.ft_search(@name, query_string, **) # Strip the index prefix from document IDs if one is set, so callers see the # logical doc id they passed to #add rather than the underlying Redis key. if @prefix && result.is_a?(SearchResult) result.documents.map! do |doc| next doc unless doc.id.is_a?(String) && doc.id.start_with?(@prefix) Document.new( doc.id.delete_prefix(@prefix), attributes: doc.attributes, score: doc.score, payload: doc.payload ) end end result end |
#spellcheck(query, distance: nil, include: nil, exclude: nil) ⇒ Hash{String=>Array<Hash>}
Perform spelling correction over a query against the index (delegates to FT.SPELLCHECK).
305 306 307 |
# File 'lib/redis/commands/modules/search/index.rb', line 305 def spellcheck(query, distance: nil, include: nil, exclude: nil) @redis.ft_spellcheck(@name, query, distance: distance, include: include, exclude: exclude) end |
#syndump ⇒ Hash{String=>Array<String>}
Dump the synonym groups of the index (delegates to FT.SYNDUMP).
322 323 324 |
# File 'lib/redis/commands/modules/search/index.rb', line 322 def syndump @redis.ft_syndump(@name) end |
#synupdate(group_id, *terms, skip_initial_scan: false) ⇒ String
Add or update a synonym group on the index (delegates to FT.SYNUPDATE).
315 316 317 |
# File 'lib/redis/commands/modules/search/index.rb', line 315 def synupdate(group_id, *terms, skip_initial_scan: false) @redis.ft_synupdate(@name, group_id, *terms, skip_initial_scan: skip_initial_scan) end |
#tagvals(field_name) ⇒ Array<String>
List the distinct values of a TAG field (delegates to FT.TAGVALS).
330 331 332 |
# File 'lib/redis/commands/modules/search/index.rb', line 330 def tagvals(field_name) @redis.ft_tagvals(@name, field_name) end |