123456789_123456789_123456789_123456789_123456789_

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

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(redis, name, schema, storage_type, prefix: nil, stopwords: nil) ⇒ Index

Parameters:

  • redis (Redis)

    the client used for all index operations

  • name (String)

    the index name

  • schema (Schema)

    the field schema

  • storage_type (String)

    the indexed data type (+"hash"+ or "json")

  • prefix (String, nil)

    key prefix for documents

  • stopwords (Array<String>, nil)

    custom stopword list

[ GitHub ]

  
# 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

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.

Examples:

Redis::Commands::Search::Index.create(redis, "idx", schema, "hash", prefix: "doc")

Parameters:

  • redis (Redis)

    the client to use

  • name (String)

    the index name

  • schema (Schema)

    the field schema

  • storage_type (String)

    the indexed data type (+"hash"+ or "json")

  • prefix (String, nil)

    key prefix for documents

  • stopwords (Array<String>, nil)

    custom stopword list

  • skip_initial_scan (Boolean)

    do not backfill existing keys (+SKIPINITIALSCAN+)

Returns:

  • (Index)

    the created index

Raises:

  • (ArgumentError)

    if schema is not a Schema

[ GitHub ]

  
# 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, **options
)
  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, **options
  )
  # 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, options[:definition]),
    prefix: key_prefix(prefix, options[: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).

[ GitHub ]

  
# 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.

[ GitHub ]

  
# 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 Attribute Details

#json?Boolean (readonly, private)

Whether this index is built over JSON documents (vs. HASH keys).

[ GitHub ]

  
# File 'lib/redis/commands/modules/search/index.rb', line 345

def json?
  @storage_type.to_s.casecmp?(IndexType::JSON)
end

#nameString? (readonly)

Returns:

  • (String)

    the index name

  • (String, nil)

    the literal key prefix prepended to (and stripped from) document ids, e.g. "doc:"; nil when the index manages no single prefix

[ GitHub ]

  
# File 'lib/redis/commands/modules/search/index.rb', line 15

attr_reader :name, :prefix

#prefixString? (readonly)

Returns:

  • (String)

    the index name

  • (String, nil)

    the literal key prefix prepended to (and stripped from) document ids, e.g. "doc:"; nil when the index manages no single prefix

[ GitHub ]

  
# 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" })+).

Examples:

index.add("1", title: "hello", price: 10)

Parameters:

  • doc_id (String)

    the logical document id

  • fields (Hash{Symbol,String => Object})

    the field/value pairs to store

Returns:

  • (Integer, String)

    the HSET reply (HASH index) or the JSON.SET reply (JSON index)

Raises:

[ GitHub ]

  
# 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.message}"
  end
end

#aggregate(query, *args) ⇒ AggregateResult

Run an aggregation pipeline against the index (delegates to FT.AGGREGATE).

Parameters:

  • query (AggregateRequest, Cursor, String)

    the aggregate request, a cursor, or a raw query string followed by raw pipeline args

  • args (Array)

    raw pipeline tokens when query is a String

Returns:

[ GitHub ]

  
# 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).

Parameters:

  • field_or_args (Search::Field, Array)

    a Field (rendered via #to_args) or a raw token array describing the field to add

Returns:

  • (String)

    "OK"

[ GitHub ]

  
# 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).

Parameters:

  • delete_documents (Boolean)

    also delete the indexed documents (+DD+)

Returns:

  • (String)

    "OK"

[ GitHub ]

  
# 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).

Parameters:

  • query (String)

    the query string

Returns:

  • (String)

    the query plan

[ GitHub ]

  
# 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).

Parameters:

Returns:

[ GitHub ]

  
# 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

#infoHash

Return information and statistics about the index (delegates to FT.INFO).

Returns:

  • (Hash)

    index metadata

[ GitHub ]

  
# 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).

Parameters:

  • args (Array)

    the profile arguments

Returns:

  • (Array)

    the raw profile reply

[ GitHub ]

  
# 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.

Examples:

with a Query object

index.search(Redis::Commands::Search::Query.new("@title:hello").paging(0, 10))

with a block

index.search { text(:title).match("hello*") }

Parameters:

  • query (Query, String, nil) (defaults to: nil)

    the query (a Query, a query string, or nil when a block is given)

  • query_params (Hash, Array, nil)

    query parameter substitutions (overrides params)

  • params (Hash, Array, nil)

    query parameter substitutions (+PARAMS+)

  • dialect (Integer, nil)

    query dialect version (+DIALECT+)

  • nocontent (Boolean, nil)

    return ids only (+NOCONTENT+)

  • verbatim (Boolean, nil)

    disable stemming (+VERBATIM+)

  • no_stopwords (Boolean, nil)

    do not remove stopwords (+NOSTOPWORDS+)

  • with_scores (Boolean, nil)

    include relevance scores (+WITHSCORES+)

  • with_payloads (Boolean, nil)

    include payloads (+WITHPAYLOADS+)

  • slop (Integer, nil)

    allowed term reordering distance (+SLOP+)

  • in_order (Boolean, nil)

    require terms in query order (+INORDER+)

  • language (String, nil)

    stemming language (+LANGUAGE+)

  • return_fields (Array<String>, nil)

    only return these fields (+RETURN+)

  • summarize (Hash, nil)

    SUMMARIZE options

  • highlight (Hash, nil)

    HIGHLIGHT options

  • sort_by (String, nil)

    sort field (+SORTBY+)

  • asc (Boolean, nil)

    sort ascending (default) when sort_by is given; false for descending

  • scorer (String, nil)

    scoring function name (+SCORER+)

  • explain_score (Boolean, nil)

    include a score explanation (+EXPLAINSCORE+)

Returns:

Raises:

  • (ArgumentError)

    if no usable query is provided

[ GitHub ]

  
# 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.options[: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 }

  options = {
    filter: query.filters,
    geo_filter: query.geo_filters,
    limit_ids: limit_ids,
    limit: query.options[:limit],
    sortby: sortby,
    dialect: pick.call(dialect, query.options[:dialect]),
    return: pick.call(return_fields, query.return_fields),
    decode_fields: query.return_fields_decode,
    highlight: pick.call(highlight, query.highlight_options),
    summarize: pick.call(summarize, query.summarize_options),
    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.options[:withscores]),
    scorer: pick.call(scorer, query.options[:scorer]),
    explain_score: pick.call(explain_score, query.options[: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.expander_value
  }
  substitutions = query_params || params
  options[:params] = substitutions if substitutions

  result = @redis.ft_search(@name, query_string, **options)

  # 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).

Parameters:

  • query (String)

    the query whose terms are checked

  • distance (Integer, nil)

    maximum Levenshtein distance for suggestions (+DISTANCE+)

  • include (String, nil)

    a custom dictionary to include terms from (+TERMS INCLUDE+)

  • exclude (String, nil)

    a custom dictionary to exclude terms from (+TERMS EXCLUDE+)

Returns:

  • (Hash{String=>Array<Hash>})

    misspelled terms mapped to suggestions

[ GitHub ]

  
# 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

#syndumpHash{String=>Array<String>}

Dump the synonym groups of the index (delegates to FT.SYNDUMP).

Returns:

  • (Hash{String=>Array<String>})

    each term mapped to its synonym group ids

[ GitHub ]

  
# 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).

Parameters:

  • group_id (String)

    the synonym group id

  • terms (Array<String>)

    the terms to add to the group

  • skip_initial_scan (Boolean)

    do not re-scan existing documents (+SKIPINITIALSCAN+)

Returns:

  • (String)

    "OK"

[ GitHub ]

  
# 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).

Parameters:

  • field_name (String)

    the TAG field name

Returns:

  • (Array<String>)

    the distinct tag values

[ GitHub ]

  
# File 'lib/redis/commands/modules/search/index.rb', line 330

def tagvals(field_name)
  @redis.ft_tagvals(@name, field_name)
end