redis/redis-rb · error · ArgumentError

Invalid schema

Error message

Invalid schema

What it means

Index.create (and Redis#create_index) require the third argument to be a Redis::Commands::Search::Schema instance: the object that renders the SCHEMA clause tokens for FT.CREATE. A bare array of field objects, a hash, or a single field raises ArgumentError before anything is sent (lib/redis/commands/modules/search/index.rb:50). Build one with Schema.new(fields) or the Schema.build block DSL.

Source

Thrown at lib/redis/commands/modules/search/index.rb:50

        # Create the index on the server (runs +FT.CREATE+) and return a handle to it.
        #
        # @example
        #   Redis::Commands::Search::Index.create(redis, "idx", schema, "hash", prefix: "doc")
        #
        # @param redis [Redis] the client to use
        # @param name [String] the index name
        # @param schema [Schema] the field schema
        # @param storage_type [String] the indexed data type (+"hash"+ or +"json"+)
        # @param prefix [String, nil] key prefix for documents
        # @param stopwords [Array<String>, nil] custom stopword list
        # @param skip_initial_scan [Boolean] do not backfill existing keys (+SKIPINITIALSCAN+)
        # @return [Index] the created index
        # @raise [ArgumentError] if +schema+ is not a {Schema}
        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

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

View on GitHub (pinned to 2ba9010b91)

Solutions

  1. Wrap fields: Schema.new([TextField.new("title"), NumericField.new("price")])
  2. Or use the block DSL: Schema.build { text_field "title"; numeric_field "price" }
  3. Type-check deserialized schema config before calling Index.create

Example fix

// before
Index.create(redis, "idx", [TextField.new("title")], "hash")
// after
Index.create(redis, "idx", Schema.new([TextField.new("title")]), "hash")
Defensive patterns

Strategy: type-guard

Validate before calling

schema = Schema.new(fields) unless schema.is_a?(Redis::Commands::Search::Schema)
Index.create(redis, name, schema, storage)

Type guard

schema.is_a?(Redis::Commands::Search::Schema)

Try / catch

begin
  Index.create(redis, name, schema, storage)
rescue ArgumentError => e
  raise ConfigError, "schema must be built with Schema.new or Schema.build"
end

Prevention

When it happens

Trigger: Index.create(redis, "idx", [TextField.new("title")], "hash"); passing the SchemaDefinition DSL object instead of the Schema that Schema.build returns; passing a Hash of name => field pairs from deserialized config.

Common situations: Assuming the API accepts a plain field list because raw FT.CREATE does; refactoring from legacy ft_create(name, *fields) call sites; schema config stored as JSON and reconstructed as the wrong type.

Related errors


AI-assisted analysis of redis/redis-rb@2ba9010b91 (2026-08-23). Data as JSON: /api/errors/52f73de99c08a9bd. Report an issue: GitHub.