From aa2ce6bce5a99162d14a2ae9fe00e685155dc687 Mon Sep 17 00:00:00 2001 From: Sebastian Castro Date: Sun, 26 Apr 2020 10:51:33 +0200 Subject: [PATCH] feature: search on multiple fields --- CHANGELOG.md | 1 + mongo_operations | 5 ++ src/Command/MigrationCommand.php | 3 +- src/EventListener/ConfigurationListener.php | 65 ++++++++++++++++++- src/Repository/ElementRepository.php | 12 +--- .../custom-fields/form-builder.html.twig | 22 ++++++- 6 files changed, 95 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd843bf..e993bb8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ v3.0.0 * FEATURE: Improve SAAS management. Get more info about each map, warn abandonned projects * BUG: Fix element export * BUG: Fix hard deleting elements +* FEATURE: Search on multi fields. Can be configured from the Form Builder v2.5.8 ====== diff --git a/mongo_operations b/mongo_operations index 119354b8..b8ffa05b 100755 --- a/mongo_operations +++ b/mongo_operations @@ -127,3 +127,8 @@ db.Element.find({'data.image': {$exists: true}}).forEach(function(e) { db.Element.updateMany({'nonDuplicates.$id': "KBL"}, { $unset: { 'nonDuplicates': ''} }) + + +db.Element.dropIndex('name_text') +db.Element.dropIndex('search_index') +db.Element.createIndex( { name: "text", "data.description": "text" }, { name: "search_index", default_language: "french", weights: { name: 10, "data.description": 5 }, }) diff --git a/src/Command/MigrationCommand.php b/src/Command/MigrationCommand.php index b55a78e2..d3dceaf6 100644 --- a/src/Command/MigrationCommand.php +++ b/src/Command/MigrationCommand.php @@ -55,7 +55,8 @@ class MigrationCommand extends GoGoAbstractCommand "Il est maintenant possible de téléverser des images et des fichiers depuis le formulaire d'ajout d'un élément ! Paramétrez ces nouveaux champs dans Modèle de Données -> Formulaire", // v3.0 "Vous pouvez maintenant écrire des actualités qui seront incluses dans la newsletter automatique! Allez dans Mails/Newsletter -> Actualités", - "L'export des éléments depuis la page Données -> Elements fonctionne de nouveau et inclus cette fois correctement tous les champs personnalisés (y compris fichiers et images)" + "L'export des éléments depuis la page Données -> Elements fonctionne de nouveau et inclus cette fois correctement tous les champs personnalisés (y compris fichiers et images)", + "Depuis le site, la recherche par élément peut maintenant fonctionenr sur plusieurs champs. Dans Modèle de Données -> Formulaire, editez un champ pour voir apparaitre la configuration liée à la recherche. Vous pouvez aussi donner des poids différents à chaque champs, par exemple la recherche sur le titre avec un poids de 3 et la recherche dans la description avec un poids de 1" ]; public function __construct(DocumentManager $dm, LoggerInterface $commandsLogger, diff --git a/src/EventListener/ConfigurationListener.php b/src/EventListener/ConfigurationListener.php index 14d208af..86b7efab 100644 --- a/src/EventListener/ConfigurationListener.php +++ b/src/EventListener/ConfigurationListener.php @@ -3,8 +3,10 @@ namespace App\EventListener; use App\Document\Configuration\ConfigurationApi; +use App\Document\Configuration; use App\Document\Configuration\ConfigurationMarker; use App\Services\AsyncService; +use Symfony\Component\Process\Process; class ConfigurationListener { @@ -34,6 +36,7 @@ class ConfigurationListener $removedProps = array_diff($oldPrivateProperties, $newPrivateProperties); $addedProps = array_diff($newPrivateProperties, $oldPrivateProperties); + // Update field path (move them between data and privateData object) $qb = $dm->createQueryBuilder('App\Document\Element'); $qb = $qb->updateMany(); foreach ($removedProps as $key => $prop) { @@ -42,10 +45,16 @@ class ConfigurationListener foreach ($addedProps as $key => $prop) { $qb = $qb->field('data.'.$prop)->rename('privateData.'.$prop); } - $qb->getQuery()->execute(); - $this->asyncService->callCommand('app:elements:updateJson', ['ids' => 'all']); + + // Update search index + $fullConfig = $dm->getRepository('App\Document\Configuration')->findConfiguration(); + $this->updateSearchIndex($fullConfig->getDbName(), + $oldPrivateProperties, + $newPrivateProperties, + $fullConfig->getElementFormFieldsJson(), + $fullConfig->getElementFormFieldsJson()); } } if ($document instanceof ConfigurationMarker) { @@ -56,5 +65,57 @@ class ConfigurationListener $this->asyncService->callCommand('app:elements:updateJson', ['ids' => 'all']); } } + + if ($document instanceof Configuration) { + $uow = $dm->getUnitOfWork(); + $uow->computeChangeSets(); + $changeset = $uow->getDocumentChangeSet($document); + if (array_key_exists('elementFormFieldsJson', $changeset)) { + $formFieldsChanged = $changeset['elementFormFieldsJson']; + $oldFormFields = $formFieldsChanged[0]; + $newFormFields = $formFieldsChanged[1]; + $this->updateSearchIndex($document->getDbName(), + $document->getApi()->getPublicApiPrivateProperties(), + $document->getApi()->getPublicApiPrivateProperties(), + $oldFormFields, + $newFormFields); + } + } + } + + private function updateSearchIndex($db, $oldPrivateProperties, $newPrivateProperties, + $oldFormFields, $newFormFields) { + $oldSearchIndex = $this->calculateSearchIndexConfig($oldPrivateProperties, $oldFormFields); + $newSearchIndex = $this->calculateSearchIndexConfig($newPrivateProperties, $newFormFields); + + if ($oldSearchIndex != $newSearchIndex) { + $command = 'db.Element.dropIndex("name_text");'; // Default index created by doctrine + $command .= 'db.Element.dropIndex("search_index");'; + $command .= "db.Element.createIndex( {$newSearchIndex["fields"]}, { name: \"search_index\", default_language: \"french\", weights: {$newSearchIndex["weights"]} });"; + + $process = new Process("mongo {$db} --eval '{$command}'"); + $process->run(); + } + } + + private function calculateSearchIndexConfig($privateProps, $formFieldsJson) { + $indexConf = []; + $indexWeight = []; + $formFields = json_decode($formFieldsJson); + foreach ($formFields as $key => $field) { + if (property_exists($field, 'search') && $field->search) { + $path = in_array($field->name, $privateProps) ? 'privateData' : 'data'; + $path .= '.' . $field->name; + if ($field->name == 'name') $path = 'name'; + $indexConf[$path] = "text"; + $indexWeight[$path] = (int) $field->searchWeight; + } + } + // default index on name + if (count($indexConf) == 0) { + $indexConf = ['name' => 'text']; + $indexWeight = ['name' => 1]; + } + return ['fields' => json_encode($indexConf), 'weights' => json_encode($indexWeight)]; } } diff --git a/src/Repository/ElementRepository.php b/src/Repository/ElementRepository.php index 7b69b1e7..cb64df57 100755 --- a/src/Repository/ElementRepository.php +++ b/src/Repository/ElementRepository.php @@ -38,8 +38,8 @@ class ElementRepository extends DocumentRepository } $qb->limit($maxResults) - ->field('status')->gt($status) - ->field('geo')->withinCenter((float) $element->getGeo()->getLatitude(), (float) $element->getGeo()->getLongitude(), $radius); + ->field('status')->gt($status) + ->field('geo')->withinCenter((float) $element->getGeo()->getLatitude(), (float) $element->getGeo()->getLongitude(), $radius); if ($element->getId()) { $qb->field('id')->notIn($element->getNonDuplicatesIds()); @@ -240,13 +240,7 @@ class ElementRepository extends DocumentRepository if ($config->getSearchExcludingWords()) { $text = $text.' --'.str_replace(',', ' --', $config->getSearchExcludingWords()); } - // remove words smaller than two letters - $filtered_words = array_filter(explode(' ', $text), function ($el) { - return strlen($el) > 2; - }); - $text = implode($filtered_words, ' '); - // return $qb->field('name')->equals(new \MongoRegex("/$text/i")); - return $qb->text($text); //->language('fr'); + return $qb->text($text); } private function filterVisibles($qb, $status = ElementStatus::PendingModification) diff --git a/templates/admin/core_custom/custom-fields/form-builder.html.twig b/templates/admin/core_custom/custom-fields/form-builder.html.twig index c12b3a6c..494f251d 100644 --- a/templates/admin/core_custom/custom-fields/form-builder.html.twig +++ b/templates/admin/core_custom/custom-fields/form-builder.html.twig @@ -40,6 +40,8 @@ var iconAttr = { label: 'Icone', placeholder: 'Choisissez une icone' } var errorMsgAttrs = { label: "Msg. Erreur", placeholder: "Oups ce texte est un peu long ! // Veuillez renseigner une adresse email valide // ..." } + var searchAttrs = { label: 'Recherche dans ce champ', type: 'checkbox' }; + var searchWeightAttrs = { label: 'Poids de la recherche', type: 'number', value: "1" }; var typeUserAttrs = { text: { icon: iconAttr, @@ -51,6 +53,8 @@ 'url': 'Url' }, }, + search: searchAttrs, + searchWeight: searchWeightAttrs, errorMsg: errorMsgAttrs }, textarea: { @@ -60,6 +64,8 @@ 'wysiwyg': 'Editeur enrichi', }, }, + search: searchAttrs, + searchWeight: searchWeightAttrs, errorMsg: errorMsgAttrs, separator: { label: '' }, // separate important attrs from others }, @@ -68,6 +74,8 @@ title: { maxlength: { label: "Longueur Max."}, icon: iconAttr, + search: searchAttrs, + searchWeight: searchWeightAttrs, errorMsg: errorMsgAttrs, separator: { label: '' }, // separate important attrs from others }, @@ -123,7 +131,13 @@ }); $(document).ready(function() { - setTimeout(function() { $('.form-field:not(.paragraph-field) .fld-label').each(function() { $(this).text($(this).html()) }); }, 0); + setTimeout(function() { + $('.form-field:not(.paragraph-field) .fld-label').each(function() { + $(this).text($(this).html()) + }); + $('input[type="checkbox"][value="true"]').prop('checked', true); + }, 0); + }); setInterval(function() { @@ -231,4 +245,10 @@ } .iconpicker-popover.popover.right { right: -200px; left: initial !important;} + + .fld-search[type="checkbox"]:after { + content: attr(title); + position: absolute; + padding-left: 2rem; + } \ No newline at end of file -- GitLab