557 lines
19 KiB
PHP
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);
|
|
}
|
|
}
|