mirror of
https://github.com/predis/predis.git
synced 2026-09-10 18:37:34 +00:00
9302204964
* Added support for SDIFFCARD and SUNIONCARD command * Fixed broken test * Fixed failing tests
1183 lines
40 KiB
PHP
1183 lines
40 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Predis package.
|
|
*
|
|
* (c) 2009-2020 Daniele Alessandri
|
|
* (c) 2021-2026 Till Krüss
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Predis\Command\Redis\Search;
|
|
|
|
use Predis\ClientContextInterface;
|
|
use Predis\ClientInterface;
|
|
use Predis\Command\Argument\Search\CreateArguments;
|
|
use Predis\Command\Argument\Search\HybridSearch\Combine\LinearCombineConfig;
|
|
use Predis\Command\Argument\Search\HybridSearch\Combine\RRFCombineConfig;
|
|
use Predis\Command\Argument\Search\HybridSearch\HybridSearchQuery;
|
|
use Predis\Command\Argument\Search\HybridSearch\Reducer;
|
|
use Predis\Command\Argument\Search\HybridSearch\ScorerConfig;
|
|
use Predis\Command\Argument\Search\HybridSearch\SearchConfig;
|
|
use Predis\Command\Argument\Search\HybridSearch\VectorSearch\KNNVectorSearchConfig;
|
|
use Predis\Command\Argument\Search\HybridSearch\VectorSearch\RangeVectorSearchConfig;
|
|
use Predis\Command\Argument\Search\SchemaFields\NumericField;
|
|
use Predis\Command\Argument\Search\SchemaFields\TagField;
|
|
use Predis\Command\Argument\Search\SchemaFields\TextField;
|
|
use Predis\Command\Argument\Search\SchemaFields\VectorField;
|
|
use Predis\Command\Redis\PredisCommandTestCase;
|
|
use Predis\Command\Redis\Utils\VectorUtility;
|
|
|
|
/**
|
|
* @group commands
|
|
* @group realm-stack
|
|
*/
|
|
class FTHYBRID_Test extends PredisCommandTestCase
|
|
{
|
|
protected function getExpectedCommand(): string
|
|
{
|
|
return FTHYBRID::class;
|
|
}
|
|
|
|
protected function getExpectedId(): string
|
|
{
|
|
return 'FTHYBRID';
|
|
}
|
|
|
|
/**
|
|
* @group disconnected
|
|
*/
|
|
public function testFilterArguments(): void
|
|
{
|
|
$command = $this->getCommand();
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config->buildScorerConfig(static function (ScorerConfig $scorerConfig) {
|
|
$scorerConfig->type(ScorerConfig::TYPE_DISMAX);
|
|
})
|
|
->query('*');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('vector', '$vector')
|
|
->k(5)
|
|
->ef(10);
|
|
})
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([0.1, 0.2, 0.3]),
|
|
]);
|
|
$index = 'idx';
|
|
|
|
$command->setArguments([$index, $query]);
|
|
|
|
$this->assertSameValues(
|
|
['idx', 'SEARCH', '*', 'SCORER', ScorerConfig::TYPE_DISMAX, 'VSIM', 'vector', '$vector', 'KNN', 4, 'K', 5, 'EF_RUNTIME', 10, 'PARAMS', 2, 'vector', VectorUtility::toBlob([0.1, 0.2, 0.3])],
|
|
$command->getArguments()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testParseResponse(): void
|
|
{
|
|
$response = ['total_results', 5, 'results', [['key', 'value'], ['key', 'value']]];
|
|
$command = new FTHYBRID();
|
|
|
|
$this->assertEquals([
|
|
'total_results' => 5,
|
|
'results' => [['key' => 'value'], ['key' => 'value']],
|
|
], $command->parseResponse($response));
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testReviewFeedbackHybridSearch()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 5);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red} @color:{green}')
|
|
->buildScorerConfig(static function (ScorerConfig $scorerConfig) {
|
|
$scorerConfig->type(ScorerConfig::TYPE_TFIDF);
|
|
});
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config->vector('@embedding', '$vector');
|
|
})
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([-100, -200, -200, -300]),
|
|
]);
|
|
|
|
$this->assertGreaterThan(0, $redis->fthybrid('idx', $query)['total_results']);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testReviewFeedbackHybridSearchResp3()
|
|
{
|
|
$redis = $this->getResp3Client();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 5);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red} @color:{green}')
|
|
->buildScorerConfig(static function (ScorerConfig $scorerConfig) {
|
|
$scorerConfig->type(ScorerConfig::TYPE_TFIDF);
|
|
});
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config->vector('@embedding', '$vector');
|
|
})
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([-100, -200, -200, -300]),
|
|
]);
|
|
|
|
$this->assertGreaterThan(0, $redis->fthybrid('idx', $query)['total_results']);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testDefaultHybridSearch()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 5);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red} @color:{green}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config->vector('@embedding', '$vector');
|
|
})
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([-100, -200, -200, -300]),
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
|
|
$this->assertEquals(10, $response['total_results']);
|
|
$this->assertCount(10, $response['results']);
|
|
$this->assertEmpty($response['warnings']);
|
|
$this->assertGreaterThan(0, $response['execution_time']);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testDefaultHybridSearchResp3()
|
|
{
|
|
$redis = $this->getResp3Client();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 5);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red} @color:{green}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config->vector('@embedding', '$vector');
|
|
})
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([-100, -200, -200, -300]),
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
|
|
$this->assertEquals(10, $response['total_results']);
|
|
$this->assertCount(10, $response['results']);
|
|
$this->assertEmpty($response['warnings']);
|
|
$this->assertGreaterThan(0, $response['execution_time']);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithScorer()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 10);
|
|
|
|
$query = (new HybridSearchQuery(KNNVectorSearchConfig::class, LinearCombineConfig::class))
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('shoes')
|
|
->buildScorerConfig(static function (ScorerConfig $scorerConfig) {
|
|
$scorerConfig->type(ScorerConfig::TYPE_TFIDF);
|
|
});
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config->vector('@embedding', '$vector');
|
|
})
|
|
->buildCombineConfig(static function (LinearCombineConfig $config) {
|
|
$config
|
|
->alpha(1)
|
|
->beta(0);
|
|
})
|
|
->load(['@description', '@color', '@price', '@size', '@__score', '@__item'])
|
|
->limit(0, 2)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 2, 3]),
|
|
]);
|
|
|
|
$expectedResultsTFIDF = [
|
|
[
|
|
'description' => 'red shoes',
|
|
'color' => 'red',
|
|
'price' => '15',
|
|
'size' => '10',
|
|
'__score' => '2',
|
|
],
|
|
[
|
|
'description' => 'green shoes with red laces',
|
|
'color' => 'green',
|
|
'price' => '16',
|
|
'size' => '11',
|
|
'__score' => '2',
|
|
],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expectedResultsTFIDF, $response['results']);
|
|
|
|
$query = (new HybridSearchQuery(KNNVectorSearchConfig::class, LinearCombineConfig::class))
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('shoes')
|
|
->buildScorerConfig(static function (ScorerConfig $scorerConfig) {
|
|
$scorerConfig->type(ScorerConfig::TYPE_BM25);
|
|
});
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config->vector('@embedding', '$vector');
|
|
})
|
|
->buildCombineConfig(static function (LinearCombineConfig $config) {
|
|
$config
|
|
->alpha(1)
|
|
->beta(0);
|
|
})
|
|
->load(['@description', '@color', '@price', '@size', '@__score', '@__item'])
|
|
->limit(0, 2)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 2, 3]),
|
|
]);
|
|
|
|
$expectedResultsBM25 = [
|
|
[
|
|
'description' => 'red shoes',
|
|
'color' => 'red',
|
|
'price' => '15',
|
|
'size' => '10',
|
|
'__score' => '0.657894719299',
|
|
],
|
|
[
|
|
'description' => 'green shoes with red laces',
|
|
'color' => 'green',
|
|
'price' => '16',
|
|
'size' => '11',
|
|
'__score' => '0.657894719299',
|
|
],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expectedResultsBM25, $response['results']);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithVsimMethodDefinedQueryInit()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 5);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('shoes');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding-hnsw', '$vector')
|
|
->k(3)
|
|
->ef(1);
|
|
})
|
|
->params([
|
|
'vector' => 'abcd1234efgh5678',
|
|
]);
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertGreaterThan(0, count($response['results']));
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithVsimFilter()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 5);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{missing}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector')
|
|
->filter('@price:[15 16] @size:[10 11]');
|
|
})
|
|
->load(['@price', '@size'])
|
|
->params([
|
|
'vector' => 'abcd1234efgh5678',
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertGreaterThan(0, count($response['results']));
|
|
|
|
foreach ($response['results'] as $result) {
|
|
$this->assertTrue(in_array($result['price'], ['15', '16']));
|
|
$this->assertTrue(in_array($result['size'], ['10', '11']));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithSearchScoreAliases()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('shoes')
|
|
->as('search_score');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->params([
|
|
'vector' => 'abcd1234efgh5678',
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertGreaterThan(0, count($response['results']));
|
|
|
|
foreach ($response['results'] as $result) {
|
|
if (in_array($result['__key'], ['item:0', 'item:1', 'item:4'])) {
|
|
$this->assertArrayHasKey('search_score', $result);
|
|
$this->assertArrayHasKey('__score', $result);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithVsimScoreAliases()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('shoes');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding-hnsw', '$vector')
|
|
->k(3)
|
|
->ef(1)
|
|
->as('vsim_score');
|
|
})
|
|
->params([
|
|
'vector' => 'abcd1234efgh5678',
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertGreaterThan(0, count($response['results']));
|
|
|
|
foreach ($response['results'] as $result) {
|
|
if (in_array($result['__key'], ['item:0', 'item:1', 'item:2'])) {
|
|
$this->assertArrayHasKey('vsim_score', $result);
|
|
$this->assertArrayHasKey('__score', $result);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithCombineScoreAliases()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis);
|
|
|
|
$query = (new HybridSearchQuery(KNNVectorSearchConfig::class, LinearCombineConfig::class))
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('shoes')
|
|
->as('search_score');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding-hnsw', '$vector')
|
|
->k(3)
|
|
->ef(1)
|
|
->as('vsim_score');
|
|
})
|
|
->buildCombineConfig(static function (LinearCombineConfig $config) {
|
|
$config
|
|
->alpha(0.5)
|
|
->beta(0.5)
|
|
->as('combine_score');
|
|
})
|
|
->params([
|
|
'vector' => 'abcd1234efgh5678',
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertGreaterThan(0, count($response['results']));
|
|
|
|
foreach ($response['results'] as $result) {
|
|
if (in_array($result['__key'], ['item:0', 'item:1', 'item:2'])) {
|
|
$this->assertArrayHasKey('vsim_score', $result);
|
|
} else {
|
|
$this->assertArrayNotHasKey('vsim_score', $result);
|
|
}
|
|
|
|
if (in_array($result['__key'], ['item:0', 'item:1', 'item:4'])) {
|
|
$this->assertArrayHasKey('search_score', $result);
|
|
} else {
|
|
$this->assertArrayNotHasKey('search_score', $result);
|
|
}
|
|
|
|
$this->assertArrayHasKey('combine_score', $result);
|
|
$this->assertArrayNotHasKey('__score', $result);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithCombineAllScoreAliases()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis);
|
|
|
|
$query = (new HybridSearchQuery(KNNVectorSearchConfig::class, LinearCombineConfig::class))
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('shoes')
|
|
->as('search_score');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding-hnsw', '$vector')
|
|
->k(3)
|
|
->ef(1)
|
|
->as('vsim_score');
|
|
})
|
|
->buildCombineConfig(static function (LinearCombineConfig $config) {
|
|
$config
|
|
->alpha(0.5)
|
|
->beta(0.5)
|
|
->as('combine_score');
|
|
})
|
|
->params([
|
|
'vector' => 'abcd1234efgh5678',
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertGreaterThan(0, count($response['results']));
|
|
|
|
foreach ($response['results'] as $result) {
|
|
$this->assertArrayHasKey('combine_score', $result);
|
|
$this->assertArrayNotHasKey('__score', $result);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithVsimKNN()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 10);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{none}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector')
|
|
->k(3);
|
|
})
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 2, 3]),
|
|
]);
|
|
|
|
$expected_results = [
|
|
['__key' => 'item:2', '__score' => '0.016393442623'],
|
|
['__key' => 'item:7', '__score' => '0.0161290322581'],
|
|
['__key' => 'item:12', '__score' => '0.015873015873'],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expected_results, $response['results']);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{none}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding-hnsw', '$vector')
|
|
->k(3)
|
|
->ef(1);
|
|
})
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 2, 3]),
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
|
|
// The dataset duplicates each vector, so HNSW may return any instance
|
|
// of the nearest item depending on the server version. Assert scores
|
|
// and that every returned key holds the expected nearest item instead.
|
|
$this->assertSame(
|
|
['0.016393442623', '0.0161290322581', '0.015873015873'],
|
|
array_column($response['results'], '__score')
|
|
);
|
|
|
|
$keys = array_column($response['results'], '__key');
|
|
$this->assertCount(3, array_unique($keys));
|
|
|
|
foreach ($keys as $key) {
|
|
$this->assertSame('red dress', $redis->hget($key, 'description'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithVsimRange()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 10);
|
|
|
|
$query = (new HybridSearchQuery(RangeVectorSearchConfig::class))
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{none}');
|
|
})
|
|
->buildVectorSearchConfig(static function (RangeVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->limit(0, 3)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$expected_results = [
|
|
['__key' => 'item:2', '__score' => '0.016393442623'],
|
|
['__key' => 'item:7', '__score' => '0.0161290322581'],
|
|
['__key' => 'item:12', '__score' => '0.015873015873'],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expected_results, $response['results']);
|
|
|
|
$query = (new HybridSearchQuery(RangeVectorSearchConfig::class))
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{none}');
|
|
})
|
|
->buildVectorSearchConfig(static function (RangeVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding-hnsw', '$vector')
|
|
->radius(2)
|
|
->epsilon(0.5);
|
|
})
|
|
->limit(0, 3)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
|
|
// The dataset duplicates each vector, so HNSW may return any instance
|
|
// of the item within the radius depending on the server version.
|
|
// Assert scores and that every returned key holds the expected item.
|
|
$this->assertSame(
|
|
['0.016393442623', '0.0161290322581', '0.015873015873'],
|
|
array_column($response['results'], '__score')
|
|
);
|
|
|
|
$keys = array_column($response['results'], '__key');
|
|
$this->assertCount(3, array_unique($keys));
|
|
|
|
foreach ($keys as $key) {
|
|
$this->assertSame('red dress', $redis->hget($key, 'description'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithCombine()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 10);
|
|
|
|
$query = (new HybridSearchQuery(KNNVectorSearchConfig::class, LinearCombineConfig::class))
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->buildCombineConfig(static function (LinearCombineConfig $config) {
|
|
$config
|
|
->alpha(0.5)
|
|
->beta(0.5);
|
|
})
|
|
->limit(0, 3)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$expected_results = [
|
|
['__key' => 'item:2', '__score' => '0.166666666667'],
|
|
['__key' => 'item:7', '__score' => '0.166666666667'],
|
|
['__key' => 'item:12', '__score' => '0.166666666667'],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expected_results, $response['results']);
|
|
|
|
$query = (new HybridSearchQuery(KNNVectorSearchConfig::class, RRFCombineConfig::class))
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->buildCombineConfig(static function (RRFCombineConfig $config) {
|
|
$config
|
|
->window(3)
|
|
->rrfConstant(0.5);
|
|
})
|
|
->limit(0, 3)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$expected_results = [
|
|
['__key' => 'item:2', '__score' => '1.5'],
|
|
['__key' => 'item:0', '__score' => '1'],
|
|
['__key' => 'item:7', '__score' => '0.5'],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expected_results, $response['results']);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithLoad()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 10);
|
|
|
|
$query = (new HybridSearchQuery(KNNVectorSearchConfig::class, LinearCombineConfig::class))
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red|green|black}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->buildCombineConfig(static function (LinearCombineConfig $config) {
|
|
$config
|
|
->alpha(0.5)
|
|
->beta(0.5);
|
|
})
|
|
->load(['@description', '@color', '@price', '@size', '@__key'])
|
|
->limit(0, 1)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$expected_results = [
|
|
'description' => 'red dress',
|
|
'color' => 'red',
|
|
'price' => '17',
|
|
'size' => '12',
|
|
'__key' => 'item:2',
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expected_results, $response['results'][0]);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithLoadAndApply()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 10);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->load(['@color', '@price', '@size'])
|
|
->apply([
|
|
'price_discount' => '@price - (@price * 0.1)',
|
|
'tax_discount' => '@price_discount * 0.2',
|
|
])
|
|
->limit(0, 3)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$expected_results = [
|
|
[
|
|
'color' => 'red',
|
|
'price' => '17',
|
|
'size' => '12',
|
|
'price_discount' => '15.3',
|
|
'tax_discount' => '3.06',
|
|
],
|
|
[
|
|
'color' => 'red',
|
|
'price' => '18',
|
|
'size' => '11',
|
|
'price_discount' => '16.2',
|
|
'tax_discount' => '3.24',
|
|
],
|
|
[
|
|
'color' => 'red',
|
|
'price' => '15',
|
|
'size' => '10',
|
|
'price_discount' => '13.5',
|
|
'tax_discount' => '2.7',
|
|
],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expected_results, $response['results']);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithLoadAndFilter()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 10);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red|green|black}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->load(['@description', '@color', '@price', '@size'])
|
|
->filter('@price=="15"')
|
|
->limit(0, 3)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertCount(3, $response['results']);
|
|
|
|
foreach ($response['results'] as $result) {
|
|
$this->assertEquals(15, $result['price']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithLoadApplyAndParams()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 5);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{$color_criteria}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->load(['@description', '@color', '@price'])
|
|
->apply([
|
|
'price_discount' => '@price - (@price * 0.1)',
|
|
])
|
|
->params([
|
|
'vector' => 'abcd1234abcd5678',
|
|
'color_criteria' => 'red',
|
|
])
|
|
->limit(0, 3);
|
|
|
|
$expected_results = [
|
|
[
|
|
'description' => 'red shoes',
|
|
'color' => 'red',
|
|
'price' => '15',
|
|
'price_discount' => '13.5',
|
|
],
|
|
[
|
|
'description' => 'red dress',
|
|
'color' => 'red',
|
|
'price' => '17',
|
|
'price_discount' => '15.3',
|
|
],
|
|
[
|
|
'description' => 'red shoes',
|
|
'color' => 'red',
|
|
'price' => '16',
|
|
'price_discount' => '14.4',
|
|
],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expected_results, $response['results']);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithApplyAndSortBy()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red|green}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->load(['@color', '@price'])
|
|
->apply([
|
|
'price_discount' => '@price - (@price * 0.1)',
|
|
])
|
|
->sortBy([
|
|
'@price_discount' => 'DESC',
|
|
'@color' => 'ASC',
|
|
])
|
|
->limit(0, 5)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$expected_results = [
|
|
[
|
|
'color' => 'orange',
|
|
'price' => '18',
|
|
'price_discount' => '16.2',
|
|
],
|
|
[
|
|
'color' => 'red',
|
|
'price' => '17',
|
|
'price_discount' => '15.3',
|
|
],
|
|
[
|
|
'color' => 'green',
|
|
'price' => '16',
|
|
'price_discount' => '14.4',
|
|
],
|
|
[
|
|
'color' => 'black',
|
|
'price' => '15',
|
|
'price_discount' => '13.5',
|
|
],
|
|
[
|
|
'color' => 'red',
|
|
'price' => '15',
|
|
'price_discount' => '13.5',
|
|
],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertEquals($expected_results, $response['results']);
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithLoadAndGroupBy()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 10);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red|green}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->load(['@color', '@price', '@size', '@item_type'])
|
|
->groupBy(
|
|
['@item_type', '@price'],
|
|
[
|
|
new Reducer(Reducer::REDUCE_COUNT_DISTINCT, ['@color'], 'colors_count'),
|
|
new Reducer(Reducer::REDUCE_MIN, ['@size']),
|
|
]
|
|
)
|
|
->sortBy([
|
|
'@price' => 'ASC',
|
|
])
|
|
->limit(0, 4)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$expected_results = [
|
|
[
|
|
'item_type' => 'dress',
|
|
'price' => '15',
|
|
'colors_count' => '1',
|
|
'__generated_aliasminsize' => '10',
|
|
],
|
|
[
|
|
'item_type' => 'shoes',
|
|
'price' => '15',
|
|
'colors_count' => '2',
|
|
'__generated_aliasminsize' => '10',
|
|
],
|
|
[
|
|
'item_type' => 'shoes',
|
|
'price' => '16',
|
|
'colors_count' => '2',
|
|
'__generated_aliasminsize' => '10',
|
|
],
|
|
[
|
|
'item_type' => 'dress',
|
|
'price' => '16',
|
|
'colors_count' => '1',
|
|
'__generated_aliasminsize' => '11',
|
|
],
|
|
];
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
|
|
// SORTBY guarantees ordering only by price; the relative order of
|
|
// groups with equal price is unspecified and varies across versions.
|
|
$this->assertEqualsCanonicalizing($expected_results, $response['results']);
|
|
$this->assertSame(['15', '15', '16', '16'], array_column($response['results'], 'price'));
|
|
}
|
|
|
|
/**
|
|
* @group connected
|
|
* @group relay-resp3
|
|
* @requiresRedisVersion >= 8.3.224
|
|
* @return void
|
|
*/
|
|
public function testHybridSearchQueryWithCursor()
|
|
{
|
|
$redis = $this->getClient();
|
|
$this->createHybridSearchIndex($redis);
|
|
$this->generateData($redis, 10);
|
|
|
|
$query = (new HybridSearchQuery())
|
|
->buildSearchConfig(static function (SearchConfig $config) {
|
|
$config
|
|
->query('@color:{red|green}');
|
|
})
|
|
->buildVectorSearchConfig(static function (KNNVectorSearchConfig $config) {
|
|
$config
|
|
->vector('@embedding', '$vector');
|
|
})
|
|
->withCursor(5, 100)
|
|
->params([
|
|
'vector' => VectorUtility::toBlob([1, 2, 7, 6]),
|
|
]);
|
|
|
|
$response = $redis->fthybrid('idx', $query);
|
|
$this->assertGreaterThan(0, $response['SEARCH']);
|
|
$this->assertGreaterThan(0, $response['VSIM']);
|
|
}
|
|
|
|
protected function createHybridSearchIndex(ClientInterface $client)
|
|
{
|
|
$fields = [
|
|
new TextField('description'),
|
|
new NumericField('price'),
|
|
new TagField('color'),
|
|
new TagField('item_type'),
|
|
new NumericField('size'),
|
|
new VectorField('embedding', 'FLAT',
|
|
['TYPE', 'FLOAT32', 'DIM', 4, 'DISTANCE_METRIC', 'L2']
|
|
),
|
|
new VectorField('embedding-hnsw', 'HNSW',
|
|
['TYPE', 'FLOAT32', 'DIM', 4, 'DISTANCE_METRIC', 'L2']
|
|
),
|
|
];
|
|
|
|
$arguments = new CreateArguments();
|
|
$arguments->prefix(['item:']);
|
|
|
|
$this->assertEquals('OK', $client->ftcreate('idx', $fields, $arguments));
|
|
sleep(0.1);
|
|
}
|
|
|
|
protected function generateData(ClientInterface $client, int $itemSets = 1)
|
|
{
|
|
$items = [
|
|
[[1.0, 2.0, 7.0, 8.0], 'red shoes'],
|
|
[[1.0, 4.0, 7.0, 8.0], 'green shoes with red laces'],
|
|
[[1.0, 2.0, 6.0, 5.0], 'red dress'],
|
|
[[2.0, 3.0, 6.0, 5.0], 'orange dress'],
|
|
[[5.0, 6.0, 7.0, 8.0], 'black shoes'],
|
|
];
|
|
$mergedItems = [];
|
|
|
|
for ($i = 0; $i < $itemSets; ++$i) {
|
|
$mergedItems = array_merge($mergedItems, $items);
|
|
}
|
|
|
|
$client->pipeline(static function (ClientContextInterface $pipe) use ($mergedItems) {
|
|
for ($i = 0; $i < count($mergedItems); ++$i) {
|
|
[$vec, $description] = $mergedItems[$i];
|
|
|
|
$pipe->hmset("item:{$i}", [
|
|
'description' => $description,
|
|
'embedding' => VectorUtility::toBlob($vec),
|
|
'embedding-hnsw' => VectorUtility::toBlob($vec),
|
|
'price' => 15 + $i % 4,
|
|
'color' => explode(' ', $description)[0],
|
|
'item_type' => explode(' ', $description)[1],
|
|
'size' => 10 + $i % 3,
|
|
]);
|
|
}
|
|
});
|
|
}
|
|
}
|