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, ]); } }); } }