Class: Redis::Commands::Search::Index
| Relationships & Source Files | |
| Inherits: | Object |
| 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 .create.
Class Method Summary
-
.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)
The single literal key prefix the
Indexshould manage, or nil when it can't be determined unambiguously (no prefix, or a definition with several prefixes). - .new(redis, name, schema, storage_type, prefix: nil, stopwords: nil) ⇒ Index constructor
-
.resolve_storage_type(storage_type, definition)
The effective storage type, mirroring FT.CREATE: a definition's own
index_typewins when set, otherwise fall back to thestorage_typekeyword.
Instance Attribute Summary
- #name ⇒ String? readonly
- #prefix ⇒ String? readonly
-
#json? ⇒ Boolean
readonly
private
Whether this index is built over JSON documents (vs.
Instance Method Summary
-
#add(doc_id, **fields) ⇒ Integer, String
Add (or replace) a document under the key
"(or" doc_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). -
#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
::Redis::Commands::Searchthe 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
.new(redis, name, schema, storage_type, prefix: nil, stopwords: nil) ⇒ Index
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.
# 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)
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).
# 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)
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.
Instance Attribute Details
#json? ⇒ Boolean (readonly, private)
Whether this index is built over JSON documents (vs. HASH keys).
#name ⇒ String? (readonly)
# File 'lib/redis/commands/modules/search/index.rb', line 15
attr_reader :name, :prefix
#prefix ⇒ String? (readonly)
# File 'lib/redis/commands/modules/search/index.rb', line 15
attr_reader :name, :prefix
Instance Method Details
#add(doc_id, **fields) ⇒ Integer, String
Add (or replace) a document under the key " (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" })+).
# 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).
# 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).
# 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).
# 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).
# 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).
# 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).
# 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).
# 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
::Redis::Commands::Search the index.
Accepts a Query object, a raw query string, or a block evaluated as a Query. Keyword
arguments are applied on top of the query before it is sent. Document ids in the result
have the index prefix stripped, so they match the ids passed to #add.
# 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).
# 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).
# 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).
# 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).
# File 'lib/redis/commands/modules/search/index.rb', line 330
def tagvals(field_name) @redis.ft_tagvals(@name, field_name) end