Files
kupshop/bundles/KupShop/CatalogBundle/Tests/FullTextElasticTest.php
2025-08-02 16:30:27 +02:00

557 lines
19 KiB
PHP

<?php
namespace KupShop\CatalogBundle\Tests;
use KupShop\CatalogBundle\Search\ElasticUpdateGroup;
use KupShop\CatalogBundle\Search\Exception\FulltextException;
use KupShop\CatalogBundle\Search\FulltextElastic;
use KupShop\ContentBundle\Util\ArticleList;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Logging\SentryLogger;
class FullTextElasticTest extends \DatabaseTestCase
{
/** @var FulltextElasticForTest */
private $fulltextElastic;
/** @var \PHPUnit\DbUnit\DataSet\ArrayDataSet */
private $data;
private array $testSynonyms = [
['from' => 'holínky', 'to' => 'nike'],
['from' => 'wpjshop', 'to' => 'kupshop'],
['from' => 'schody', 'to' => 'žebříky'],
];
public function setUp(): void
{
parent::setUp();
$this->fulltextElastic = $this->autowire(FulltextElasticForTest::class);
}
public function testUpdateProduct()
{
// Indexing
$this->fulltextElastic->setDataOnly(true);
$this->fulltextElastic->updateProductFulltext([1, 2]);
$data = $this->fulltextElastic->getData();
$this->assertArraySubset($this->data->getTable('products')->getRow(0), $data);
// Index settings
$indexSettings = $this->fulltextElastic->returnIndexSettings(FulltextElastic::INDEX_PRODUCTS);
$this->assertEquals($this->data->getTable('products')->getRow(1)[0], $indexSettings);
// Searching
$this->fulltextElastic->searchProducts('testik neco', 10, 5, 'price');
$data = $this->fulltextElastic->getData();
$this->assertEquals($this->data->getTable('products')->getRow(2)[0], $data);
}
public function testUpdateProductPartial()
{
$this->fulltextElastic->setDataOnly(true);
$this->fulltextElastic->partialProductsUpdate([
1 => ['title', 'ean', 'discount', ElasticUpdateGroup::Sections],
2 => [ElasticUpdateGroup::Price, ElasticUpdateGroup::Parameters],
]);
$data = $this->fulltextElastic->getData();
$this->assertArrayHasKeys(['title', 'ean', 'sections', 'parent_sections', 'price'], $data[1]['doc']);
$this->assertArrayNotHasKey('parameters', $data[1]['doc']);
$this->assertArrayNotHasKey('variations', $data[1]['doc']);
$this->assertArrayNotHasKey('code', $data[1]['doc']);
$this->assertArrayHasKeys(['parameters', 'price'], $data[3]['doc']);
$this->assertArrayNotHasKey('ean', $data[3]['doc']);
$this->assertArrayNotHasKey('sections', $data[3]['doc']);
}
public function testUpdateSection()
{
// Indexing
$this->fulltextElastic->setDataOnly(true);
$this->fulltextElastic->updateSection([1, 2]);
$data = $this->fulltextElastic->getData();
$this->assertArraySubset($this->data->getTable('section')->getRow(0), $data);
// Index settings
$indexSettings = $this->fulltextElastic->returnIndexSettings(FulltextElastic::INDEX_SECTIONS);
$this->assertEquals($this->data->getTable('section')->getRow(1)[0], $indexSettings);
// Searching
$config = [FulltextElastic::INDEX_SECTIONS => ['count' => 10, 'offset' => 5]];
$this->fulltextElastic->search('testik neco', $config, [FulltextElastic::INDEX_SECTIONS]);
$data = $this->fulltextElastic->getData();
$this->assertEquals($this->data->getTable('section')->getRow(2)[0], $data[1]);
}
public function testUpdateProducer()
{
// Indexing
$this->fulltextElastic->setDataOnly(true);
$this->fulltextElastic->updateProducer(10);
$data = $this->fulltextElastic->getData();
$this->assertArraySubset($this->data->getTable('producer')->getRow(0), $data);
// Index settings
$indexSettings = $this->fulltextElastic->returnIndexSettings(FulltextElastic::INDEX_PRODUCERS);
$this->assertEquals($this->data->getTable('producer')->getRow(1)[0], $indexSettings);
// Searching
$config = [FulltextElastic::INDEX_PRODUCERS => ['count' => 10, 'offset' => 5]];
$this->fulltextElastic->search('testik neco', $config, [FulltextElastic::INDEX_PRODUCERS]);
$data = $this->fulltextElastic->getData();
$this->assertEquals($this->data->getTable('producer')->getRow(2)[0], $data[1]);
}
public function testUpdateArticle()
{
$this->fulltextElastic->setDataOnly(true);
$this->fulltextElastic->updateArticle(6);
$data = $this->fulltextElastic->getData();
$this->assertArraySubset($this->data->getTable('article')->getRow(0), $data);
// Index settings
$indexSettings = $this->fulltextElastic->returnIndexSettings(FulltextElastic::INDEX_ARTICLES);
$this->assertEquals($this->data->getTable('article')->getRow(1)[0], $indexSettings);
// Searching
$config = [FulltextElastic::INDEX_ARTICLES => ['count' => 10, 'offset' => 5]];
$this->fulltextElastic->search('testik neco', $config, [FulltextElastic::INDEX_ARTICLES]);
$data = $this->fulltextElastic->getData();
$this->assertEquals($this->data->getTable('article')->getRow(2)[0], $data[1]);
}
public function testUpdateProducerEmpty()
{
$this->fulltextElastic->setDataOnly(true);
$dbcfg = \Settings::getDefault();
$dbcfg->cat_show_empty = 'N';
$result = $this->fulltextElastic->updateProducer(10);
$this->assertTrue($result);
$data = $this->fulltextElastic->getData();
$this->assertEquals($this->data->getTable('producer')->getRow(0), $data);
sqlQuery('UPDATE `products` SET `producer` = NULL WHERE `producer` = 10');
$result = $this->fulltextElastic->updateProducer(10);
$this->assertFalse($result);
$data = $this->fulltextElastic->getData();
$this->assertEmpty($data);
}
public function testUpdateSectionEmpty()
{
$this->fulltextElastic->setDataOnly(true);
$dbcfg = \Settings::getDefault();
$dbcfg->cat_show_empty = 'N';
$this->fulltextElastic->updateSection(1);
$data = $this->fulltextElastic->getData();
$this->assertEmpty($data);
$this->fulltextElastic->updateSection([1, 2]);
$data = $this->fulltextElastic->getData();
$this->assertArraySubset($this->data->getTable('section2')->getRow(0), $data);
}
public function testSearch(): void
{
$this->fulltextElastic->updateIndex();
$this->fulltextElastic->commit();
$config = [];
foreach (FulltextElastic::INDEX_TYPES as $type) {
$config[$type] = [
'count' => 5,
];
}
$result = $this->fulltextElastic->search('nike', $config);
$this->assertEquals(FulltextElastic::INDEX_TYPES, array_keys($result), 'Assert that result has all search types');
$this->assertCount(1, $result[FulltextElastic::INDEX_PRODUCTS]);
$this->assertCount(1, $result[FulltextElastic::INDEX_SECTIONS]);
$this->assertCount(1, $result[FulltextElastic::INDEX_PRODUCERS]);
$this->assertCount(0, $result[FulltextElastic::INDEX_ARTICLES]);
// Product id=10 has very long product code
$prodRes = $this->fulltextElastic->searchProducts('RD102velmidlouhykodproduktuprotest', 5, 0);
$this->assertArrayHasKey('10', $prodRes);
$this->assertCount(1, $prodRes);
$prodRes = $this->fulltextElastic->searchProducts('RD102', 5, 0);
$this->assertArrayHasKey('10', $prodRes);
$this->assertCount(1, $prodRes);
$prodRes = $this->fulltextElastic->searchProducts('RD102velmidlouhykodproduktuprotest vrtačka', 5, 0);
$this->assertArrayHasKey('10', $prodRes);
$this->assertCount(1, $prodRes);
// Search in "Columbia Lay D Down" without spaces
$prodRes = $this->fulltextElastic->searchProducts('ydd', 5, 0);
$this->assertArrayHasKey('4', $prodRes);
$this->assertCount(1, $prodRes);
}
public function getDataSet()
{
$this->data = $this->getJsonDataSetFromFile();
return parent::getDataSet();
}
public function testSynonymsProducts()
{
$this->fulltextElastic->updateIndex(FulltextElastic::INDEX_PRODUCTS);
$this->fulltextElastic->updateSynonyms($this->testSynonyms);
$this->fulltextElastic->commit();
$testTerm = $this->testSynonyms[0]['from'];
$realTerm = $this->testSynonyms[0]['to'];
$prodRes = $this->fulltextElastic->searchProducts($testTerm, 5, 0);
$prodRes = array_keys($prodRes);
$this->assertNotEmpty($prodRes);
$sqlProdRes = sqlQueryBuilder()
->select('*')
->from('products', 'p')
->where("p.title LIKE '%{$realTerm}%'")
->execute()
->fetchAllAssociative();
$this->assertEquals((int) $prodRes[0], (int) $sqlProdRes[0]['id']);
}
public function testSynonymsProducers()
{
$this->fulltextElastic->updateIndex(FulltextElastic::INDEX_PRODUCERS);
$this->fulltextElastic->updateSynonyms($this->testSynonyms);
$this->fulltextElastic->commit();
$testTerm = $this->testSynonyms[0]['from'];
$realTerm = $this->testSynonyms[0]['to'];
$config = [FulltextElastic::INDEX_PRODUCERS => ['count' => 5, 'offset' => 0]];
$res = $this->fulltextElastic->search($testTerm, $config, [FulltextElastic::INDEX_PRODUCERS]);
$this->assertNotEmpty($res[FulltextElastic::INDEX_PRODUCERS]);
$sqlRes = sqlQueryBuilder()
->select('*')
->from('producers', 'p')
->where("p.name LIKE '%{$realTerm}%'")
->execute()
->fetchAllAssociative();
$this->assertEquals((int) $res[FulltextElastic::INDEX_PRODUCERS][0]['id'], (int) $sqlRes[0]['id']);
}
public function testSynonymsArticles()
{
$this->fulltextElastic->updateIndex(FulltextElastic::INDEX_ARTICLES);
$this->fulltextElastic->updateSynonyms($this->testSynonyms);
$this->fulltextElastic->commit();
$testTerm = $this->testSynonyms[1]['from'];
$realTerm = $this->testSynonyms[1]['to'];
$config = [FulltextElastic::INDEX_ARTICLES => ['count' => 5, 'offset' => 0]];
$res = $this->fulltextElastic->search($testTerm, $config, [FulltextElastic::INDEX_ARTICLES]);
$this->assertNotEmpty($res[FulltextElastic::INDEX_ARTICLES]);
$sqlRes = sqlQueryBuilder()
->select('*')
->from('articles', 'a')
->where("a.title LIKE '%{$realTerm}%'")
->execute()
->fetchAllAssociative();
$this->assertEquals((int) $res[FulltextElastic::INDEX_ARTICLES][0]['id'], (int) $sqlRes[0]['id']);
}
public function testSynonymsSections()
{
$this->fulltextElastic->updateIndex(FulltextElastic::INDEX_SECTIONS);
$this->fulltextElastic->updateSynonyms($this->testSynonyms);
$this->fulltextElastic->commit();
$testTerm = $this->testSynonyms[2]['from'];
$realTerm = $this->testSynonyms[2]['to'];
$config = [FulltextElastic::INDEX_SECTIONS => ['count' => 5]];
$res = $this->fulltextElastic->search($testTerm, $config, [FulltextElastic::INDEX_SECTIONS])[FulltextElastic::INDEX_SECTIONS];
$this->assertNotEmpty($res);
$sqlRes = sqlQueryBuilder()
->select('*')
->from('sections', 's')
->where("s.name LIKE '%{$realTerm}%'")
->execute()
->fetchAllAssociative();
$this->assertEquals((int) $res[0]['id'], (int) $sqlRes[0]['id']);
}
/**
* Product#8 -> title LIKE '%mobilní%'
* Product#11 -> long_descr LIKE '%mobilní%'.
*
* title has higher priority -> Product#8 has to be higher up
*/
public function testSearchOrderSimple()
{
$this->fulltextElastic->updateIndex(FulltextElastic::INDEX_PRODUCTS);
$this->fulltextElastic->commit();
$config = [
FulltextElastic::INDEX_PRODUCTS => [
'count' => 5,
],
];
$res = $this->fulltextElastic->search('mobilní', $config, [FulltextElastic::INDEX_PRODUCTS]);
$products = $res[FulltextElastic::INDEX_PRODUCTS];
$this->assertGreaterThanOrEqual(2, count($products));
// Product search result format:
// product_id => search position
$this->assertTrue($products[8] < $products[11]);
}
public function testSearchOrder()
{
$this->fulltextElastic->updateIndex(FulltextElastic::INDEX_PRODUCTS);
$this->fulltextElastic->commit();
$config = [
FulltextElastic::INDEX_PRODUCTS => [
'count' => 5,
],
];
$res = $this->fulltextElastic->search('vrtačka', $config, [FulltextElastic::INDEX_PRODUCTS]);
$products = array_keys($res[FulltextElastic::INDEX_PRODUCTS]);
$this->assertTrue(array_search(10, $products) < array_search(8, $products), 'Product#10 has to be higher up then Product#8');
$this->assertTrue(array_search(9, $products) < array_search(8, $products), 'Product#9 has to be higher up then Product#8');
$this->assertTrue(array_search(9, $products) < array_search(10, $products), 'Product#9 has to be higher up then Product#10');
}
public function testThesaurus()
{
$this->fulltextElastic->updateSynonyms([]);
$this->fulltextElastic->updateIndex(FulltextElastic::INDEX_PRODUCTS);
$this->fulltextElastic->commit();
$config = [
FulltextElastic::INDEX_PRODUCTS => [
'count' => 5,
],
];
$actualInput = 'vrtačkou';
$expectedTitleContains = 'vrtačka';
$res = $this->fulltextElastic->search($actualInput, $config, [FulltextElastic::INDEX_PRODUCTS]);
$products = array_keys($res[FulltextElastic::INDEX_PRODUCTS]);
$this->assertNotEmpty($products);
$sqlRes = sqlQueryBuilder()->select('*')
->from('products', 'p')
->where("p.id = {$products[0]}")
->execute()->fetchAllAssociative();
$this->assertNotEmpty($sqlRes);
$this->assertStringContainsString($expectedTitleContains, $sqlRes[0]['title']);
}
public function testRepairCurrentAsIndexNotAsAlias()
{
$sUrl = $this->fulltextElastic->getServerUrl();
$index = $this->fulltextElastic->getIndex(FulltextElastic::INDEX_PRODUCTS);
// Delete "current" alias and index
$this->fulltextElastic->curlInitSession("{$sUrl}/_aliases", 'POST', json_encode([
'actions' => [
[
'remove' => [
'index' => '*',
'alias' => $index,
],
],
],
]));
$this->fulltextElastic->curlInitSession("{$sUrl}/{$index}", 'DELETE', '');
// Create index using incorrect name - current in index name
try {
$this->fulltextElastic->updateProduct(1);
} catch (FulltextException) {
}
$indexSettings = $this->fulltextElastic->curlInitSession("{$sUrl}/{$index}", 'GET', '');
// Autocreate is disabled, check index does not exists
$this->assertEquals(key($indexSettings), 'error');
// Create index manually
$alias = $this->fulltextElastic->getIndex(FulltextElastic::INDEX_PRODUCTS, false);
$this->fulltextElastic->curlInitSession("{$sUrl}/{$alias}", 'PUT', '');
// Run reindex - should fix alias
$this->fulltextElastic->updateIndex(FulltextElastic::INDEX_PRODUCTS);
$this->fulltextElastic->commit();
// Check alias if fixed
$indexSettings = $this->fulltextElastic->curlInitSession("{$sUrl}/{$index}", 'GET', '');
$this->assertTrue(strpos(key($indexSettings), 'current') === false);
}
protected function assertArrayHasKeys(array $keys, $data)
{
foreach ($keys as $key) {
$this->assertArrayHasKey($key, $data);
}
}
}
class FulltextElasticForTest extends FulltextElastic
{
private $data;
protected bool $dataOnly = false;
public function __construct()
{
$languageContext = ServiceContainer::getService(LanguageContext::class);
$sentryLogger = ServiceContainer::getService(SentryLogger::class);
parent::__construct($languageContext, $sentryLogger);
$this->setArticleList(
ServiceContainer::getService(ArticleList::class)
);
}
public function executeCurl($category, $param, $customRequest, $urlEnd)
{
$this->data = json_decode($param, true);
if ($this->dataOnly) {
return [];
}
return parent::executeCurl($category, $param, $customRequest, $urlEnd);
}
public function bulkUpdate($values, $category, $type)
{
$finalParam = [];
switch ($type) {
case 'put':
foreach ($values as $line) {
$finalParam[] = [
'index' => [
'_index' => $this->settings['index'],
'_type' => $category,
'_id' => $line['id'],
],
];
$finalParam[] = $line;
}
break;
case 'update':
foreach ($values as $line) {
$finalParam[] = [
'update' => [
'_id' => $line['id'],
'_type' => $category,
'_index' => $this->settings['index'],
],
];
$finalParam[] = [
'doc' => [
'weight' => (floatval($line['delivery']) / 10) * (30 + floatval($line['sold']) / 30),
'sold' => intval($line['sold']),
'delivery' => intval($line['delivery']),
],
];
}
break;
case 'update_partial':
foreach ($values as $line) {
$finalParam[] = [
'update' => [
'_id' => $line['id'],
'_type' => '_doc',
'_index' => $this->settings['index'],
],
];
$finalParam[] = [
'doc' => $line,
'upsert' => new \stdClass(),
];
}
break;
}
$this->data = $finalParam;
if ($this->dataOnly) {
return [];
}
return parent::bulkUpdate($values, $category, $type);
}
public function getData()
{
$data = $this->data;
$this->data = [];
return $data;
}
public function commit()
{
$sUrl = $this->getServerUrl();
$this->curlInitSession("{$sUrl}/_refresh", 'POST', '');
}
public function setDataOnly(bool $dataOnly): FulltextElasticForTest
{
$this->dataOnly = $dataOnly;
return $this;
}
public function multiSearch(array $queries)
{
$this->data = array_map(function ($x) {return json_decode($x, true); }, $queries);
if ($this->dataOnly) {
return [];
}
return parent::multiSearch($queries);
}
}