Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filtering based on average Review/Comment scores and Sorting #569

Open
wants to merge 32 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0f75dba
Merge pull request #205 from PrestaShop/dev
Progi1984 Sep 8, 2020
1178dd9
Merge pull request #280 from PrestaShop/dev
PierreRambaud Dec 14, 2020
a767762
Merge pull request #369 from PrestaShop/dev
atomiix Mar 15, 2021
db0fccc
Merge pull request #646 from PrestaShop/dev
Progi1984 Apr 14, 2022
080b08c
adds ProductComment hook
samberrry Dec 12, 2021
a029235
methods to index reviews
samberrry Dec 12, 2021
9666de2
admin items
samberrry Dec 12, 2021
8f2f4f7
admin add filter view
samberrry Dec 12, 2021
f9f96a6
adds review block
samberrry Dec 12, 2021
1c99bff
adds review type to converter
samberrry Dec 12, 2021
d50e526
adds review sort
samberrry Dec 12, 2021
a4b11d7
updates css and js
samberrry Dec 12, 2021
9a70fda
updates facets front view
samberrry Dec 12, 2021
21a9f79
adds avg_score to mysql table mapping
samberrry Dec 12, 2021
0d6b378
updates search
samberrry Dec 12, 2021
8ee9a3a
adds review indexer file
samberrry Dec 12, 2021
e049400
admin manage view
samberrry Dec 12, 2021
90354ff
index valid comments
samberrry Dec 12, 2021
aca5aa1
invalidate block cache after adding comment
samberrry Dec 13, 2021
9191b0f
Update ps_facetedsearch.php spaces
samberrry Dec 13, 2021
c13edac
Update ps_facetedsearch.php float avg_score
samberrry Dec 13, 2021
e79531f
Update ps_facetedsearch.php space sql create
samberrry Dec 13, 2021
501fa80
Update ps_facetedsearch.php space
samberrry Dec 13, 2021
b8cb6a1
ps prefix and beautify sql query
samberrry Dec 13, 2021
ba9c020
removes useless if condition
samberrry Dec 13, 2021
c3eeabe
get category id with getValue
samberrry Dec 13, 2021
e3754c1
removes review filters when product comments module is disabled
samberrry Dec 15, 2021
745d289
feat: support for add after and delete after comment indexing
samberrry Jun 20, 2022
0073146
fix: a little fix in if condition
samberrry Jun 20, 2022
0a02241
fix: added check for comment approval
samberrry Jun 20, 2022
3160ebd
fix: removed the index record on last comment removal for a product
samberrry Jul 12, 2022
4f79cb8
chore: change version to 3.9.0
samberrry Sep 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions ps_facetedsearch-review-indexer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to [email protected] so we can send you a copy immediately.
*
* @author PrestaShop SA <[email protected]>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';

if (substr(Tools::encrypt('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
exit('Bad token');
}

if (!Module::isEnabled('productcomments')){
exit('Product Comment module is not installed or is disabled.');
}

Shop::setContext(Shop::CONTEXT_ALL);

$module = new Ps_Facetedsearch();

if (Tools::getValue('full')){
$module->rebuildCommentIndexTable();
echo $module->indexReviews(true);
}else{
echo $module->indexReviews();
}
114 changes: 111 additions & 3 deletions ps_facetedsearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function __construct()
{
$this->name = 'ps_facetedsearch';
$this->tab = 'front_office_features';
$this->version = '3.8.0';
$this->version = '3.9.0';
$this->author = 'PrestaShop';
$this->need_instance = 0;
$this->bootstrap = true;
Expand Down Expand Up @@ -144,7 +144,7 @@ public function getContext()

protected function getDefaultFilters()
{
return [
$defaultFilters = [
'layered_selection_subcategories' => [
'label' => 'Sub-categories filter',
],
Expand All @@ -166,6 +166,15 @@ protected function getDefaultFilters()
'slider' => true,
],
];

if (Module::isEnabled('productcomments')){
$defaultFilters['layered_selection_review_star'] = [
'label' => 'Avg. Customer Review',
'star' => true
];
}

return $defaultFilters;
}

public function install()
Expand Down Expand Up @@ -213,6 +222,8 @@ public function install()
$this->rebuildPriceIndexTable();
$this->installIndexableAttributeTable();
$this->installProductAttributeTable();
$this->rebuildCommentIndexTable();
$this->indexReviews(true);

if ($productsCount < static::LOCK_TOO_MANY_PRODUCTS) {
$this->fullPricesIndexProcess();
Expand Down Expand Up @@ -772,6 +783,7 @@ public function getContent()
]);

$this->context->smarty->assign('categories_tree', $treeCategoriesHelper->render());
$this->context->smarty->assign('comment_module_enabled', Module::isEnabled('productcomments'));

return $this->display(__FILE__, 'views/templates/admin/add.tpl');
}
Expand Down Expand Up @@ -821,6 +833,8 @@ public function getContent()
'full_price_indexer_url' => $moduleUrl . 'ps_facetedsearch-price-indexer.php' . '?token=' . substr(Tools::hash('ps_facetedsearch/index'), 0, 10) . '&full=1',
'attribute_indexer_url' => $moduleUrl . 'ps_facetedsearch-attribute-indexer.php' . '?token=' . substr(Tools::hash('ps_facetedsearch/index'), 0, 10),
'clear_cache_url' => $moduleUrl . 'ps_facetedsearch-clear-cache.php' . '?token=' . substr(Tools::hash('ps_facetedsearch/index'), 0, 10),
'index_reviews_full' => $moduleUrl . 'ps_facetedsearch-review-indexer.php' . '?full=true&token=' . substr(Tools::encrypt('ps_facetedsearch/index'), 0, 10),
'index_reviews_missing' => $moduleUrl . 'ps_facetedsearch-review-indexer.php' . '?token=' . substr(Tools::encrypt('ps_facetedsearch/index'), 0, 10),
'filters_templates' => $this->getDatabase()->executeS('SELECT * FROM ' . _DB_PREFIX_ . 'layered_filter ORDER BY date_add DESC'),
'show_quantities' => Configuration::get('PS_LAYERED_SHOW_QTIES'),
'cache_enabled' => Configuration::get('PS_LAYERED_CACHE_ENABLED'),
Expand All @@ -833,6 +847,8 @@ public function getContent()
'filter_by_default_category' => (bool) Configuration::get('PS_LAYERED_FILTER_BY_DEFAULT_CATEGORY'),
]);

$this->context->smarty->assign('comment_module_enabled', Module::isEnabled('productcomments'));

return $this->display(__FILE__, 'views/templates/admin/manage.tpl');
}

Expand Down Expand Up @@ -880,7 +896,7 @@ public function rebuildLayeredStructure()
`id_shop` INT(11) UNSIGNED NOT NULL,
`id_category` INT(10) UNSIGNED NOT NULL,
`id_value` INT(10) UNSIGNED NULL DEFAULT \'0\',
`type` ENUM(\'category\',\'id_feature\',\'id_attribute_group\',\'quantity\',\'condition\',\'manufacturer\',\'weight\',\'price\') NOT NULL,
`type` ENUM(\'category\',\'id_feature\',\'id_attribute_group\',\'quantity\',\'condition\',\'manufacturer\',\'weight\',\'price\',\'review\') NOT NULL,
`position` INT(10) UNSIGNED NOT NULL,
`filter_type` int(10) UNSIGNED NOT NULL DEFAULT 0,
`filter_show_limit` int(10) UNSIGNED NOT NULL DEFAULT 0,
Expand Down Expand Up @@ -1162,6 +1178,8 @@ public function buildLayeredCategories()
} elseif (substr($key, 0, 23) == 'layered_selection_feat_') {
$sqlInsert .= '(' . (int) $idCategory . ', ' . (int) $idShop . ', ' . (int) str_replace('layered_selection_feat_', '', $key) . ',
\'id_feature\',' . (int) $n . ', ' . (int) $limit . ', ' . (int) $type . '),';
} elseif ($key == 'layered_selection_review_star') {
$sqlInsert .= '(' . (int) $idCategory . ', ' . (int) $idShop . ', NULL, \'review\', ' . (int) $n . ', ' . (int) $limit . ', ' . (int) $type . '),';
}

++$nbSqlValuesToInsert;
Expand Down Expand Up @@ -1503,4 +1521,94 @@ public function getWidgetVariables($hookName, array $configuration)
{
return [];
}

/**
* Install review indexes table
*/
public function rebuildCommentIndexTable()
{
$this->getDatabase()->execute('DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'layered_comment_index`');

$this->getDatabase()->execute('DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'layered_comment_index_log`');

$this->getDatabase()->execute(
'CREATE TABLE `' . _DB_PREFIX_ . 'layered_comment_index` (
`id_product` INT NOT NULL,
`score` INT NOT NULL,
`avg_score` FLOAT(5, 4) NOT NULL,
PRIMARY KEY (`id_product`),
INDEX `score` (`score`),
INDEX `avg_score` (`score`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;'
);

$this->getDatabase()->execute(
'CREATE TABLE `' . _DB_PREFIX_ . 'layered_comment_index_log` (
`id_comment` INT NOT NULL,
`id_product` INT NOT NULL,
`indexed` TINYINT(1) NOT NULL,
PRIMARY KEY (`id_comment`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;'
);
}

public function indexReviews($full = false)
{
if ($full) {
$validateCondition = '';
if (Configuration::get('PRODUCT_COMMENTS_MODERATE')){
$validateCondition = ' where validate = 1';
}

$query = 'SELECT avg(`grade`) as avg_grade, sum(`grade`) as sum_grade, `id_product` FROM ' . _DB_PREFIX_ . 'product_comment '. $validateCondition .' group by id_product';

foreach ($this->getDatabase()->executeS($query) as $comment) {
$this->addCommentIndex($comment);
}

$query = 'SELECT `id_product_comment`, `id_product` FROM ' . _DB_PREFIX_ . 'product_comment' . $validateCondition;

foreach ($this->getDatabase()->executeS($query) as $commentLog) {
$this->addCommentIndexLog($commentLog);
}

} else {
$validateCondition = '';
if (Configuration::get('PRODUCT_COMMENTS_MODERATE')){
$validateCondition = ' AND WHERE validate = 1';
}

$query = 'SELECT pc.id_product_comment, pc.id_product, pc.grade ' .
'FROM ' . _DB_PREFIX_ . '_product_comment as pc ' .
'LEFT JOIN ' . _DB_PREFIX_ . '_layered_comment_index_log as lc ' .
'pc.id_product_comment = lc.id_comment ' .
'WHERE lc.indexed IS NULL' . $validateCondition;

//returns non matching records
foreach ($this->getDatabase()->executeS($query) as $commentLog) {
$gradeCommentRow = $this->database->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'layered_comment_index`
WHERE id_product =' . $commentLog['id_product']);

$productCommentLogRow = $this->database->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'layered_comment_index_log`
WHERE id_product = ' . $commentLog['id_product'] . ' AND indexed = 1');

$newGradeValue = (int)$gradeCommentRow[0]['score'] + (int)$commentLog['grade'];
$avg_score = $newGradeValue/(count($productCommentLogRow)+1);

$this->database->execute('update ' . _DB_PREFIX_ . 'layered_comment_index set score='.$newGradeValue.', avg_score= '.$avg_score .' where id_product=' . $commentLog['id_product']);

$this->addCommentIndexLog($commentLog);
}
}

return "true";
}

public function addCommentIndex($comment){
$this->database->execute('INSERT INTO `'._DB_PREFIX_.'layered_comment_index` (`id_product`, `score`, `avg_score`) VALUES ('.$comment['id_product'].', '.$comment['sum_grade'].','.$comment['avg_grade'].')');
}

public function addCommentIndexLog($commentLog){
$this->database->execute('INSERT INTO `'._DB_PREFIX_.'layered_comment_index_log` (`id_comment`, `indexed`, `id_product`) VALUES ('.$commentLog['id_product_comment'].', 1,'. $commentLog['id_product'] .')');
}
}
6 changes: 6 additions & 0 deletions src/Adapter/MySQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,12 @@ protected function getFieldMapping()
'joinCondition' => '(psales.id_product = p.id_product)',
'joinType' => self::LEFT_JOIN,
],
'avg_score' => [
'tableName' => 'layered_comment_index',
'tableAlias' => 'coms',
'joinCondition' => '(coms.id_product = p.id_product)',
'joinType' => self::INNER_JOIN,
],
];

return $filterToTableMapping;
Expand Down
73 changes: 73 additions & 0 deletions src/Filters/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ public function getFilterBlock(
case 'price':
$filterBlocks[] = $this->getPriceRangeBlock($filter, $selectedFilters, $nbProducts);
break;
case 'review':
$filterBlocks[] = $this->getReviweBlock($filter, $selectedFilters);
break;
case 'weight':
$filterBlocks[] = $this->getWeightRangeBlock($filter, $selectedFilters, $nbProducts);
break;
Expand Down Expand Up @@ -979,4 +982,74 @@ private function preparePriceSpecifications()
'currencySymbol' => $currency->sign,
];
}

/**
* Get the reviews filter block
*
* @param array $filter
* @param array $selectedFilters
*
* @return array
*/
private function getReviweBlock($filter, $selectedFilters)
{
$values = [
'4' => ['name'=>"4"],
'3' => ['name'=>"3"],
'2' => ['name'=>"2"],
'1' => ['name'=>"1"],
];

$idParent = (int) Tools::getValue(
'id_category',
Tools::getValue('id_category_layered', Configuration::get('PS_HOME_CATEGORY'))
);

$query = 'SELECT count(g.grade) as count ,g.grade FROM (SELECT t.id_product, case
when t.avg_grade between 1 and 1.99 then "1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't be able to do 0.5 star? I think it costs nothing to start at 0 instead of 1

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually the average score can not be less than 1, because we do not have comments with 0 grade, user must choose between 1-5 values.

when t.avg_grade between 2 and 2.99 then "2"
when t.avg_grade between 3 and 3.99 then "3"
when t.avg_grade between 4 and 5 then "4"
end as grade
FROM (SELECT avg(h.grade) as avg_grade,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two much subqueries, I'm afraid of performance problems 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you are right, I think at least 3 joins are inevitable, but an alternative could be another table which keeps counts for all categories, and can be calculated during indexing.

h.id_product FROM
(select pc.id_product, pc.grade from ' . _DB_PREFIX_ . 'product_comment as pc
inner join (SELECT * FROM ' . _DB_PREFIX_ . 'category_product where id_category = '.$idParent.') cp
on pc.id_product = cp.id_product) h
group by id_product) t) g
group by g.grade
order by g.grade ASC';
$gradeCounts = $this->database->executeS($query);

if (!empty($gradeCounts)){
foreach ($values as $value){
$nbr = 0;
foreach ($gradeCounts as $count){
if ((int)$value['name'] <= $count['grade']){
$nbr += $count['count'];
}
}
$values[$value['name']]['nbr'] = $nbr;
}
}

if (isset($selectedFilters['review'])){
$reviewValues = $selectedFilters['review'];
foreach ($reviewValues as $rv){
$values[$rv]['checked'] = true;
}
}

$reviewBlock = [
'type_lite' => 'review',
'type' => 'review',
'id_key' => 0,
'name' => $this->context->getTranslator()->trans('Avg. Customer Reviews', [], 'Modules.Facetedsearch.Shop'),
'values' => $values,
'filter_show_limit' => (int) $filter['filter_show_limit'],
'filter_type' => Converter::WIDGET_TYPE_STAR,
];

return $reviewBlock;
}
}
36 changes: 36 additions & 0 deletions src/Filters/Converter.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Converter
const WIDGET_TYPE_RADIO = 1;
const WIDGET_TYPE_DROPDOWN = 2;
const WIDGET_TYPE_SLIDER = 3;
const WIDGET_TYPE_STAR = 4;

const TYPE_ATTRIBUTE_GROUP = 'id_attribute_group';
const TYPE_AVAILABILITY = 'availability';
Expand All @@ -48,6 +49,7 @@ class Converter
const TYPE_MANUFACTURER = 'manufacturer';
const TYPE_PRICE = 'price';
const TYPE_WEIGHT = 'weight';
const TYPE_REVIEW = 'review';

const PROPERTY_URL_NAME = 'url_name';
const PROPERTY_COLOR = 'color';
Expand Down Expand Up @@ -195,6 +197,34 @@ public function getFacetsFromFilterBlocks(array $filterBlocks)

$facet->addFilter($filter);

break;
case self::TYPE_REVIEW:
$type = $filterBlock['type'];

$facet
->setType($type)
->setMultipleSelectionAllowed(false);

$filters = [];
foreach ($filterBlock['values'] as $id => $filterArray) {
$filter = new Filter();
$filter
->setType($type)
->setLabel($filterArray['name'])
->setMagnitude($filterArray['nbr'])
->setValue($id);

if (array_key_exists('checked', $filterArray)) {
$filter->setActive($filterArray['checked']);
}

$filters[] = $filter;
}

foreach ($filters as $filter) {
$facet->addFilter($filter);
}

break;
}

Expand All @@ -215,6 +245,10 @@ public function getFacetsFromFilterBlocks(array $filterBlocks)
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('slider');
break;
case self::WIDGET_TYPE_STAR:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('star');
break;
}

$facets[] = $facet;
Expand Down Expand Up @@ -438,6 +472,8 @@ public function createFacetedSearchFiltersFromQuery(ProductSearchQuery $query)
private function convertFilterTypeToLabel($filterType)
{
switch ($filterType) {
case self::TYPE_REVIEW:
return $this->context->getTranslator()->trans('Avg. Customer Reviews', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_PRICE:
return $this->context->getTranslator()->trans('Price', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_WEIGHT:
Expand Down
Loading