Search code examples
phpredisredisearch

RediSearch PHP expects me to define all the fields on every use


Creating a new Index and adding data in same step is working as seen in the docs.

But when i try to add data to an existing index I'm getting the following exception.

Ehann\RediSearch\Exception\FieldNotInSchemaException

The field is not a property in the index

Here is my code:

$adapter = new PredisAdapter();

$host = config('database.redis.default.host');
$port = config('database.redis.default.port');
$database = config('database.redis.default.database');
$password = config('database.redis.default.password');
$redis = $adapter->connect($host, $port, $database, $password);

$bookIndex = new Index($redis, 'books');

$bookIndex->add([
    new TextField('title', 'Tale of Two Cities'),
    new TextField('author', 'Charles Dickens'),
    new NumericField('price', 9.99),
    new NumericField('stock', 231),
]);

I've looked into the source code of redisearch-php and it seems that there is no way this could work without always adding the fields with addTextField or others. But this seem not very practical to me. I thought I had to add the fields once on index creation. Am I wrong?

The getFields method (used in some methods like makeDocument) gets the object vars from Index but the field related object vars aren't in the instance if not added manually beforehand.

vendor/ethanhann/redisearch-php/src/Index.php

/**
 * @return array
 */
protected function getFields(): array
{
    $fields = [];
    foreach (get_object_vars($this) as $field) {
        if ($field instanceof FieldInterface) {
            $fields[$field->getName()] = clone $field;
        }
    }
    return $fields;
}

/**
 * @param string $name
 * @param float $weight
 * @param bool $sortable
 * @param bool $noindex
 * @return IndexInterface
 */
public function addTextField(string $name, float $weight = 1.0, bool $sortable = false, bool $noindex = false): IndexInterface
{
    $this->$name = (new TextField($name))->setSortable($sortable)->setNoindex($noindex)->setWeight($weight);
    return $this;
}

/**
 * @param string $name
 * @param bool $sortable
 * @param bool $noindex
 * @return IndexInterface
 */
public function addNumericField(string $name, bool $sortable = false, bool $noindex = false): IndexInterface
{
    $this->$name = (new NumericField($name))->setSortable($sortable)->setNoindex($noindex);
    return $this;
}

/**
 * @param string $name
 * @return IndexInterface
 */
public function addGeoField(string $name, bool $noindex = false): IndexInterface
{
    $this->$name = (new GeoField($name))->setNoindex($noindex);
    return $this;
}

Solution

  • So, I played around with this today. I think you are correct. You have to redeclare the schema each time even if it already exists on redis. I can't see any functions that are used to pull the schema from redis which is kind of annyoying.

    You can see the fields in the output from the $bookIndex->info() function but it doesn't set up the schema for you.

    My solution to this problem would be to create a class that deals with setting up the index object so you don't have to keep doing it all the time. Something like this:

    class book_index
    {
        private $index;
    
        public function __construct($redis)
        {
            $this->index = new Index($redis);
            $this->index->setIndexName("book");
            $this->set_schema();
        }
    
        private function set_schema(){
            $this->index->addTextField('title')
                ->addTextField('author')
                ->addNumericField('price')
                ->addNumericField('stock');
    
            // attempt to create the index, I couldn't find any function to check if it already exists
            // so catching the failure or using info() and catching the exception on that was the only way
            try {
                $this->index->create();
            }catch (Ehann\RediSearch\Exceptions\RediSearchException $exception){
                error_log("book index already exists.");
                error_log( $exception->getMessage());
            }
        }
    
        public function get_index(){
            return $this->index;
        }
    
        public static function get($redis){
            $obj = new self($redis);
            return  $obj->get_index();
        }
    }
    

    Then your code would look like this:

    include_once ("book_index.php"); // include the class file 
    $adapter = new PredisAdapter();
    
    $host = config('database.redis.default.host');
    $port = config('database.redis.default.port');
    $database = config('database.redis.default.database');
    $password = config('database.redis.default.password');
    $redis = $adapter->connect($host, $port, $database, $password);
    
    $bookIndex = book_index::get($redis);
    
    $bookIndex->add([
        new TextField('title', 'Tale of Two Cities'),
        new TextField('author', 'Charles Dickens'),
        new NumericField('price', 9.99),
        new NumericField('stock', 231),
    ]);
    

    You could move more functionality to the class but I just tried to keep it simple.