Full Code of ethanhann/redisearch-php for AI

main 4533569846d5 cached
126 files
278.1 KB
73.1k tokens
685 symbols
1 requests
Download .txt
Showing preview only (309K chars total). Download the full file or copy to clipboard to get everything.
Repository: ethanhann/redisearch-php
Branch: main
Commit: 4533569846d5
Files: 126
Total size: 278.1 KB

Directory structure:
gitextract__zin6sq1/

├── .claude/
│   └── skills/
│       ├── contribute/
│       │   └── SKILL.md
│       └── unit-test/
│           └── SKILL.md
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci.yml
│       └── docs.yml
├── .gitignore
├── .php-cs-fixer.php
├── CLAUDE.md
├── LICENSE
├── README.md
├── bin/
│   └── redisearch
├── composer.json
├── docker-compose.yml
├── docs-site/
│   ├── .gitignore
│   ├── astro.config.mjs
│   ├── package.json
│   ├── src/
│   │   ├── content/
│   │   │   └── docs/
│   │   │       ├── aggregating.mdx
│   │   │       ├── changelog.mdx
│   │   │       ├── cli.mdx
│   │   │       ├── index.mdx
│   │   │       ├── indexing.mdx
│   │   │       ├── laravel-support.mdx
│   │   │       ├── searching.mdx
│   │   │       └── suggesting.mdx
│   │   ├── content.config.ts
│   │   └── styles/
│   │       └── custom.css
│   └── tsconfig.json
├── justfile
├── phpunit.xml
├── src/
│   ├── AbstractIndex.php
│   ├── AbstractRediSearchClientAdapter.php
│   ├── Aggregate/
│   │   ├── AggregationResult.php
│   │   ├── Builder.php
│   │   ├── BuilderInterface.php
│   │   ├── Operations/
│   │   │   ├── AbstractFieldNameOperation.php
│   │   │   ├── Apply.php
│   │   │   ├── Filter.php
│   │   │   ├── GroupBy.php
│   │   │   ├── Limit.php
│   │   │   ├── Load.php
│   │   │   └── SortBy.php
│   │   └── Reducers/
│   │       ├── AbstractFieldNameReducer.php
│   │       ├── Aliasable.php
│   │       ├── Avg.php
│   │       ├── Count.php
│   │       ├── CountDistinct.php
│   │       ├── CountDistinctApproximate.php
│   │       ├── FirstValue.php
│   │       ├── Max.php
│   │       ├── Min.php
│   │       ├── Quantile.php
│   │       ├── StandardDeviation.php
│   │       ├── Sum.php
│   │       └── ToList.php
│   ├── CanBecomeArrayInterface.php
│   ├── Console/
│   │   ├── AbstractRedisCommand.php
│   │   ├── Application.php
│   │   ├── Command/
│   │   │   ├── AggregateCommand.php
│   │   │   ├── DocumentAddCommand.php
│   │   │   ├── DocumentDeleteCommand.php
│   │   │   ├── DocumentGetCommand.php
│   │   │   ├── ExplainCommand.php
│   │   │   ├── IndexCreateCommand.php
│   │   │   ├── IndexDropCommand.php
│   │   │   ├── IndexInfoCommand.php
│   │   │   ├── IndexListCommand.php
│   │   │   ├── ProfileCommand.php
│   │   │   ├── SearchCommand.php
│   │   │   └── ShellCommand.php
│   │   └── SchemaParser.php
│   ├── Document/
│   │   ├── AbstractDocumentFactory.php
│   │   ├── Document.php
│   │   └── DocumentInterface.php
│   ├── Exceptions/
│   │   ├── AliasDoesNotExistException.php
│   │   ├── DocumentAlreadyInIndexException.php
│   │   ├── FieldNotInSchemaException.php
│   │   ├── NoFieldsInIndexException.php
│   │   ├── OutOfRangeDocumentScoreException.php
│   │   ├── RediSearchException.php
│   │   ├── UnknownIndexNameException.php
│   │   ├── UnknownIndexNameOrNameIsAnAliasItselfException.php
│   │   ├── UnknownRediSearchCommandException.php
│   │   └── UnsupportedRediSearchLanguageException.php
│   ├── Fields/
│   │   ├── AbstractField.php
│   │   ├── FieldFactory.php
│   │   ├── FieldInterface.php
│   │   ├── GeoField.php
│   │   ├── GeoLocation.php
│   │   ├── Noindex.php
│   │   ├── NumericField.php
│   │   ├── Sortable.php
│   │   ├── Tag.php
│   │   ├── TagField.php
│   │   ├── TextField.php
│   │   └── VectorField.php
│   ├── Index.php
│   ├── IndexInterface.php
│   ├── Language.php
│   ├── Query/
│   │   ├── Builder.php
│   │   ├── BuilderInterface.php
│   │   └── SearchResult.php
│   ├── RediSearchRedisClient.php
│   ├── RuntimeConfiguration.php
│   └── Suggestion.php
└── tests/
    ├── RediSearch/
    │   ├── Aggregate/
    │   │   ├── AggregationResultTest.php
    │   │   └── BuilderTest.php
    │   ├── Document/
    │   │   └── DocumentTest.php
    │   ├── Exceptions/
    │   │   ├── FieldNotInSchemaExceptionTest.php
    │   │   ├── NoFieldsInIndexExceptionTest.php
    │   │   ├── RedisRawCommandExceptionTest.php
    │   │   └── UnknownIndexNameExceptionTest.php
    │   ├── Fields/
    │   │   ├── FieldFactoryTest.php
    │   │   ├── GeoFieldTest.php
    │   │   ├── GeoLocationTest.php
    │   │   ├── NumericFieldTest.php
    │   │   └── TextFieldTest.php
    │   ├── IndexTest.php
    │   ├── Query/
    │   │   └── BuilderTest.php
    │   ├── Redis/
    │   │   └── RedisClientTest.php
    │   ├── RuntimeConfigurationTest.php
    │   └── SuggestionTest.php
    ├── RediSearchTestCase.php
    ├── Stubs/
    │   ├── IndexWithoutFields.php
    │   ├── TestDocument.php
    │   └── TestIndex.php
    └── bootstrap.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .claude/skills/contribute/SKILL.md
================================================
# Contribute Skill

Guide for implementing a new feature or bug fix in redisearch-php.

## Workflow

Make a todo list for all the tasks below and work through them in order.

### 1. Understand the Change

Read the relevant existing code before touching anything:

- For new field types: look at an existing field in `src/Fields/`
- For new query features: look at `src/Query/`
- For new aggregation reducers/operations: look at `src/Aggregate/Reducers/` or `src/Aggregate/Operations/`
- For index-level changes: look at `src/Index.php` and `src/AbstractIndex.php`

Identify the interface(s) the new code must implement or extend.

### 2. Implement the Change

**Source code conventions:**

- Namespace: `Ehann\RediSearch\<Subdirectory>`
- Use native PHP 8.2+ type hints on all parameters and return types
- Interfaces end in `Interface`; abstract classes start with `Abstract`
- Follow the fluent builder pattern used throughout (methods return `$this` or a new builder)
- Keep RediSearch command names as close to the official docs as possible

**Code style** is enforced by php-cs-fixer. After writing code, fix style:

```bash
vendor/bin/robo task:fix-code-style
```

### 3. Write Tests

Every change needs a corresponding test. See the `/unit-test` skill for details.

Quick checklist:

- Add a test file at `tests/RediSearch/<matching path>/<ClassName>Test.php`
- Extend `Ehann\Tests\RediSearchTestCase`
- Cover the happy path and any notable edge cases

### 4. Verify Everything Passes

```bash
# Start Redis if not running
docker compose up -d

# Run the full test suite
vendor/bin/robo test

# Check code style
vendor/bin/php-cs-fixer fix src --dry-run --diff

# Lint all files
find src tests -name "*.php" -print0 | xargs -0 php -l
```

All three must pass cleanly before the change is ready.

### 5. Optional: Test Against All Clients

If the change touches the Redis command layer, verify it works with every supported client:

```bash
vendor/bin/robo test:all
```

### 6. Commit

Write a clear commit message describing *what* changed and *why*. Reference any related issue numbers.

```bash
git add <files>
git commit -m "Add <feature>: <one-line description>"
```

## Common Patterns

### Adding a new Field type

```php
namespace Ehann\RediSearch\Fields;

class MyField extends AbstractField implements FieldInterface
{
    public function getTypeString(): string
    {
        return 'MYTYPE';
    }
}
```

### Adding a new Reducer

```php
namespace Ehann\RediSearch\Aggregate\Reducers;

class MyReducer extends AbstractReducer
{
    public function __construct(string $property)
    {
        parent::__construct('MY_REDUCER', $property);
    }
}
```

### Adding a new Query Operation

Follow the pattern in `src/Aggregate/Operations/` — implement `OperationInterface` and emit the correct RediSearch
command fragment from `__toString()`.


================================================
FILE: .claude/skills/unit-test/SKILL.md
================================================
# Unit Test Skill

Guide for writing, running, and debugging unit tests in redisearch-php.

## Test Infrastructure

- **Framework**: PHPUnit 11 (`vendor/bin/phpunit`)
- **Config**: `phpunit.xml` (sets Redis connection, default client library, coverage source)
- **Base class**: `Ehann\Tests\RediSearchTestCase` in `tests/RediSearchTestCase.php`
- **Redis**: must be running on `localhost:6381` (start with `just up`)

## Writing a Test

### File location and naming

Mirror the source path under `tests/RediSearch/`:

| Source file                      | Test file                                         |
|----------------------------------|---------------------------------------------------|
| `src/Fields/NumericField.php`    | `tests/RediSearch/Fields/NumericFieldTest.php`    |
| `src/Aggregate/Reducers/Avg.php` | `tests/RediSearch/Aggregate/Reducers/AvgTest.php` |

### AAA structure

Every test method must follow Arrange-Act-Assert with explicit section comments:

```php
<?php

namespace Ehann\Tests\RediSearch\Fields;

use Ehann\Tests\RediSearchTestCase;
use Ehann\RediSearch\Fields\NumericField;

class NumericFieldTest extends RediSearchTestCase
{
    private NumericField $field;

    protected function setUp(): void
    {
        parent::setUp();
        $this->field = new NumericField('price');
    }

    public function testGetName(): void
    {
        // Arrange — see setUp()

        // Act
        $name = $this->field->getName();

        // Assert
        $this->assertSame('price', $name);
    }

    public function testSetSortable(): void
    {
        // Arrange
        $expected = true;

        // Act
        $result = $this->field->setSortable($expected)->isSortable();

        // Assert
        $this->assertSame($expected, $result);
    }
}
```

**AAA rules:**

- Always include all three section comments, even when one section is trivial.
- If the full arrange is in `setUp()`, use `// Arrange — see setUp()` as a one-liner with no body.
- For exception tests, place `expectException()` in the Assert section (before the act), because PHPUnit registers the
  expectation before execution:

```php
public function testThrowsWhenIndexHasNoFields(): void
{
    // Arrange
    $index = new IndexWithoutFields($this->redisClient, $this->indexName);

    // Assert
    $this->expectException(NoFieldsInIndexException::class);

    // Act
    $index->create();
}
```

### Quality conventions

- Use `assertSame` instead of `assertEquals` when type identity matters (e.g., comparing ints, floats, or booleans).
- One logical assertion per test where practical; multiple assertions are acceptable when they together verify a single
  behaviour.
- Mark all test methods `void`: `public function testFoo(): void`.
- Use `@group <name>` PHPDoc to tag logical groups (e.g., `@group aggregate`, `@group query`).
- Do not commit permanently-skipped tests — fix the underlying issue or remove the test.

### Using the index in tests

`RediSearchTestCase` provides `$this->redisClient` and `$this->indexName`. Use the stubs for a pre-configured index:

```php
use Ehann\Tests\Stubs\TestIndex;

protected function setUp(): void
{
    parent::setUp();
    $index = new TestIndex($this->redisClient, $this->indexName);
    $index->create();
    // add documents, run queries…
}

protected function tearDown(): void
{
    // RediSearchTestCase::tearDown() calls flushAll() automatically
    parent::tearDown();
}
```

### Testing against a specific Redis client

The `REDIS_LIBRARY` env var selects the client. Skip a test when a client is not in use:

```php
public function testSomethingPhpRedisOnly(): void
{
    // Arrange
    if (!$this->isUsingPhpRedis()) {
        $this->markTestSkipped('PhpRedis only');
    }

    // Act
    // …

    // Assert
    // …
}
```

## Running Tests

```bash
# Start Redis
just up

# All tests (Predis, the default)
vendor/bin/phpunit
# or
just test

# Specific client
just test-predis
just test-php-redis
just test-redis-client

# All clients sequentially
just test-all

# Single test file
vendor/bin/phpunit tests/RediSearch/Fields/NumericFieldTest.php

# Single test method
vendor/bin/phpunit --filter testGetName tests/RediSearch/Fields/NumericFieldTest.php

# Group
vendor/bin/phpunit --group aggregate

# With coverage report (requires Xdebug or PCOV driver)
vendor/bin/phpunit --coverage-text
```

## Code Coverage

PHPUnit 11 generates coverage from the `<coverage>` block in `phpunit.xml`. To produce a report:

```bash
# Terminal summary
vendor/bin/phpunit --coverage-text

# HTML report
vendor/bin/phpunit --coverage-html coverage/
```

Coverage requires a driver: install **Xdebug** (`php -m | grep xdebug`) or **PCOV** (`php -m | grep pcov`). If neither
is present, PHPUnit will warn and skip coverage collection.

## Debugging Failures

1. **Connection refused**: Redis isn't running — `just up`
2. **Command not found (FT.*)**: Redis Stack module not loaded — ensure you're using the `redis/redis-stack` or
   `redis/redis-stack-server` image, not plain Redis
3. **Index already exists**: a previous test run didn't clean up — `tearDown` calls `flushAll`; if interrupted, run
   `redis-cli -p 6381 flushall` manually
4. **Style errors in test files**: run `vendor/bin/php-cs-fixer fix tests --dry-run --diff` to see what needs fixing (
   php-cs-fixer only auto-fixes `src/` by default, but the check applies to `tests/` too)

## Test Conventions

- Method names: `test{Feature}` (e.g., `testSortByDescending`, `testGetAverageOfNumeric`)
- All three AAA section comments required in every test method
- `assertSame` over `assertEquals` when type matters
- One logical assertion per test where practical
- Group related tests with `@group <name>` PHPDoc annotation
- Do not commit tests that are skipped permanently — remove them or fix the underlying issue


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

ko_fi: ethanhann
custom: https://www.paypal.com/paypalme/EthanHann


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
  pull_request:

jobs:
  lint-and-format:
    name: Lint & Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          coverage: none

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: ~/.composer/cache
          key: composer-${{ runner.os }}-${{ hashFiles('composer.lock') }}
          restore-keys: composer-

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist --no-progress

      - name: Check PHP syntax (lint)
        run: find src tests -name "*.php" -print0 | xargs -0 php -l

      - name: Check code format
        run: vendor/bin/php-cs-fixer fix --dry-run --diff src

  test:
    name: Test (PHP ${{ matrix.php-version }})
    runs-on: ubuntu-latest
    needs: lint-and-format
    strategy:
      fail-fast: false
      matrix:
        php-version: ['8.2', '8.3', '8.4', '8.5']

    services:
      redis:
        image: redis/redis-stack-server:latest
        ports:
          - 6381:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP ${{ matrix.php-version }}
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-version }}
          coverage: none

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: ~/.composer/cache
          key: composer-${{ matrix.php-version }}-${{ hashFiles('composer.lock') }}
          restore-keys: composer-${{ matrix.php-version }}-

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist --no-progress

      - name: Run tests
        run: vendor/bin/phpunit


================================================
FILE: .github/workflows/docs.yml
================================================
name: Deploy Docs

on:
  push:
    branches: [ main ]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: docs-site

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Bun
        uses: oven-sh/setup-bun@v1

      - name: Install dependencies
        run: bun install

      - name: Build site
        run: bun run astro build

      - name: Upload Pages artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: docs-site/dist

  deploy:
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: Deploy to GitHub Pages
        uses: actions/deploy-pages@v4


================================================
FILE: .gitignore
================================================
/vendor/
.idea/
.php-cs-fixer.cache
composer.lock
composer.phar
tests.log
/.phpunit.result.cache
/docs/
/.dev/


================================================
FILE: .php-cs-fixer.php
================================================
<?php

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__ . '/src');

return (new PhpCsFixer\Config())
    ->setRules([
        '@PSR12' => true,
    ])
    ->setFinder($finder);


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance for Claude Code when working in this repository.

## Project Overview

`redisearch-php` is a PHP client library for the RediSearch module. It wraps the Redis client adapters (Predis, PhpRedis, RedisClient) behind a unified interface and exposes RediSearch commands as fluent PHP objects.

## Development Setup

### Start Redis

```bash
docker compose up -d
```

This starts `redis/redis-stack` (includes the RediSearch module) on port **6381**.

### Install Dependencies

```bash
composer install
```

## Running Tests

```bash
# Default (Predis client)
just test

# Specific client
just test-predis
just test-php-redis
just test-redis-client

# All clients
just test-all


# Run phpunit directly (used by CI)
vendor/bin/phpunit
```

Tests flush the entire Redis database on teardown. Never run against a Redis instance with important data.

## Code Style

```bash
# Fix in place
just fmt

# Check only (no modifications — used by CI)
vendor/bin/php-cs-fixer fix src --dry-run --diff
```

## Lint

```bash
find src tests -name "*.php" -print0 | xargs -0 php -l
```

## Project Structure

```
src/                  # Ehann\RediSearch\ namespace (PSR-4)
  Aggregate/          # Aggregation pipeline builders
    Operations/
    Reducers/
  Query/              # Query string builders
  Fields/             # Field type definitions
  Document/           # Document abstraction
  Exceptions/
  Index.php           # Primary entry point
tests/                # Ehann\Tests\ namespace (PSR-4)
  RediSearch/         # Mirrors src/ structure
  Stubs/              # Fixtures (TestIndex, etc.)
  RediSearchTestCase.php  # Base test class
```

## Key Conventions

- **Namespaces**: `Ehann\RediSearch\` for source, `Ehann\Tests\` for tests
- **Interfaces**: suffix `Interface` (e.g., `IndexInterface`)
- **Abstract classes**: prefix `Abstract` (e.g., `AbstractIndex`)
- **Test files**: `{ClassName}Test.php`
- **Test methods**: `test{Description}` (e.g., `testGetAverageOfNumeric`)
- **PHP version**: 8.2+, use native type hints throughout

## CI

GitHub Actions (`.github/workflows/ci.yml`) runs two jobs:

1. **lint-and-format** — PHP syntax check + php-cs-fixer dry-run on PHP 8.2
2. **test** — PHPUnit matrix across PHP 8.2 / 8.3 / 8.4 (requires lint to pass first)

## Custom Skills

- `/contribute` — step-by-step guide for adding a new feature or bug fix
- `/unit-test` — guide for writing and running unit tests


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2017 Ethan Hann

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# RediSearch PHP Client

[![Latest Stable Version](https://poser.pugx.org/ethanhann/redisearch-php/v/stable)](https://packagist.org/packages/ethanhann/redisearch-php)
[![Total Downloads](https://poser.pugx.org/ethanhann/redisearch-php/downloads)](https://packagist.org/packages/ethanhann/redisearch-php)
[![Latest Unstable Version](https://poser.pugx.org/ethanhann/redisearch-php/v/unstable)](https://packagist.org/packages/ethanhann/redisearch-php)
[![License](https://poser.pugx.org/ethanhann/redisearch-php/license)](https://packagist.org/packages/ethanhann/redisearch-php)

**What is this?**

RediSearch-PHP is a PHP client library for the [RediSearch](http://redisearch.io/) module which adds Full-Text search to Redis.

See the [documentation](http://www.ethanhann.com/redisearch-php/) for more information.

**Contributing**

Contributions are welcome. Before submitting a PR for review, please run confirm all tests in the test suite pass.

Start the local Docker dev environment by running:

```shell
just up
```

Then run the tests:

```shell
just test
```

Specific Redis clients can be tested:

```shell
just test-predis
just test-php-redis
just test-redis-client
```

Or to run tests for all clients:

```shell
just test-all
```

Do not run tests on a prod system (of course), or any system that has a Redis instance with data you care about -
Redis is flushed between tests.

To fix code style, before submitting a PR:

```shell
just fmt
```

**Laravel Support**

[Laravel-RediSearch](https://github.com/ethanhann/Laravel-RediSearch) - Exposes RediSearch-PHP to Laravel as a Scout driver.


================================================
FILE: bin/redisearch
================================================
#!/usr/bin/env php
<?php

// Installed as a dependency (vendor/ethanhann/redisearch-php/bin/redisearch)
$autoloadPaths = [
    __DIR__ . '/../../../autoload.php',
    __DIR__ . '/../vendor/autoload.php',
];

foreach ($autoloadPaths as $autoloadPath) {
    if (file_exists($autoloadPath)) {
        require $autoloadPath;
        break;
    }
}

use Ehann\RediSearch\Console\Application;

$app = new Application();
$app->run();


================================================
FILE: composer.json
================================================
{
    "name": "ethanhann/redisearch-php",
    "type": "library",
    "autoload": {
        "psr-4": {
            "Ehann\\RediSearch\\": "src"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Ehann\\Tests\\": "tests/"
        }
    },
    "license": "MIT",
    "authors": [
        {
            "name": "Ethan Hann",
            "email": "ethanhann@gmail.com"
        }
    ],
    "minimum-stability": "dev",
    "prefer-stable": true,
    "bin": ["bin/redisearch"],
    "require": {
        "php": ">=8.2",
        "psr/log": "^3.0.0",
        "ethanhann/redis-raw": "^3.0.2",
        "symfony/console": "^7.0 || ^8.0"
    },
    "suggest": {
        "predis/predis": "Required for the predis adapter (default CLI adapter)",
        "ext-redis": "Required for the phpredis adapter",
        "cheprasov/php-redis-client": "Required for the RedisClient adapter"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0",
        "mockery/mockery": "^1.6.0",
        "predis/predis": "^v2.0.0",
        "friendsofphp/php-cs-fixer": "^v3.10.0",
        "monolog/monolog": "^3.2.0",
        "ukko/phpredis-phpdoc": "^5.0@beta",
        "cheprasov/php-redis-client": "^1.9"
    }
}


================================================
FILE: docker-compose.yml
================================================
services:
    redis:
      container_name: redisSearchPhpTest
      image: 'redis/redis-stack'
      ports:
        - '6381:6379'


================================================
FILE: docs-site/.gitignore
================================================
node_modules/
dist/
.astro/


================================================
FILE: docs-site/astro.config.mjs
================================================
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';

export default defineConfig({
  site: 'https://ethanhann.com',
  base: '/redisearch-php/',

  integrations: [
    starlight({
      title: 'RediSearch-PHP',
      logo: { src: './src/assets/logo.png' },
      social: [
        {
          icon: 'github',
          label: 'GitHub',
          href: 'https://github.com/ethanhann/redisearch-php',
        },
      ],
      customCss: ['./src/styles/custom.css'],
      editLink: {
        baseUrl:
            'https://github.com/ethanhann/redisearch-php/edit/master/docs-site/src/content/docs/',
      },
      sidebar: [
        { label: 'Getting Started', link: '/' },
        {
          label: 'Documentation',
          items: [
            { label: 'Indexing', link: '/indexing/' },
            { label: 'Searching', link: '/searching/' },
            { label: 'Aggregating', link: '/aggregating/' },
            { label: 'Suggesting', link: '/suggesting/' },
            { label: 'CLI', link: '/cli/' },
          ],
        },
        { label: 'Laravel Support', link: '/laravel-support/' },
        { label: 'Changelog', link: '/changelog/' },
      ],
    }),
  ],
});

================================================
FILE: docs-site/package.json
================================================
{
  "name": "redisearch-php-docs",
  "type": "module",
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview"
  },
  "dependencies": {
    "@astrojs/starlight": "latest",
    "astro": "latest"
  }
}


================================================
FILE: docs-site/src/content/docs/aggregating.mdx
================================================
---
title: Aggregating
draft: false
description: Aggregation pipelines and cursor-based pagination.
---

## The Basics

Make an [index](/redisearch-php/indexing/) and add a few documents to it:

```php
use Ehann\RediSearch\Index;

$bookIndex = new Index($redis);

$bookIndex->add([
    'title' => 'How to be awesome',
    'price' => 9.99
]);

$bookIndex->add([
    'title' => 'Aggregating is awesome',
    'price' => 19.99
]);
```

Now group by title and get the average price:

```php
$results = $bookIndex->makeAggregateBuilder()
    ->groupBy('title')
    ->avg('price');
```

## Cursor-Based Pagination

For large result sets, use `withCursor()` to retrieve results in batches instead of all at once.
The first call returns an initial batch; subsequent batches are read with `cursorRead()`.
Always call `cursorDelete()` when done to free the server-side cursor.

```php
$builder = $bookIndex->makeAggregateBuilder();

// Request the first batch of up to 50 results.
$result = $builder
    ->groupBy('author')
    ->count()
    ->withCursor(50)
    ->search();

// $result contains the first batch and a cursor ID.
$cursorId = $result->getCursorId();

// Read subsequent batches until the cursor is exhausted (cursorId becomes 0).
while ($cursorId !== 0) {
    $next = $builder->cursorRead($cursorId, 50);
    $cursorId = $next->getCursorId();
    // process $next->getDocuments() ...
}

// If you need to abandon iteration early, delete the cursor explicitly.
$builder->cursorDelete($cursorId);
```


================================================
FILE: docs-site/src/content/docs/changelog.mdx
================================================
---
title: Changelog
draft: false
description: Version history and release notes.
---

## 3.1.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/3.0.0...3.1.0)

### PHP Compatibility

* **PHP 8.5** — Added PHP 8.5 to the CI build matrix.

### New Features

* Added `loadFields` method to `Index` to load fields from Redis.
* Added `bin/redisearch` CLI tool for managing indexes.

### Bug Fixes

* Upgraded redis-raw to 3.0.2 to fix a bug where FT.INFO would respond with garbage.

## 3.0.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/2.1.0...3.0.0)

### PHP Compatibility

* **PHP 8.3 support** — fixed "Typed property must not be accessed before initialization" fatal errors.
* **PHP 8.4+ compatibility** — fixed implicitly nullable parameters and dynamic property deprecations.

### Tooling

* **Replaced Robo with `justfile`** — `just test`, `just fmt`, etc. are now the standard commands.
* **GitHub Actions CI** — lint-and-format job plus PHPUnit test matrix across PHP 8.2, 8.3, and 8.4.
* Removed deprecated `version` field from `docker-compose.yml`.

### Developer Experience

* Added `CLAUDE.md` with contributor guidance and project structure documentation.
* Added `/contribute` and `/unit-test` Claude Code skills for contributors.
* Upgraded `/unit-test` skill to target PHPUnit 11 and enforce AAA (Arrange-Act-Assert) test style.

## 2.1.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/2.0.0...2.1.0)

### New Features

* **Index prefix support** — fixed index creation to correctly apply prefix options.
* **Improved hash support** — better `addHash` functionality for adding documents as Redis hashes.

## 2.0.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/1.9.0...2.0.0)

### Breaking Changes

* **PHP 8.0+** is now required.
* **`FT.ADDHASH` removed** — replaced with `HSET` to align with RediSearch v2 API.

### New Features

* **`replaceMany()`** — bulk replace documents alongside the existing `addMany()`.
* **`FT.TAGVALS` support** — retrieve all values in a tag field.
* **Tag field auto-detection** — array values passed to `FieldFactory::make` are automatically treated as Tag fields.
* **Aggregation improvements** — operations now support optional field lists; group-by filter feature added.
* **`Index::getFields()` made public.**
* **Suggestion scores** — support for retrieving scores with autocomplete suggestions.

### Bug Fixes

* Fixed suggestion score incrementation.
* Escaped special characters in tag field filters.
* Fixed handling of "document already exists" and "unsupported language" return values from Redis.
* `DocumentAlreadyInIndexException` now includes the index name and document ID in the message.

## 1.1.2

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/1.1.1...1.1.2)

* Loosen version requirement for redis raw to pull in bug fix(es).

## 1.1.1

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/1.1.0...1.1.1)

* Fix issue with implicitly named indexes.

## 1.1.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/1.0.1...1.1.0)

* Support [aggregations](/redisearch-php/aggregating/).

## 1.0.1

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/1.0.0...1.0.1)

* Support NOINDEX fields.

## 1.0.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/0.11.0...1.0.0)

* Support complete RediSearch API, now including RETURN, SUMMARIZE, HIGHLIGHT, EXPANDER, and PAYLOAD in search queries.

## 0.11.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/0.10.1...0.11.0)

* Add [hash indexing](/redisearch-php/indexing/#document-storage-in-v2).

## 0.10.1

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/0.10.0...0.10.1)

* Polished docs, and added a section on the Laravel RediSearch package.
* Made internal changes to how numeric and geo search queries are generated.

## 0.10.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/0.9.0...0.10.0)

* Remove RedisClient class and add adapter functionality.
* There are now adapters for Predis, PhpRedis, and the Cheprasov client. They all extend [AbstractRedisClient](https://github.com/ethanhann/redisearch-php/blob/master/src/Redis/AbstractRedisClient.php) which implements [RedisClientInterface](https://github.com/ethanhann/redisearch-php/blob/master/src/Redis/RedisClientInterface.php). An additional adapter can be created by extending AbstractRedisClient or by implementing RedisClientInterface if needed for some reason.
* Handle RediSearch module error when index is created on a Redis database other than 0.
* Return boolean true instead of "OK" when using PredisAdapter.
* A new index now requires that a redis client is passed into its constructor - removed the magic behavior where a default RedisClient instance was auto initialized.

## 0.9.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/0.8.0...0.9.0)

* An exception is now thrown when the RediSearch module is not loaded in Redis.
* Allow a [language to be specified](/redisearch-php/searching/#setting-a-language) when searching.

## 0.8.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/0.7.0...0.8.0)

* Add [search result sorting](/redisearch-php/searching/#sorting-results).
* Remove NoScoreIdx and Optimize methods as they are deprecated and/or non-functional in RediSearch.
* Add [explain method](/redisearch-php/searching/#explaining-a-query) for explaining a query.
* Add optional [query logging](/redisearch-php/searching/#logging-queries).
* Add [suggestions](/redisearch-php/suggesting/).

## 0.7.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/0.6.0...0.7.0)

* Many bug fixes and code quality improvements.

## 0.6.0

[Changes since last release](https://github.com/ethanhann/redisearch-php/compare/0.5.0...0.6.0)

* Add [batch indexing](/redisearch-php/indexing/#batch-indexing).

## 0.5.0

* Rename vendor namespace from **Eeh** to **Ehann**
* **AbstractIndex** was renamed to **Index** and is no longer abstract.
* Custom document ID is now properly set when adding to an index.


================================================
FILE: docs-site/src/content/docs/cli.mdx
================================================
---
title: CLI
draft: false
description: A command-line interface for interacting with RediSearch directly from the terminal.
---

RediSearch-PHP includes a CLI tool powered by Symfony Console. It lets you manage indexes, documents, and queries directly from the terminal — useful for debugging, exploration, and rapid prototyping.

## Installation

The CLI is included with the library. After installing via Composer, the binary is available at:

```bash
vendor/bin/redisearch
```

Run it without arguments to see all available commands:

```bash
vendor/bin/redisearch list
```

## Global Options

All commands accept these connection options:

```bash
vendor/bin/redisearch <command> --host 127.0.0.1 --port 6379 --password secret --adapter predis
```

| Option | Default | Description |
|--------|---------|-------------|
| `--host` | `127.0.0.1` | Redis host |
| `--port` / `-p` | `6379` | Redis port |
| `--password` / `-a` | — | Redis password |
| `--adapter` | `predis` | Adapter: `predis`, `phpredis`, or `redisclient` |

## Index Management

### Creating an Index

Create an index from a JSON schema file:

```bash
vendor/bin/redisearch index:create products schema.json
```

The schema file defines the fields for the index:

```json
{
  "fields": [
    { "name": "title", "type": "TEXT", "weight": 2.0, "sortable": true },
    { "name": "price", "type": "NUMERIC", "sortable": true },
    { "name": "tags", "type": "TAG", "separator": "," },
    { "name": "location", "type": "GEO" }
  ]
}
```

Supported field types: `TEXT`, `NUMERIC`, `TAG`, `GEO`, `VECTOR`.

Additional options:

```bash
vendor/bin/redisearch index:create products schema.json \
  --on HASH \
  --prefix "product:" \
  --filter "@price > 0" \
  --stopwords the a an
```

### Listing Indexes

```bash
vendor/bin/redisearch index:list
vendor/bin/redisearch index:list --json
```

### Viewing Index Info

```bash
vendor/bin/redisearch index:info products
vendor/bin/redisearch index:info products --json
```

### Dropping an Index

```bash
vendor/bin/redisearch index:drop products
vendor/bin/redisearch index:drop products --delete-docs
```

## Document Management

### Adding a Document

```bash
vendor/bin/redisearch document:add products doc1 title="Laptop" price=999 tags="electronics,computer"
```

Use `--replace` to upsert, and optionally set language or score:

```bash
vendor/bin/redisearch document:add products doc1 title="Laptop Pro" price=1299 --replace --score 0.9
```

### Getting a Document

Retrieve a document by its full Redis key:

```bash
vendor/bin/redisearch document:get products doc1
vendor/bin/redisearch document:get products doc1 --json
```

### Deleting a Document

```bash
vendor/bin/redisearch document:delete products doc1
```

## Searching

Run a full-text search query against an index:

```bash
vendor/bin/redisearch search products "laptop"
```

### Search Options

```bash
vendor/bin/redisearch search products "laptop" \
  --limit 0,20 \
  --sort price:ASC \
  --fields title,price \
  --highlight title \
  --scores \
  --json
```

| Option | Format | Description |
|--------|--------|-------------|
| `--limit` | `offset,count` | Paginate results (default `0,10`) |
| `--sort` | `field:ASC\|DESC` | Sort by a sortable field |
| `--fields` | `field1,field2` | Return only specific fields |
| `--highlight` | `field1,field2` | Highlight matching terms |
| `--scores` | flag | Include relevance scores |
| `--verbatim` | flag | Disable stemming |
| `--language` | `name` | Set stemming language |
| `--dialect` | `1\|2\|3` | Query dialect version |
| `--json` | flag | Output as JSON |

### Filters

Apply numeric, tag, or geo filters:

```bash
# Numeric filter
vendor/bin/redisearch search products "laptop" --numeric-filter price:500:2000

# Tag filter
vendor/bin/redisearch search products "*" --tag-filter tags:electronics,computer

# Geo filter (field:lon:lat:radius:unit)
vendor/bin/redisearch search products "*" --geo-filter location:-73.9:40.7:10:km
```

Filters can be combined and repeated.

## Aggregation

Run aggregation queries with grouping and reducers:

```bash
vendor/bin/redisearch aggregate products "*" \
  --group-by tags \
  --reduce avg:price \
  --reduce count \
  --sort-by price:DESC \
  --limit 0,10
```

| Option | Format | Description |
|--------|--------|-------------|
| `--group-by` | `field` | Group results by field |
| `--reduce` | `func:field` | Apply reducer (repeatable) |
| `--sort-by` | `field:ASC\|DESC` | Sort aggregated results |
| `--apply` | `expr:alias` | Apply expression |
| `--filter` | `expression` | Filter aggregated results |
| `--limit` | `offset,count` | Limit results |
| `--load` | `field1,field2` | Load additional fields |
| `--json` | flag | Output as JSON |

Available reducers: `avg`, `sum`, `min`, `max`, `count`, `count_distinct`, `count_distinctish`, `stddev`, `tolist`, `first_value`.

## Developer Tools

### Query Explain

View the execution plan for a query using `FT.EXPLAIN`:

```bash
vendor/bin/redisearch explain products "laptop"
```

This helps understand how RediSearch parses and executes a query.

### Query Profile

Profile a query to see timing and execution details using `FT.PROFILE`:

```bash
vendor/bin/redisearch profile products "laptop"
vendor/bin/redisearch profile products "laptop" --json
```

## Interactive Shell

Start a REPL session for rapid experimentation:

```bash
vendor/bin/redisearch shell --host 127.0.0.1 --port 6379
```

Example session:

```
redisearch> index:list
redisearch> use products
redisearch (products)> search "laptop"
redisearch (products)> explain "laptop"
redisearch (products)> aggregate "*" --group-by tags --reduce count
redisearch (products)> exit
```

The shell supports:

- `use <index>` — set a default index so you don't have to repeat it
- `help` — list available commands
- `exit` / `quit` — leave the shell
- Command history via readline
- Quoted strings for queries with spaces

## JSON Output

Most commands support `--json` for machine-readable output, making the CLI useful in scripts:

```bash
vendor/bin/redisearch search products "laptop" --json | jq '.documents[].title'
```


================================================
FILE: docs-site/src/content/docs/index.mdx
================================================
---
title: RediSearch-PHP
draft: false
description: A PHP client library for the RediSearch module, adding full-text search to Redis.
---

RediSearch-PHP is a PHP client library for the [RediSearch](https://redis.io/docs/latest/develop/ai/search-and-query/)
module which adds full-text search to Redis.

## Requirements

* [Redis Stack](https://redis.io/about/about-stack/) or Redis with the RediSearch module v2.x loaded.
* PHP >=8.2
* [PhpRedis](https://github.com/phpredis/phpredis), [Predis](https://github.com/nrk/predis), or [php-redis-client](https://github.com/cheprasov/php-redis-client).

## Install

```bash
composer require ethanhann/redisearch-php
```

## Load

```php
require_once 'vendor/autoload.php';
```


## Create a Redis Client

```php
use Ehann\RedisRaw\PredisAdapter;
use Ehann\RedisRaw\PhpRedisAdapter;
use Ehann\RedisRaw\RedisClientAdapter;

$redis = (new PredisAdapter())->connect('127.0.0.1', 6379);
// or
$redis = (new PhpRedisAdapter())->connect('127.0.0.1', 6379);
// or
$redis = (new RedisClientAdapter())->connect('127.0.0.1', 6379);

```

## Create the Schema

```php
use Ehann\RediSearch\Index;

$bookIndex = new Index($redis);

$bookIndex->addTextField('title')
    ->addTextField('author')
    ->addNumericField('price')
    ->addNumericField('stock')
    ->create();
```

## Add a Document

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

## Search the Index

```php
$result = $bookIndex->search('two cities');

$result->getCount();     // Number of documents.
$result->getDocuments(); // Array of matches.

// Documents are returned as objects by default.
$firstResult = $result->getDocuments()[0];
$firstResult->title;
$firstResult->author;
```


================================================
FILE: docs-site/src/content/docs/indexing.mdx
================================================
---
title: Indexing
draft: false
description: Field types, adding, updating, and batch indexing documents.
---

## Field Types

There are five types of fields that can be added to a document: **TextField**, **NumericField**, **GeoField**, **TagField**, and **VectorField**.

They are instantiated like this:

```php
new TextField('author', 'Charles Dickens');
new NumericField('price', 9.99);
new GeoField('place', new GeoLocation(-77.0366, 38.8977));
new TagField('color', 'red');
new VectorField('embedding', VectorField::ALGORITHM_HNSW, VectorField::TYPE_FLOAT32, 128, VectorField::DISTANCE_COSINE);
```

Fields can also be made with the FieldFactory class:

```php
// Alternative syntax for: new TextField('author', 'Charles Dickens');
FieldFactory::make('author', 'Charles Dickens');

// Alternative syntax for: new NumericField('price', 9.99);
FieldFactory::make('price', 9.99);

// Alternative syntax for: new GeoField('place', new GeoLocation(-77.0366, 38.8977));
FieldFactory::make('place', new GeoLocation(-77.0366, 38.8977));

// Alternative syntax for: new TagField('color', 'red');
FieldFactory::make('color', 'red');
```

### Vector Fields

Vector fields enable nearest-neighbor similarity search (available in RediSearch v2.2+).
Use `addVectorField()` when defining the schema:

```php
use Ehann\RediSearch\Fields\VectorField;

$bookIndex->addVectorField(
    'embedding',
    VectorField::ALGORITHM_HNSW,  // FLAT or HNSW
    VectorField::TYPE_FLOAT32,    // FLOAT32 or FLOAT64
    128,                          // number of dimensions
    VectorField::DISTANCE_COSINE  // L2, IP, or COSINE
);
```

## Adding Documents

Add an array of field objects:

```php
$bookIndex->add([
    new TextField('title', 'Tale of Two Cities'),
    new TextField('author', 'Charles Dickens'),
    new NumericField('price', 9.99),
    new GeoField('place', new GeoLocation(-77.0366, 38.8977)),
    new TagField('color', 'red'),
]);
```

Add an associative array:

```php
$bookIndex->add([
    'title' => 'Tale of Two Cities',
    'author' => 'Charles Dickens',
    'price' => 9.99,
    'place' => new GeoLocation(-77.0366, 38.8977),
    'color' => new TagField('color', 'red'),
]);
```

Create a document with the index's makeDocument method, then set field values:

```php
$document = $bookIndex->makeDocument();
$document->title->setValue('How to be awesome.');
$document->author->setValue('Jack');
$document->price->setValue(9.99);
$document->place->setValue(new GeoLocation(-77.0366, 38.8977));
$document->color->setValue(new Tag('red'));

$bookIndex->add($document);
```

DocBlocks can (optionally) be used to type hint field property names:

```php
/** @var BookDocument $document */
$document = $bookIndex->makeDocument();

// "title" will auto-complete correctly in your IDE provided BookDocument has a "title" property or @property annotation.
$document->title->setValue('How to be awesome.');

$bookIndex->add($document);
```

```php
<?php

namespace Your\Documents;

use Ehann\RediSearch\Document\Document;
use Ehann\RediSearch\Fields\NumericField;
use Ehann\RediSearch\Fields\TextField;

/**
 * @property TextField title
 * @property TextField author
 * @property NumericField price
 * @property GeoField place
 */
class BookDocument extends Document
{
}
```

## Updating a Document

Documents are updated with an index's replace method.

```php
// Make a document.
$document = $bookIndex->makeDocument();
$document->title->setValue('How to be awesome.');
$document->author->setValue('Jack');
$document->price->setValue(9.99);
$document->place->setValue(new GeoLocation(-77.0366, 38.8977));
$bookIndex->add($document);

// Update a couple fields
$document->title->setValue('How to be awesome: Part 2.');
$document->price->setValue(19.99);

// Update the document.
$bookIndex->replace($document);
```

A document can also be updating when its ID is specified:

```php
// Make a document.
$document = $bookIndex->makeDocument();
$document->title->setValue('How to be awesome.');
$document->author->setValue('Jack');
$document->price->setValue(9.99);
$document->place->setValue(new GeoLocation(-77.0366, 38.8977));
$bookIndex->add($document);

// Create a new document and assign the old document's ID to it.
$newDocument = $bookIndex->makeDocument($document->getId());

// Set a couple fields.
$document->title->setValue('');
$document->author->setValue('Jack');
$newDocument->title->setValue('How to be awesome: Part 2.');
$newDocument->price->setValue(19.99);

// Update the document.
$bookIndex->replace($newDocument);
```

## Batch Indexing

Batch indexing is possible with the **addMany** method.
To index an external collection, make sure to set the document's ID to the ID of the record in the external collection.

```php
// Get a record set from your DB (or some other datastore).
$records = $someDatabase->findAll();

$documents = [];
foreach ($records as $record) {
    // Make a new document with the external record's ID.
    $newDocument = $bookIndex->makeDocument($record->id);
    $newDocument->title->setValue($record->title);
    $newDocument->author->setValue($record->author);
    $documents[] = $newDocument;
}

// Add all the documents at once.
$bookIndex->addMany($documents);

// It is possible to increase indexing speed by disabling atomicity by passing true as the second parameter.
// Note that this is only possible when using the phpredis extension.
$bookIndex->addMany($documents, true);
```

## Document Storage in v2

In RediSearch v2, all documents are stored as Redis hashes (key/value pairs) internally.
The library writes every document via `HSET` — there is no separate indexing step.

`addHash()` and `replaceHash()` are available as aliases for `add()` and `replace()` respectively,
both using upsert semantics:

```php
$document = $bookIndex->makeDocument('foo');
$document->title->setValue('How to be awesome.');
$bookIndex->addHash($document);   // upsert (same as add/replace)
$bookIndex->replaceHash($document); // also upsert
```

## Key Prefixes

You can configure one or more key prefixes so RediSearch only indexes hashes whose Redis key
starts with a given string. Multiple prefixes are treated as **alternatives** — each is a
separate key namespace, not a compound path.

When writing documents, the library always uses the **first** configured prefix to build the
hash key. Each prefix must include its own separator character (e.g. `'post:'`, not `'post'`).

```php
// Index covers keys starting with 'post:' OR 'blog:'
$index->setPrefixes(['post:', 'blog:'])->create();

// Documents are written under the first prefix: 'post:{id}'
$index->add($document);
```

## Index Creation Options

Several `FT.CREATE` options can be set before calling `create()`:

```php
$index
    ->setIndexType('HASH')       // 'HASH' (default) or 'JSON' (requires RedisJSON)
    ->setFilter('@price > 0')    // only index documents matching this expression
    ->setMaxTextFields()         // allow more than 32 TEXT fields
    ->setTemporary(3600)         // auto-expire index after 3600 seconds of inactivity
    ->setSkipInitialScan()       // don't scan existing keys on creation
    ->addTextField('title')
    ->addNumericField('price')
    ->create();
```

## Schema Expansion

Fields can be added to an existing index without recreating it using `alter()`.
Note that existing documents are not retroactively re-indexed for new fields;
only newly added or updated documents will include them.

```php
use Ehann\RediSearch\Fields\NumericField;

$bookIndex->alter(new NumericField('year'));
```

## Loading Fields from an Existing Index

`loadFields()` introspects a pre-existing RediSearch index and reconstructs all of
its field definitions on the `Index` object — without you having to re-declare every
field manually. This is especially useful when multiple services share the same index,
or when you want to instantiate an `Index` that reflects the live schema in Redis.

Internally the method calls `FT.INFO` and parses the returned attribute descriptors to
create the appropriate `TextField`, `NumericField`, `TagField`, `GeoField`, or
`VectorField` objects, preserving weights, sortable flags, separators, and vector
parameters. Internal fields (`__score`, `__language`, etc.) are silently ignored.

```php
// The index already exists in Redis — no need to call create() or add any fields.
$bookIndex = (new Index($redisClient, 'bookIndex'))->loadFields();

// All fields are now available for search, document creation, etc.
$results = $bookIndex->search('Dickens');
$document = $bookIndex->makeDocument();
```

`loadFields()` returns `static`, so it chains with other methods:

```php
$results = (new Index($redisClient, 'bookIndex'))
    ->loadFields()
    ->search('Dickens');
```

Field metadata is fully restored:

```php
// TEXT field with a custom weight and the SORTABLE flag
$bookIndex->addTextField('title', weight: 2.0, sortable: true)->create();

// Later, in a different request / process:
$bookIndex2 = (new Index($redisClient, 'bookIndex'))->loadFields();
// $bookIndex2->title is a TextField with weight=2.0 and sortable=true
```

Works with all five field types:

```php
$index->addTextField('title', weight: 2.0, sortable: true)
      ->addNumericField('price', sortable: true)
      ->addTagField('categories', separator: '|')
      ->addGeoField('location')
      ->addVectorField('embedding', VectorField::ALGORITHM_HNSW, VectorField::TYPE_FLOAT32, 128, VectorField::DISTANCE_COSINE)
      ->create();

// Reconstitute the same schema elsewhere:
$index2 = (new Index($redisClient, 'myIndex'))->loadFields();
```

## Listing Indexes

All index names in the current Redis instance can be retrieved:

```php
$names = $bookIndex->listIndexes(); // e.g. ['bookIndex', 'authorIndex']
```

## Aliasing

Indexes can be aliased.

Note that an exception will be thrown if any alias method is called before an index's [schema](/#create-the-schema) is created.

### Adding an Alias

An alias can be added for an index like this:

```php
$index->addAlias('foo');
```

### Updating an Alias

Assuming an alias has already been added to an index, like this:

```php
$oldIndex->addAlias('foo');
```

...it can be reassigned to a different index like this:

```php
$newIndex->updateAlias('foo');
```

### Deleting an Alias

An alias can be deleted like this:

```php
$index->deleteAlias('foo');
```

## Managing an Index

Whether or not an index exists can be checked:

```php
$indexExists = $index->exists();
```

An index can be removed:

```php
$index->drop();
```

Passing `true` also deletes all underlying document hashes from Redis:

```php
$index->drop(true);
```


================================================
FILE: docs-site/src/content/docs/laravel-support.mdx
================================================
---
title: Laravel Support
draft: false
description: Laravel Scout driver integration for RediSearch.
---

[Laravel-RediSearch](https://github.com/ethanhann/Laravel-RediSearch) allows for indexing and searching Laravel models.
It provides a [Laravel Scout](https://laravel.com/docs/5.6/scout) driver.

## Getting Started

### Install

```bash
composer require ethanhann/laravel-redisearch
```

###  Register the Provider

Add this entry to the providers array in config/app.php.

```php
Ehann\LaravelRediSearch\RediSearchServiceProvider::class
```

### Configure the Scout Driver

Update the Scout driver in config/scout.php.

```php
'driver' => env('SCOUT_DRIVER', 'ehann-redisearch'),
```

### Define Searchable Schema

Define the field types that will be used on indexing

```php
<?php

namespace App;

use Laravel\Scout\Searchable;
...
use Ehann\RediSearch\Fields\TextField;
use Ehann\RediSearch\Fields\GeoField;
use Ehann\RediSearch\Fields\NumericField;
use Ehann\RediSearch\Fields\TagField;
use Ehann\RediSearch\Fields\GeoLocation;
...

class User extends Model {
    use Searchable;

    public function searchableAs()
    {
        return "user_index";
    }

    public function toSearchableArray()
    {
        return [
            "name" => $this->name,
            "username" => $this->username,
            "location" => new GeoLocation(
                                $this->longitude,
                                $this->latitude
                            )
            "age" => $this->age,
       ];
    }

    public function searchableSchema()
    {
        return [
            "name" => TextField::class,
            "username" => TextField::class,
            "location" => GeoField::class,
            "age" => NumericField::class
      ];
    }
}
```

### Import a Model

Import a "Product" model that is [configured to be searchable](https://laravel.com/docs/5.6/scout#configuration):

```bash
artisan ehann:redisearch:import App\\Product
```

Delete the index before importing:

```bash
artisan ehann:redisearch:import App\\Product --recreate-index
```

Import models without an ID field (this should be rarely needed):

```bash
artisan ehann:redisearch:import App\\Product --no-id
```

### Query Filters

How To Query Filters? [Filtering Tag Fields](/redisearch-php/searching/#tag-fields)

```php
App\User::search("Search Query", function($index){
    return $filter->geoFilter("location", 5.56475, 5.75516, 100)
                  ->numericFilter('age', 18, 32)
})->get()
```

## What now?

See the [Laravel Scout](https://laravel.com/docs/5.6/scout) documentation for additional information.


================================================
FILE: docs-site/src/content/docs/searching.mdx
================================================
---
title: Searching
draft: false
description: Text search, filtering, sorting, vector search, spell checking, and synonyms.
---

## Simple Text Search

Text fields can be filtered with the index's search method.

```php
$result = $bookIndex->search('two cities');

$result->getCount();     // Number of documents.
$result->getDocuments(); // Array of stdObjects.
```

Documents can also be returned as arrays instead of objects by passing true as the second parameter to the search method.

```php
$result = $bookIndex->search('two cities', true);

$result->getDocuments(); // Array of arrays.
```

## Filtering
### Tag Fields

Tag fields can be filtered with the index's tagFilter method.

Specifying multiple tags creates a union of documents.

```php
$result = $bookIndex
    ->tagFilter('color', ['blue', 'red'])
    ->search('two cities');
```

Use multiple separate tagFilter calls to create an intersection of documents.

```php
$result = $bookIndex
    ->tagFilter('color', ['blue'])
    ->tagFilter('color', ['red'])
    ->search('two cities');
```

### Numeric Fields

Numeric fields can be filtered with the index's numericFilter method.

```php
$result = $bookIndex
    ->numericFilter('price', 4.99, 19.99)
    ->search('two cities');
```

### Geo Fields

Geo fields can be filtered with the index's geoFilter method.

```php
$result = $bookIndex
    ->geoFilter('place', -77.0366, 38.897, 100)
    ->search('two cities');
```

## Sorting Results

Search results can be sorted with the index's sort method.

```php
$result = $bookIndex
    ->sortBy('price')
    ->search('two cities');
```


## Number of Results

The number of documents can be retrieved after performing a search.

```php
$result = $bookIndex->search('two cities');

$result->getCount(); // Number of documents.
```

Alternatively, the number of documents can be queried without returning the documents themselves.
This is useful if you want to check the total number of documents without returning any other data from the Redis server.

```php
$numberOfDocuments = $bookIndex->count('two cities');
```

## Setting a Language

A supported language can be specified when running a query.
Supported languages are represented as constants in the **Ehann\RediSearch\Language** class.

```php
$result = $bookIndex
    ->language(Language::ITALIAN)
    ->search('two cities');
```

## Query Dialect

RediSearch v2.4+ supports multiple query dialects that unlock different syntax features.
Use `dialect()` to select a version (1, 2, or 3):

```php
$result = $bookIndex
    ->dialect(2)
    ->search('two cities');
```

Dialect 2 is required for vector/KNN queries and extended query syntax.

## Vector Search

Vector similarity search allows you to find documents whose vector fields are nearest to a
query vector. This requires a field indexed with `addVectorField()`, dialect 2, and the
`params()` method to pass the query vector as a named parameter.

```php
// Pack your float32 values into a binary string.
$queryVector = pack('f*', 0.1, 0.2, 0.3, /* ... 128 floats total */);

$result = $bookIndex
    ->params(['vec' => $queryVector])
    ->dialect(2)
    ->search('*=>[KNN 5 @embedding $vec]');
```

## Spell Checking

`spellCheck()` returns suggestions for potentially misspelled terms in a query.
The optional second argument sets the maximum edit distance (1–4, default 1).

```php
$suggestions = $bookIndex->spellCheck('helo');      // distance 1
$suggestions = $bookIndex->spellCheck('helo', 2);   // distance 2
```

## Synonyms

Synonym groups let you treat different terms as equivalent during search.

```php
// Register 'book', 'novel', and 'tome' as synonyms.
$bookIndex->synUpdate('group1', 'book', 'novel', 'tome');

// Inspect all synonym mappings for the index.
$map = $bookIndex->synDump();
```

## Explaining a Query

An explanation for a query can be generated with the index's explain method.

This can be helpful for understanding why a query is returning a set of results.

```php
$result = $bookIndex
    ->numericFilter('price', 4.99, 19.99)
    ->sortBy('price')
    ->explain('two cities');
```

## Logging Queries

Logging is optional. It can be enabled by injecting a PSR compliant logger, such as Monolog, into a RedisClient instance.

Install Monolog:

```bash
composer require monolog/monolog
```

Inject a logger instance (with a stream handler in this example):

```php
$logger = new Logger('Ehann\RediSearch');
$logger->pushHandler(new StreamHandler('MyLogFile.log', Logger::DEBUG));
$this->redisClient->setLogger($logger);
```


================================================
FILE: docs-site/src/content/docs/suggesting.mdx
================================================
---
title: Suggesting
draft: false
description: Suggestion index creation and management.
---

## Creating a Suggestion Index

Create a suggestion index called "MySuggestions":

```php
use Ehann\RediSearch\Suggestion;

$suggestion = new Suggestion($redisClient, 'MySuggestions');

```

## Adding a Suggestion

Add a suggestion with a score:

```php
$suggestion->add('Tale of Two Cities', 1.10);
```

## Getting a Suggestion

Pass a partial string to the get method:

```php
$result = $suggestion->get('Cities');
```

## Deleting a Suggestion

Pass the entire suggestion string to the delete method:

```php
$result = $suggestion->delete('Tale of Two Cities');
```

## Getting the Number of Possible Suggestions

Simply use the suggestion index's length method:

```php
$numberOfPossibleSuggestions = $suggestion->length();
```


================================================
FILE: docs-site/src/content.config.ts
================================================
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';

export const collections = {
  docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};


================================================
FILE: docs-site/src/styles/custom.css
================================================
:root {
  --sl-color-accent-low: #ffcdd2;
  --sl-color-accent: #e53935;
  --sl-color-accent-high: #b71c1c;
}


================================================
FILE: docs-site/tsconfig.json
================================================
{
  "extends": "astro/tsconfigs/strict"
}


================================================
FILE: justfile
================================================
# Default: run tests with Predis
default: test

# Start Redis (detached)
up:
    docker compose up -d

# Install dependencies
install:
    composer install

# Run tests with default client (Predis)
test:
    vendor/bin/phpunit

# Run tests with specific clients
test-predis:
    REDIS_LIBRARY=Predis vendor/bin/phpunit

test-php-redis:
    REDIS_LIBRARY=PhpRedis vendor/bin/phpunit

test-redis-client:
    REDIS_LIBRARY=RedisClient vendor/bin/phpunit

# Run tests with all three clients sequentially
test-all: test-predis test-php-redis test-redis-client

# Fix code style in-place
fmt:
    vendor/bin/php-cs-fixer fix src

# Check code style without modifying (used by CI)
lint-fmt:
    vendor/bin/php-cs-fixer fix src --dry-run --diff

# Lint PHP syntax
lint:
    find src tests -name "*.php" -print0 | xargs -0 php -l

# Full build: fmt then test
build: fmt test-all


================================================
FILE: phpunit.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="tests/bootstrap.php"
         colors="true"
>
    <testsuites>
        <testsuite name="redisearch-php Test Suite">
            <directory suffix="Test.php">./tests</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory suffix=".php">./src</directory>
        </include>
    </source>
    <php>
        <env name="REDIS_LIBRARY" value="Predis"/>
        <env name="REDIS_HOST" value="localhost"/>
        <env name="REDIS_PORT" value="6381"/>
        <env name="REDIS_DB" value="0"/>
        <env name="LOG_FILE" value="./tests.log"/>
        <env name="IS_LOGGING_ENABLED" value="true"/>
    </php>
</phpunit>


================================================
FILE: src/AbstractIndex.php
================================================
<?php

namespace Ehann\RediSearch;

use Ehann\RedisRaw\RedisRawClientInterface;

abstract class AbstractIndex extends AbstractRediSearchClientAdapter
{
    /** @var string */
    protected $indexName;

    public function __construct(?RedisRawClientInterface $redisClient = null, string $indexName = '')
    {
        parent::__construct($redisClient);
        $this->indexName = $indexName;
    }
}


================================================
FILE: src/AbstractRediSearchClientAdapter.php
================================================
<?php

namespace Ehann\RediSearch;

use Ehann\RedisRaw\RedisRawClientInterface;

abstract class AbstractRediSearchClientAdapter
{
    /** @var RediSearchRedisClient */
    protected $redisClient;

    public function __construct(?RedisRawClientInterface $redisClient = null)
    {
        $this->redisClient = new RediSearchRedisClient($redisClient);
    }

    /**
     * @param string $command
     * @param array $arguments
     * @return mixed
     */
    protected function rawCommand(string $command, array $arguments)
    {
        return $this->redisClient->rawCommand($command, $arguments);
    }
}


================================================
FILE: src/Aggregate/AggregationResult.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate;

class AggregationResult
{
    protected $count;
    protected $documents;

    public function __construct(int $count, array $documents)
    {
        $this->count = $count;
        $this->documents = $documents;
    }

    public function getCount(): int
    {
        return $this->count;
    }

    public function getDocuments(): array
    {
        return $this->documents;
    }

    public static function makeAggregationResult(array $rawRediSearchResult, bool $documentsAsArray)
    {
        if (!$rawRediSearchResult) {
            return false;
        }

        $documentWidth = 1;
        array_shift($rawRediSearchResult);
        $documents = [];
        for ($i = 0; $i < count($rawRediSearchResult); $i += $documentWidth) {
            $document = $documentsAsArray ? [] : new \stdClass();
            $fields = $rawRediSearchResult[$i + ($documentWidth - 1)];
            if (is_array($fields)) {
                for ($j = 0; $j < count($fields); $j += 2) {
                    $normalizedKey = preg_replace("/[^A-Za-z0-9 ]/", '_', $fields[$j]);
                    if ($normalizedKey !== '_') {
                        // Avoid a situation where the key is empty by only trimming the key if it is not "_".
                        $normalizedKey = trim($normalizedKey, '_');
                    }
                    $documentsAsArray ?
                        $document[$normalizedKey] = $fields[$j + 1] :
                        $document->$normalizedKey = $fields[$j + 1];

                    if (strpos($fields[$j], '(')) {
                        $normalizedKeyField = $normalizedKey . '_field';
                        $documentsAsArray ?
                            $document[$normalizedKeyField] = $fields[$j] :
                            $document->$normalizedKeyField = $fields[$j];
                    }
                }
            }
            $documents[] = $document;
        }
        return new AggregationResult(count($documents), $documents);
    }
}


================================================
FILE: src/Aggregate/Builder.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate;

use Ehann\RediSearch\Aggregate\Operations\Apply;
use Ehann\RediSearch\Aggregate\Operations\Filter;
use Ehann\RediSearch\Aggregate\Operations\GroupBy;
use Ehann\RediSearch\Aggregate\Operations\Limit;
use Ehann\RediSearch\Aggregate\Operations\Load;
use Ehann\RediSearch\Aggregate\Operations\SortBy;
use Ehann\RediSearch\Aggregate\Reducers\CountDistinctApproximate;
use Ehann\RediSearch\Aggregate\Reducers\FirstValue;
use Ehann\RediSearch\Aggregate\Reducers\Max;
use Ehann\RediSearch\Aggregate\Reducers\Min;
use Ehann\RediSearch\Aggregate\Reducers\Quantile;
use Ehann\RediSearch\Aggregate\Reducers\StandardDeviation;
use Ehann\RediSearch\Aggregate\Reducers\Sum;
use Ehann\RediSearch\Aggregate\Reducers\ToList;
use Ehann\RediSearch\CanBecomeArrayInterface;
use Ehann\RedisRaw\Exceptions\RedisRawCommandException;
use Ehann\RediSearch\RediSearchRedisClient;
use Ehann\RediSearch\Aggregate\Reducers\Avg;
use Ehann\RediSearch\Aggregate\Reducers\Count;
use Ehann\RediSearch\Aggregate\Reducers\CountDistinct;

class Builder implements BuilderInterface
{
    protected $redis;
    private $indexName = '';
    protected $pipeline = [];
    private $load = [];
    private string $cursor = '';


    public function __construct(RediSearchRedisClient $redis, string $indexName)
    {
        $this->redis = $redis;
        $this->indexName = $indexName;
    }

    /**
     * Get pipeline.
     */
    public function getPipeline(): array
    {
        return $this->pipeline;
    }

    /**
     * Delete all operations from the aggregation pipeline.
     */
    public function clear()
    {
        $this->pipeline = [];
    }

    /**
     * Only use this method if absolutely necessary. It has a detrimental impact on performance.
     * @param array $fieldNames
     * @return BuilderInterface
     */
    public function load(array $fieldNames): BuilderInterface
    {
        $this->pipeline[] = new Load($fieldNames);
        return $this;
    }

    /**
     * @param string|array $fieldName
     * @param CanBecomeArrayInterface|array $reducer
     * @return BuilderInterface
     */
    public function groupBy($fieldName = [], ?CanBecomeArrayInterface $reducer = null): BuilderInterface
    {
        $this->pipeline[] = new GroupBy(is_array($fieldName) ? $fieldName : [$fieldName]);
        if (!is_null($reducer)) {
            $this->reduce($reducer);
        }
        return $this;
    }

    /**
     * @param CanBecomeArrayInterface $reducer
     * @return BuilderInterface
     */
    public function reduce(CanBecomeArrayInterface $reducer): BuilderInterface
    {
        $this->pipeline[] = $reducer;
        return $this;
    }

    /**
     * @param string $fieldName
     * @return BuilderInterface
     */
    public function avg(string $fieldName): BuilderInterface
    {
        $this->pipeline[] = new Avg($fieldName);
        return $this;
    }

    /**
     * @param int $group
     * @return BuilderInterface
     */
    public function count(int $group = 0): BuilderInterface
    {
        $this->pipeline[] = new Count($group);
        return $this;
    }

    /**
     * @param string $fieldName
     * @return BuilderInterface
     */
    public function countDistinct(string $fieldName): BuilderInterface
    {
        $this->pipeline[] = new CountDistinct($fieldName);
        return $this;
    }

    /**
     * @param array|string $fieldName
     * @return BuilderInterface
     */
    public function countDistinctApproximate(string $fieldName): BuilderInterface
    {
        $this->pipeline[] = new CountDistinctApproximate($fieldName);
        return $this;
    }

    /**
     * @param string $fieldName
     * @return BuilderInterface
     */
    public function sum(string $fieldName): BuilderInterface
    {
        $this->pipeline[] = new Sum($fieldName);
        return $this;
    }

    /**
     * @param string $fieldName
     * @return BuilderInterface
     */
    public function max(string $fieldName): BuilderInterface
    {
        $this->pipeline[] = new Max($fieldName);
        return $this;
    }

    /**
     * @param string $fieldName
     * @return BuilderInterface
     */
    public function min(string $fieldName): BuilderInterface
    {
        $this->pipeline[] = new Min($fieldName);
        return $this;
    }

    /**
     * @param string $fieldName
     * @param float $quantile
     * @return BuilderInterface
     */
    public function quantile(string $fieldName, float $quantile): BuilderInterface
    {
        $this->pipeline[] = new Quantile($fieldName, $quantile);
        return $this;
    }

    /**
     * @param string $fieldName
     * @return BuilderInterface
     */
    public function standardDeviation(string $fieldName): BuilderInterface
    {
        $this->pipeline[] = new StandardDeviation($fieldName);
        return $this;
    }

    /**
     * @param string $fieldName
     * @param string|null $byFieldName
     * @param bool $isAscending
     * @return BuilderInterface
     */
    public function firstValue(string $fieldName, ?string $byFieldName = null, bool $isAscending = true): BuilderInterface
    {
        $this->pipeline[] = new FirstValue($fieldName, $byFieldName, $isAscending);
        return $this;
    }

    /**
     * @param string $fieldName
     * @return BuilderInterface
     */
    public function toList(string $fieldName): BuilderInterface
    {
        $this->pipeline[] = new ToList($fieldName);
        return $this;
    }

    /**
     * @param array|string $fieldName
     * @param bool $isAscending
     * @param int $max
     * @return BuilderInterface
     */
    public function sortBy($fieldName, $isAscending = true, int $max = -1): BuilderInterface
    {
        $this->pipeline[] = new SortBy(is_array($fieldName) ? $fieldName : [$fieldName], $isAscending, $max);
        return $this;
    }

    /**
     * @param string $expression An expression that can be used to perform arithmetic operations on numeric properties.
     * @param string $asFieldName The name of the fieldName to add or replace.
     * @return BuilderInterface
     */
    public function apply(string $expression, string $asFieldName): BuilderInterface
    {
        $this->pipeline[] = new Apply($expression, $asFieldName);
        return $this;
    }

    /**
     * @param string $expression
     * @return BuilderInterface
     */
    public function filter(string $expression): BuilderInterface
    {
        $this->pipeline[] = new Filter($expression);
        return $this;
    }

    /**
     * @param int $offset
     * @param int $pageSize
     * @return BuilderInterface
     */
    public function limit(int $offset, int $pageSize = 10): BuilderInterface
    {
        $this->pipeline[] = new Limit($offset, $pageSize);
        return $this;
    }

    /**
     * Enables cursor-based iteration of aggregate results (WITHCURSOR COUNT {n}).
     * Use cursorRead() to retrieve subsequent pages.
     *
     * @param int $count Number of results to return per cursor batch
     * @return BuilderInterface
     */
    public function withCursor(int $count = 100): BuilderInterface
    {
        $this->cursor = "WITHCURSOR COUNT $count";
        return $this;
    }

    /**
     * Reads the next batch from an open aggregate cursor (FT.CURSOR READ).
     *
     * @param int $cursorId  The cursor ID returned in the previous aggregate result
     * @param int $count     Number of results to return
     * @return mixed
     */
    public function cursorRead(int $cursorId, int $count = 100): mixed
    {
        return $this->redis->rawCommand('FT.CURSOR', ['READ', $this->indexName, $cursorId, 'COUNT', $count]);
    }

    /**
     * Deletes an open aggregate cursor, freeing server-side resources (FT.CURSOR DEL).
     *
     * @param int $cursorId
     * @return mixed
     */
    public function cursorDelete(int $cursorId): mixed
    {
        return $this->redis->rawCommand('FT.CURSOR', ['DEL', $this->indexName, $cursorId]);
    }

    /**
     * @param string $query
     * @return array
     */
    public function makeAggregateCommandArguments(string $query): array
    {
        $pipelineOperations = array_map(function (CanBecomeArrayInterface $operation) {
            return $operation->toArray();
        }, $this->pipeline);

        $pipelineOperations = array_reduce($pipelineOperations, function ($prev, $next) {
            return is_null($prev) ? $next : array_merge($prev, $next);
        });

        $cursorArgs = $this->cursor !== '' ? explode(' ', $this->cursor) : [];

        return array_filter(
            array_merge(
                trim($query) === '' ? [$this->indexName] : [$this->indexName, $query],
                $this->load,
                $pipelineOperations,
                $cursorArgs
            ),
            function ($item) {
                return !is_null($item) && $item !== '';
            }
        );
    }

    /**
     * @param string $query
     * @param bool $documentsAsArray
     * @return AggregationResult
     * @throws RedisRawCommandException
     */
    public function search(string $query = '', bool $documentsAsArray = false): AggregationResult
    {
        $args = $this->makeAggregateCommandArguments($query === '' ? '*' : $query);
        $rawResult = $this->redis->rawCommand(
            'FT.AGGREGATE',
            $args
        );

        return $rawResult ? AggregationResult::makeAggregationResult(
            $rawResult,
            $documentsAsArray
        ) : new AggregationResult(0, []);
    }
}


================================================
FILE: src/Aggregate/BuilderInterface.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate;

use Ehann\RediSearch\CanBecomeArrayInterface;

interface BuilderInterface
{
    public function load(array $fieldNames): BuilderInterface;
    public function groupBy($fieldName, ?CanBecomeArrayInterface $reducer = null): BuilderInterface;
    public function reduce(CanBecomeArrayInterface $reducer): BuilderInterface;
    public function sortBy($fieldName, $isAscending = true, int $max = -1): BuilderInterface;
    public function apply(string $expression, string $asName): BuilderInterface;
    public function filter(string $expression): BuilderInterface;
    public function limit(int $offset, int $pageSize = 10): BuilderInterface;
    public function search(string $query = '', bool $documentsAsArray = false): AggregationResult;
    public function avg(string $fieldName): BuilderInterface;
    public function count(int $group = 0): BuilderInterface;
    public function countDistinct(string $fieldName): BuilderInterface;
    public function countDistinctApproximate(string $fieldName): BuilderInterface;
    public function sum(string $fieldName): BuilderInterface;
    public function max(string $fieldName): BuilderInterface;
    public function min(string $fieldName): BuilderInterface;
    public function standardDeviation(string $fieldName): BuilderInterface;
    public function firstValue(string $fieldName, ?string $byFieldName = null, bool $isAscending = true): BuilderInterface;
    public function quantile(string $fieldName, float $quantile): BuilderInterface;
    public function toList(string $fieldName): BuilderInterface;
    public function withCursor(int $count = 100): BuilderInterface;
    public function cursorRead(int $cursorId, int $count = 100): mixed;
    public function cursorDelete(int $cursorId): mixed;
}


================================================
FILE: src/Aggregate/Operations/AbstractFieldNameOperation.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Operations;

use Ehann\RediSearch\CanBecomeArrayInterface;

abstract class AbstractFieldNameOperation implements CanBecomeArrayInterface
{
    protected $operationName;
    protected $fieldNames;

    public function __construct(string $operationName, array $fieldNames)
    {
        $this->fieldNames = $fieldNames;
        $this->operationName = $operationName;
    }

    public function toArray(): array
    {
        return array_merge(
            [$this->operationName, count($this->fieldNames)],
            array_map(function ($fieldName) {
                return "@$fieldName";
            }, $this->fieldNames)
        );
    }
}


================================================
FILE: src/Aggregate/Operations/Apply.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Operations;

use Ehann\RediSearch\CanBecomeArrayInterface;

class Apply implements CanBecomeArrayInterface
{
    public $expression;
    public $asFieldName;

    public function __construct(string $expression, string $asFieldName)
    {
        $this->expression = $expression;
        $this->asFieldName = $asFieldName;
    }

    public function toArray(): array
    {
        return ['APPLY', $this->expression, 'AS', $this->asFieldName];
    }
}


================================================
FILE: src/Aggregate/Operations/Filter.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Operations;

use Ehann\RediSearch\CanBecomeArrayInterface;

class Filter implements CanBecomeArrayInterface
{
    public $expression;

    public function __construct(string $expression)
    {
        $this->expression = $expression;
    }

    public function toArray(): array
    {
        return ['FILTER', $this->expression];
    }
}


================================================
FILE: src/Aggregate/Operations/GroupBy.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Operations;

class GroupBy extends AbstractFieldNameOperation
{
    public function __construct(array $fieldNames)
    {
        parent::__construct('GROUPBY', $fieldNames);
    }
}


================================================
FILE: src/Aggregate/Operations/Limit.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Operations;

use Ehann\RediSearch\CanBecomeArrayInterface;

class Limit implements CanBecomeArrayInterface
{
    private $offset;
    private $pageSize;

    public function __construct(int $offset, int $pageSize)
    {
        $this->offset = $offset;
        $this->pageSize = $pageSize;
    }

    public function toArray(): array
    {
        return ['LIMIT', $this->offset, $this->pageSize];
    }
}


================================================
FILE: src/Aggregate/Operations/Load.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Operations;

class Load extends AbstractFieldNameOperation
{
    public function __construct(array $fieldNames)
    {
        parent::__construct('LOAD', $fieldNames);
    }
}


================================================
FILE: src/Aggregate/Operations/SortBy.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Operations;

class SortBy extends AbstractFieldNameOperation
{
    protected $isAscending;
    protected $max;

    public function __construct(array $fieldNames, $isAscending = true, int $max = -1)
    {
        parent::__construct('SORTBY', $fieldNames);
        $this->isAscending = $isAscending;
        $this->max = $max;
    }

    public function toArray(): array
    {
        $options = [
            $this->isAscending ? 'ASC' : 'DESC'
        ];
        $count = count($this->fieldNames) + count($options);
        if ($this->max >= 0) {
            $options[] = 'MAX';
            $options[] = $this->max;
        }
        return $count > 0 ? array_merge(
            [$this->operationName, $count],
            array_map(function ($fieldName) {
                return "@$fieldName";
            }, $this->fieldNames),
            $options
        ) : [];
    }
}


================================================
FILE: src/Aggregate/Reducers/AbstractFieldNameReducer.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

use Ehann\RediSearch\CanBecomeArrayInterface;

abstract class AbstractFieldNameReducer implements CanBecomeArrayInterface
{
    use Aliasable;

    public $fieldName;
    protected $reducerKeyword;

    public function __construct(string $fieldName, string $alias = '')
    {
        $this->fieldName = $fieldName;
        $this->alias = $alias;
    }

    public function toArray(): array
    {
        return [];
    }

    protected function makeAlias(): string
    {
        return empty($this->alias) ? strtolower($this->reducerKeyword) . "_" . $this->fieldName : $this->alias;
    }
}


================================================
FILE: src/Aggregate/Reducers/Aliasable.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

trait Aliasable
{
    public $alias;
}


================================================
FILE: src/Aggregate/Reducers/Avg.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class Avg extends AbstractFieldNameReducer
{
    protected $reducerKeyword = 'AVG';

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, '1', $this->fieldName, 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/Aggregate/Reducers/Count.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

use Ehann\RediSearch\CanBecomeArrayInterface;

class Count implements CanBecomeArrayInterface
{
    use Aliasable;

    private $group;
    protected $reducerKeyword = 'COUNT';

    public function __construct(int $group)
    {
        $this->group = $group;
    }

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, $this->group, 'AS', empty($this->alias) ? 'count' : $this->alias];
    }
}


================================================
FILE: src/Aggregate/Reducers/CountDistinct.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class CountDistinct extends AbstractFieldNameReducer
{
    protected $reducerKeyword = 'COUNT_DISTINCT';

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, '1', $this->fieldName, 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/Aggregate/Reducers/CountDistinctApproximate.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class CountDistinctApproximate extends AbstractFieldNameReducer
{
    protected $reducerKeyword = 'COUNT_DISTINCTISH';

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, '1', $this->fieldName, 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/Aggregate/Reducers/FirstValue.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class FirstValue extends AbstractFieldNameReducer
{
    protected $reducerKeyword = 'FIRST_VALUE';
    public $byFieldName;
    public $isAscending;

    public function __construct(string $fieldName, ?string $byFieldName = null, bool $isAscending = true)
    {
        parent::__construct($fieldName);
        $this->byFieldName = $byFieldName;
        $this->isAscending = $isAscending;
    }

    public function toArray(): array
    {
        return is_null($this->byFieldName) ?
            ['REDUCE', $this->reducerKeyword, '1', $this->fieldName, 'AS', $this->makeAlias()] :
            ['REDUCE', $this->reducerKeyword, '4', $this->fieldName, 'BY', $this->byFieldName, $this->isAscending ? 'ASC' : 'DESC', 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/Aggregate/Reducers/Max.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class Max extends AbstractFieldNameReducer
{
    protected $reducerKeyword = 'MAX';

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, '1', $this->fieldName, 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/Aggregate/Reducers/Min.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class Min extends AbstractFieldNameReducer
{
    protected $reducerKeyword = 'MIN';

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, '1', $this->fieldName, 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/Aggregate/Reducers/Quantile.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class Quantile extends AbstractFieldNameReducer
{
    public $quantile;
    protected $reducerKeyword = 'QUANTILE';

    public function __construct(string $fieldName, float $quantile)
    {
        parent::__construct($fieldName);
        $this->quantile = $quantile;
    }

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, '2', $this->fieldName, $this->quantile, 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/Aggregate/Reducers/StandardDeviation.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class StandardDeviation extends AbstractFieldNameReducer
{
    protected $reducerKeyword = 'STDDEV';

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, '1', $this->fieldName, 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/Aggregate/Reducers/Sum.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class Sum extends AbstractFieldNameReducer
{
    protected $reducerKeyword = 'SUM';

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, '1', $this->fieldName, 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/Aggregate/Reducers/ToList.php
================================================
<?php

namespace Ehann\RediSearch\Aggregate\Reducers;

class ToList extends AbstractFieldNameReducer
{
    protected $reducerKeyword = 'TOLIST';

    public function toArray(): array
    {
        return ['REDUCE', $this->reducerKeyword, '1', $this->fieldName, 'AS', $this->makeAlias()];
    }
}


================================================
FILE: src/CanBecomeArrayInterface.php
================================================
<?php

namespace Ehann\RediSearch;

interface CanBecomeArrayInterface
{
    public function toArray(): array;
}


================================================
FILE: src/Console/AbstractRedisCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console;

use Ehann\RediSearch\Index;
use Ehann\RediSearch\RediSearchRedisClient;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

abstract class AbstractRedisCommand extends Command
{
    protected function configure(): void
    {
        $this
            ->addOption('host', null, InputOption::VALUE_REQUIRED, 'Redis host', '127.0.0.1')
            ->addOption('port', 'p', InputOption::VALUE_REQUIRED, 'Redis port', 6379)
            ->addOption('password', 'a', InputOption::VALUE_REQUIRED, 'Redis password')
            ->addOption('adapter', null, InputOption::VALUE_REQUIRED, 'Redis adapter (predis, phpredis, redisclient)', 'predis');
    }

    protected function createClient(InputInterface $input): RediSearchRedisClient
    {
        $host = $input->getOption('host');
        $port = (int) $input->getOption('port');
        $password = $input->getOption('password');
        $adapter = strtolower($input->getOption('adapter'));

        try {
            $rawClient = match ($adapter) {
                'predis' => new \Ehann\RedisRaw\PredisAdapter(),
                'phpredis' => new \Ehann\RedisRaw\PhpRedisAdapter(),
                'redisclient' => new \Ehann\RedisRaw\RedisClientAdapter(),
                default => throw new \InvalidArgumentException("Unknown adapter: $adapter. Use predis, phpredis, or redisclient."),
            };
        } catch (\Error $e) {
            $hints = [
                'predis' => 'Install it with: composer require predis/predis',
                'phpredis' => 'Requires the ext-redis PHP extension',
                'redisclient' => 'Install it with: composer require cheprasov/php-redis-client',
            ];
            throw new \RuntimeException(
                "Adapter '$adapter' is not available. " . ($hints[$adapter] ?? $e->getMessage())
            );
        }

        $rawClient->connect($host, $port, 0, $password);

        return new RediSearchRedisClient($rawClient);
    }

    protected function createIndex(InputInterface $input, string $indexName): Index
    {
        $client = $this->createClient($input);

        return (new Index($client, $indexName));
    }

    protected function renderTable(OutputInterface $output, array $headers, array $rows): void
    {
        $table = new Table($output);
        $table->setHeaders($headers);
        $table->setRows($rows);
        $table->render();
    }

    protected function renderJson(OutputInterface $output, mixed $data): void
    {
        $output->writeln(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
    }
}


================================================
FILE: src/Console/Application.php
================================================
<?php

namespace Ehann\RediSearch\Console;

use Ehann\RediSearch\Console\Command\AggregateCommand;
use Ehann\RediSearch\Console\Command\DocumentAddCommand;
use Ehann\RediSearch\Console\Command\DocumentDeleteCommand;
use Ehann\RediSearch\Console\Command\DocumentGetCommand;
use Ehann\RediSearch\Console\Command\ExplainCommand;
use Ehann\RediSearch\Console\Command\IndexCreateCommand;
use Ehann\RediSearch\Console\Command\IndexDropCommand;
use Ehann\RediSearch\Console\Command\IndexInfoCommand;
use Ehann\RediSearch\Console\Command\IndexListCommand;
use Ehann\RediSearch\Console\Command\ProfileCommand;
use Ehann\RediSearch\Console\Command\SearchCommand;
use Ehann\RediSearch\Console\Command\ShellCommand;
use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Command\Command;

class Application extends BaseApplication
{
    public function __construct()
    {
        parent::__construct('redisearch', '1.0.0');

        $this->registerCommands(
            new IndexCreateCommand(),
            new IndexDropCommand(),
            new IndexListCommand(),
            new IndexInfoCommand(),
            new DocumentAddCommand(),
            new DocumentGetCommand(),
            new DocumentDeleteCommand(),
            new SearchCommand(),
            new AggregateCommand(),
            new ExplainCommand(),
            new ProfileCommand(),
            new ShellCommand(),
        );
    }

    private function registerCommands(Command ...$commands): void
    {
        foreach ($commands as $command) {
            if (method_exists($this, 'addCommand')) {
                $this->addCommand($command); // Symfony 8+
            } else {
                $this->add($command); // Symfony <=7
            }
        }
    }
}


================================================
FILE: src/Console/Command/AggregateCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class AggregateCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('aggregate')
            ->setDescription('Run an aggregation query on a RediSearch index')
            ->addArgument('index', InputArgument::REQUIRED, 'Index name')
            ->addArgument('query', InputArgument::OPTIONAL, 'Search query', '*')
            ->addOption('group-by', null, InputOption::VALUE_REQUIRED, 'Group by field name')
            ->addOption('reduce', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Reduce function (func:field, e.g. avg:price, count)')
            ->addOption('sort-by', null, InputOption::VALUE_REQUIRED, 'Sort by field (field:ASC|DESC)')
            ->addOption('apply', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Apply expression (expression:alias)')
            ->addOption('filter', null, InputOption::VALUE_REQUIRED, 'Filter expression')
            ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Limit results (offset,count)')
            ->addOption('load', null, InputOption::VALUE_REQUIRED, 'Load fields (comma-separated)')
            ->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $indexName = $input->getArgument('index');
        $query = $input->getArgument('query');

        $index = $this->createIndex($input, $indexName);
        $builder = $index->makeAggregateBuilder();

        // Load
        $load = $input->getOption('load');
        if ($load !== null) {
            $builder->load(explode(',', $load));
        }

        // Group by with reducers
        $groupBy = $input->getOption('group-by');
        $reducers = $input->getOption('reduce');

        if ($groupBy !== null) {
            if (empty($reducers)) {
                $builder->groupBy($groupBy);
            } else {
                $first = true;
                foreach ($reducers as $reducer) {
                    $parts = explode(':', $reducer);
                    $func = strtolower($parts[0]);
                    $field = $parts[1] ?? null;

                    if ($first) {
                        $builder->groupBy($groupBy);
                        $first = false;
                    }

                    match ($func) {
                        'avg' => $builder->avg($field),
                        'sum' => $builder->sum($field),
                        'min' => $builder->min($field),
                        'max' => $builder->max($field),
                        'count' => $builder->count(),
                        'count_distinct' => $builder->countDistinct($field),
                        'count_distinctish' => $builder->countDistinctApproximate($field),
                        'stddev' => $builder->standardDeviation($field),
                        'tolist' => $builder->toList($field),
                        'first_value' => $builder->firstValue($field),
                        default => throw new \InvalidArgumentException("Unknown reducer: $func"),
                    };
                }
            }
        }

        // Sort by
        $sortBy = $input->getOption('sort-by');
        if ($sortBy !== null) {
            $sortParts = explode(':', $sortBy);
            $builder->sortBy($sortParts[0], $sortParts[1] ?? 'ASC');
        }

        // Apply
        foreach ($input->getOption('apply') as $apply) {
            $pos = strrpos($apply, ':');
            if ($pos !== false) {
                $expression = substr($apply, 0, $pos);
                $alias = substr($apply, $pos + 1);
                $builder->apply($expression, $alias);
            }
        }

        // Filter
        $filter = $input->getOption('filter');
        if ($filter !== null) {
            $builder->filter($filter);
        }

        // Limit
        $limit = $input->getOption('limit');
        if ($limit !== null) {
            $limitParts = explode(',', $limit);
            if (count($limitParts) === 2) {
                $builder->limit((int) $limitParts[0], (int) $limitParts[1]);
            }
        }

        $result = $builder->search($query);

        $documents = $result->getDocuments();
        $count = $result->getCount();

        if ($input->getOption('json')) {
            $this->renderJson($output, [
                'count' => $count,
                'documents' => $documents,
            ]);
            return self::SUCCESS;
        }

        $output->writeln("Aggregation returned $count result(s).");

        if (empty($documents)) {
            return self::SUCCESS;
        }

        $first = $documents[0];
        $headers = array_keys(is_array($first) ? $first : (array) $first);

        $rows = [];
        foreach ($documents as $doc) {
            $row = [];
            $docArray = is_array($doc) ? $doc : (array) $doc;
            foreach ($headers as $header) {
                $val = $docArray[$header] ?? '';
                $row[] = is_array($val) ? json_encode($val) : (string) $val;
            }
            $rows[] = $row;
        }

        $this->renderTable($output, $headers, $rows);

        return self::SUCCESS;
    }
}


================================================
FILE: src/Console/Command/DocumentAddCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Ehann\RediSearch\Fields\NumericField;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class DocumentAddCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('document:add')
            ->setDescription('Add a document to a RediSearch index')
            ->addArgument('index', InputArgument::REQUIRED, 'Index name')
            ->addArgument('id', InputArgument::REQUIRED, 'Document ID')
            ->addArgument('fields', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Field values (field=value ...)')
            ->addOption('replace', null, InputOption::VALUE_NONE, 'Replace if document already exists')
            ->addOption('language', null, InputOption::VALUE_REQUIRED, 'Document language')
            ->addOption('score', null, InputOption::VALUE_REQUIRED, 'Document score (0.0-1.0)');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $indexName = $input->getArgument('index');
        $docId = $input->getArgument('id');
        $fieldArgs = $input->getArgument('fields');

        $index = $this->createIndex($input, $indexName);
        $index->loadFields();

        $document = $index->makeDocument($docId);

        $language = $input->getOption('language');
        if ($language !== null) {
            $document->setLanguage($language);
        }

        $score = $input->getOption('score');
        if ($score !== null) {
            $document->setScore((float) $score);
        }

        $schema = $index->getFields();

        foreach ($fieldArgs as $fieldArg) {
            $pos = strpos($fieldArg, '=');
            if ($pos === false) {
                $output->writeln("<error>Invalid field format: '$fieldArg'. Use field=value.</error>");
                return self::FAILURE;
            }

            $name = substr($fieldArg, 0, $pos);
            $value = substr($fieldArg, $pos + 1);

            if (isset($schema[$name]) && $schema[$name] instanceof NumericField) {
                $value = is_numeric($value) ? (float) $value : $value;
            }

            $document->$name = $value;
        }

        if ($input->getOption('replace')) {
            $index->replace($document);
        } else {
            $index->add($document);
        }

        $output->writeln("Document '$docId' added to index '$indexName'.");

        return self::SUCCESS;
    }
}


================================================
FILE: src/Console/Command/DocumentDeleteCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class DocumentDeleteCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('document:delete')
            ->setDescription('Delete a document from a RediSearch index')
            ->addArgument('index', InputArgument::REQUIRED, 'Index name')
            ->addArgument('id', InputArgument::REQUIRED, 'Document ID');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $indexName = $input->getArgument('index');
        $docId = $input->getArgument('id');

        $index = $this->createIndex($input, $indexName);
        $deleted = $index->delete($docId);

        if ($deleted) {
            $output->writeln("Document '$docId' deleted from index '$indexName'.");
        } else {
            $output->writeln("<error>Document '$docId' not found.</error>");
            return self::FAILURE;
        }

        return self::SUCCESS;
    }
}


================================================
FILE: src/Console/Command/DocumentGetCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class DocumentGetCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('document:get')
            ->setDescription('Get a document by ID from Redis')
            ->addArgument('index', InputArgument::REQUIRED, 'Index name (used for connection context)')
            ->addArgument('id', InputArgument::REQUIRED, 'Document ID (full Redis key)')
            ->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $docId = $input->getArgument('id');
        $client = $this->createClient($input);

        $result = $client->rawCommand('HGETALL', [$docId]);

        if (empty($result)) {
            $output->writeln("<error>Document '$docId' not found.</error>");
            return self::FAILURE;
        }

        $data = $this->parseHashResult($result);

        if ($input->getOption('json')) {
            $this->renderJson($output, $data);
            return self::SUCCESS;
        }

        $rows = [];
        foreach ($data as $field => $value) {
            $rows[] = [$field, (string) $value];
        }

        $this->renderTable($output, ['Field', 'Value'], $rows);

        return self::SUCCESS;
    }

    private function parseHashResult(array $result): array
    {
        if (!array_is_list($result)) {
            return array_map(fn ($v) => (string) $v, $result);
        }

        $data = [];
        for ($i = 0; $i < count($result) - 1; $i += 2) {
            $data[(string) $result[$i]] = (string) $result[$i + 1];
        }

        return $data;
    }
}


================================================
FILE: src/Console/Command/ExplainCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ExplainCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('explain')
            ->setDescription('Show the execution plan for a query (FT.EXPLAIN)')
            ->addArgument('index', InputArgument::REQUIRED, 'Index name')
            ->addArgument('query', InputArgument::REQUIRED, 'Search query');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $indexName = $input->getArgument('index');
        $query = $input->getArgument('query');

        $index = $this->createIndex($input, $indexName);
        $explanation = $index->explain($query);

        $output->writeln($explanation);

        return self::SUCCESS;
    }
}


================================================
FILE: src/Console/Command/IndexCreateCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Ehann\RediSearch\Console\SchemaParser;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class IndexCreateCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('index:create')
            ->setDescription('Create a new RediSearch index from a JSON schema')
            ->addArgument('name', InputArgument::REQUIRED, 'Index name')
            ->addArgument('schema-file', InputArgument::REQUIRED, 'Path to JSON schema file')
            ->addOption('on', null, InputOption::VALUE_REQUIRED, 'Index type (HASH or JSON)', 'HASH')
            ->addOption('prefix', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Key prefix(es)')
            ->addOption('filter', null, InputOption::VALUE_REQUIRED, 'Filter expression')
            ->addOption('stopwords', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Stop words')
            ->addOption('maxtextfields', null, InputOption::VALUE_NONE, 'Allow more than 32 text fields')
            ->addOption('temporary', null, InputOption::VALUE_REQUIRED, 'TTL in seconds for temporary index')
            ->addOption('skipinitialscan', null, InputOption::VALUE_NONE, 'Skip scanning existing keys')
            ->addOption('nooffsets', null, InputOption::VALUE_NONE, 'Disable term offsets')
            ->addOption('nofields', null, InputOption::VALUE_NONE, 'Disable field flags')
            ->addOption('nofreqs', null, InputOption::VALUE_NONE, 'Disable term frequencies');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $indexName = $input->getArgument('name');
        $schemaFile = $input->getArgument('schema-file');

        $index = $this->createIndex($input, $indexName);

        $index->setIndexType($input->getOption('on'));

        $prefixes = $input->getOption('prefix');
        if (!empty($prefixes)) {
            $index->setPrefixes($prefixes);
        }

        $filter = $input->getOption('filter');
        if ($filter !== null) {
            $index->setFilter($filter);
        }

        $stopwords = $input->getOption('stopwords');
        if (!empty($stopwords)) {
            $index->setStopWords($stopwords);
        }

        if ($input->getOption('maxtextfields')) {
            $index->setMaxTextFields();
        }

        $temporary = $input->getOption('temporary');
        if ($temporary !== null) {
            $index->setTemporary((int) $temporary);
        }

        if ($input->getOption('skipinitialscan')) {
            $index->setSkipInitialScan();
        }

        $index->setNoOffsetsEnabled($input->getOption('nooffsets'));
        $index->setNoFieldsEnabled($input->getOption('nofields'));
        $index->setNoFrequenciesEnabled($input->getOption('nofreqs'));

        SchemaParser::applySchema($schemaFile, $index);

        $index->create();

        $output->writeln("Index '$indexName' created successfully.");

        return self::SUCCESS;
    }
}


================================================
FILE: src/Console/Command/IndexDropCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class IndexDropCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('index:drop')
            ->setDescription('Drop a RediSearch index')
            ->addArgument('name', InputArgument::REQUIRED, 'Index name')
            ->addOption('delete-docs', null, InputOption::VALUE_NONE, 'Also delete all indexed documents');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $indexName = $input->getArgument('name');
        $deleteDocs = $input->getOption('delete-docs');

        $index = $this->createIndex($input, $indexName);
        $index->drop($deleteDocs);

        $output->writeln("Index '$indexName' dropped successfully.");

        return self::SUCCESS;
    }
}


================================================
FILE: src/Console/Command/IndexInfoCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class IndexInfoCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('index:info')
            ->setDescription('Show information about a RediSearch index')
            ->addArgument('name', InputArgument::REQUIRED, 'Index name')
            ->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $indexName = $input->getArgument('name');
        $index = $this->createIndex($input, $indexName);
        $info = $index->info();

        if ($input->getOption('json')) {
            $this->renderJson($output, $this->normalizeInfo($info));
            return self::SUCCESS;
        }

        $rows = $this->infoToRows($info);
        $this->renderTable($output, ['Property', 'Value'], $rows);

        return self::SUCCESS;
    }

    private function normalizeInfo(mixed $info): array
    {
        if (!is_array($info)) {
            return [];
        }

        if (!array_is_list($info)) {
            return array_map(
                fn ($v) => is_array($v) ? $v : (string) $v,
                $info
            );
        }

        $result = [];
        for ($i = 0; $i < count($info) - 1; $i += 2) {
            $key = (string) $info[$i];
            $result[$key] = is_array($info[$i + 1]) ? $info[$i + 1] : (string) $info[$i + 1];
        }

        return $result;
    }

    private function infoToRows(mixed $info): array
    {
        $normalized = $this->normalizeInfo($info);
        $rows = [];

        foreach ($normalized as $key => $value) {
            if (is_array($value)) {
                $value = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            }
            $rows[] = [$key, (string) $value];
        }

        return $rows;
    }
}


================================================
FILE: src/Console/Command/IndexListCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class IndexListCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('index:list')
            ->setDescription('List all RediSearch indexes')
            ->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $index = $this->createIndex($input, '_cli_tmp');
        $indexes = $index->listIndexes();
        $indexes = array_map(fn ($i) => (string) $i, $indexes);

        if ($input->getOption('json')) {
            $this->renderJson($output, $indexes);
            return self::SUCCESS;
        }

        if (empty($indexes)) {
            $output->writeln('No indexes found.');
            return self::SUCCESS;
        }

        $this->renderTable($output, ['Index Name'], array_map(fn ($i) => [$i], $indexes));

        return self::SUCCESS;
    }
}


================================================
FILE: src/Console/Command/ProfileCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class ProfileCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('profile')
            ->setDescription('Profile a search query (FT.PROFILE)')
            ->addArgument('index', InputArgument::REQUIRED, 'Index name')
            ->addArgument('query', InputArgument::REQUIRED, 'Search query')
            ->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $indexName = $input->getArgument('index');
        $query = $input->getArgument('query');

        $client = $this->createClient($input);
        $result = $client->rawCommand('FT.PROFILE', [$indexName, 'SEARCH', 'QUERY', $query]);

        if ($input->getOption('json')) {
            $this->renderJson($output, $result);
            return self::SUCCESS;
        }

        $this->printProfileResult($output, $result);

        return self::SUCCESS;
    }

    private function printProfileResult(OutputInterface $output, mixed $result, int $depth = 0): void
    {
        $indent = str_repeat('  ', $depth);

        if (is_array($result)) {
            if (array_is_list($result)) {
                foreach ($result as $item) {
                    $this->printProfileResult($output, $item, $depth);
                }
            } else {
                foreach ($result as $key => $value) {
                    if (is_array($value)) {
                        $output->writeln("{$indent}<info>{$key}:</info>");
                        $this->printProfileResult($output, $value, $depth + 1);
                    } else {
                        $output->writeln("{$indent}<info>{$key}:</info> " . (string) $value);
                    }
                }
            }
        } else {
            $output->writeln($indent . (string) $result);
        }
    }
}


================================================
FILE: src/Console/Command/SearchCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class SearchCommand extends AbstractRedisCommand
{
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('search')
            ->setDescription('Search a RediSearch index')
            ->addArgument('index', InputArgument::REQUIRED, 'Index name')
            ->addArgument('query', InputArgument::REQUIRED, 'Search query')
            ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Limit results (offset,count)', '0,10')
            ->addOption('sort', null, InputOption::VALUE_REQUIRED, 'Sort by field (field:ASC|DESC)')
            ->addOption('fields', null, InputOption::VALUE_REQUIRED, 'Return only these fields (comma-separated)')
            ->addOption('highlight', null, InputOption::VALUE_REQUIRED, 'Highlight fields (comma-separated)')
            ->addOption('scores', null, InputOption::VALUE_NONE, 'Include relevance scores')
            ->addOption('verbatim', null, InputOption::VALUE_NONE, 'Disable stemming')
            ->addOption('language', null, InputOption::VALUE_REQUIRED, 'Stemming language')
            ->addOption('dialect', null, InputOption::VALUE_REQUIRED, 'Query dialect version')
            ->addOption('numeric-filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Numeric filter (field:min:max)')
            ->addOption('tag-filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Tag filter (field:val1,val2)')
            ->addOption('geo-filter', null, InputOption::VALUE_REQUIRED, 'Geo filter (field:lon:lat:radius:unit)')
            ->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $indexName = $input->getArgument('index');
        $query = $input->getArgument('query');

        $index = $this->createIndex($input, $indexName);

        $builder = $index;

        // Limit
        $limit = $input->getOption('limit');
        $parts = explode(',', $limit);
        if (count($parts) === 2) {
            $builder = $builder->limit((int) $parts[0], (int) $parts[1]);
        }

        // Sort
        $sort = $input->getOption('sort');
        if ($sort !== null) {
            $sortParts = explode(':', $sort);
            $builder = $builder->sortBy($sortParts[0], $sortParts[1] ?? 'ASC');
        }

        // Return fields
        $fields = $input->getOption('fields');
        if ($fields !== null) {
            $builder = $builder->return(explode(',', $fields));
        }

        // Highlight
        $highlight = $input->getOption('highlight');
        if ($highlight !== null) {
            $builder = $builder->highlight(explode(',', $highlight));
        }

        // Scores
        if ($input->getOption('scores')) {
            $builder = $builder->withScores();
        }

        // Verbatim
        if ($input->getOption('verbatim')) {
            $builder = $builder->verbatim();
        }

        // Language
        $language = $input->getOption('language');
        if ($language !== null) {
            $builder = $builder->language($language);
        }

        // Dialect
        $dialect = $input->getOption('dialect');
        if ($dialect !== null) {
            $builder = $builder->dialect((int) $dialect);
        }

        // Numeric filters
        foreach ($input->getOption('numeric-filter') as $nf) {
            $nfParts = explode(':', $nf);
            if (count($nfParts) >= 3) {
                $builder = $builder->numericFilter($nfParts[0], (float) $nfParts[1], (float) $nfParts[2]);
            }
        }

        // Tag filters
        foreach ($input->getOption('tag-filter') as $tf) {
            $pos = strpos($tf, ':');
            if ($pos !== false) {
                $field = substr($tf, 0, $pos);
                $values = explode(',', substr($tf, $pos + 1));
                $builder = $builder->tagFilter($field, $values);
            }
        }

        // Geo filter
        $geoFilter = $input->getOption('geo-filter');
        if ($geoFilter !== null) {
            $gfParts = explode(':', $geoFilter);
            if (count($gfParts) >= 5) {
                $builder = $builder->geoFilter(
                    $gfParts[0],
                    (float) $gfParts[1],
                    (float) $gfParts[2],
                    (float) $gfParts[3],
                    $gfParts[4]
                );
            }
        }

        $result = $builder->search($query, true);

        $documents = $result->getDocuments();
        $count = $result->getCount();

        if ($input->getOption('json')) {
            $this->renderJson($output, [
                'count' => $count,
                'documents' => $documents,
            ]);
            return self::SUCCESS;
        }

        $output->writeln("Found $count result(s).");

        if (empty($documents)) {
            return self::SUCCESS;
        }

        $first = $documents[0];
        $headers = array_keys(is_array($first) ? $first : (array) $first);

        $rows = [];
        foreach ($documents as $doc) {
            $row = [];
            $docArray = is_array($doc) ? $doc : (array) $doc;
            foreach ($headers as $header) {
                $val = $docArray[$header] ?? '';
                $row[] = is_array($val) ? json_encode($val) : (string) $val;
            }
            $rows[] = $row;
        }

        $this->renderTable($output, $headers, $rows);

        return self::SUCCESS;
    }
}


================================================
FILE: src/Console/Command/ShellCommand.php
================================================
<?php

namespace Ehann\RediSearch\Console\Command;

use Ehann\RediSearch\Console\AbstractRedisCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ShellCommand extends AbstractRedisCommand
{
    private ?string $defaultIndex = null;

    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('shell')
            ->setDescription('Start an interactive RediSearch shell');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $output->writeln('<info>RediSearch Interactive Shell</info>');
        $output->writeln('Type "help" for available commands, "exit" to quit.');
        $output->writeln('Use "use <index>" to set a default index.');
        $output->writeln('');

        $globalOptions = [
            '--host' => $input->getOption('host'),
            '--port' => $input->getOption('port'),
            '--adapter' => $input->getOption('adapter'),
        ];

        $password = $input->getOption('password');
        if ($password !== null) {
            $globalOptions['--password'] = $password;
        }

        while (true) {
            $prompt = $this->defaultIndex !== null
                ? "redisearch ({$this->defaultIndex})> "
                : 'redisearch> ';

            $line = readline($prompt);

            if ($line === false) {
                $output->writeln('');
                break;
            }

            $line = trim($line);

            if ($line === '') {
                continue;
            }

            readline_add_history($line);

            if ($line === 'exit' || $line === 'quit') {
                $output->writeln('Goodbye.');
                break;
            }

            if ($line === 'help') {
                $this->showHelp($output);
                continue;
            }

            if (str_starts_with($line, 'use ')) {
                $this->defaultIndex = trim(substr($line, 4));
                $output->writeln("Default index set to '{$this->defaultIndex}'.");
                continue;
            }

            $tokens = $this->tokenize($line);

            if (empty($tokens)) {
                continue;
            }

            $commandName = array_shift($tokens);

            try {
                $app = $this->getApplication();
                $command = $app->find($commandName);
            } catch (\Exception $e) {
                $output->writeln("<error>Unknown command: $commandName</error>");
                continue;
            }

            $args = array_merge(['command' => $commandName], $globalOptions);

            $definition = $command->getDefinition();

            // Inject default index for commands that need an 'index' or 'name' argument
            if ($this->defaultIndex !== null) {
                if ($definition->hasArgument('index') || $definition->hasArgument('name')) {
                    $argName = $definition->hasArgument('index') ? 'index' : 'name';
                    $needsIndex = true;

                    // Check if the user already provided the index in tokens
                    foreach ($tokens as $token) {
                        if (!str_starts_with($token, '-')) {
                            $needsIndex = false;
                            break;
                        }
                    }

                    if ($needsIndex) {
                        array_unshift($tokens, $this->defaultIndex);
                    }
                }
            }

            // Parse remaining tokens as positional args and options
            $positionalArgs = [];
            $parsedOptions = [];
            $i = 0;
            while ($i < count($tokens)) {
                $token = $tokens[$i];
                if (str_starts_with($token, '--')) {
                    $eqPos = strpos($token, '=');
                    if ($eqPos !== false) {
                        $parsedOptions[substr($token, 0, $eqPos)] = substr($token, $eqPos + 1);
                    } else {
                        $optionName = $token;
                        if (isset($tokens[$i + 1]) && !str_starts_with($tokens[$i + 1], '--')) {
                            $parsedOptions[$optionName] = $tokens[$i + 1];
                            $i++;
                        } else {
                            $parsedOptions[$optionName] = true;
                        }
                    }
                } else {
                    $positionalArgs[] = $token;
                }
                $i++;
            }

            // Map positional args to argument names
            $argDefinitions = $definition->getArguments();
            $argIndex = 0;
            foreach ($argDefinitions as $argDef) {
                if ($argDef->getName() === 'command') {
                    continue;
                }
                if ($argIndex < count($positionalArgs)) {
                    if ($argDef->isArray()) {
                        $args[$argDef->getName()] = array_slice($positionalArgs, $argIndex);
                        break;
                    }
                    $args[$argDef->getName()] = $positionalArgs[$argIndex];
                    $argIndex++;
                }
            }

            $args = array_merge($args, $parsedOptions);

            try {
                $arrayInput = new ArrayInput($args);
                $arrayInput->setInteractive(false);
                $command->run($arrayInput, $output);
            } catch (\Exception $e) {
                $output->writeln('<error>' . $e->getMessage() . '</error>');
            }

            $output->writeln('');
        }

        return self::SUCCESS;
    }

    private function showHelp(OutputInterface $output): void
    {
        $output->writeln('<info>Available commands:</info>');
        $output->writeln('  index:create <name> <schema-file>  Create an index from JSON schema');
        $output->writeln('  index:drop <name>                  Drop an index');
        $output->writeln('  index:list                         List all indexes');
        $output->writeln('  index:info <name>                  Show index information');
        $output->writeln('  document:add <index> <id> <f=v...> Add a document');
        $output->writeln('  document:get <index> <id>          Get a document');
        $output->writeln('  document:delete <index> <id>       Delete a document');
        $output->writeln('  search <index> <query>             Search an index');
        $output->writeln('  aggregate <index> [query]          Aggregate query');
        $output->writeln('  explain <index> <query>            Explain query plan');
        $output->writeln('  profile <index> <query>            Profile a query');
        $output->writeln('');
        $output->writeln('<info>Shell commands:</info>');
        $output->writeln('  use <index>                        Set default index');
        $output->writeln('  help                               Show this help');
        $output->writeln('  exit / quit                        Exit the shell');
    }

    /**
     * Tokenize input respecting quoted strings.
     */
    private function tokenize(string $input): array
    {
        $tokens = [];
        $current = '';
        $inQuote = null;
        $len = strlen($input);

        for ($i = 0; $i < $len; $i++) {
            $char = $input[$i];

            if ($inQuote !== null) {
                if ($char === $inQuote) {
                    $inQuote = null;
                } else {
                    $current .= $char;
                }
            } elseif ($char === '"' || $char === "'") {
                $inQuote = $char;
            } elseif ($char === ' ') {
                if ($current !== '') {
                    $tokens[] = $current;
                    $current = '';
                }
            } else {
                $current .= $char;
            }
        }

        if ($current !== '') {
            $tokens[] = $current;
        }

        return $tokens;
    }
}


================================================
FILE: src/Console/SchemaParser.php
================================================
<?php

namespace Ehann\RediSearch\Console;

use Ehann\RediSearch\Index;

class SchemaParser
{
    /**
     * Parses a JSON schema file and applies field definitions to the given index.
     *
     * @param string $filePath Path to the JSON schema file
     * @param Index $index The index to apply fields to
     * @return Index
     * @throws \InvalidArgumentException
     * @throws \RuntimeException
     */
    public static function applySchema(string $filePath, Index $index): Index
    {
        if (!file_exists($filePath)) {
            throw new \InvalidArgumentException("Schema file not found: $filePath");
        }

        $json = file_get_contents($filePath);
        $schema = json_decode($json, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \RuntimeException('Invalid JSON in schema file: ' . json_last_error_msg());
        }

        if (!isset($schema['fields']) || !is_array($schema['fields'])) {
            throw new \RuntimeException('Schema must contain a "fields" array.');
        }

        foreach ($schema['fields'] as $field) {
            if (!isset($field['name'], $field['type'])) {
                throw new \RuntimeException('Each field must have a "name" and "type".');
            }

            $name = $field['name'];
            $type = strtoupper($field['type']);
            $sortable = $field['sortable'] ?? false;
            $noindex = $field['noindex'] ?? false;

            match ($type) {
                'TEXT' => $index->addTextField(
                    $name,
                    (float) ($field['weight'] ?? 1.0),
                    $sortable,
                    $noindex
                ),
                'NUMERIC' => $index->addNumericField($name, $sortable, $noindex),
                'TAG' => $index->addTagField(
                    $name,
                    $sortable,
                    $noindex,
                    $field['separator'] ?? ','
                ),
                'GEO' => $index->addGeoField($name, $noindex),
                'VECTOR' => $index->addVectorField(
                    $name,
                    $field['algorithm'] ?? 'FLAT',
                    $field['vectorType'] ?? 'FLOAT32',
                    (int) ($field['dim'] ?? 128),
                    $field['distanceMetric'] ?? 'COSINE',
                    $field['extraAttributes'] ?? []
                ),
                default => throw new \RuntimeException("Unknown field type: $type"),
            };
        }

        return $index;
    }
}


================================================
FILE: src/Document/AbstractDocumentFactory.php
================================================
<?php

namespace Ehann\RediSearch\Document;

use Ehann\RediSearch\Exceptions\FieldNotInSchemaException;
use Ehann\RediSearch\Fields\FieldFactory;
use Ehann\RediSearch\Fields\FieldInterface;

abstract class AbstractDocumentFactory
{
    public static function make(string $id): DocumentInterface
    {
        return new Document($id);
    }

    public static function makeFromArray(array $fields, array $availableSchemaFields, $id = null): DocumentInterface
    {
        $document = new Document($id);
        foreach ($fields as $index => $field) {
            if ($field instanceof FieldInterface) {
                if (!in_array($field->getName(), array_keys($availableSchemaFields))) {
                    throw new FieldNotInSchemaException($field->getName());
                }
                $document->{$field->getName()} = $field;
            } elseif (is_string($index)) {
                if (!isset($availableSchemaFields[$index])) {
                    throw new FieldNotInSchemaException($index);
                }
                $document->{$index} = ($field instanceof FieldInterface) ?
                    $availableSchemaFields[$index]->setValue($field) :
                    FieldFactory::make($index, $field);
            }
        }
        return $document;
    }
}


================================================
FILE: src/Document/Document.php
================================================
<?php

namespace Ehann\RediSearch\Document;

use Ehann\RediSearch\Exceptions\OutOfRangeDocumentScoreException;
use Ehann\RediSearch\Fields\FieldInterface;

class Document implements DocumentInterface
{
    protected $id;
    protected $score = 1.0;
    protected $noSave = false;
    protected $replace = false;
    protected $partial = false;
    protected $noCreate = false;
    protected $payload;
    protected $language;
    protected array $fields = [];

    public function __construct($id = null)
    {
        $this->id = $id ?? uniqid(true);
    }

    public function __set(string $name, FieldInterface $value): void
    {
        $this->fields[$name] = $value;
    }

    public function __get(string $name): ?FieldInterface
    {
        return $this->fields[$name] ?? null;
    }

    public function __isset(string $name): bool
    {
        return isset($this->fields[$name]);
    }

    protected function addFieldsToProperties($properties): array
    {
        /** @var FieldInterface $field */
        foreach ($this->fields as $field) {
            if ($field instanceof FieldInterface && !is_null($field->getValue())) {
                $properties[] = $field->getName();
                $properties[] = $field->getValue();
            }
        }
        return $properties;
    }

    public function getHashDefinition(?array $prefixes = null): array
    {
        $id = $this->getId();
        $completeId = !is_null($prefixes) && count($prefixes) > 0
            ? $prefixes[0] . $id
            : $id;

        $properties = [
            $completeId,
            '__score',
            $this->score,
        ];

        if (!is_null($this->getLanguage())) {
            $properties[] = '__language';
            $properties[] = $this->getLanguage();
        }

        return $this->addFieldsToProperties($properties);
    }

    public function getDefinition(): array
    {
        $properties = [
            $this->getId(),
            $this->getScore(),
        ];

        if ($this->isNoSave()) {
            $properties[] = 'NOSAVE';
        }

        if ($this->isReplace()) {
            $properties[] = 'REPLACE';

            if ($this->isPartial()) {
                $properties[] = 'PARTIAL';
            }

            if ($this->isNoCreate()) {
                $properties[] = 'NOCREATE';
            }
        }

        if (!is_null($this->getLanguage())) {
            $properties[] = 'LANGUAGE';
            $properties[] = $this->getLanguage();
        }

        if (!is_null($this->getPayload())) {
            $properties[] = 'PAYLOAD';
            $properties[] = $this->getPayload();
        }

        $properties[] = 'FIELDS';

        return $this->addFieldsToProperties($properties);
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function setId(string $id)
    {
        $this->id = $id;
        return $this;
    }

    public function getScore(): float
    {
        return $this->score;
    }

    public function setScore(float $score)
    {
        if ($score < 0.0 || $score > 1.0) {
            throw new OutOfRangeDocumentScoreException();
        }
        $this->score = $score;
        return $this;
    }

    public function isNoSave(): bool
    {
        return $this->noSave;
    }

    public function setNoSave(bool $noSave): Document
    {
        $this->noSave = $noSave;
        return $this;
    }

    public function isReplace(): bool
    {
        return $this->replace;
    }

    public function setReplace(bool $replace): Document
    {
        $this->replace = $replace;
        return $this;
    }

    public function isPartial(): bool
    {
        return $this->partial;
    }

    public function setPartial(bool $partial): Document
    {
        $this->partial = $partial;
        return $this;
    }

    public function isNoCreate(): bool
    {
        return $this->noCreate;
    }

    public function setNoCreate(bool $noCreate): Document
    {
        $this->noCreate = $noCreate;
        return $this;
    }

    public function getPayload()
    {
        return $this->payload;
    }

    public function setPayload($payload)
    {
        $this->payload = $payload;
        return $this;
    }

    public function getLanguage()
    {
        return $this->language;
    }

    public function setLanguage($language)
    {
        $this->language = $language;
        return $this;
    }
}


================================================
FILE: src/Document/DocumentInterface.php
================================================
<?php

namespace Ehann\RediSearch\Document;

interface DocumentInterface
{
    public function getHashDefinition(array|null $prefixes): array;
    public function getDefinition(): array;
    public function getId(): string;
    public function setId(string $id);
    public function getScore(): float;
    public function setScore(float $score);
    public function isNoSave(): bool;
    public function setNoSave(bool $noSave): Document;
    public function isReplace(): bool;
    public function setReplace(bool $replace): Document;
    public function isPartial(): bool;
    public function setPartial(bool $partial): Document;
    public function isNoCreate(): bool;
    public function setNoCreate(bool $noCreate): Document;
    public function getPayload();
    public function setPayload($payload);
    public function getLanguage();
    public function setLanguage($language);
}


================================================
FILE: src/Exceptions/AliasDoesNotExistException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class AliasDoesNotExistException extends Exception
{
    public function __construct($message = '', $code = 0, ?Exception $previous = null)
    {
        parent::__construct(
            trim("Alias does not exist. $message"),
            $code,
            $previous
        );
    }
}


================================================
FILE: src/Exceptions/DocumentAlreadyInIndexException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class DocumentAlreadyInIndexException extends Exception
{
    public function __construct($indexName, $documentId, $code = 0, ?Exception $previous = null)
    {
        parent::__construct("Document ($documentId) already in index ($indexName).", $code, $previous);
    }
}


================================================
FILE: src/Exceptions/FieldNotInSchemaException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class FieldNotInSchemaException extends Exception
{
    public function __construct($message = '', $code = 0, ?Exception $previous = null)
    {
        parent::__construct(trim("The field is not a property in the index. $message"), $code, $previous);
    }
}


================================================
FILE: src/Exceptions/NoFieldsInIndexException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class NoFieldsInIndexException extends Exception
{
    public function __construct($message = '', $code = 0, ?Exception $previous = null)
    {
        parent::__construct(
            trim("There needs to be at least one field defined as a property in the index. $message"),
            $code,
            $previous
        );
    }
}


================================================
FILE: src/Exceptions/OutOfRangeDocumentScoreException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class OutOfRangeDocumentScoreException extends Exception
{
    public function __construct($message = '', $code = 0, ?Exception $previous = null)
    {
        parent::__construct(trim("Document scores must be normalized between 0.0 ... 1.0. $message"), $code, $previous);
    }
}


================================================
FILE: src/Exceptions/RediSearchException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class RediSearchException extends Exception
{
}


================================================
FILE: src/Exceptions/UnknownIndexNameException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class UnknownIndexNameException extends Exception
{
    public function __construct($message = '', $code = 0, ?Exception $previous = null)
    {
        parent::__construct(
            trim("Unknown index name. $message"),
            $code,
            $previous
        );
    }
}


================================================
FILE: src/Exceptions/UnknownIndexNameOrNameIsAnAliasItselfException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class UnknownIndexNameOrNameIsAnAliasItselfException extends Exception
{
    public function __construct($message = '', $code = 0, ?Exception $previous = null)
    {
        parent::__construct(
            trim("Unknown index name (or name is an alias itself). $message"),
            $code,
            $previous
        );
    }
}


================================================
FILE: src/Exceptions/UnknownRediSearchCommandException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class UnknownRediSearchCommandException extends Exception
{
    public function __construct($message = '', $code = 0, ?Exception $previous = null)
    {
        parent::__construct(
            trim("Unknown RediSearch command. Are you sure the RediSearch module is enabled in Redis? $message"),
            $code,
            $previous
        );
    }
}


================================================
FILE: src/Exceptions/UnsupportedRediSearchLanguageException.php
================================================
<?php

namespace Ehann\RediSearch\Exceptions;

use Exception;

class UnsupportedRediSearchLanguageException extends Exception
{
    public function __construct($message = '', $code = 0, ?Exception $previous = null)
    {
        parent::__construct(trim("Unsupported language. $message"), $code, $previous);
    }
}


================================================
FILE: src/Fields/AbstractField.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

abstract class AbstractField implements FieldInterface
{
    protected $name;
    protected $value;

    public function __construct(string $name, $value = null)
    {
        $this->name = $name;
        $this->value = $value;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getValue()
    {
        return $this->value;
    }

    public function setValue($value)
    {
        $this->value = $value;
        return $this;
    }

    public function getTypeDefinition(): array
    {
        return [
            $this->getName(),
            $this->getType(),
        ];
    }
}


================================================
FILE: src/Fields/FieldFactory.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

use InvalidArgumentException;

class FieldFactory
{
    public static function make($name, $value, $tagSeparator = ',')
    {
        if (is_array($value)) {
            return (new TagField($name, implode($tagSeparator, $value)))->setSeparator($tagSeparator);
        }
        if ($value instanceof Tag) {
            return new TagField($name, $value);
        }
        if (is_string($value)) {
            return new TextField($name, $value);
        }
        if (is_numeric($value)) {
            return new NumericField($name, $value);
        }
        if ($value instanceof GeoLocation) {
            return new GeoField($name, $value);
        }
        throw new InvalidArgumentException('There is no mapping field type between for the value.');
    }
}


================================================
FILE: src/Fields/FieldInterface.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

interface FieldInterface
{
    public function getTypeDefinition(): array;
    public function getType(): string;
    public function getName(): string;
    public function getValue();
    public function setValue($value);
}


================================================
FILE: src/Fields/GeoField.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

class GeoField extends AbstractField
{
    use Noindex;

    public function getType(): string
    {
        return 'GEO';
    }

    public function getTypeDefinition(): array
    {
        $properties = parent::getTypeDefinition();
        if ($this->isNoindex()) {
            $properties[] = 'NOINDEX';
        }

        return $properties;
    }
}


================================================
FILE: src/Fields/GeoLocation.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

class GeoLocation
{
    protected $longitude;
    protected $latitude;

    public function __construct(float $longitude, float $latitude)
    {
        $this->longitude = $longitude;
        $this->latitude = $latitude;
    }

    public function __toString()
    {
        return "{$this->longitude} {$this->latitude}";
    }
}


================================================
FILE: src/Fields/Noindex.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

trait Noindex
{
    protected $isNoindex = false;

    public function isNoindex(): bool
    {
        return $this->isNoindex;
    }

    public function setNoindex(bool $noindex)
    {
        $this->isNoindex = $noindex;
        return $this;
    }
}


================================================
FILE: src/Fields/NumericField.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

class NumericField extends AbstractField
{
    use Sortable;
    use Noindex;

    public function getType(): string
    {
        return 'NUMERIC';
    }

    public function getTypeDefinition(): array
    {
        $properties = parent::getTypeDefinition();
        if ($this->isSortable()) {
            $properties[] = 'SORTABLE';
        }
        if ($this->isNoindex()) {
            $properties[] = 'NOINDEX';
        }
        return $properties;
    }
}


================================================
FILE: src/Fields/Sortable.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

trait Sortable
{
    protected $isSortable = false;

    public function isSortable(): bool
    {
        return $this->isSortable;
    }

    public function setSortable(bool $sortable)
    {
        $this->isSortable = $sortable;
        return $this;
    }
}


================================================
FILE: src/Fields/Tag.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

class Tag
{
    protected $value;

    public function __construct($value)
    {
        $this->value = $value;
    }

    public function __toString()
    {
        return $this->value;
    }
}


================================================
FILE: src/Fields/TagField.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

class TagField extends AbstractField
{
    use Sortable;
    use Noindex;

    protected $separator = ',';

    public function getType(): string
    {
        return 'TAG';
    }

    public function getSeparator(): string
    {
        return $this->separator;
    }

    public function setSeparator(string $separator)
    {
        $this->separator = $separator;
        return $this;
    }

    public function getTypeDefinition(): array
    {
        $properties = parent::getTypeDefinition();

        $properties[] = 'SEPARATOR';
        $properties[] = $this->getSeparator();

        if ($this->isSortable()) {
            $properties[] = 'SORTABLE';
        }

        if ($this->isNoindex()) {
            $properties[] = 'NOINDEX';
        }

        return $properties;
    }
}


================================================
FILE: src/Fields/TextField.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

class TextField extends AbstractField
{
    use Sortable;
    use Noindex;

    protected $weight = 1.0;
    protected $noStem = false;

    public function getType(): string
    {
        return 'TEXT';
    }

    public function getWeight(): float
    {
        return $this->weight;
    }

    public function setWeight(float $weight)
    {
        $this->weight = $weight;
        return $this;
    }

    public function isNoStem(): bool
    {
        return $this->noStem;
    }

    public function setNoStem(bool $noStem): TextField
    {
        $this->noStem = $noStem;
        return $this;
    }

    public function getTypeDefinition(): array
    {
        $properties = parent::getTypeDefinition();
        if ($this->isNoStem()) {
            $properties[] = 'NOSTEM';
        }
        $properties[] = 'WEIGHT';
        $properties[] = $this->getWeight();
        if ($this->isSortable()) {
            $properties[] = 'SORTABLE';
        }
        if ($this->isNoindex()) {
            $properties[] = 'NOINDEX';
        }
        return $properties;
    }
}


================================================
FILE: src/Fields/VectorField.php
================================================
<?php

namespace Ehann\RediSearch\Fields;

/**
 * Represents a VECTOR field in a RediSearch index. Available in RediSearch v2.2+.
 *
 * Supports FLAT (brute-force) and HNSW (hierarchical navigable small world graph) algorithms
 * for approximate/exact nearest-neighbor search.
 *
 * Example:
 *   $index->addVectorField('embedding', VectorField::ALGORITHM_HNSW, VectorField::TYPE_FLOAT32, 128, VectorField::DISTANCE_COSINE);
 */
class VectorField extends AbstractField
{
    public const ALGORITHM_FLAT = 'FLAT';
    public const ALGORITHM_HNSW = 'HNSW';

    public const TYPE_FLOAT32 = 'FLOAT32';
    public const TYPE_FLOAT64 = 'FLOAT64';

    public const DISTANCE_L2 = 'L2';
    public const DISTANCE_IP = 'IP';
    public const DISTANCE_COSINE = 'COSINE';

    private string $algorithm;
    private string $type;
    private int $dim;
    private string $distanceMetric;
    private array $extraAttributes;

    public function __construct(
        string $name,
        string $algorithm = self::ALGORITHM_FLAT,
        string $type = self::TYPE_FLOAT32,
        int $dim = 128,
        string $distanceMetric = self::DISTANCE_COSINE,
        array $extraAttributes = []
    ) {
        $validAlgorithms = [self::ALGORITHM_FLAT, self::ALGORITHM_HNSW];
        if (!in_array($algorithm, $validAlgorithms, true)) {
            throw new \InvalidArgumentException("Invalid algorithm '$algorithm'. Expected one of: " . implode(', ', $validAlgorithms));
        }
        $validTypes = [self::TYPE_FLOAT32, self::TYPE_FLOAT64];
        if (!in_array($type, $validTypes, true)) {
            throw new \InvalidArgumentException("Invalid type '$type'. Expected one of: " . implode(', ', $validTypes));
        }
        $validMetrics = [self::DISTANCE_L2, self::DISTANCE_IP, self::DISTANCE_COSINE];
        if (!in_array($distanceMetric, $validMetrics, true)) {
            throw new \InvalidArgumentException("Invalid distance metric '$distanceMetric'. Expected one of: " . implode(', ', $validMetrics));
        }
        if ($dim < 1) {
            throw new \InvalidArgumentException("Dimension must be >= 1, got $dim.");
        }

        parent::__construct($name);
        $this->algorithm = $algorithm;
        $this->type = $type;
        $this->dim = $dim;
        $this->distanceMetric = $distanceMetric;
        $this->extraAttributes = $extraAttributes;
    }

    public function getType(): string
    {
        return 'VECTOR';
    }

    public function getTypeDefinition(): array
    {
        // Base attributes: TYPE, DIM, DISTANCE_METRIC (3 pairs = 6 values)
        $attributes = [
            'TYPE', $this->type,
            'DIM', $this->dim,
            'DISTANCE_METRIC', $this->distanceMetric,
        ];

        // Flatten extra attributes (key => value pairs)
        foreach ($this->extraAttributes as $key => $value) {
            $attributes[] = strtoupper($key);
            $attributes[] = $value;
        }

        $attributeCount = count($attributes);

        return array_merge(
            [$this->getName(), 'VECTOR', $this->algorithm, $attributeCount],
            $attributes
        );
    }

    public function getAlgorithm(): string
    {
        return $this->algorithm;
    }

    public function getDim(): int
    {
        return $this->dim;
    }

    public function getDistanceMetric(): string
    {
        return $this->distanceMetric;
    }
}


================================================
FILE: src/Index.php
================================================
<?php

namespace Ehann\RediSearch;

use Ehann\RediSearch\Aggregate\Builder as AggregateBuilder;
use Ehann\RediSearch\Aggregate\BuilderInterface as AggregateBuilderInterface;
use Ehann\RediSearch\Document\AbstractDocumentFactory;
use Ehann\RediSearch\Document\DocumentInterface;
use Ehann\RediSearch\Exceptions\DocumentAlreadyInIndexException;
use Ehann\RediSearch\Exceptions\NoFieldsInIndexException;
use Ehann\RediSearch\Exceptions\UnknownIndexNameException;
use Ehann\RediSearch\Exceptions\UnsupportedRediSearchLanguageException;
use Ehann\RediSearch\Fields\FieldInterface;
use Ehann\RediSearch\Fields\GeoField;
use Ehann\RediSearch\Fields\NumericField;
use Ehann\RediSearch\Fields\TagField;
use Ehann\RediSearch\Fields\TextField;
use Ehann\RediSearch\Fields\VectorField;
use Ehann\RediSearch\Query\Builder as QueryBuilder;
use Ehann\RediSearch\Query\BuilderInterface as QueryBuilderInterface;
use Ehann\RediSearch\Query\SearchResult;
use Ehann\RedisRaw\Exceptions\RawCommandErrorException;
use RedisException;

class Index extends AbstractIndex implements IndexInterface
{
    /** @var bool */
    private $noOffsetsEnabled = false;
    /** @var bool */
    private $noFieldsEnabled = false;
    /** @var bool */
    private $noFrequenciesEnabled = false;
    /** @var array */
    private $stopWords = null;
    /** @var array|null */
    private $prefixes;
    /** @var array */
    private $fields = [];
    private string $indexType = 'HASH';
    private ?string $filter = null;
    private bool $maxTextFields = false;
    private ?int $temporary = null;
    private bool $skipInitialScan = false;

    /**
     * @return mixed
     * @throws NoFieldsInIndexException
     */
    public function create()
    {
        $properties = [$this->getIndexName()];

        $properties[] = 'ON';
        $properties[] = $this->indexType;

        if (!is_null($this->filter)) {
            $properties[] = 'FILTER';
            $properties[] = $this->filter;
        }
        if ($this->maxTextFields) {
            $properties[] = 'MAXTEXTFIELDS';
        }
        if (!is_null($this->temporary)) {
            $properties[] = 'TEMPORARY';
            $properties[] = $this->temporary;
        }
        if ($this->skipInitialScan) {
            $properties[] = 'SKIPINITIALSCAN';
        }

        if (!is_null($this->prefixes)) {
            $properties[] = 'PREFIX';
            $properties[] = count($this->prefixes);
            $properties = array_merge($properties, $this->prefixes);
        }
        if ($this->isNoOffsetsEnabled()) {
            $properties[] = 'NOOFFSETS';
        }
        if ($this->isNoFieldsEnabled()) {
            $properties[] = 'NOFIELDS';
        }
        if ($this->isNoFrequenciesEnabled()) {
            $properties[] = 'NOFREQS';
        }
        if (!is_null($this->stopWords)) {
            $properties[] = 'STOPWORDS';
            $properties[] = count($this->stopWords);
            $properties = array_merge($properties, $this->stopWords);
        }
        $properties[] = 'SCORE_FIELD';
        $properties[] = '__score';
        $properties[] = 'LANGUAGE_FIELD';
        $properties[] = '__language';
        $properties[] = 'SCHEMA';

        $fieldDefinitions = [];
        foreach ($this->getFields() as $field) {
            $fieldDefinitions = array_merge($fieldDefinitions, $field->getTypeDefinition());
        }

        if (count($fieldDefinitions) === 0) {
            throw new NoFieldsInIndexException();
        }

        return $this->rawCommand('FT.CREATE', array_merge($properties, $fieldDefinitions));
    }

    /**
     * @return bool
     */
    public function exists(): bool
    {
        try {
            $this->info();
            return true;
        } catch (UnknownIndexNameException $exception) {
            return false;
        }
    }

    /**
     * @param string $name
     * @param FieldInterface $value
     *
     * @return void
     */
    public function __set(string $name, FieldInterface $value): void
    {
        $this->fields[$name] = $value;
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function __isset(string $name): bool
    {
        return array_key_exists($name, $this->fields) !== false;
    }

    /**
     * @param string $name
     *
     * @return ?FieldInterface
     */
    public function __get(string $name): ?FieldInterface
    {
        return $this->fields[$name] ?? null;
    }

    /**
     * @return array
     */
    public function getFields(): array
    {
        return $this->fields;
    }

    /**
     * Returns an array of fields as cloned objects
     *
     * @return array
     */
    public function getFieldsCloned(): array
    {
        return array_map(fn ($field) => clone $field, $this->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
     * @param bool $noindex
     * @return IndexInterface
     */
    public function addGeoField(string $name, bool $noindex = false): IndexInterface
    {
        $this->$name = (new GeoField($name))->setNoindex($noindex);
        return $this;
    }

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

    /**
     * Adds a VECTOR field to the index schema. Available in RediSearch v2.2+.
     *
     * @param string $name
     * @param string $algorithm FLAT or HNSW
     * @param string $type FLOAT32 or FLOAT64
     * @param int $dim Number of vector dimensions
     * @param string $distanceMetric L2, IP, or COSINE
     * @param array $extraAttributes Additional algorithm-specific attributes (key => value pairs)
     * @return IndexInterface
     */
    public function addVectorField(
        string $name,
        string $algorithm = VectorField::ALGORITHM_FLAT,
        string $type = VectorField::TYPE_FLOAT32,
        int $dim = 128,
        string $distanceMetric = VectorField::DISTANCE_COSINE,
        array $extraAttributes = []
    ): IndexInterface {
        $this->$name = new VectorField($name, $algorithm, $type, $dim, $distanceMetric, $extraAttributes);
        return $this;
    }

    /**
     * @param string $name
     * @return array
     */
    public function tagValues(string $name): array
    {
        return $this->rawCommand('FT.TAGVALS', [$this->getIndexName(), $name]);
    }

    /**
     * @param bool $deleteDocuments When true, also deletes all documents (hashes) associated with this index.
     * @return mixed
     */
    public function drop(bool $deleteDocuments = false)
    {
        $arguments = [$this->getIndexName()];
        if ($deleteDocuments) {
            $arguments[] = 'DD';
        }
        return $this->rawCommand('FT.DROPINDEX', $arguments);
    }

    /**
     * @return mixed
     */
    public function info()
    {
        return $this->rawCommand('FT.INFO', [$this->getIndexName()]);
    }

    /**
     * Loads field definitions from an existing RediSearch index by calling FT.INFO and
     * parsing the schema. This allows working with a pre-existing index without having
     * to manually re-define all fields on every instantiation.
     *
     * @return static
     */
    public function loadFields(): static
    {
        $info = $this->info();

        // FT.INFO returns either:
        //   RESP2 – a flat [key, value, key, value, …] list (array_is_list === true)
        //   RESP3 – an associative map keyed by string (array_is_list === false)
        // Handle both so the same code works regardless of the Redis client / protocol version.
        $attributes = null;
        if (!array_is_list($info)) {
            // RESP3: direct associative lookup (keys may be mixed-case).
            foreach ($info as $k => $v) {
                if (strtolower((string)$k) === 'attributes') {
                    $attributes = $v;
                    break;
                }
            }
        } else {
            // RESP2: iterate in pairs, casting to string to handle Predis Status objects.
            for ($i = 0; $i < count($info) - 1; $i += 2) {
                if ((string)$info[$i] === 'attributes') {
                    $attributes = $info[$i + 1];
                    break;
                }
            }
        }

        if (!is_array($attributes)) {
            return $this;
        }

        foreach ($attributes as $attr) {
            $map = $this->parseAttributeDescriptor($attr);

            $name = (string)($map['attribute'] ?? $map['identifier'] ?? '');
            if ($name === '' || str_starts_with($name, '__')) {
                continue; // skip internal fields like __score, __language
            }

            // Cast each flag to string to handle Predis Status objects.
            // Older Redis Stack puts SORTABLE/NOINDEX/NOSTEM inside a 'flags' sub-array;
            // newer versions represent them as standalone key-value pairs in the descriptor.
            // Check both forms for compatibility.
            $rawFlags = $map['flags'] ?? [];
            $flags = is_array($rawFlags) ? array_map(fn ($f) => strtoupper((string)$f), $rawFlags) : [];
            $sortable = in_array('SORTABLE', $flags, true) || array_key_exists('sortable', $map);
            $noindex = in_array('NOINDEX', $flags, true) || array_key_exists('noindex', $map);
            $type = strtoupper((string)($map['type'] ?? ''));

            $field = match ($type) {
                'TEXT' => (new TextField($name))
                    ->setWeight((float)($map['weight'] ?? 1.0))
                    ->setSortable($sortable)
                    ->setNoindex($noindex)
                    ->setNoStem(in_array('NOSTEM', $flags, true) || array_key_exists('nostem', $map)),
                'NUMERIC' => (new NumericField($name))
                    ->setSortable($sortable)
                    ->setNoindex($noindex),
                'TAG' => (new TagField($name))
                    ->setSeparator((string)($map['separator'] ?? ','))
                    ->setSortable($sortable)
                    ->setNoindex($noindex),
                'GEO' => (new GeoField($name))
                    ->setNoindex($noindex),
                'VECTOR' => new VectorField(
                    $name,
                    strtoupper((string)($map['algorithm'] ?? VectorField::ALGORITHM_FLAT)),
                    strtoupper((string)($map['data_type'] ?? VectorField::TYPE_FLOAT32)),
                    (int)($map['dim'] ?? 128),
                    strtoupper((string)($map['distance_metric'] ?? VectorField::DISTANCE_COSINE)),
                ),
                default => null,
            };

            if ($field !== null) {
                $this->fields[$name] = $field;
            }
        }

        return $this;
    }

    /**
     * Converts an attribute descriptor from FT.INFO into an associative array with
     * lowercased keys.  Handles two wire formats:
     *   RESP2 – flat alternating [key, value, key, value, …] list
     *   RESP3 – already an associative map (array_is_list === false)
     */
    private function parseAttributeDescriptor(array $attr): array
    {
        if (!array_is_list($attr)) {
            // RESP3: already a map — just lowercase the keys.
            $map = [];
            foreach ($attr as $k => $v) {
                $map[strtolower((string)$k)] = $v;
            }
            return $map;
        }

        // RESP2: flat alternating [key, value, …] list.
        // Boolean flags (SORTABLE, NOSTEM, NOINDEX, UNF) are appended as standalone
        // elements after the key-value pairs with no paired value.  This makes the
        // array odd-length for one flag, or even-length for two (where they would
        // mis-parse as a key-value pair).  Handle both:
        //   1. Process the normal pairs.
        //   2. If count is odd, the last element is a standalone flag.
        //   3. Re-scan every element for known flag names and mark them explicitly
        //      (catches the even-length multi-flag mis-pairing case).
        $map = [];
        $i = 0;
        $count = count($attr);
        while ($i < $count - 1) {
            $map[strtolower((string)$attr[$i])] = $attr[$i + 1];
            $i += 2;
        }
        if ($count % 2 === 1) {
            $map[strtolower((string)$attr[$count - 1])] = true;
        }
        static $booleanFlags = ['sortable', 'unf', 'nostem', 'noindex'];
        foreach ($attr as $element) {
            $lower = strtolower((string)$element);
            if (in_array($lower, $booleanFlags, true)) {
                $map[$lower] = true;
            }
        }
        return $map;
    }

    /**
     * Deletes a document by its ID. In RediSearch v2.x documents are stored as Redis hashes,
     * so this deletes the underlying hash key, removing the document from the index.
     *
     * @param string $id The document ID.
     * @param bool $deleteDocument Kept for API compatibility; deletion always removes the hash in v2.x.
     * @return bool
     */
    public function delete($id, $deleteDocument = false)
    {
        $key = $this->buildDocumentKey($id);
        return boolval($this->rawCommand('DEL', [$key]));
    }

    /**
     * @param null $id
     * @return DocumentInterface
     * @throws Exceptions\FieldNotInSchemaException
     */
    public function makeDocument($id = null): DocumentInterface
    {
        $fields = $this->getFieldsCloned();
        $document = AbstractDocumentFactory::makeFromArray($fields, $fields, $id);
        return $document;
    }

    /**
     * @return AggregateBuilderInterface
     */
    public function makeAggregateBuilder(): AggregateBuilderInterface
    {
        return new AggregateBuilder($this->getRedisClient(), $this->getIndexName());
    }

    /**
     * @return RediSearchRedisClient
     */
    public function getRedisClient(): RediSearchRedisClient
    {
        return $this->redisClient;
    }

    /**
     * @param RediSearchRedisClient $redisClient
     * @return IndexInterface
     */
    public function setRedisClient(RediSearchRedisClient $redisClient): IndexInterface
    {
        $this->redisClient = $redisClient;
        return $this;
    }

    /**
     * @return string
     */
    public function getIndexName(): string
    {
        return !is_string($this->indexName) || $this->indexName === '' ? self::class : $this->indexName;
    }

    /**
     * @param string $indexName
     * @return IndexInterface
     */
    public function setIndexName(string $indexName): IndexInterface
    {
        $this->indexName = $indexName;
        return $this;
    }

    /**
     * @return bool
     */
    public function isNoOffsetsEnabled(): bool
    {
        return $this->noOffsetsEnabled;
    }

    /**
     * @param bool $noOffsetsEnabled
     * @return IndexInterface
     */
    public function setNoOffsetsEnabled(bool $noOffsetsEnabled): IndexInterface
    {
        $this->noOffsetsEnabled = $noOffsetsEnabled;
        return $this;
    }

    /**
     * @return bool
     */
    public function isNoFieldsEnabled(): bool
    {
        return $this->noFieldsEnabled;
    }

    /**
     * @param bool $noFieldsEnabled
     * @return IndexInterface
     */
    public function setNoFieldsEnabled(bool $noFieldsEnabled): IndexInterface
    {
        $this->noFieldsEnabled = $noFieldsEnabled;
        return $this;
    }

    /**
     * @return bool
     */
    public function isNoFrequenciesEnabled(): bool
    {
        return $this->noFrequenciesEnabled;
    }

    /**
     * @param bool $noFrequenciesEnabled
     * @return IndexInterface
     */
    public function setNoFrequenciesEnabled(bool $noFrequenciesEnabled): IndexInterface
    {
        $this->noFrequenciesEnabled = $noFrequenciesEnabled;
        return $this;
    }

    /**
     * @param array $stopWords
     * @return IndexInterface
     */
    public function setStopWords(array $stopWords = []): IndexInterface
    {
        $this->stopWords = $stopWords;
        return $this;
    }

    /**
     * Sets the key prefixes used in both FT.CREATE and document key construction.
     *
     * RediSearch supports multiple PREFIX alternatives (e.g. ['post:', 'blog:'])
     * so the index covers hashes under any of those prefixes. However, when writing
     * documents via add()/replace(), only the first prefix is used to construct the
     * hash key. Each prefix must include its own separator (e.g. 'post:', not 'post').
     *
     * @param array $prefixes
     * @return IndexInterface
     */
    public function setPrefixes(array $prefixes = []): IndexInterface
    {
        $this->prefixes = $prefixes;

        return $this;
    }

    /**
     * Sets the index data type. Use 'HASH' (default) or 'JSON' (requires RedisJSON module).
     *
     * @param string $type 'HASH' or 'JSON'
     * @return IndexInterface
     */
    public function setIndexType(string $type): IndexInterface
    {
        $valid = ['HASH', 'JSON'];
        if (!in_array(strtoupper($type), $valid, true)) {
            throw new \InvalidArgumentException("Invalid index type '$type'. Expected one of: " . implode(', ', $valid));
        }
        $this->indexType = strtoupper($type);
        return $this;
    }

    /**
     * Sets a filter expression applied to documents at index creation time.
     * Only documents for which the expression is true are indexed.
     *
     * @param string $expression RediSearch filter expression (e.g. '@age > 18')
     * @return IndexInterface
     */
    public function setFilter(string $expression): IndexInterface
    {
        $this->filter = $expression;
        return $this;
    }

    /**
     * Enables MAXTEXTFIELDS, allowing more than the default 32 text attributes.
     *
     * @return IndexInterface
     */
    public function setMaxTextFields(bool $enable = true): IndexInterface
    {
        $this->maxTextFields = $enable;
        return $this;
    }

    /**
     * Creates a temporary index that expires after the given number of seconds of inactivity.
     *
     * @param int $seconds TTL in seconds
     * @return IndexInterface
     */
    public function setTemporary(int $seconds): IndexInterface
    {
        $this->temporary = $seconds;
        return $this;
    }

    /**
     * When enabled, the index is created without scanning existing keys.
     * Newly added/modified keys matching the prefix will still be indexed.
     *
     * @return IndexInterface
     */
    public function setSkipInitialScan(bool $skip = true): IndexInterface
    {
        $this->skipInitialScan = $skip;
        return $this;
    }

    /**
     * @return QueryBuilder
     */
    protected function makeQueryBuilder(): QueryBuilder
    {
        return (new QueryBuilder($this->redisClient, $this->getIndexName()));
    }

    /**
     * @param string $fieldName
     * @param array $values
     * @param array|null $charactersToEscape
     * @return QueryBuilderInterface
     */
    public function tagFilter(string $fieldName, array $values, ?array $charactersToEscape = null): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->tagFilter($fieldName, $values, $charactersToEscape);
    }

    /**
     * @param string $fieldName
     * @param $min
     * @param $max
     * @return QueryBuilderInterface
     */
    public function numericFilter(string $fieldName, $min, $max = null): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->numericFilter($fieldName, $min, $max);
    }

    /**
     * @param string $fieldName
     * @param float $longitude
     * @param float $latitude
     * @param float $radius
     * @param string $distanceUnit
     * @return QueryBuilderInterface
     */
    public function geoFilter(string $fieldName, float $longitude, float $latitude, float $radius, string $distanceUnit = 'km'): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->geoFilter($fieldName, $longitude, $latitude, $radius, $distanceUnit);
    }

    /**
     * @param string $fieldName
     * @param $order
     * @return QueryBuilderInterface
     */
    public function sortBy(string $fieldName, $order = 'ASC'): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->sortBy($fieldName, $order);
    }

    /**
     * @param string $scoringFunction
     * @return QueryBuilderInterface
     */
    public function scorer(string $scoringFunction): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->scorer($scoringFunction);
    }

    /**
     * @param string $languageName
     * @return QueryBuilderInterface
     */
    public function language(string $languageName): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->language($languageName);
    }

    /**
     * @param string $query
     * @return string
     */
    public function explain(string $query): string
    {
        return $this->makeQueryBuilder()->explain($query);
    }

    /**
     * Sets the query dialect. Available in RediSearch v2.4+.
     *
     * @param int $version Dialect version (1, 2, or 3)
     * @return QueryBuilderInterface
     */
    public function dialect(int $version): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->dialect($version);
    }

    /**
     * Sets named parameters for parameterized queries (e.g. vector KNN search).
     * Emits PARAMS {n} key1 val1 ... in FT.SEARCH. Requires DIALECT 2+.
     *
     * @param array $params Associative array of parameter names to values.
     * @return QueryBuilderInterface
     */
    public function params(array $params): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->params($params);
    }

    /**
     * @param string $query
     * @param bool $documentsAsArray
     * @return SearchResult
     * @throws \Ehann\RedisRaw\Exceptions\RedisRawCommandException
     */
    public function search(string $query = '', bool $documentsAsArray = false): SearchResult
    {
        return $this->makeQueryBuilder()->search($query, $documentsAsArray);
    }

    /**
     * @return QueryBuilderInterface
     */
    public function noContent(): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->noContent();
    }

    /**
     * @param int $offset
     * @param int $pageSize
     * @return QueryBuilderInterface
     */
    public function limit(int $offset, int $pageSize = 10): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->limit($offset, $pageSize);
    }

    /**
     * @param int $number
     * @param array $fields
     * @return QueryBuilderInterface
     */
    public function inFields(int $number, array $fields): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->inFields($number, $fields);
    }

    /**
     * @param int $number
     * @param array $keys
     * @return QueryBuilderInterface
     */
    public function inKeys(int $number, array $keys): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->inKeys($number, $keys);
    }

    /**
     * @param int $slop
     * @return QueryBuilderInterface
     */
    public function slop(int $slop): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->slop($slop);
    }

    /**
     * @return QueryBuilderInterface
     */
    public function noStopWords(): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->noStopWords();
    }

    /**
     * @return QueryBuilderInterface
     */
    public function withPayloads(): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->withPayloads();
    }

    /**
     * @return QueryBuilderInterface
     */
    public function withScores(): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->withScores();
    }

    /**
     * @return QueryBuilderInterface
     */
    public function verbatim(): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->verbatim();
    }

    /**
     * Builds the Redis key for a document, incorporating any configured prefix.
     *
     * Uses only the first configured prefix. RediSearch's PREFIX option accepts
     * multiple alternative prefixes (e.g. PREFIX 2 post: blog:), meaning the
     * index covers hashes under either prefix. When writing a document, a single
     * concrete prefix must be chosen — the first entry is used. Prefixes should
     * include their own separator (e.g. 'post:' not 'post').
     */
    private function buildDocumentKey(string $id): string
    {
        return !is_null($this->prefixes) && count($this->prefixes) > 0
            ? $this->prefixes[0] . $id
            : $id;
    }

    /**
     * Core HSET operation — stores a document as a Redis hash. No existence checks.
     * Used internally by addMany() and addHash().
     *
     * @param DocumentInterface $document
     * @return mixed
     */
    protected function _add(DocumentInterface $document)
    {
        if (is_null($document->getId())) {
            $document->setId(uniqid(true));
        }

        $properties = $document->getHashDefinition($this->prefixes);
        return $this->rawCommand('HSET', $properties);
    }

    /**
     * @param array $documents
     * @param bool $disableAtomicity
     * @param bool $replace Kept for API compatibility; HSET always upserts.
     */
    public function addMany(array $documents, $disableAtomicity = false, $replace = false)
    {
        $result = null;

        $pipe = $this->redisClient->multi($disableAtomicity);
        foreach ($documents as $document) {
            if (is_array($document)) {
                $document = $this->arrayToDocument($document);
            }
            $this->_add($document);
        }
        try {
            $pipe->exec();
        } catch (RedisException $exception) {
            $result = $exception->getMessage();
        } catch (RawCommandErrorException $exception) {
            $result = $exception->getPrevious()->getMessage();
        }

        if ($result) {
            $this->redisClient->validateRawCommandResults($result, 'PIPE', [$this->indexName, '*MANY']);
        }
    }

    /**
     * @param $document
     * @return DocumentInterface
     * @throws Exceptions\FieldNotInSchemaException
     */
    public function arrayToDocument($document): DocumentInterface
    {
        return is_array($document) ? AbstractDocumentFactory::makeFromArray($document, $this->getFields()) : $document;
    }

    /**
     * Adds a new document to the index. Throws if the index does not exist or the
     * document ID already exists in Redis.
     *
     * @param $document
     * @return bool
     * @throws Exceptions\FieldNotInSchemaException
     * @throws DocumentAlreadyInIndexException
     * @throws UnsupportedRediSearchLanguageException
     */
    public function add($document): bool
    {
        $typedDocument = $this->arrayToDocument($document);

        // Ensure the index exists — throws UnknownIndexNameException if not.
        $this->info();

        // Validate language before storing.
        if (!is_null($typedDocument->getLanguage()) && !Language::isSupported($typedDocument->getLanguage())) {
            throw new UnsupportedRediSearchLanguageException();
        }

        if (is_null($typedDocument->getId())) {
            $typedDocument->setId(uniqid(true));
        }

        $key = $this->buildDocumentKey($typedDocument->getId());
        if ($this->rawCommand('EXISTS', [$key])) {
            throw new DocumentAlreadyInIndexException($this->getIndexName(), $typedDocument->getId());
        }

        return boolval($this->_add($typedDocument));
    }

    /**
     * Updates (upserts) a document in the index using HSET.
     *
     * @param $document
     * @return bool
     * @throws Exceptions\FieldNotInSchemaException
     */
    public function replace($document): bool
    {
        $this->_add($this->arrayToDocument($document));
        return true;
    }

    /**
     * @param array $documents
     * @param bool $disableAtomicity
     */
    public function replaceMany(array $documents, $disableAtomicity = false)
    {
        $this->addMany($documents, $disableAtomicity, true);
    }

    /**
     * Adds or replaces a document stored as a Redis hash. Upsert semantics (HSET).
     *
     * @param $document
     * @return bool
     * @throws Exceptions\FieldNotInSchemaException
     */
    public function addHash($document): bool
    {
        $this->_add($this->arrayToDocument($document));
        return true;
    }

    /**
     * Replaces a document stored as a Redis hash. Alias for addHash() — HSET always upserts.
     *
     * @param $document
     * @return bool
     * @throws Exceptions\FieldNotInSchemaException
     */
    public function replaceHash($document): bool
    {
        $this->_add($this->arrayToDocument($document));
        return true;
    }

    /**
     * @param array $fields
     * @return QueryBuilderInterface
     */
    public function return(array $fields): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->return($fields);
    }

    /**
     * @param array $fields
     * @param int $fragmentCount
     * @param int $fragmentLength
     * @param string $separator
     * @return QueryBuilderInterface
     */
    public function summarize(array $fields, int $fragmentCount = 3, int $fragmentLength = 50, string $separator = '...'): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->summarize($fields, $fragmentCount, $fragmentLength, $separator);
    }

    /**
     * @param array $fields
     * @param string $openTag
     * @param string $closeTag
     * @return QueryBuilderInterface
     */
    public function highlight(array $fields, string $openTag = '<strong>', string $closeTag = '</strong>'): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->highlight($fields, $openTag, $closeTag);
    }

    /**
     * @param string $expander
     * @return QueryBuilderInterface
     */
    public function expander(string $expander): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->expander($expander);
    }

    /**
     * @param string $payload
     * @return QueryBuilderInterface
     */
    public function payload(string $payload): QueryBuilderInterface
    {
        return $this->makeQueryBuilder()->payload($payload);
    }

    /**
     * @param string $query
     * @return int
     */
    public function count(string $query = ''): int
    {
        return $this->makeQueryBuilder()->count($query);
    }

    /**
     * @param string $name
     * @return bool
     */
    public function addAlias(string $name): bool
    {
        return $this->rawCommand('FT.ALIASADD', [$name, $this->getIndexName()]);
    }

    /**
     * @param string $name
     * @return bool
     */
    public function updateAlias(string $name): bool
    {
        return $this->rawCommand('FT.ALIASUPDATE', [$name, $this->getIndexName()]);
    }

    /**
     * @param string $name
     * @return bool
     */
    public function deleteAlias(string $name): bool
    {
        return $this->rawCommand('FT.ALIASDEL', [$name]);
    }

    /**
     * Adds one or more fields to an existing index schema (FT.ALTER).
     * Existing documents are not re-indexed for new fields; only newly
     * added/updated documents will include the new fields.
     *
     * @param FieldInterface ...$fields
     * @return mixed
     */
    public function alter(FieldInterface ...$fields): mixed
    {
        $args = [$this->getIndexName(), 'SCHEMA', 'ADD'];
        foreach ($fields as $field) {
            $args = array_merge($args, $field->getTypeDefinition());
        }
        return $this->rawCommand('FT.ALTER', $args);
    }

    /**
     * Returns a list of all index names in the current Redis instance (FT._LIST).
     *
     * @return array
     */
    public function listIndexes(): array
    {
        return $this->rawCommand('FT._LIST', []) ?? [];
    }

    /**
     * Creates or updates a synonym group with the given terms (FT.SYNUPDATE).
     *
     * @param string $synonymGroupId
     * @param string ...$terms
     * @return mixed
     */
    public function synUpdate(string $synonymGroupId, string ...$terms): mixed
    {
        return $this->rawCommand('FT.SYNUPDATE', array_merge([$this->getIndexName(), $synonymGroupId], $terms));
    }

    /**
     * Returns all synonym mappings for the index (FT.SYNDUMP).
     *
     * @return array
     */
    public function synDump(): array
    {
        return $this->rawCommand('FT.SYNDUMP', [$this->getIndexName()]) ?? [];
    }

    /**
     * Performs spell checking on a query string (FT.SPELLCHECK).
     * Returns suggestions for misspelled terms.
     *
     * @param string $query
     * @param int $distance Maximum Levenshtein distance for suggestions (1–4)
     * @return array
     */
    public function spellCheck(string $query, int $distance = 1): array
    {
        return $this->rawCommand('FT.SPELLCHECK', [$this->getIndexName(), $query, 'DISTANCE', $distance]) ?? [];
    }

    /**
     * Adds terms to a custom dictionary used by FT.SPELLCHECK (FT.DICTADD).
     *
     * @param string $dict Dictionary name
     * @param string ...$terms
     * @return int Number of terms added
     */
    public function dictAdd(string $dict, string ...$terms): int
    {
        return (int) $this->rawCommand('FT.DICTADD', array_merge([$dict], $terms));
    }

    /**
     * Removes terms from a custom dictionary (FT.DICTDEL).
     *
     * @param string $dict Dictionary name
     * @param string ...$terms
     * @return int Number of terms removed
     */
    public function dictDelete(string $dict, string ...$terms): int
    {
        return (int) $this->rawCommand('FT.DICTDEL', array_merge([$dict], $terms));
    }

    /**
     * Returns all terms in a custom dictionary (FT.DICTDUMP).
     *
     * @param string $dict Dictionary name
     * @return array
     */
    public function dictDump(string $dict): array
    {
        return $this->rawCommand('FT.DICTDUMP', [$dict]) ?? [];
    }
}


================================================
FILE: src/IndexInterface.php
================================================
<?php

namespace Ehann\RediSearch;

use Ehann\RediSearch\Aggregate\BuilderInterface as AggregateBuilderInterface;
use Ehann\RediSearch\Document\DocumentInterface;
use Ehann\RediSearch\Fields\FieldInterface;
use Ehann\RediSearch\Fields\VectorField;
use Ehann\RediSearch\Query\BuilderInterface;

interface IndexInterface extends BuilderInterface
{
    public function create();
    public function exists(): bool;
    public function drop(bool $deleteDocuments = false);
    public function info();
    public function loadFields(): static;
    public function delete($id, $deleteDocument = false);
    public function getFields(): array;
    public function makeDocument($id = null): DocumentInterface;
    public function makeAggregateBuilder(): AggregateBuilderInterface;
    public function getRedisClient(): RediSearchRedisClient;
    public function setRedisClient(RediSearchRedisClient $redisClient): IndexInterface;
    public function getIndexName(): string;
    public function setIndexName(string $indexName): IndexInterface;
    public function isNoOffsetsEnabled(): bool;
    public function setNoOffsetsEnabled(bool $noOffsetsEnabled): IndexInterface;
    public function isNoFieldsEnabled(): bool;
    public function setNoFieldsEnabled(bool $noFieldsEnabled): IndexInterface;
    public function isNoFrequenciesEnabled(): bool;
    public function setNoFrequenciesEnabled(bool $noFieldsEnabled): IndexInterface;
    public function setStopWords(array $stopWords): IndexInterface;
    public function setPrefixes(array $prefixes): IndexInterface;
    public function addTextField(string $name, float $weight = 1.0, bool $sortable = false, bool $noindex = false): IndexInterface;
    public function addNumericField(string $name, bool $sortable = false, bool $noindex = false): IndexInterface;
    public function addGeoField(string $name, bool $noindex = false): IndexInterface;
    public function addTagField(string $name, bool $sortable = false, bool $noindex = false, string $separator = ','): IndexInterface;
    public function addVectorField(
        string $name,
        string $algorithm = VectorField::ALGORITHM_FLAT,
        string $type = VectorField::TYPE_FLOAT32,
        int $dim = 128,
        string $distanceMetric = VectorField::DISTANCE_COSINE,
        array $extraAttributes = []
    ): IndexInterface;
    public function tagValues(string $name): array;
    public function add($document): bool;
    public function addMany(array $documents, $disableAtomicity = false, $replace = false);
    public function replace($document): bool;
    public function replaceMany(array $documents, $disableAtomicity = false);
    public function addHash($document): bool;
    public function replaceHash($document): bool;
    public function addAlias(string $name): bool;
    public function updateAlias(string $name): bool;
    public function deleteAlias(string $name): bool;
    public function params(array $params): BuilderInterface;
    public function setIndexType(string $type): IndexInterface;
    public function setFilter(string $expression): IndexInterface;
    public function setMaxTextFields(bool $enable = true): IndexInterface;
    public function setTemporary(int $seconds): IndexInterface;
    public function setSkipInitialScan(bool $skip = true): IndexInterface;
    public function alter(FieldInterface ...$fields): mixed;
    public function listIndexes(): array;
    public function synUpdate(string $synonymGroupId, string ...$terms): mixed;
    public function synDump(): array;
    public function spellCheck(string $query, int $distance = 1): array;
    public function dictAdd(string $dict, string ...$terms): int;
    public function dictDelete(string $dict, string ...$terms): int;
    public function dictDump(string $dict): array;
}


================================================
FILE: src/Language.php
================================================
<?php

namespace Ehann\RediSearch;

class Language
{
    public const ARABIC = 'arabic';
    public const BASQUE = 'basque';
    public const CATALAN = 'catalan';
    public const CHINESE = 'chinese';
    public const DANISH = 'danish';
    public const DUTCH = 'dutch';
    public const ENGLISH = 'english';
    public const FINNISH = 'finnish';
    public const FRENCH = 'french';
    public const GERMAN = 'german';
    public const GREEK = 'greek';
    public const HUNGARIAN = 'hungarian';
    public const INDONESIAN = 'indonesian';
    public const IRISH = 'irish';
    public const ITALIAN = 'italian';
    public const LITHUANIAN = 'lithuanian';
    public const NEPALI = 'nepali';
    public const NORWEGIAN = 'norwegian';
    public const PORTUGUESE = 'portuguese';
    public const ROMANIAN = 'romanian';
    public const RUSSIAN = 'russian';
    public const SPANISH = 'spanish';
    public const SWEDISH = 'swedish';
    public const TAMIL = 'tamil';
    public const TURKISH = 'turkish';

    private static array $supported = [
        self::ARABIC,
        self::BASQUE,
        self::CATALAN,
        self::CHINESE,
        self::DANISH,
        self::DUTCH,
        self::ENGLISH,
        self::FINNISH,
        self::FRENCH,
        self::GERMAN,
        self::GREEK,
        self::HUNGARIAN,
        self::INDONESIAN,
        self::IRISH,
        self::ITALIAN,
        self::LITHUANIAN,
        self::NEPALI,
        self::NORWEGIAN,
        self::PORTUGUESE,
        self::ROMANIAN,
        self::RUSSIAN,
        self::SPANISH,
        self::SWEDISH,
        self::TAMIL,
        self::TURKISH,
    ];

    public static function isSupported(string $language): bool
    {
        return in_array(strtolower($language), self::$supported, true);
    }

    public static function getSupported(): array
    {
        return self::$supported;
    }
}


================================================
FILE: src/Query/Builder.php
================================================
<?php

namespace Ehann\RediSearch\Query;

use Ehann\RediSearch\RediSearchRedisClient;
use InvalidArgumentException;

class Builder implements BuilderInterface
{
    public const GEO_FILTER_UNITS = ['m', 'km', 'mi', 'ft'];

    protected $return = '';
    protected $summarize = '';
    protected $highlight = '';
    protected $expander = '';
    protected $payload = '';
    protected $limit = '';
    protected $slop = null;
    protected $verbatim = '';
    protected $withScores = '';
    protected $withPayloads = '';
    protected $noStopWords = '';
    protected $noContent = '';
    protected $inFields = '';
    protected $inKeys = '';
    protected $tagFilters = [];
    protected $numericFilters = [];
    protected $geoFilters = [];
    protected $sortBy = '';
    protected $scorer = '';
    protected $language = '';
    protected $dialect = '';
    protected array $params = [];
    protected $redis;
    private $indexName;

    public function __construct(RediSearchRedisClient $redis, string $indexName)
    {
        $this->redis = $redis;
        $this->indexName = $indexName;
    }

    public function noContent(): BuilderInterface
    {
        $this->noContent = 'NOCONTENT';
        return $this;
    }

    public function return(array $fields): BuilderInterface
    {
        $count = empty($fields) ? 0 : count($fields);
        $field = implode(' ', $fields);
        $this->return = "RETURN $count $field";
        return $this;
    }

    public function summarize(array $fields, int $fragmentCount = 3, int $fragmentLength = 50, string $separator = '...'): BuilderInterface
    {
        $count = empty($fields) ? 0 : count($fields);
        $field = implode(' ', $fields);
        $this->summarize = "SUMMARIZE FIELDS $count $field FRAGS $fragmentCount LEN $fragmentLength SEPARATOR $separator";
        return $this;
    }

    public function highlight(array $fields, string $openTag = '<strong>', string $closeTag = '</strong>'): BuilderInterface
    {
        $count = empty($fields) ? 0 : count($fields);
        $field = implode(' ', $fields);
        $this->highlight = "HIGHLIGHT FIELDS $count $field TAGS $openTag $closeTag";
        return $this;
    }

    public function expander(string $expander): BuilderInterface
    {
        $this->expander = "EXPANDER $expander";
        return $this;
    }

    public function payload(string $payload): BuilderInterface
    {
        $this->payload = "PAYLOAD $payload";
        return $this;
    }

    public function limit(int $offset, int $pageSize = 10): BuilderInterface
    {
        $this->limit = "LIMIT $offset $pageSize";
        return $this;
    }

Download .txt
gitextract__zin6sq1/

├── .claude/
│   └── skills/
│       ├── contribute/
│       │   └── SKILL.md
│       └── unit-test/
│           └── SKILL.md
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci.yml
│       └── docs.yml
├── .gitignore
├── .php-cs-fixer.php
├── CLAUDE.md
├── LICENSE
├── README.md
├── bin/
│   └── redisearch
├── composer.json
├── docker-compose.yml
├── docs-site/
│   ├── .gitignore
│   ├── astro.config.mjs
│   ├── package.json
│   ├── src/
│   │   ├── content/
│   │   │   └── docs/
│   │   │       ├── aggregating.mdx
│   │   │       ├── changelog.mdx
│   │   │       ├── cli.mdx
│   │   │       ├── index.mdx
│   │   │       ├── indexing.mdx
│   │   │       ├── laravel-support.mdx
│   │   │       ├── searching.mdx
│   │   │       └── suggesting.mdx
│   │   ├── content.config.ts
│   │   └── styles/
│   │       └── custom.css
│   └── tsconfig.json
├── justfile
├── phpunit.xml
├── src/
│   ├── AbstractIndex.php
│   ├── AbstractRediSearchClientAdapter.php
│   ├── Aggregate/
│   │   ├── AggregationResult.php
│   │   ├── Builder.php
│   │   ├── BuilderInterface.php
│   │   ├── Operations/
│   │   │   ├── AbstractFieldNameOperation.php
│   │   │   ├── Apply.php
│   │   │   ├── Filter.php
│   │   │   ├── GroupBy.php
│   │   │   ├── Limit.php
│   │   │   ├── Load.php
│   │   │   └── SortBy.php
│   │   └── Reducers/
│   │       ├── AbstractFieldNameReducer.php
│   │       ├── Aliasable.php
│   │       ├── Avg.php
│   │       ├── Count.php
│   │       ├── CountDistinct.php
│   │       ├── CountDistinctApproximate.php
│   │       ├── FirstValue.php
│   │       ├── Max.php
│   │       ├── Min.php
│   │       ├── Quantile.php
│   │       ├── StandardDeviation.php
│   │       ├── Sum.php
│   │       └── ToList.php
│   ├── CanBecomeArrayInterface.php
│   ├── Console/
│   │   ├── AbstractRedisCommand.php
│   │   ├── Application.php
│   │   ├── Command/
│   │   │   ├── AggregateCommand.php
│   │   │   ├── DocumentAddCommand.php
│   │   │   ├── DocumentDeleteCommand.php
│   │   │   ├── DocumentGetCommand.php
│   │   │   ├── ExplainCommand.php
│   │   │   ├── IndexCreateCommand.php
│   │   │   ├── IndexDropCommand.php
│   │   │   ├── IndexInfoCommand.php
│   │   │   ├── IndexListCommand.php
│   │   │   ├── ProfileCommand.php
│   │   │   ├── SearchCommand.php
│   │   │   └── ShellCommand.php
│   │   └── SchemaParser.php
│   ├── Document/
│   │   ├── AbstractDocumentFactory.php
│   │   ├── Document.php
│   │   └── DocumentInterface.php
│   ├── Exceptions/
│   │   ├── AliasDoesNotExistException.php
│   │   ├── DocumentAlreadyInIndexException.php
│   │   ├── FieldNotInSchemaException.php
│   │   ├── NoFieldsInIndexException.php
│   │   ├── OutOfRangeDocumentScoreException.php
│   │   ├── RediSearchException.php
│   │   ├── UnknownIndexNameException.php
│   │   ├── UnknownIndexNameOrNameIsAnAliasItselfException.php
│   │   ├── UnknownRediSearchCommandException.php
│   │   └── UnsupportedRediSearchLanguageException.php
│   ├── Fields/
│   │   ├── AbstractField.php
│   │   ├── FieldFactory.php
│   │   ├── FieldInterface.php
│   │   ├── GeoField.php
│   │   ├── GeoLocation.php
│   │   ├── Noindex.php
│   │   ├── NumericField.php
│   │   ├── Sortable.php
│   │   ├── Tag.php
│   │   ├── TagField.php
│   │   ├── TextField.php
│   │   └── VectorField.php
│   ├── Index.php
│   ├── IndexInterface.php
│   ├── Language.php
│   ├── Query/
│   │   ├── Builder.php
│   │   ├── BuilderInterface.php
│   │   └── SearchResult.php
│   ├── RediSearchRedisClient.php
│   ├── RuntimeConfiguration.php
│   └── Suggestion.php
└── tests/
    ├── RediSearch/
    │   ├── Aggregate/
    │   │   ├── AggregationResultTest.php
    │   │   └── BuilderTest.php
    │   ├── Document/
    │   │   └── DocumentTest.php
    │   ├── Exceptions/
    │   │   ├── FieldNotInSchemaExceptionTest.php
    │   │   ├── NoFieldsInIndexExceptionTest.php
    │   │   ├── RedisRawCommandExceptionTest.php
    │   │   └── UnknownIndexNameExceptionTest.php
    │   ├── Fields/
    │   │   ├── FieldFactoryTest.php
    │   │   ├── GeoFieldTest.php
    │   │   ├── GeoLocationTest.php
    │   │   ├── NumericFieldTest.php
    │   │   └── TextFieldTest.php
    │   ├── IndexTest.php
    │   ├── Query/
    │   │   └── BuilderTest.php
    │   ├── Redis/
    │   │   └── RedisClientTest.php
    │   ├── RuntimeConfigurationTest.php
    │   └── SuggestionTest.php
    ├── RediSearchTestCase.php
    ├── Stubs/
    │   ├── IndexWithoutFields.php
    │   ├── TestDocument.php
    │   └── TestIndex.php
    └── bootstrap.php
Download .txt
SYMBOL INDEX (685 symbols across 96 files)

FILE: src/AbstractIndex.php
  class AbstractIndex (line 7) | abstract class AbstractIndex extends AbstractRediSearchClientAdapter
    method __construct (line 12) | public function __construct(?RedisRawClientInterface $redisClient = nu...

FILE: src/AbstractRediSearchClientAdapter.php
  class AbstractRediSearchClientAdapter (line 7) | abstract class AbstractRediSearchClientAdapter
    method __construct (line 12) | public function __construct(?RedisRawClientInterface $redisClient = null)
    method rawCommand (line 22) | protected function rawCommand(string $command, array $arguments)

FILE: src/Aggregate/AggregationResult.php
  class AggregationResult (line 5) | class AggregationResult
    method __construct (line 10) | public function __construct(int $count, array $documents)
    method getCount (line 16) | public function getCount(): int
    method getDocuments (line 21) | public function getDocuments(): array
    method makeAggregationResult (line 26) | public static function makeAggregationResult(array $rawRediSearchResul...

FILE: src/Aggregate/Builder.php
  class Builder (line 26) | class Builder implements BuilderInterface
    method __construct (line 35) | public function __construct(RediSearchRedisClient $redis, string $inde...
    method getPipeline (line 44) | public function getPipeline(): array
    method clear (line 52) | public function clear()
    method load (line 62) | public function load(array $fieldNames): BuilderInterface
    method groupBy (line 73) | public function groupBy($fieldName = [], ?CanBecomeArrayInterface $red...
    method reduce (line 86) | public function reduce(CanBecomeArrayInterface $reducer): BuilderInter...
    method avg (line 96) | public function avg(string $fieldName): BuilderInterface
    method count (line 106) | public function count(int $group = 0): BuilderInterface
    method countDistinct (line 116) | public function countDistinct(string $fieldName): BuilderInterface
    method countDistinctApproximate (line 126) | public function countDistinctApproximate(string $fieldName): BuilderIn...
    method sum (line 136) | public function sum(string $fieldName): BuilderInterface
    method max (line 146) | public function max(string $fieldName): BuilderInterface
    method min (line 156) | public function min(string $fieldName): BuilderInterface
    method quantile (line 167) | public function quantile(string $fieldName, float $quantile): BuilderI...
    method standardDeviation (line 177) | public function standardDeviation(string $fieldName): BuilderInterface
    method firstValue (line 189) | public function firstValue(string $fieldName, ?string $byFieldName = n...
    method toList (line 199) | public function toList(string $fieldName): BuilderInterface
    method sortBy (line 211) | public function sortBy($fieldName, $isAscending = true, int $max = -1)...
    method apply (line 222) | public function apply(string $expression, string $asFieldName): Builde...
    method filter (line 232) | public function filter(string $expression): BuilderInterface
    method limit (line 243) | public function limit(int $offset, int $pageSize = 10): BuilderInterface
    method withCursor (line 256) | public function withCursor(int $count = 100): BuilderInterface
    method cursorRead (line 269) | public function cursorRead(int $cursorId, int $count = 100): mixed
    method cursorDelete (line 280) | public function cursorDelete(int $cursorId): mixed
    method makeAggregateCommandArguments (line 289) | public function makeAggregateCommandArguments(string $query): array
    method search (line 320) | public function search(string $query = '', bool $documentsAsArray = fa...

FILE: src/Aggregate/BuilderInterface.php
  type BuilderInterface (line 7) | interface BuilderInterface
    method load (line 9) | public function load(array $fieldNames): BuilderInterface;
    method groupBy (line 10) | public function groupBy($fieldName, ?CanBecomeArrayInterface $reducer ...
    method reduce (line 11) | public function reduce(CanBecomeArrayInterface $reducer): BuilderInter...
    method sortBy (line 12) | public function sortBy($fieldName, $isAscending = true, int $max = -1)...
    method apply (line 13) | public function apply(string $expression, string $asName): BuilderInte...
    method filter (line 14) | public function filter(string $expression): BuilderInterface;
    method limit (line 15) | public function limit(int $offset, int $pageSize = 10): BuilderInterface;
    method search (line 16) | public function search(string $query = '', bool $documentsAsArray = fa...
    method avg (line 17) | public function avg(string $fieldName): BuilderInterface;
    method count (line 18) | public function count(int $group = 0): BuilderInterface;
    method countDistinct (line 19) | public function countDistinct(string $fieldName): BuilderInterface;
    method countDistinctApproximate (line 20) | public function countDistinctApproximate(string $fieldName): BuilderIn...
    method sum (line 21) | public function sum(string $fieldName): BuilderInterface;
    method max (line 22) | public function max(string $fieldName): BuilderInterface;
    method min (line 23) | public function min(string $fieldName): BuilderInterface;
    method standardDeviation (line 24) | public function standardDeviation(string $fieldName): BuilderInterface;
    method firstValue (line 25) | public function firstValue(string $fieldName, ?string $byFieldName = n...
    method quantile (line 26) | public function quantile(string $fieldName, float $quantile): BuilderI...
    method toList (line 27) | public function toList(string $fieldName): BuilderInterface;
    method withCursor (line 28) | public function withCursor(int $count = 100): BuilderInterface;
    method cursorRead (line 29) | public function cursorRead(int $cursorId, int $count = 100): mixed;
    method cursorDelete (line 30) | public function cursorDelete(int $cursorId): mixed;

FILE: src/Aggregate/Operations/AbstractFieldNameOperation.php
  class AbstractFieldNameOperation (line 7) | abstract class AbstractFieldNameOperation implements CanBecomeArrayInter...
    method __construct (line 12) | public function __construct(string $operationName, array $fieldNames)
    method toArray (line 18) | public function toArray(): array

FILE: src/Aggregate/Operations/Apply.php
  class Apply (line 7) | class Apply implements CanBecomeArrayInterface
    method __construct (line 12) | public function __construct(string $expression, string $asFieldName)
    method toArray (line 18) | public function toArray(): array

FILE: src/Aggregate/Operations/Filter.php
  class Filter (line 7) | class Filter implements CanBecomeArrayInterface
    method __construct (line 11) | public function __construct(string $expression)
    method toArray (line 16) | public function toArray(): array

FILE: src/Aggregate/Operations/GroupBy.php
  class GroupBy (line 5) | class GroupBy extends AbstractFieldNameOperation
    method __construct (line 7) | public function __construct(array $fieldNames)

FILE: src/Aggregate/Operations/Limit.php
  class Limit (line 7) | class Limit implements CanBecomeArrayInterface
    method __construct (line 12) | public function __construct(int $offset, int $pageSize)
    method toArray (line 18) | public function toArray(): array

FILE: src/Aggregate/Operations/Load.php
  class Load (line 5) | class Load extends AbstractFieldNameOperation
    method __construct (line 7) | public function __construct(array $fieldNames)

FILE: src/Aggregate/Operations/SortBy.php
  class SortBy (line 5) | class SortBy extends AbstractFieldNameOperation
    method __construct (line 10) | public function __construct(array $fieldNames, $isAscending = true, in...
    method toArray (line 17) | public function toArray(): array

FILE: src/Aggregate/Reducers/AbstractFieldNameReducer.php
  class AbstractFieldNameReducer (line 7) | abstract class AbstractFieldNameReducer implements CanBecomeArrayInterface
    method __construct (line 14) | public function __construct(string $fieldName, string $alias = '')
    method toArray (line 20) | public function toArray(): array
    method makeAlias (line 25) | protected function makeAlias(): string

FILE: src/Aggregate/Reducers/Aliasable.php
  type Aliasable (line 5) | trait Aliasable

FILE: src/Aggregate/Reducers/Avg.php
  class Avg (line 5) | class Avg extends AbstractFieldNameReducer
    method toArray (line 9) | public function toArray(): array

FILE: src/Aggregate/Reducers/Count.php
  class Count (line 7) | class Count implements CanBecomeArrayInterface
    method __construct (line 14) | public function __construct(int $group)
    method toArray (line 19) | public function toArray(): array

FILE: src/Aggregate/Reducers/CountDistinct.php
  class CountDistinct (line 5) | class CountDistinct extends AbstractFieldNameReducer
    method toArray (line 9) | public function toArray(): array

FILE: src/Aggregate/Reducers/CountDistinctApproximate.php
  class CountDistinctApproximate (line 5) | class CountDistinctApproximate extends AbstractFieldNameReducer
    method toArray (line 9) | public function toArray(): array

FILE: src/Aggregate/Reducers/FirstValue.php
  class FirstValue (line 5) | class FirstValue extends AbstractFieldNameReducer
    method __construct (line 11) | public function __construct(string $fieldName, ?string $byFieldName = ...
    method toArray (line 18) | public function toArray(): array

FILE: src/Aggregate/Reducers/Max.php
  class Max (line 5) | class Max extends AbstractFieldNameReducer
    method toArray (line 9) | public function toArray(): array

FILE: src/Aggregate/Reducers/Min.php
  class Min (line 5) | class Min extends AbstractFieldNameReducer
    method toArray (line 9) | public function toArray(): array

FILE: src/Aggregate/Reducers/Quantile.php
  class Quantile (line 5) | class Quantile extends AbstractFieldNameReducer
    method __construct (line 10) | public function __construct(string $fieldName, float $quantile)
    method toArray (line 16) | public function toArray(): array

FILE: src/Aggregate/Reducers/StandardDeviation.php
  class StandardDeviation (line 5) | class StandardDeviation extends AbstractFieldNameReducer
    method toArray (line 9) | public function toArray(): array

FILE: src/Aggregate/Reducers/Sum.php
  class Sum (line 5) | class Sum extends AbstractFieldNameReducer
    method toArray (line 9) | public function toArray(): array

FILE: src/Aggregate/Reducers/ToList.php
  class ToList (line 5) | class ToList extends AbstractFieldNameReducer
    method toArray (line 9) | public function toArray(): array

FILE: src/CanBecomeArrayInterface.php
  type CanBecomeArrayInterface (line 5) | interface CanBecomeArrayInterface
    method toArray (line 7) | public function toArray(): array;

FILE: src/Console/AbstractRedisCommand.php
  class AbstractRedisCommand (line 13) | abstract class AbstractRedisCommand extends Command
    method configure (line 15) | protected function configure(): void
    method createClient (line 24) | protected function createClient(InputInterface $input): RediSearchRedi...
    method createIndex (line 54) | protected function createIndex(InputInterface $input, string $indexNam...
    method renderTable (line 61) | protected function renderTable(OutputInterface $output, array $headers...
    method renderJson (line 69) | protected function renderJson(OutputInterface $output, mixed $data): void

FILE: src/Console/Application.php
  class Application (line 20) | class Application extends BaseApplication
    method __construct (line 22) | public function __construct()
    method registerCommands (line 42) | private function registerCommands(Command ...$commands): void

FILE: src/Console/Command/AggregateCommand.php
  class AggregateCommand (line 11) | class AggregateCommand extends AbstractRedisCommand
    method configure (line 13) | protected function configure(): void
    method execute (line 32) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Console/Command/DocumentAddCommand.php
  class DocumentAddCommand (line 12) | class DocumentAddCommand extends AbstractRedisCommand
    method configure (line 14) | protected function configure(): void
    method execute (line 29) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Console/Command/DocumentDeleteCommand.php
  class DocumentDeleteCommand (line 10) | class DocumentDeleteCommand extends AbstractRedisCommand
    method configure (line 12) | protected function configure(): void
    method execute (line 23) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Console/Command/DocumentGetCommand.php
  class DocumentGetCommand (line 11) | class DocumentGetCommand extends AbstractRedisCommand
    method configure (line 13) | protected function configure(): void
    method execute (line 25) | protected function execute(InputInterface $input, OutputInterface $out...
    method parseHashResult (line 54) | private function parseHashResult(array $result): array

FILE: src/Console/Command/ExplainCommand.php
  class ExplainCommand (line 10) | class ExplainCommand extends AbstractRedisCommand
    method configure (line 12) | protected function configure(): void
    method execute (line 23) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Console/Command/IndexCreateCommand.php
  class IndexCreateCommand (line 12) | class IndexCreateCommand extends AbstractRedisCommand
    method configure (line 14) | protected function configure(): void
    method execute (line 35) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Console/Command/IndexDropCommand.php
  class IndexDropCommand (line 11) | class IndexDropCommand extends AbstractRedisCommand
    method configure (line 13) | protected function configure(): void
    method execute (line 24) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Console/Command/IndexInfoCommand.php
  class IndexInfoCommand (line 11) | class IndexInfoCommand extends AbstractRedisCommand
    method configure (line 13) | protected function configure(): void
    method execute (line 24) | protected function execute(InputInterface $input, OutputInterface $out...
    method normalizeInfo (line 41) | private function normalizeInfo(mixed $info): array
    method infoToRows (line 63) | private function infoToRows(mixed $info): array

FILE: src/Console/Command/IndexListCommand.php
  class IndexListCommand (line 10) | class IndexListCommand extends AbstractRedisCommand
    method configure (line 12) | protected function configure(): void
    method execute (line 22) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Console/Command/ProfileCommand.php
  class ProfileCommand (line 11) | class ProfileCommand extends AbstractRedisCommand
    method configure (line 13) | protected function configure(): void
    method execute (line 25) | protected function execute(InputInterface $input, OutputInterface $out...
    method printProfileResult (line 43) | private function printProfileResult(OutputInterface $output, mixed $re...

FILE: src/Console/Command/SearchCommand.php
  class SearchCommand (line 11) | class SearchCommand extends AbstractRedisCommand
    method configure (line 13) | protected function configure(): void
    method execute (line 36) | protected function execute(InputInterface $input, OutputInterface $out...

FILE: src/Console/Command/ShellCommand.php
  class ShellCommand (line 10) | class ShellCommand extends AbstractRedisCommand
    method configure (line 14) | protected function configure(): void
    method execute (line 23) | protected function execute(InputInterface $input, OutputInterface $out...
    method showHelp (line 175) | private function showHelp(OutputInterface $output): void
    method tokenize (line 199) | private function tokenize(string $input): array

FILE: src/Console/SchemaParser.php
  class SchemaParser (line 7) | class SchemaParser
    method applySchema (line 18) | public static function applySchema(string $filePath, Index $index): Index

FILE: src/Document/AbstractDocumentFactory.php
  class AbstractDocumentFactory (line 9) | abstract class AbstractDocumentFactory
    method make (line 11) | public static function make(string $id): DocumentInterface
    method makeFromArray (line 16) | public static function makeFromArray(array $fields, array $availableSc...

FILE: src/Document/Document.php
  class Document (line 8) | class Document implements DocumentInterface
    method __construct (line 20) | public function __construct($id = null)
    method __set (line 25) | public function __set(string $name, FieldInterface $value): void
    method __get (line 30) | public function __get(string $name): ?FieldInterface
    method __isset (line 35) | public function __isset(string $name): bool
    method addFieldsToProperties (line 40) | protected function addFieldsToProperties($properties): array
    method getHashDefinition (line 52) | public function getHashDefinition(?array $prefixes = null): array
    method getDefinition (line 73) | public function getDefinition(): array
    method getId (line 111) | public function getId(): string
    method setId (line 116) | public function setId(string $id)
    method getScore (line 122) | public function getScore(): float
    method setScore (line 127) | public function setScore(float $score)
    method isNoSave (line 136) | public function isNoSave(): bool
    method setNoSave (line 141) | public function setNoSave(bool $noSave): Document
    method isReplace (line 147) | public function isReplace(): bool
    method setReplace (line 152) | public function setReplace(bool $replace): Document
    method isPartial (line 158) | public function isPartial(): bool
    method setPartial (line 163) | public function setPartial(bool $partial): Document
    method isNoCreate (line 169) | public function isNoCreate(): bool
    method setNoCreate (line 174) | public function setNoCreate(bool $noCreate): Document
    method getPayload (line 180) | public function getPayload()
    method setPayload (line 185) | public function setPayload($payload)
    method getLanguage (line 191) | public function getLanguage()
    method setLanguage (line 196) | public function setLanguage($language)

FILE: src/Document/DocumentInterface.php
  type DocumentInterface (line 5) | interface DocumentInterface
    method getHashDefinition (line 7) | public function getHashDefinition(array|null $prefixes): array;
    method getDefinition (line 8) | public function getDefinition(): array;
    method getId (line 9) | public function getId(): string;
    method setId (line 10) | public function setId(string $id);
    method getScore (line 11) | public function getScore(): float;
    method setScore (line 12) | public function setScore(float $score);
    method isNoSave (line 13) | public function isNoSave(): bool;
    method setNoSave (line 14) | public function setNoSave(bool $noSave): Document;
    method isReplace (line 15) | public function isReplace(): bool;
    method setReplace (line 16) | public function setReplace(bool $replace): Document;
    method isPartial (line 17) | public function isPartial(): bool;
    method setPartial (line 18) | public function setPartial(bool $partial): Document;
    method isNoCreate (line 19) | public function isNoCreate(): bool;
    method setNoCreate (line 20) | public function setNoCreate(bool $noCreate): Document;
    method getPayload (line 21) | public function getPayload();
    method setPayload (line 22) | public function setPayload($payload);
    method getLanguage (line 23) | public function getLanguage();
    method setLanguage (line 24) | public function setLanguage($language);

FILE: src/Exceptions/AliasDoesNotExistException.php
  class AliasDoesNotExistException (line 7) | class AliasDoesNotExistException extends Exception
    method __construct (line 9) | public function __construct($message = '', $code = 0, ?Exception $prev...

FILE: src/Exceptions/DocumentAlreadyInIndexException.php
  class DocumentAlreadyInIndexException (line 7) | class DocumentAlreadyInIndexException extends Exception
    method __construct (line 9) | public function __construct($indexName, $documentId, $code = 0, ?Excep...

FILE: src/Exceptions/FieldNotInSchemaException.php
  class FieldNotInSchemaException (line 7) | class FieldNotInSchemaException extends Exception
    method __construct (line 9) | public function __construct($message = '', $code = 0, ?Exception $prev...

FILE: src/Exceptions/NoFieldsInIndexException.php
  class NoFieldsInIndexException (line 7) | class NoFieldsInIndexException extends Exception
    method __construct (line 9) | public function __construct($message = '', $code = 0, ?Exception $prev...

FILE: src/Exceptions/OutOfRangeDocumentScoreException.php
  class OutOfRangeDocumentScoreException (line 7) | class OutOfRangeDocumentScoreException extends Exception
    method __construct (line 9) | public function __construct($message = '', $code = 0, ?Exception $prev...

FILE: src/Exceptions/RediSearchException.php
  class RediSearchException (line 7) | class RediSearchException extends Exception

FILE: src/Exceptions/UnknownIndexNameException.php
  class UnknownIndexNameException (line 7) | class UnknownIndexNameException extends Exception
    method __construct (line 9) | public function __construct($message = '', $code = 0, ?Exception $prev...

FILE: src/Exceptions/UnknownIndexNameOrNameIsAnAliasItselfException.php
  class UnknownIndexNameOrNameIsAnAliasItselfException (line 7) | class UnknownIndexNameOrNameIsAnAliasItselfException extends Exception
    method __construct (line 9) | public function __construct($message = '', $code = 0, ?Exception $prev...

FILE: src/Exceptions/UnknownRediSearchCommandException.php
  class UnknownRediSearchCommandException (line 7) | class UnknownRediSearchCommandException extends Exception
    method __construct (line 9) | public function __construct($message = '', $code = 0, ?Exception $prev...

FILE: src/Exceptions/UnsupportedRediSearchLanguageException.php
  class UnsupportedRediSearchLanguageException (line 7) | class UnsupportedRediSearchLanguageException extends Exception
    method __construct (line 9) | public function __construct($message = '', $code = 0, ?Exception $prev...

FILE: src/Fields/AbstractField.php
  class AbstractField (line 5) | abstract class AbstractField implements FieldInterface
    method __construct (line 10) | public function __construct(string $name, $value = null)
    method getName (line 16) | public function getName(): string
    method getValue (line 21) | public function getValue()
    method setValue (line 26) | public function setValue($value)
    method getTypeDefinition (line 32) | public function getTypeDefinition(): array

FILE: src/Fields/FieldFactory.php
  class FieldFactory (line 7) | class FieldFactory
    method make (line 9) | public static function make($name, $value, $tagSeparator = ',')

FILE: src/Fields/FieldInterface.php
  type FieldInterface (line 5) | interface FieldInterface
    method getTypeDefinition (line 7) | public function getTypeDefinition(): array;
    method getType (line 8) | public function getType(): string;
    method getName (line 9) | public function getName(): string;
    method getValue (line 10) | public function getValue();
    method setValue (line 11) | public function setValue($value);

FILE: src/Fields/GeoField.php
  class GeoField (line 5) | class GeoField extends AbstractField
    method getType (line 9) | public function getType(): string
    method getTypeDefinition (line 14) | public function getTypeDefinition(): array

FILE: src/Fields/GeoLocation.php
  class GeoLocation (line 5) | class GeoLocation
    method __construct (line 10) | public function __construct(float $longitude, float $latitude)
    method __toString (line 16) | public function __toString()

FILE: src/Fields/Noindex.php
  type Noindex (line 5) | trait Noindex
    method isNoindex (line 9) | public function isNoindex(): bool
    method setNoindex (line 14) | public function setNoindex(bool $noindex)

FILE: src/Fields/NumericField.php
  class NumericField (line 5) | class NumericField extends AbstractField
    method getType (line 10) | public function getType(): string
    method getTypeDefinition (line 15) | public function getTypeDefinition(): array

FILE: src/Fields/Sortable.php
  type Sortable (line 5) | trait Sortable
    method isSortable (line 9) | public function isSortable(): bool
    method setSortable (line 14) | public function setSortable(bool $sortable)

FILE: src/Fields/Tag.php
  class Tag (line 5) | class Tag
    method __construct (line 9) | public function __construct($value)
    method __toString (line 14) | public function __toString()

FILE: src/Fields/TagField.php
  class TagField (line 5) | class TagField extends AbstractField
    method getType (line 12) | public function getType(): string
    method getSeparator (line 17) | public function getSeparator(): string
    method setSeparator (line 22) | public function setSeparator(string $separator)
    method getTypeDefinition (line 28) | public function getTypeDefinition(): array

FILE: src/Fields/TextField.php
  class TextField (line 5) | class TextField extends AbstractField
    method getType (line 13) | public function getType(): string
    method getWeight (line 18) | public function getWeight(): float
    method setWeight (line 23) | public function setWeight(float $weight)
    method isNoStem (line 29) | public function isNoStem(): bool
    method setNoStem (line 34) | public function setNoStem(bool $noStem): TextField
    method getTypeDefinition (line 40) | public function getTypeDefinition(): array

FILE: src/Fields/VectorField.php
  class VectorField (line 14) | class VectorField extends AbstractField
    method __construct (line 32) | public function __construct(
    method getType (line 64) | public function getType(): string
    method getTypeDefinition (line 69) | public function getTypeDefinition(): array
    method getAlgorithm (line 92) | public function getAlgorithm(): string
    method getDim (line 97) | public function getDim(): int
    method getDistanceMetric (line 102) | public function getDistanceMetric(): string

FILE: src/Index.php
  class Index (line 25) | class Index extends AbstractIndex implements IndexInterface
    method create (line 49) | public function create()
    method exists (line 111) | public function exists(): bool
    method __set (line 127) | public function __set(string $name, FieldInterface $value): void
    method __isset (line 137) | public function __isset(string $name): bool
    method __get (line 147) | public function __get(string $name): ?FieldInterface
    method getFields (line 155) | public function getFields(): array
    method getFieldsCloned (line 165) | public function getFieldsCloned(): array
    method addTextField (line 177) | public function addTextField(string $name, float $weight = 1.0, bool $...
    method addNumericField (line 189) | public function addNumericField(string $name, bool $sortable = false, ...
    method addGeoField (line 200) | public function addGeoField(string $name, bool $noindex = false): Inde...
    method addTagField (line 213) | public function addTagField(string $name, bool $sortable = false, bool...
    method addVectorField (line 230) | public function addVectorField(
    method tagValues (line 246) | public function tagValues(string $name): array
    method drop (line 255) | public function drop(bool $deleteDocuments = false)
    method info (line 267) | public function info()
    method loadFields (line 279) | public function loadFields(): static
    method parseAttributeDescriptor (line 367) | private function parseAttributeDescriptor(array $attr): array
    method delete (line 415) | public function delete($id, $deleteDocument = false)
    method makeDocument (line 426) | public function makeDocument($id = null): DocumentInterface
    method makeAggregateBuilder (line 436) | public function makeAggregateBuilder(): AggregateBuilderInterface
    method getRedisClient (line 444) | public function getRedisClient(): RediSearchRedisClient
    method setRedisClient (line 453) | public function setRedisClient(RediSearchRedisClient $redisClient): In...
    method getIndexName (line 462) | public function getIndexName(): string
    method setIndexName (line 471) | public function setIndexName(string $indexName): IndexInterface
    method isNoOffsetsEnabled (line 480) | public function isNoOffsetsEnabled(): bool
    method setNoOffsetsEnabled (line 489) | public function setNoOffsetsEnabled(bool $noOffsetsEnabled): IndexInte...
    method isNoFieldsEnabled (line 498) | public function isNoFieldsEnabled(): bool
    method setNoFieldsEnabled (line 507) | public function setNoFieldsEnabled(bool $noFieldsEnabled): IndexInterface
    method isNoFrequenciesEnabled (line 516) | public function isNoFrequenciesEnabled(): bool
    method setNoFrequenciesEnabled (line 525) | public function setNoFrequenciesEnabled(bool $noFrequenciesEnabled): I...
    method setStopWords (line 535) | public function setStopWords(array $stopWords = []): IndexInterface
    method setPrefixes (line 552) | public function setPrefixes(array $prefixes = []): IndexInterface
    method setIndexType (line 565) | public function setIndexType(string $type): IndexInterface
    method setFilter (line 582) | public function setFilter(string $expression): IndexInterface
    method setMaxTextFields (line 593) | public function setMaxTextFields(bool $enable = true): IndexInterface
    method setTemporary (line 605) | public function setTemporary(int $seconds): IndexInterface
    method setSkipInitialScan (line 617) | public function setSkipInitialScan(bool $skip = true): IndexInterface
    method makeQueryBuilder (line 626) | protected function makeQueryBuilder(): QueryBuilder
    method tagFilter (line 637) | public function tagFilter(string $fieldName, array $values, ?array $ch...
    method numericFilter (line 648) | public function numericFilter(string $fieldName, $min, $max = null): Q...
    method geoFilter (line 661) | public function geoFilter(string $fieldName, float $longitude, float $...
    method sortBy (line 671) | public function sortBy(string $fieldName, $order = 'ASC'): QueryBuilde...
    method scorer (line 680) | public function scorer(string $scoringFunction): QueryBuilderInterface
    method language (line 689) | public function language(string $languageName): QueryBuilderInterface
    method explain (line 698) | public function explain(string $query): string
    method dialect (line 709) | public function dialect(int $version): QueryBuilderInterface
    method params (line 721) | public function params(array $params): QueryBuilderInterface
    method search (line 732) | public function search(string $query = '', bool $documentsAsArray = fa...
    method noContent (line 740) | public function noContent(): QueryBuilderInterface
    method limit (line 750) | public function limit(int $offset, int $pageSize = 10): QueryBuilderIn...
    method inFields (line 760) | public function inFields(int $number, array $fields): QueryBuilderInte...
    method inKeys (line 770) | public function inKeys(int $number, array $keys): QueryBuilderInterface
    method slop (line 779) | public function slop(int $slop): QueryBuilderInterface
    method noStopWords (line 787) | public function noStopWords(): QueryBuilderInterface
    method withPayloads (line 795) | public function withPayloads(): QueryBuilderInterface
    method withScores (line 803) | public function withScores(): QueryBuilderInterface
    method verbatim (line 811) | public function verbatim(): QueryBuilderInterface
    method buildDocumentKey (line 825) | private function buildDocumentKey(string $id): string
    method _add (line 839) | protected function _add(DocumentInterface $document)
    method addMany (line 854) | public function addMany(array $documents, $disableAtomicity = false, $...
    method arrayToDocument (line 883) | public function arrayToDocument($document): DocumentInterface
    method add (line 898) | public function add($document): bool
    method replace (line 929) | public function replace($document): bool
    method replaceMany (line 939) | public function replaceMany(array $documents, $disableAtomicity = false)
    method addHash (line 951) | public function addHash($document): bool
    method replaceHash (line 964) | public function replaceHash($document): bool
    method return (line 974) | public function return(array $fields): QueryBuilderInterface
    method summarize (line 986) | public function summarize(array $fields, int $fragmentCount = 3, int $...
    method highlight (line 997) | public function highlight(array $fields, string $openTag = '<strong>',...
    method expander (line 1006) | public function expander(string $expander): QueryBuilderInterface
    method payload (line 1015) | public function payload(string $payload): QueryBuilderInterface
    method count (line 1024) | public function count(string $query = ''): int
    method addAlias (line 1033) | public function addAlias(string $name): bool
    method updateAlias (line 1042) | public function updateAlias(string $name): bool
    method deleteAlias (line 1051) | public function deleteAlias(string $name): bool
    method alter (line 1064) | public function alter(FieldInterface ...$fields): mixed
    method listIndexes (line 1078) | public function listIndexes(): array
    method synUpdate (line 1090) | public function synUpdate(string $synonymGroupId, string ...$terms): m...
    method synDump (line 1100) | public function synDump(): array
    method spellCheck (line 1113) | public function spellCheck(string $query, int $distance = 1): array
    method dictAdd (line 1125) | public function dictAdd(string $dict, string ...$terms): int
    method dictDelete (line 1137) | public function dictDelete(string $dict, string ...$terms): int
    method dictDump (line 1148) | public function dictDump(string $dict): array

FILE: src/IndexInterface.php
  type IndexInterface (line 11) | interface IndexInterface extends BuilderInterface
    method create (line 13) | public function create();
    method exists (line 14) | public function exists(): bool;
    method drop (line 15) | public function drop(bool $deleteDocuments = false);
    method info (line 16) | public function info();
    method loadFields (line 17) | public function loadFields(): static;
    method delete (line 18) | public function delete($id, $deleteDocument = false);
    method getFields (line 19) | public function getFields(): array;
    method makeDocument (line 20) | public function makeDocument($id = null): DocumentInterface;
    method makeAggregateBuilder (line 21) | public function makeAggregateBuilder(): AggregateBuilderInterface;
    method getRedisClient (line 22) | public function getRedisClient(): RediSearchRedisClient;
    method setRedisClient (line 23) | public function setRedisClient(RediSearchRedisClient $redisClient): In...
    method getIndexName (line 24) | public function getIndexName(): string;
    method setIndexName (line 25) | public function setIndexName(string $indexName): IndexInterface;
    method isNoOffsetsEnabled (line 26) | public function isNoOffsetsEnabled(): bool;
    method setNoOffsetsEnabled (line 27) | public function setNoOffsetsEnabled(bool $noOffsetsEnabled): IndexInte...
    method isNoFieldsEnabled (line 28) | public function isNoFieldsEnabled(): bool;
    method setNoFieldsEnabled (line 29) | public function setNoFieldsEnabled(bool $noFieldsEnabled): IndexInterf...
    method isNoFrequenciesEnabled (line 30) | public function isNoFrequenciesEnabled(): bool;
    method setNoFrequenciesEnabled (line 31) | public function setNoFrequenciesEnabled(bool $noFieldsEnabled): IndexI...
    method setStopWords (line 32) | public function setStopWords(array $stopWords): IndexInterface;
    method setPrefixes (line 33) | public function setPrefixes(array $prefixes): IndexInterface;
    method addTextField (line 34) | public function addTextField(string $name, float $weight = 1.0, bool $...
    method addNumericField (line 35) | public function addNumericField(string $name, bool $sortable = false, ...
    method addGeoField (line 36) | public function addGeoField(string $name, bool $noindex = false): Inde...
    method addTagField (line 37) | public function addTagField(string $name, bool $sortable = false, bool...
    method addVectorField (line 38) | public function addVectorField(
    method tagValues (line 46) | public function tagValues(string $name): array;
    method add (line 47) | public function add($document): bool;
    method addMany (line 48) | public function addMany(array $documents, $disableAtomicity = false, $...
    method replace (line 49) | public function replace($document): bool;
    method replaceMany (line 50) | public function replaceMany(array $documents, $disableAtomicity = false);
    method addHash (line 51) | public function addHash($document): bool;
    method replaceHash (line 52) | public function replaceHash($document): bool;
    method addAlias (line 53) | public function addAlias(string $name): bool;
    method updateAlias (line 54) | public function updateAlias(string $name): bool;
    method deleteAlias (line 55) | public function deleteAlias(string $name): bool;
    method params (line 56) | public function params(array $params): BuilderInterface;
    method setIndexType (line 57) | public function setIndexType(string $type): IndexInterface;
    method setFilter (line 58) | public function setFilter(string $expression): IndexInterface;
    method setMaxTextFields (line 59) | public function setMaxTextFields(bool $enable = true): IndexInterface;
    method setTemporary (line 60) | public function setTemporary(int $seconds): IndexInterface;
    method setSkipInitialScan (line 61) | public function setSkipInitialScan(bool $skip = true): IndexInterface;
    method alter (line 62) | public function alter(FieldInterface ...$fields): mixed;
    method listIndexes (line 63) | public function listIndexes(): array;
    method synUpdate (line 64) | public function synUpdate(string $synonymGroupId, string ...$terms): m...
    method synDump (line 65) | public function synDump(): array;
    method spellCheck (line 66) | public function spellCheck(string $query, int $distance = 1): array;
    method dictAdd (line 67) | public function dictAdd(string $dict, string ...$terms): int;
    method dictDelete (line 68) | public function dictDelete(string $dict, string ...$terms): int;
    method dictDump (line 69) | public function dictDump(string $dict): array;

FILE: src/Language.php
  class Language (line 5) | class Language
    method isSupported (line 61) | public static function isSupported(string $language): bool
    method getSupported (line 66) | public static function getSupported(): array

FILE: src/Query/Builder.php
  class Builder (line 8) | class Builder implements BuilderInterface
    method __construct (line 37) | public function __construct(RediSearchRedisClient $redis, string $inde...
    method noContent (line 43) | public function noContent(): BuilderInterface
    method return (line 49) | public function return(array $fields): BuilderInterface
    method summarize (line 57) | public function summarize(array $fields, int $fragmentCount = 3, int $...
    method highlight (line 65) | public function highlight(array $fields, string $openTag = '<strong>',...
    method expander (line 73) | public function expander(string $expander): BuilderInterface
    method payload (line 79) | public function payload(string $payload): BuilderInterface
    method limit (line 85) | public function limit(int $offset, int $pageSize = 10): BuilderInterface
    method inFields (line 91) | public function inFields(int $number, array $fields): BuilderInterface
    method inKeys (line 97) | public function inKeys(int $number, array $keys): BuilderInterface
    method slop (line 103) | public function slop(int $slop): BuilderInterface
    method noStopWords (line 109) | public function noStopWords(): BuilderInterface
    method withPayloads (line 115) | public function withPayloads(): BuilderInterface
    method withScores (line 121) | public function withScores(): BuilderInterface
    method verbatim (line 127) | public function verbatim(): BuilderInterface
    method tagFilter (line 133) | public function tagFilter(string $fieldName, array $values, ?array $ch...
    method numericFilter (line 151) | public function numericFilter(string $fieldName, $min, $max = null): B...
    method geoFilter (line 158) | public function geoFilter(string $fieldName, float $longitude, float $...
    method sortBy (line 168) | public function sortBy(string $fieldName, $order = 'ASC'): BuilderInte...
    method scorer (line 174) | public function scorer(string $scoringFunction): BuilderInterface
    method language (line 180) | public function language(string $languageName): BuilderInterface
    method dialect (line 193) | public function dialect(int $version): BuilderInterface
    method params (line 210) | public function params(array $params): BuilderInterface
    method explodeArgument (line 216) | protected function explodeArgument(?string $argument): array
    method buildParamsArguments (line 221) | protected function buildParamsArguments(): array
    method makeSearchCommandArguments (line 234) | public function makeSearchCommandArguments(string $query): array
    method search (line 270) | public function search(string $query = '', bool $documentsAsArray = fa...
    method explain (line 290) | public function explain(string $query): string
    method count (line 295) | public function count(string $query = ''): int

FILE: src/Query/BuilderInterface.php
  type BuilderInterface (line 5) | interface BuilderInterface
    method noContent (line 7) | public function noContent(): BuilderInterface;
    method return (line 8) | public function return(array $fields): BuilderInterface;
    method summarize (line 9) | public function summarize(array $fields, int $fragmentCount = 3, int $...
    method highlight (line 10) | public function highlight(array $fields, string $openTag = '<strong>',...
    method expander (line 11) | public function expander(string $expander): BuilderInterface;
    method payload (line 12) | public function payload(string $payload): BuilderInterface;
    method limit (line 13) | public function limit(int $offset, int $pageSize = 10): BuilderInterface;
    method inFields (line 14) | public function inFields(int $number, array $fields): BuilderInterface;
    method inKeys (line 15) | public function inKeys(int $number, array $keys): BuilderInterface;
    method slop (line 16) | public function slop(int $slop): BuilderInterface;
    method noStopWords (line 17) | public function noStopWords(): BuilderInterface;
    method withPayloads (line 18) | public function withPayloads(): BuilderInterface;
    method withScores (line 19) | public function withScores(): BuilderInterface;
    method verbatim (line 20) | public function verbatim(): BuilderInterface;
    method tagFilter (line 21) | public function tagFilter(string $fieldName, array $values, ?array $ch...
    method numericFilter (line 22) | public function numericFilter(string $fieldName, $min, $max = null): B...
    method geoFilter (line 23) | public function geoFilter(string $fieldName, float $longitude, float $...
    method sortBy (line 24) | public function sortBy(string $fieldName, $order = 'ASC'): BuilderInte...
    method scorer (line 25) | public function scorer(string $scoringFunction): BuilderInterface;
    method language (line 26) | public function language(string $languageName): BuilderInterface;
    method dialect (line 27) | public function dialect(int $version): BuilderInterface;
    method params (line 28) | public function params(array $params): BuilderInterface;
    method search (line 29) | public function search(string $query = '', bool $documentsAsArray = fa...
    method explain (line 30) | public function explain(string $query): string;
    method count (line 31) | public function count(string $query = ''): int;

FILE: src/Query/SearchResult.php
  class SearchResult (line 5) | class SearchResult
    method __construct (line 10) | public function __construct(int $count, array $documents)
    method getCount (line 16) | public function getCount(): int
    method getDocuments (line 21) | public function getDocuments(): array
    method makeSearchResult (line 27) | public static function makeSearchResult(

FILE: src/RediSearchRedisClient.php
  class RediSearchRedisClient (line 18) | class RediSearchRedisClient implements RedisRawClientInterface
    method __construct (line 23) | public function __construct(RedisRawClientInterface $redis)
    method validateRawCommandResults (line 37) | public function validateRawCommandResults($rawResult, string $command,...
    method connect (line 75) | public function connect($hostname = '127.0.0.1', $port = 6379, $db = 0...
    method flushAll (line 81) | public function flushAll()
    method multi (line 86) | public function multi(bool $usePipeline = false)
    method rawCommand (line 100) | public function rawCommand(string $command, array $arguments = [])
    method setLogger (line 121) | public function setLogger(LoggerInterface $logger): RedisRawClientInte...
    method prepareRawCommandArguments (line 126) | public function prepareRawCommandArguments(string $command, array $arg...

FILE: src/RuntimeConfiguration.php
  class RuntimeConfiguration (line 5) | class RuntimeConfiguration extends AbstractRediSearchClientAdapter
    method getOption (line 7) | protected function getOption($name)
    method setOption (line 12) | protected function setOption($name, $value)
    method convertRawResponseToString (line 17) | protected function convertRawResponseToString(array $rawResponse): string
    method convertRawResponseToInt (line 26) | protected function convertRawResponseToInt($rawResponse): int
    method getMinPrefix (line 31) | public function getMinPrefix(): int
    method setMinPrefix (line 36) | public function setMinPrefix(int $value = 2)
    method getMaxExpansions (line 41) | public function getMaxExpansions(): int
    method setMaxExpansions (line 46) | public function setMaxExpansions(int $value = 200): bool
    method getTimeoutInMilliseconds (line 51) | public function getTimeoutInMilliseconds(): int
    method setTimeoutInMilliseconds (line 56) | public function setTimeoutInMilliseconds(int $value = 500)
    method isOnTimeoutPolicyReturn (line 61) | public function isOnTimeoutPolicyReturn(): bool
    method isOnTimeoutPolicyFail (line 66) | public function isOnTimeoutPolicyFail(): bool
    method setOnTimeoutPolicyToReturn (line 71) | public function setOnTimeoutPolicyToReturn(): bool
    method setOnTimeoutPolicyToFail (line 76) | public function setOnTimeoutPolicyToFail(): bool
    method getMinPhoneticTermLength (line 81) | public function getMinPhoneticTermLength(): int
    method setMinPhoneticTermLength (line 86) | public function setMinPhoneticTermLength(int $value = 3): bool

FILE: src/Suggestion.php
  class Suggestion (line 5) | class Suggestion extends AbstractIndex
    method add (line 18) | public function add(string $string, float $score, bool $increment = fa...
    method delete (line 41) | public function delete(string $string): bool
    method length (line 51) | public function length(): int
    method get (line 66) | public function get(string $prefix, bool $fuzzy = false, bool $withPay...

FILE: tests/RediSearch/Aggregate/AggregationResultTest.php
  class AggregationResultTest (line 8) | #[PHPUnit\Framework\Attributes\Group('aggregate')]
    method setUp (line 14) | public function setUp(): void
    method testGetCount (line 23) | public function testGetCount(): void
    method testGetDocuments (line 35) | public function testGetDocuments(): void
    method testMakeAggregationResultWithInvalidRedisResult (line 46) | public function testMakeAggregationResultWithInvalidRedisResult(): void

FILE: tests/RediSearch/Aggregate/BuilderTest.php
  class BuilderTest (line 10) | #[PHPUnit\Framework\Attributes\Group('aggregate')]
    method setUp (line 19) | public function setUp(): void
    method tearDown (line 68) | public function tearDown(): void
    method testGetAverageOfNumeric (line 73) | public function testGetAverageOfNumeric(): void
    method testGetAggregationAsArray (line 90) | public function testGetAggregationAsArray(): void
    method testGetGroupByAndReduce (line 107) | public function testGetGroupByAndReduce(): void
    method testGetGroupByAndReduceAndFilter (line 124) | public function testGetGroupByAndReduceAndFilter(): void
    method testPipelineHasCommands (line 142) | public function testPipelineHasCommands(): void
    method testClearPipeline (line 157) | public function testClearPipeline(): void
    method testGetCount (line 171) | public function testGetCount(): void
    method testGetCountDistinct (line 190) | public function testGetCountDistinct(): void
    method testGetCountDistinctWithReduceByField (line 209) | public function testGetCountDistinctWithReduceByField(): void
    method testGetCountDistinctApproximate (line 226) | public function testGetCountDistinctApproximate(): void
    method testGetSum (line 245) | public function testGetSum(): void
    method testGetMax (line 264) | public function testGetMax(): void
    method testGetMin (line 279) | public function testGetMin(): void
    method testGetAbsoluteMin (line 294) | public function testGetAbsoluteMin(): void
    method testGetAbsoluteMax (line 309) | public function testGetAbsoluteMax(): void
    method testGetQuantile (line 324) | public function testGetQuantile(): void
    method testGetAbsoluteQuantile (line 344) | public function testGetAbsoluteQuantile(): void
    method testGetStandardDeviation (line 359) | public function testGetStandardDeviation(): void
    method testSortByAscending (line 374) | public function testSortByAscending(): void
    method testSortByDescending (line 393) | public function testSortByDescending(): void
    method testSortByWithMax (line 412) | public function testSortByWithMax(): void

FILE: tests/RediSearch/Document/DocumentTest.php
  class DocumentTest (line 10) | class DocumentTest extends TestCase
    method testShouldGetDefinition (line 12) | public function testShouldGetDefinition(): void
    method testShouldGetDefinitionWithOptions (line 29) | public function testShouldGetDefinitionWithOptions(): void
    method testShouldThrowExceptionWhenScoreIsTooLow (line 74) | public function testShouldThrowExceptionWhenScoreIsTooLow(): void
    method testShouldThrowExceptionWhenScoreIsTooHigh (line 86) | public function testShouldThrowExceptionWhenScoreIsTooHigh(): void

FILE: tests/RediSearch/Exceptions/FieldNotInSchemaExceptionTest.php
  class FieldNotInSchemaExceptionTest (line 8) | class FieldNotInSchemaExceptionTest extends TestCase
    method testShouldShowCustomMessage (line 10) | public function testShouldShowCustomMessage(): void

FILE: tests/RediSearch/Exceptions/NoFieldsInIndexExceptionTest.php
  class NoFieldsInIndexExceptionTest (line 8) | class NoFieldsInIndexExceptionTest extends TestCase
    method testShouldShowCustomMessage (line 10) | public function testShouldShowCustomMessage(): void

FILE: tests/RediSearch/Exceptions/RedisRawCommandExceptionTest.php
  class RedisRawCommandExceptionTest (line 8) | class RedisRawCommandExceptionTest extends TestCase
    method testShouldShowCustomMessage (line 10) | public function testShouldShowCustomMessage(): void

FILE: tests/RediSearch/Exceptions/UnknownIndexNameExceptionTest.php
  class UnknownIndexNameExceptionTest (line 8) | class UnknownIndexNameExceptionTest extends TestCase
    method testShouldShowCustomMessage (line 10) | public function testShouldShowCustomMessage(): void

FILE: tests/RediSearch/Fields/FieldFactoryTest.php
  class FieldFactoryTest (line 9) | class FieldFactoryTest extends TestCase
    method testShouldThrowWhenFieldTypeIsUnknown (line 11) | public function testShouldThrowWhenFieldTypeIsUnknown(): void

FILE: tests/RediSearch/Fields/GeoFieldTest.php
  class GeoFieldTest (line 8) | class GeoFieldTest extends TestCase
    method testShouldGetCorrectType (line 10) | public function testShouldGetCorrectType(): void

FILE: tests/RediSearch/Fields/GeoLocationTest.php
  class GeoLocationTest (line 8) | class GeoLocationTest extends TestCase
    method testShouldGetStringValueOfGeoLocation (line 10) | public function testShouldGetStringValueOfGeoLocation(): void

FILE: tests/RediSearch/Fields/NumericFieldTest.php
  class NumericFieldTest (line 8) | class NumericFieldTest extends TestCase
    method testShouldGetCorrectType (line 10) | public function testShouldGetCorrectType(): void

FILE: tests/RediSearch/Fields/TextFieldTest.php
  class TextFieldTest (line 8) | class TextFieldTest extends TestCase
    method setUp (line 16) | public function setUp(): void
    method testShouldGetCorrectType (line 21) | public function testShouldGetCorrectType(): void
    method testShouldGetWeight (line 32) | public function testShouldGetWeight(): void
    method testShouldSetWeight (line 43) | public function testShouldSetWeight(): void
    method testShouldGetTypeDefinition (line 55) | public function testShouldGetTypeDefinition(): void

FILE: tests/RediSearch/IndexTest.php
  class IndexTest (line 29) | class IndexTest extends RediSearchTestCase
    method setUp (line 33) | public function setUp(): void
    method tearDown (line 48) | public function tearDown(): void
    method testShouldFailToCreateIndexWhenThereAreNoFieldsDefined (line 53) | public function testShouldFailToCreateIndexWhenThereAreNoFieldsDefined...
    method testShouldCreateIndex (line 65) | public function testShouldCreateIndex(): void
    method testShouldVerifyIndexExists (line 76) | public function testShouldVerifyIndexExists(): void
    method testShouldVerifyIndexDoesNotExist (line 88) | public function testShouldVerifyIndexDoesNotExist(): void
    method testShouldDropIndex (line 99) | public function testShouldDropIndex(): void
    method testShouldGetInfo (line 111) | public function testShouldGetInfo(): void
    method testShouldLoadFieldsFromExistingIndex (line 124) | public function testShouldLoadFieldsFromExistingIndex(): void
    method testLoadedFieldsCanBeUsedToMakeDocuments (line 148) | public function testLoadedFieldsCanBeUsedToMakeDocuments(): void
    method testLoadFieldsShouldNotIncludeInternalFields (line 163) | public function testLoadFieldsShouldNotIncludeInternalFields(): void
    method testLoadFieldsShouldRestoreTextFieldWeight (line 178) | public function testLoadFieldsShouldRestoreTextFieldWeight(): void
    method testLoadFieldsShouldRestoreSortableFlag (line 195) | public function testLoadFieldsShouldRestoreSortableFlag(): void
    method testLoadFieldsShouldRestoreTagFieldSeparator (line 211) | public function testLoadFieldsShouldRestoreTagFieldSeparator(): void
    method testLoadFieldsShouldReturnSelfForFluentChaining (line 228) | public function testLoadFieldsShouldReturnSelfForFluentChaining(): void
    method testShouldDeleteDocumentById (line 241) | public function testShouldDeleteDocumentById(): void
    method testShouldPhysicallyDeleteDocumentById (line 261) | public function testShouldPhysicallyDeleteDocumentById(): void
    method testCreateIndexWithSortableFields (line 281) | public function testCreateIndexWithSortableFields(): void
    method testCreateIndexWithNoindexFields (line 298) | public function testCreateIndexWithNoindexFields(): void
    method testCreateIndexWithTagField (line 315) | public function testCreateIndexWithTagField(): void
    method testGetTagValues (line 330) | public function testGetTagValues(): void
    method testAddDocumentWithZeroScore (line 361) | public function testAddDocumentWithZeroScore(): void
    method testAddDocumentWithNonDefaultScore (line 381) | public function testAddDocumentWithNonDefaultScore(): void
    method testAddDocumentUsingArrayOfFieldsCreatedWithFieldFactory (line 400) | public function testAddDocumentUsingArrayOfFieldsCreatedWithFieldFacto...
    method testAddDocumentUsingArrayOfFields (line 419) | public function testAddDocumentUsingArrayOfFields(): void
    method testAddDocumentUsingAssociativeArrayOfValues (line 437) | public function testAddDocumentUsingAssociativeArrayOfValues(): void
    method testAddDocument (line 454) | public function testAddDocument(): void
    method testAddDocumentWithUnsupportedLanguage (line 472) | public function testAddDocumentWithUnsupportedLanguage(): void
    method testSearchWithUnsupportedLanguage (line 487) | public function testSearchWithUnsupportedLanguage(): void
    method testAddDocumentToIndexWithAnUndefinedField (line 499) | public function testAddDocumentToIndexWithAnUndefinedField(): void
    method testAddDocumentToUndefinedIndex (line 511) | public function testAddDocumentToUndefinedIndex(): void
    method testAddDocumentAlreadyInIndex (line 528) | public function testAddDocumentAlreadyInIndex(): void
    method testReplaceDocument (line 546) | public function testReplaceDocument(): void
    method testAddDocumentFromHash (line 569) | public function testAddDocumentFromHash(): void
    method testFindDocumentAddedWithHash (line 586) | public function testFindDocumentAddedWithHash(): void
    method testReplaceDocumentFromHash (line 606) | public function testReplaceDocumentFromHash(): void
    method testSearch (line 634) | public function testSearch(): void
    method testGetCountDirectly (line 654) | public function testGetCountDirectly(): void
    method testSearchForNumeric (line 674) | public function testSearchForNumeric(): void
    method testAddDocumentWithGeoField (line 694) | public function testAddDocumentWithGeoField(): void
    method testAddDocumentWithTagField (line 720) | public function testAddDocumentWithTagField(): void
    method testAddDocumentWithTagFieldAndAlternateTagSeparator (line 744) | public function testAddDocumentWithTagFieldAndAlternateTagSeparator():...
    method testFilterTagFieldsAsUnionOfDocuments (line 766) | public function testFilterTagFieldsAsUnionOfDocuments(): void
    method testFilterTagFieldsAsIntersectionOfDocuments (line 787) | public function testFilterTagFieldsAsIntersectionOfDocuments(): void
    method testAddDocumentWithCustomId (line 810) | public function testAddDocumentWithCustomId(): void
    method testBatchIndexWithAdd (line 832) | public function testBatchIndexWithAdd(): void
    method testBatchIndexWithAddMany (line 853) | public function testBatchIndexWithAddMany(): void
    method testBatchIndexWithAddManyUsingPhpRedisWithAtomicityDisabled (line 872) | #[PHPUnit\Framework\Attributes\RequiresPhpExtension('redis')]
    method makeDocuments (line 900) | private function makeDocuments($count = 3000): array
    method testShouldCreateIndexWithImplicitName (line 911) | public function testShouldCreateIndexWithImplicitName(): void
    method testSetStopWordsOnCreateIndex (line 927) | public function testSetStopWordsOnCreateIndex(): void
    method testShouldNotSearchEveryIndexWhenAPrefixIsSpecified (line 949) | public function testShouldNotSearchEveryIndexWhenAPrefixIsSpecified():...
    method testShouldSearchEveryIndexWhenAPrefixIsNotSpecified (line 976) | public function testShouldSearchEveryIndexWhenAPrefixIsNotSpecified():...
    method testShouldCreateIndexWithNoFrequencies (line 998) | public function testShouldCreateIndexWithNoFrequencies(): void
    method testShouldNotChangeOriginalSchemaFieldWhenAddingNewDocument (line 1011) | public function testShouldNotChangeOriginalSchemaFieldWhenAddingNewDoc...
    method testShouldCreateAlias (line 1035) | public function testShouldCreateAlias(): void
    method testShouldUpdateAlias (line 1047) | public function testShouldUpdateAlias(): void
    method testShouldDeleteAlias (line 1063) | public function testShouldDeleteAlias(): void
    method testShouldFailToCreateAliasIfIndexDoesNotExist (line 1076) | public function testShouldFailToCreateAliasIfIndexDoesNotExist(): void
    method testShouldFailToUpdateAliasIfIndexDoesNotExist (line 1087) | public function testShouldFailToUpdateAliasIfIndexDoesNotExist(): void
    method testShouldFailToDeleteAliasIfIndexDoesNotExist (line 1098) | public function testShouldFailToDeleteAliasIfIndexDoesNotExist(): void
    method testShouldGetFields (line 1109) | public function testShouldGetFields(): void
    method testShouldConvertAnArrayToDocument (line 1132) | public function testShouldConvertAnArrayToDocument(): void

FILE: tests/RediSearch/Query/BuilderTest.php
  class BuilderTest (line 10) | class BuilderTest extends RediSearchTestCase
    method setUp (line 17) | public function setUp(): void
    method tearDown (line 58) | public function tearDown(): void
    method testSearch (line 63) | public function testSearch(): void
    method testGetCountDirectly (line 74) | public function testGetCountDirectly(): void
    method testReturnsZeroResultsWhenNotIndexed (line 85) | public function testReturnsZeroResultsWhenNotIndexed(): void
    method testSearchWithReturn (line 96) | public function testSearchWithReturn(): void
    method testSearchWithSummarize (line 111) | public function testSearchWithSummarize(): void
    method testSearchWithHighlight (line 124) | public function testSearchWithHighlight(): void
    method testSearchWithScores (line 137) | public function testSearchWithScores(): void
    method testSearchWithPayloads (line 149) | public function testSearchWithPayloads(): void
    method testVerbatimSearch (line 161) | public function testVerbatimSearch(): void
    method testVerbatimSearchFails (line 172) | public function testVerbatimSearchFails(): void
    method testNumericRangeQuery (line 183) | public function testNumericRangeQuery(): void
    method testGeoQuery (line 198) | public function testGeoQuery(): void
    method testGeoQueryWithoutSearchTerm (line 213) | public function testGeoQueryWithoutSearchTerm(): void
    method testLimitSearch (line 228) | public function testLimitSearch(): void
    method testSearchWithNoContent (line 240) | public function testSearchWithNoContent(): void
    method testSearchWithDefaultSlop (line 252) | public function testSearchWithDefaultSlop(): void
    method testSearchWithNonDefaultSlop (line 263) | public function testSearchWithNonDefaultSlop(): void
    method testExplainSimpleSearchQuery (line 274) | public function testExplainSimpleSearchQuery(): void
    method testExplainComplexSearchQuery (line 286) | public function testExplainComplexSearchQuery(): void
    method testSearchWithScorerFunction (line 300) | public function testSearchWithScorerFunction(): void
    method testSearchWithSortBy (line 311) | public function testSearchWithSortBy(): void

FILE: tests/RediSearch/Redis/RedisClientTest.php
  class RedisClientTest (line 8) | class RedisClientTest extends RediSearchTestCase
    method testShouldThrowUnknownIndexNameExceptionIfIndexDoesNotExist (line 10) | public function testShouldThrowUnknownIndexNameExceptionIfIndexDoesNot...

FILE: tests/RediSearch/RuntimeConfigurationTest.php
  class RuntimeConfigurationTest (line 8) | class RuntimeConfigurationTest extends RediSearchTestCase
    method setUp (line 12) | public function setUp(): void
    method tearDown (line 18) | public function tearDown(): void
    method testShouldSetMinPrefix (line 23) | public function testShouldSetMinPrefix(): void
    method testShouldSetMaxExpansions (line 39) | public function testShouldSetMaxExpansions(): void
    method testShouldSetTimeout (line 55) | public function testShouldSetTimeout(): void
    method testIsOnTimeoutPolicyReturn (line 71) | public function testIsOnTimeoutPolicyReturn(): void
    method testIsOnTimeoutPolicyFail (line 86) | public function testIsOnTimeoutPolicyFail(): void
    method testShouldSetMinPhoneticTermLength (line 101) | public function testShouldSetMinPhoneticTermLength(): void

FILE: tests/RediSearch/SuggestionTest.php
  class SuggestionTest (line 8) | class SuggestionTest extends RediSearchTestCase
    method setUp (line 12) | public function setUp(): void
    method tearDown (line 18) | public function tearDown(): void
    method testShouldAddSuggestion (line 23) | public function testShouldAddSuggestion(): void
    method testShouldIncrementExistingSuggestion (line 35) | public function testShouldIncrementExistingSuggestion(): void
    method testShouldDeleteSuggestion (line 54) | public function testShouldDeleteSuggestion(): void
    method testShouldGetDictionaryLength (line 67) | public function testShouldGetDictionaryLength(): void
    method testShouldGetSuggestion (line 82) | public function testShouldGetSuggestion(): void
    method testShouldGetSuggestionWithScore (line 101) | public function testShouldGetSuggestionWithScore(): void

FILE: tests/RediSearchTestCase.php
  class RediSearchTestCase (line 16) | abstract class RediSearchTestCase extends TestCase
    method setUp (line 29) | public function setUp(): void
    method makePhpRedisAdapter (line 46) | protected function makePhpRedisAdapter(): RedisRawClientInterface
    method makePredisAdapter (line 55) | protected function makePredisAdapter(): RedisRawClientInterface
    method makeRedisClientAdapter (line 64) | protected function makeRedisClientAdapter(): RedisRawClientInterface
    method isUsingPredis (line 73) | protected function isUsingPredis(): bool
    method isUsingPhpRedis (line 78) | protected function isUsingPhpRedis(): bool
    method isUsingRedisClient (line 83) | protected function isUsingRedisClient(): bool

FILE: tests/Stubs/IndexWithoutFields.php
  class IndexWithoutFields (line 7) | class IndexWithoutFields extends Index

FILE: tests/Stubs/TestDocument.php
  class TestDocument (line 15) | class TestDocument extends Document

FILE: tests/Stubs/TestIndex.php
  class TestIndex (line 7) | class TestIndex extends Index
Condensed preview — 126 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (304K chars).
[
  {
    "path": ".claude/skills/contribute/SKILL.md",
    "chars": 2856,
    "preview": "# Contribute Skill\n\nGuide for implementing a new feature or bug fix in redisearch-php.\n\n## Workflow\n\nMake a todo list fo"
  },
  {
    "path": ".claude/skills/unit-test/SKILL.md",
    "chars": 5832,
    "preview": "# Unit Test Skill\n\nGuide for writing, running, and debugging unit tests in redisearch-php.\n\n## Test Infrastructure\n\n- **"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 114,
    "preview": "# These are supported funding model platforms\n\nko_fi: ethanhann\ncustom: https://www.paypal.com/paypalme/EthanHann\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1970,
    "preview": "name: CI\n\non:\n  push:\n  pull_request:\n\njobs:\n  lint-and-format:\n    name: Lint & Format\n    runs-on: ubuntu-latest\n    s"
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 821,
    "preview": "name: Deploy Docs\n\non:\n  push:\n    branches: [ main ]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write"
  },
  {
    "path": ".gitignore",
    "chars": 111,
    "preview": "/vendor/\n.idea/\n.php-cs-fixer.cache\ncomposer.lock\ncomposer.phar\ntests.log\n/.phpunit.result.cache\n/docs/\n/.dev/\n"
  },
  {
    "path": ".php-cs-fixer.php",
    "chars": 183,
    "preview": "<?php\n\n$finder = PhpCsFixer\\Finder::create()\n    ->in(__DIR__ . '/src');\n\nreturn (new PhpCsFixer\\Config())\n    ->setRule"
  },
  {
    "path": "CLAUDE.md",
    "chars": 2450,
    "preview": "# CLAUDE.md\n\nThis file provides guidance for Claude Code when working in this repository.\n\n## Project Overview\n\n`redisea"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2017 Ethan Hann\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 1603,
    "preview": "# RediSearch PHP Client\n\n[![Latest Stable Version](https://poser.pugx.org/ethanhann/redisearch-php/v/stable)](https://pa"
  },
  {
    "path": "bin/redisearch",
    "chars": 427,
    "preview": "#!/usr/bin/env php\n<?php\n\n// Installed as a dependency (vendor/ethanhann/redisearch-php/bin/redisearch)\n$autoloadPaths ="
  },
  {
    "path": "composer.json",
    "chars": 1205,
    "preview": "{\n    \"name\": \"ethanhann/redisearch-php\",\n    \"type\": \"library\",\n    \"autoload\": {\n        \"psr-4\": {\n            \"Ehann"
  },
  {
    "path": "docker-compose.yml",
    "chars": 130,
    "preview": "services:\n    redis:\n      container_name: redisSearchPhpTest\n      image: 'redis/redis-stack'\n      ports:\n        - '6"
  },
  {
    "path": "docs-site/.gitignore",
    "chars": 28,
    "preview": "node_modules/\ndist/\n.astro/\n"
  },
  {
    "path": "docs-site/astro.config.mjs",
    "chars": 1217,
    "preview": "import { defineConfig } from 'astro/config';\nimport starlight from '@astrojs/starlight';\n\nexport default defineConfig({\n"
  },
  {
    "path": "docs-site/package.json",
    "chars": 242,
    "preview": "{\n  \"name\": \"redisearch-php-docs\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"build\": \"astro build\""
  },
  {
    "path": "docs-site/src/content/docs/aggregating.mdx",
    "chars": 1503,
    "preview": "---\ntitle: Aggregating\ndraft: false\ndescription: Aggregation pipelines and cursor-based pagination.\n---\n\n## The Basics\n\n"
  },
  {
    "path": "docs-site/src/content/docs/changelog.mdx",
    "chars": 6373,
    "preview": "---\ntitle: Changelog\ndraft: false\ndescription: Version history and release notes.\n---\n\n## 3.1.0\n\n[Changes since last rel"
  },
  {
    "path": "docs-site/src/content/docs/cli.mdx",
    "chars": 6130,
    "preview": "---\ntitle: CLI\ndraft: false\ndescription: A command-line interface for interacting with RediSearch directly from the term"
  },
  {
    "path": "docs-site/src/content/docs/index.mdx",
    "chars": 1841,
    "preview": "---\ntitle: RediSearch-PHP\ndraft: false\ndescription: A PHP client library for the RediSearch module, adding full-text sea"
  },
  {
    "path": "docs-site/src/content/docs/indexing.mdx",
    "chars": 10629,
    "preview": "---\ntitle: Indexing\ndraft: false\ndescription: Field types, adding, updating, and batch indexing documents.\n---\n\n## Field"
  },
  {
    "path": "docs-site/src/content/docs/laravel-support.mdx",
    "chars": 2626,
    "preview": "---\ntitle: Laravel Support\ndraft: false\ndescription: Laravel Scout driver integration for RediSearch.\n---\n\n[Laravel-Redi"
  },
  {
    "path": "docs-site/src/content/docs/searching.mdx",
    "chars": 4543,
    "preview": "---\ntitle: Searching\ndraft: false\ndescription: Text search, filtering, sorting, vector search, spell checking, and synon"
  },
  {
    "path": "docs-site/src/content/docs/suggesting.mdx",
    "chars": 827,
    "preview": "---\ntitle: Suggesting\ndraft: false\ndescription: Suggestion index creation and management.\n---\n\n## Creating a Suggestion "
  },
  {
    "path": "docs-site/src/content.config.ts",
    "chars": 270,
    "preview": "import { defineCollection } from 'astro:content';\nimport { docsLoader } from '@astrojs/starlight/loaders';\nimport { docs"
  },
  {
    "path": "docs-site/src/styles/custom.css",
    "chars": 109,
    "preview": ":root {\n  --sl-color-accent-low: #ffcdd2;\n  --sl-color-accent: #e53935;\n  --sl-color-accent-high: #b71c1c;\n}\n"
  },
  {
    "path": "docs-site/tsconfig.json",
    "chars": 42,
    "preview": "{\n  \"extends\": \"astro/tsconfigs/strict\"\n}\n"
  },
  {
    "path": "justfile",
    "chars": 870,
    "preview": "# Default: run tests with Predis\ndefault: test\n\n# Start Redis (detached)\nup:\n    docker compose up -d\n\n# Install depende"
  },
  {
    "path": "phpunit.xml",
    "chars": 859,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNam"
  },
  {
    "path": "src/AbstractIndex.php",
    "chars": 400,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch;\n\nuse Ehann\\RedisRaw\\RedisRawClientInterface;\n\nabstract class AbstractIndex extends Ab"
  },
  {
    "path": "src/AbstractRediSearchClientAdapter.php",
    "chars": 608,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch;\n\nuse Ehann\\RedisRaw\\RedisRawClientInterface;\n\nabstract class AbstractRediSearchClient"
  },
  {
    "path": "src/Aggregate/AggregationResult.php",
    "chars": 2039,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate;\n\nclass AggregationResult\n{\n    protected $count;\n    protected $documents;\n"
  },
  {
    "path": "src/Aggregate/Builder.php",
    "chars": 9548,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate;\n\nuse Ehann\\RediSearch\\Aggregate\\Operations\\Apply;\nuse Ehann\\RediSearch\\Aggr"
  },
  {
    "path": "src/Aggregate/BuilderInterface.php",
    "chars": 1795,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate;\n\nuse Ehann\\RediSearch\\CanBecomeArrayInterface;\n\ninterface BuilderInterface\n"
  },
  {
    "path": "src/Aggregate/Operations/AbstractFieldNameOperation.php",
    "chars": 686,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Operations;\n\nuse Ehann\\RediSearch\\CanBecomeArrayInterface;\n\nabstract class A"
  },
  {
    "path": "src/Aggregate/Operations/Apply.php",
    "chars": 495,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Operations;\n\nuse Ehann\\RediSearch\\CanBecomeArrayInterface;\n\nclass Apply impl"
  },
  {
    "path": "src/Aggregate/Operations/Filter.php",
    "chars": 382,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Operations;\n\nuse Ehann\\RediSearch\\CanBecomeArrayInterface;\n\nclass Filter imp"
  },
  {
    "path": "src/Aggregate/Operations/GroupBy.php",
    "chars": 226,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Operations;\n\nclass GroupBy extends AbstractFieldNameOperation\n{\n    public f"
  },
  {
    "path": "src/Aggregate/Operations/Limit.php",
    "chars": 450,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Operations;\n\nuse Ehann\\RediSearch\\CanBecomeArrayInterface;\n\nclass Limit impl"
  },
  {
    "path": "src/Aggregate/Operations/Load.php",
    "chars": 220,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Operations;\n\nclass Load extends AbstractFieldNameOperation\n{\n    public func"
  },
  {
    "path": "src/Aggregate/Operations/SortBy.php",
    "chars": 921,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Operations;\n\nclass SortBy extends AbstractFieldNameOperation\n{\n    protected"
  },
  {
    "path": "src/Aggregate/Reducers/AbstractFieldNameReducer.php",
    "chars": 646,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nuse Ehann\\RediSearch\\CanBecomeArrayInterface;\n\nabstract class Abs"
  },
  {
    "path": "src/Aggregate/Reducers/Aliasable.php",
    "chars": 94,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\ntrait Aliasable\n{\n    public $alias;\n}\n"
  },
  {
    "path": "src/Aggregate/Reducers/Avg.php",
    "chars": 290,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass Avg extends AbstractFieldNameReducer\n{\n    protected $reduc"
  },
  {
    "path": "src/Aggregate/Reducers/Count.php",
    "chars": 488,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nuse Ehann\\RediSearch\\CanBecomeArrayInterface;\n\nclass Count implem"
  },
  {
    "path": "src/Aggregate/Reducers/CountDistinct.php",
    "chars": 311,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass CountDistinct extends AbstractFieldNameReducer\n{\n    protec"
  },
  {
    "path": "src/Aggregate/Reducers/CountDistinctApproximate.php",
    "chars": 325,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass CountDistinctApproximate extends AbstractFieldNameReducer\n{"
  },
  {
    "path": "src/Aggregate/Reducers/FirstValue.php",
    "chars": 803,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass FirstValue extends AbstractFieldNameReducer\n{\n    protected"
  },
  {
    "path": "src/Aggregate/Reducers/Max.php",
    "chars": 290,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass Max extends AbstractFieldNameReducer\n{\n    protected $reduc"
  },
  {
    "path": "src/Aggregate/Reducers/Min.php",
    "chars": 290,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass Min extends AbstractFieldNameReducer\n{\n    protected $reduc"
  },
  {
    "path": "src/Aggregate/Reducers/Quantile.php",
    "chars": 498,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass Quantile extends AbstractFieldNameReducer\n{\n    public $qua"
  },
  {
    "path": "src/Aggregate/Reducers/StandardDeviation.php",
    "chars": 307,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass StandardDeviation extends AbstractFieldNameReducer\n{\n    pr"
  },
  {
    "path": "src/Aggregate/Reducers/Sum.php",
    "chars": 290,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass Sum extends AbstractFieldNameReducer\n{\n    protected $reduc"
  },
  {
    "path": "src/Aggregate/Reducers/ToList.php",
    "chars": 296,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Aggregate\\Reducers;\n\nclass ToList extends AbstractFieldNameReducer\n{\n    protected $re"
  },
  {
    "path": "src/CanBecomeArrayInterface.php",
    "chars": 112,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch;\n\ninterface CanBecomeArrayInterface\n{\n    public function toArray(): array;\n}\n"
  },
  {
    "path": "src/Console/AbstractRedisCommand.php",
    "chars": 2829,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console;\n\nuse Ehann\\RediSearch\\Index;\nuse Ehann\\RediSearch\\RediSearchRedisClient;\nuse "
  },
  {
    "path": "src/Console/Application.php",
    "chars": 1767,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console;\n\nuse Ehann\\RediSearch\\Console\\Command\\AggregateCommand;\nuse Ehann\\RediSearch\\"
  },
  {
    "path": "src/Console/Command/AggregateCommand.php",
    "chars": 5605,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/Command/DocumentAddCommand.php",
    "chars": 2728,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Ehann\\RediSea"
  },
  {
    "path": "src/Console/Command/DocumentDeleteCommand.php",
    "chars": 1252,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/Command/DocumentGetCommand.php",
    "chars": 2011,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/Command/ExplainCommand.php",
    "chars": 1050,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/Command/IndexCreateCommand.php",
    "chars": 3298,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Ehann\\RediSea"
  },
  {
    "path": "src/Console/Command/IndexDropCommand.php",
    "chars": 1134,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/Command/IndexInfoCommand.php",
    "chars": 2226,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/Command/IndexListCommand.php",
    "chars": 1236,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/Command/ProfileCommand.php",
    "chars": 2256,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/Command/SearchCommand.php",
    "chars": 5838,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/Command/ShellCommand.php",
    "chars": 8140,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console\\Command;\n\nuse Ehann\\RediSearch\\Console\\AbstractRedisCommand;\nuse Symfony\\Compo"
  },
  {
    "path": "src/Console/SchemaParser.php",
    "chars": 2537,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Console;\n\nuse Ehann\\RediSearch\\Index;\n\nclass SchemaParser\n{\n    /**\n     * Parses a JS"
  },
  {
    "path": "src/Document/AbstractDocumentFactory.php",
    "chars": 1291,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Document;\n\nuse Ehann\\RediSearch\\Exceptions\\FieldNotInSchemaException;\nuse Ehann\\RediSe"
  },
  {
    "path": "src/Document/Document.php",
    "chars": 4436,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Document;\n\nuse Ehann\\RediSearch\\Exceptions\\OutOfRangeDocumentScoreException;\nuse Ehann"
  },
  {
    "path": "src/Document/DocumentInterface.php",
    "chars": 887,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Document;\n\ninterface DocumentInterface\n{\n    public function getHashDefinition(array|n"
  },
  {
    "path": "src/Exceptions/AliasDoesNotExistException.php",
    "chars": 350,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass AliasDoesNotExistException extends Exception\n{\n    "
  },
  {
    "path": "src/Exceptions/DocumentAlreadyInIndexException.php",
    "chars": 336,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass DocumentAlreadyInIndexException extends Exception\n{"
  },
  {
    "path": "src/Exceptions/FieldNotInSchemaException.php",
    "chars": 323,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass FieldNotInSchemaException extends Exception\n{\n    p"
  },
  {
    "path": "src/Exceptions/NoFieldsInIndexException.php",
    "chars": 399,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass NoFieldsInIndexException extends Exception\n{\n    pu"
  },
  {
    "path": "src/Exceptions/OutOfRangeDocumentScoreException.php",
    "chars": 344,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass OutOfRangeDocumentScoreException extends Exception\n"
  },
  {
    "path": "src/Exceptions/RediSearchException.php",
    "chars": 111,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass RediSearchException extends Exception\n{\n}\n"
  },
  {
    "path": "src/Exceptions/UnknownIndexNameException.php",
    "chars": 347,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass UnknownIndexNameException extends Exception\n{\n    p"
  },
  {
    "path": "src/Exceptions/UnknownIndexNameOrNameIsAnAliasItselfException.php",
    "chars": 397,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass UnknownIndexNameOrNameIsAnAliasItselfException exte"
  },
  {
    "path": "src/Exceptions/UnknownRediSearchCommandException.php",
    "chars": 419,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass UnknownRediSearchCommandException extends Exception"
  },
  {
    "path": "src/Exceptions/UnsupportedRediSearchLanguageException.php",
    "chars": 316,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Exceptions;\n\nuse Exception;\n\nclass UnsupportedRediSearchLanguageException extends Exce"
  },
  {
    "path": "src/Fields/AbstractField.php",
    "chars": 682,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\nabstract class AbstractField implements FieldInterface\n{\n    protected $name;"
  },
  {
    "path": "src/Fields/FieldFactory.php",
    "chars": 809,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\nuse InvalidArgumentException;\n\nclass FieldFactory\n{\n    public static functio"
  },
  {
    "path": "src/Fields/FieldInterface.php",
    "chars": 268,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\ninterface FieldInterface\n{\n    public function getTypeDefinition(): array;\n  "
  },
  {
    "path": "src/Fields/GeoField.php",
    "chars": 397,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\nclass GeoField extends AbstractField\n{\n    use Noindex;\n\n    public function "
  },
  {
    "path": "src/Fields/GeoLocation.php",
    "chars": 373,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\nclass GeoLocation\n{\n    protected $longitude;\n    protected $latitude;\n\n    p"
  },
  {
    "path": "src/Fields/Noindex.php",
    "chars": 297,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\ntrait Noindex\n{\n    protected $isNoindex = false;\n\n    public function isNoin"
  },
  {
    "path": "src/Fields/NumericField.php",
    "chars": 507,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\nclass NumericField extends AbstractField\n{\n    use Sortable;\n    use Noindex;"
  },
  {
    "path": "src/Fields/Sortable.php",
    "chars": 305,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\ntrait Sortable\n{\n    protected $isSortable = false;\n\n    public function isSo"
  },
  {
    "path": "src/Fields/Tag.php",
    "chars": 238,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\nclass Tag\n{\n    protected $value;\n\n    public function __construct($value)\n  "
  },
  {
    "path": "src/Fields/TagField.php",
    "chars": 835,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\nclass TagField extends AbstractField\n{\n    use Sortable;\n    use Noindex;\n\n  "
  },
  {
    "path": "src/Fields/TextField.php",
    "chars": 1119,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\nclass TextField extends AbstractField\n{\n    use Sortable;\n    use Noindex;\n\n "
  },
  {
    "path": "src/Fields/VectorField.php",
    "chars": 3403,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Fields;\n\n/**\n * Represents a VECTOR field in a RediSearch index. Available in RediSear"
  },
  {
    "path": "src/Index.php",
    "chars": 35139,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch;\n\nuse Ehann\\RediSearch\\Aggregate\\Builder as AggregateBuilder;\nuse Ehann\\RediSearch\\Agg"
  },
  {
    "path": "src/IndexInterface.php",
    "chars": 3788,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch;\n\nuse Ehann\\RediSearch\\Aggregate\\BuilderInterface as AggregateBuilderInterface;\nuse Eh"
  },
  {
    "path": "src/Language.php",
    "chars": 1871,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch;\n\nclass Language\n{\n    public const ARABIC = 'arabic';\n    public const BASQUE = 'basq"
  },
  {
    "path": "src/Query/Builder.php",
    "chars": 9393,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Query;\n\nuse Ehann\\RediSearch\\RediSearchRedisClient;\nuse InvalidArgumentException;\n\ncla"
  },
  {
    "path": "src/Query/BuilderInterface.php",
    "chars": 1970,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Query;\n\ninterface BuilderInterface\n{\n    public function noContent(): BuilderInterface"
  },
  {
    "path": "src/Query/SearchResult.php",
    "chars": 2438,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch\\Query;\n\nclass SearchResult\n{\n    protected $count;\n    protected $documents;\n\n    publ"
  },
  {
    "path": "src/RediSearchRedisClient.php",
    "chars": 4452,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch;\n\nuse Ehann\\RediSearch\\Exceptions\\AliasDoesNotExistException;\nuse Ehann\\RediSearch\\Exc"
  },
  {
    "path": "src/RuntimeConfiguration.php",
    "chars": 2431,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch;\n\nclass RuntimeConfiguration extends AbstractRediSearchClientAdapter\n{\n    protected f"
  },
  {
    "path": "src/Suggestion.php",
    "chars": 2184,
    "preview": "<?php\n\nnamespace Ehann\\RediSearch;\n\nclass Suggestion extends AbstractIndex\n{\n    /**\n     * Add a suggestion string to a"
  },
  {
    "path": "tests/RediSearch/Aggregate/AggregationResultTest.php",
    "chars": 1385,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Aggregate;\n\nuse Ehann\\RediSearch\\Aggregate\\AggregationResult;\nuse Ehann\\Tests\\Re"
  },
  {
    "path": "tests/RediSearch/Aggregate/BuilderTest.php",
    "chars": 11813,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Aggregate;\n\nuse Ehann\\RediSearch\\Aggregate\\Builder;\nuse Ehann\\RediSearch\\Aggrega"
  },
  {
    "path": "tests/RediSearch/Document/DocumentTest.php",
    "chars": 3147,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Document;\n\nuse Ehann\\RediSearch\\Document\\Document;\nuse Ehann\\RediSearch\\Exceptio"
  },
  {
    "path": "tests/RediSearch/Exceptions/FieldNotInSchemaExceptionTest.php",
    "chars": 505,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Exceptions;\n\nuse Ehann\\RediSearch\\Exceptions\\FieldNotInSchemaException;\nuse PHPU"
  },
  {
    "path": "tests/RediSearch/Exceptions/NoFieldsInIndexExceptionTest.php",
    "chars": 533,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Exceptions;\n\nuse Ehann\\RediSearch\\Exceptions\\NoFieldsInIndexException;\nuse PHPUn"
  },
  {
    "path": "tests/RediSearch/Exceptions/RedisRawCommandExceptionTest.php",
    "chars": 534,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch;\n\nuse Ehann\\RedisRaw\\Exceptions\\RedisRawCommandException;\nuse PHPUnit\\Framework\\"
  },
  {
    "path": "tests/RediSearch/Exceptions/UnknownIndexNameExceptionTest.php",
    "chars": 525,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch;\n\nuse Ehann\\RediSearch\\Exceptions\\UnknownIndexNameException;\nuse PHPUnit\\Framewo"
  },
  {
    "path": "tests/RediSearch/Fields/FieldFactoryTest.php",
    "chars": 503,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Fields;\n\nuse Ehann\\RediSearch\\Fields\\FieldFactory;\nuse InvalidArgumentException;"
  },
  {
    "path": "tests/RediSearch/Fields/GeoFieldTest.php",
    "chars": 408,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Fields;\n\nuse Ehann\\RediSearch\\Fields\\GeoField;\nuse PHPUnit\\Framework\\TestCase;\n\n"
  },
  {
    "path": "tests/RediSearch/Fields/GeoLocationTest.php",
    "chars": 447,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Fields;\n\nuse Ehann\\RediSearch\\Fields\\GeoLocation;\nuse PHPUnit\\Framework\\TestCase"
  },
  {
    "path": "tests/RediSearch/Fields/NumericFieldTest.php",
    "chars": 428,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Fields;\n\nuse Ehann\\RediSearch\\Fields\\NumericField;\nuse PHPUnit\\Framework\\TestCas"
  },
  {
    "path": "tests/RediSearch/Fields/TextFieldTest.php",
    "chars": 1661,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Fields;\n\nuse Ehann\\RediSearch\\Fields\\TextField;\nuse PHPUnit\\Framework\\TestCase;\n"
  },
  {
    "path": "tests/RediSearch/IndexTest.php",
    "chars": 36028,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch;\n\nuse Ehann\\RediSearch\\Exceptions\\AliasDoesNotExistException;\nuse Ehann\\RediSear"
  },
  {
    "path": "tests/RediSearch/Query/BuilderTest.php",
    "chars": 10240,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Query;\n\nuse Ehann\\RediSearch\\Fields\\GeoLocation;\nuse Ehann\\RediSearch\\Query\\Buil"
  },
  {
    "path": "tests/RediSearch/Redis/RedisClientTest.php",
    "chars": 528,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch\\Redis;\n\nuse Ehann\\RediSearch\\Exceptions\\UnknownIndexNameException;\nuse Ehann\\Tes"
  },
  {
    "path": "tests/RediSearch/RuntimeConfigurationTest.php",
    "chars": 3197,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch;\n\nuse Ehann\\RediSearch\\RuntimeConfiguration;\nuse Ehann\\Tests\\RediSearchTestCase;"
  },
  {
    "path": "tests/RediSearch/SuggestionTest.php",
    "chars": 3237,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\RediSearch;\n\nuse Ehann\\RediSearch\\Suggestion;\nuse Ehann\\Tests\\RediSearchTestCase;\n\nclass Su"
  },
  {
    "path": "tests/RediSearchTestCase.php",
    "chars": 2637,
    "preview": "<?php\n\nnamespace Ehann\\Tests;\n\nuse Ehann\\RediSearch\\RediSearchRedisClient;\nuse Ehann\\RedisRaw\\AbstractRedisRawClient;\nus"
  },
  {
    "path": "tests/Stubs/IndexWithoutFields.php",
    "chars": 109,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\Stubs;\n\nuse Ehann\\RediSearch\\Index;\n\nclass IndexWithoutFields extends Index\n{\n}\n"
  },
  {
    "path": "tests/Stubs/TestDocument.php",
    "chars": 330,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\Stubs;\n\nuse Ehann\\RediSearch\\Document\\Document;\nuse Ehann\\RediSearch\\Fields\\NumericField;\nu"
  },
  {
    "path": "tests/Stubs/TestIndex.php",
    "chars": 100,
    "preview": "<?php\n\nnamespace Ehann\\Tests\\Stubs;\n\nuse Ehann\\RediSearch\\Index;\n\nclass TestIndex extends Index\n{\n}\n"
  },
  {
    "path": "tests/bootstrap.php",
    "chars": 50,
    "preview": "<?php\n\nrequire __DIR__.'/../vendor/autoload.php';\n"
  }
]

About this extraction

This page contains the full source code of the ethanhann/redisearch-php GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 126 files (278.1 KB), approximately 73.1k tokens, and a symbol index with 685 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!