x Contact us ask us for an offer
Thank you for your message. We will contact you as soon as we understand your needs!
  • Witaj Hello!
  • Masz pytania? Any questions?
  • Skontaktuj się contact us
    we can help you with your business!
Blog
  • 27
    Dec
    Author: matix December 27, 2017 20:42
    Optymalizacja platformy e-commerce z wykorzystaniem ElasticSearch

    Na pewno każdy słyszał o ElasticSearch. Jest to nic innego jak baza danych, wykorzystująca Zend Lucene w celu jak najszybszego oraz najtrafniejszego wyszukiwania.
    Alternatywą dla ElasticSearch jest m.in. Apache Solr czy Sphinx, o którym w przyszłości jeszcze napiszemy, lecz dziś skupmy się na optymalizacji platformy klienta w oparciu o silnik ElasticSearch.

    Agenda
    1. Co to jest ElasticSearch?
    2. Nasz klient oraz problem z bazą danych Key Value.
    3. Rozwiązanie za pomocą Elastic Search - Case Study



    Co to jest ElasticSearch?


    Czym jest ElasticSearch? Autorzy opisują to bardzo prosto:
    Elasticsearch is a distributed, RESTful search and analytics engine capable of solving a growing number of use cases.
    Mówiąć krótko jest to silnik oparty o Zend Lucene, pozwalający w jak najoptymalniejszy sposób wyszukiwać rekordy w bazie za pomocą podanych kryteriów.
    Więcej informacji można doczytać w wikipedii w teorii, jak to wygląda. My skupmy się na praktycznym rozwiązaniu problemu klienta.

    Baza Key Value


    Klient narzekał na wydajność aplikacji. Aplikacja jest typu e-commerce, gdzie użytkownicy mogą zamawiać produkty oraz usługi (sporo ofert).
    Baza danych, która zawiera oprogramowanie to baza Key => Value. Poniżej przedstawiamy struturę encji Doctrine 2:

    /**
    * @ORM\Entity
    * @ORM\Table(name="TaxonomyItem")
    */
    class TaxonomyItem
    {
    /**
    * @ORM\Column(type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    protected $id;

    /**
    * @ORM\Column(type="string", length=100)
    * @Assert\NotBlank()
    */
    protected $title;

    /**
    * @Gedmo\Slug(fields={"title"}, updatable=false)
    * @ORM\Column(length=128, unique=true)
    */
    protected $slug;

    /**
    * @ORM\ManyToOne(targetEntity="TaxonomyCategory", inversedBy="taxonomyItems")
    * @ORM\JoinColumn(name="taxonomy_category_id", referencedColumnName="id")
    */
    protected $taxonomyCategory;

    /**
    * @ORM\OneToMany(targetEntity="TaxonomyField", mappedBy="taxonomyItem",cascade={"persist", "remove"})
    */
    protected $customFields;

    Jak widzimy powyżej, problem polega na tym, iż każdy obiekt w bazie (Np. Produkt / Oferta) ma tylko tytuł i id.
    Reszta to relacje OneToMany do TaxonomyField, gdzie poszczególne atrybuty są przechowywane. Przykładowo dla produktu są to: description, description_en, photo, seo_slug, seo_title, itp.

    Niestety wadą tego rozwiązania jest to, iż każdy element strony wykonuje dodatkowe zapytanie do bazy danych o kolejne atrybuty.



    Przykład:

    Mamy na stronie 30 produktów. Każdy produkt ma pola: description, description_en. Tworzy to 31 zapytań, gdyż:
    1. Pobieramy listę 30 TaxonomyItem (Produktów),
    2. Do każdego TaxonomyItem (Produktu) pobieramy jego atrybuty - 30 zapytań.

    Tworzyło to potężny problem optymalizacyjny, w miarę gdy aplikacja się rozrastała.
    Niektore podstrony sięgały nawet 3000 zapytań / request !

    Rozwiązanie za pomocą Elastic Search - Case Study


    Oczywiście zamiast przepisywać bazę danych (sporo pracy) postanowiliśmy w 16h zoptymalizować aplikację.

    Etap 1 - określenie struktury dokumentu ElasticSearch

    Wykorzystując fakt, iż będziemy nakładali warstwę danych, nie keszowaliśmy całego TaxonomyItem + TaxoanomyFields, lecz zbudowaliśmy osobny dokument (Model). Wyglądał on następująco:

    class ProductService {
    protected $title;
    protected $title_en;
    protected $description;
    protected $description_en;
    protected $photo;
    protected $category;
    protected $categoryTree;
    ....
    }


    Dokument po prostu tworzył się na podstawie TaxonomyItem + TaxonomyFields. Atrybut $categoryTree to dodatkowy atrybut, budujący (i jednocześnie cachujący) drzewo kategorii, w której znajduje się Produkt / Usługa.

    Migracja starych rekordów w bazie do ES (ElasticSearch)

    Odpowiada za to następująca metoda (komenda).
    namespace AppBundle\Command;

    use AppBundle\Entity\User;
    use AppBundle\Services\ElasticSearchService;
    use AppBundle\Services\TaxonomyManager;
    use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
    use Symfony\Component\Console\Input\InputArgument;
    use Symfony\Component\Console\Input\InputInterface;
    use Symfony\Component\Console\Input\InputOption;
    use Symfony\Component\Console\Output\Output;
    use Symfony\Component\Console\Output\OutputInterface;

    use League\Csv\Reader;
    use League\Csv\Statement;

    class BuildProductIndexCommand extends ContainerAwareCommand
    {
    protected function configure()
    {
    $this
    ->setName('index:products')
    ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
    define('tradeSubdomain', 'hagebau');

    $container = $this->getContainer();
    $output->writeln('Building index...');

    /** @var TaxonomyManager $taxonomyManager */
    $taxonomyManager = $container->get('manager.taxonomy');

    /** @var ElasticSearchService $elasticSearchService */
    $elasticSearchService = $container->get('service.elastic_search');
    $products = $taxonomyManager->findAllTaxonomyItems('products');

    foreach ($products as $product) {
    if ((String) $product->lookup('root_product') != '') {
    continue;
    }

    if ((String) $product->lookup('category') == '') {
    continue;
    }

    $elasticSearchService->buildProductIndex($product);
    $output->writeln(sprintf('Product with id: %s has been indexed.', $product->getId()));
    }
    }
    }




    Jak widać kluczowa jest tutaj metoda buildProductIndex($product), która to zapisuje dane do bazy danych ElasticSearch-a.
    Poniżej prosta implementacja:

    public function buildProductIndex(TaxonomyItem $product)
    {
    $this->elasticSearchClient->setType('product');

    $fieldsToIndex = [
    'slug', 'title', 'title_en', 'long_description', 'long_description_en', 'important_information', 'important_information_en',
    'industry', 'agb', 'agb_en', 'photo', 'uncountable', 'responsible_person', 'custom_template', 'category', 'thumbnail',
    ];

    $index = [
    'id' => $product->getId(),
    'position' => $product->lookup('position') ? (Integer) $product->lookup('position')->getValue() : null,
    'invoice_include' => (Boolean) $product->lookup('invoice_include'),
    'price' => unserialize((String) $product->lookup('price')),
    'price_en' => unserialize((String) $product->lookup('price_en')),
    ];

    foreach ($fieldsToIndex as $field) {
    $index[$field] = (String) $product->lookup($field);
    }

    $categoryTree = [];
    $parent = $product->lookup('category');

    do {
    $c = $this->taxonomyManager->findOneTaxonomyItem('categories', ['slug' => $parent]);
    if (!$c) {
    break;
    }

    $category = $this->taxonomyManager->build($c);

    array_unshift($categoryTree, $category);

    $parent = $category->lookup('subcategory')->getValue();
    } while ($parent != '');

    $tree = [];
    foreach ($categoryTree as $t) {
    $tree[] = [
    'slug' => (String) $t->lookup('slug'),
    'title' => (String) $t->lookup('title'),
    'title_en' => (String) $t->lookup('title_en'),
    ];
    }

    $index['category_path'] = $tree;

    if (count($tree) == 0) {
    return;
    }

    return $this->elasticSearchClient->index($index, $product->getId());
    }


    Zasoby serwera zostały zmniejszone o 40%.
    Ilość zapytań do baz danych została również zredukowana o 80-90%.

    Strona Główna (80 zapytań) -> po optymalizacji 3,
    Wyszukiwarka wyników (140 zapytań) -> po optymaliacji 14,
    Podstrona kategorii (75 zapytań) -> po optymalizacji 7,

    Oczywiście po optymalizacji kodu, również klient zauważył diametralną różnicę w samym użytkowaniu.

    Efektem naszej współpracy jest długoterminowa umowa rozwijania aplikacji. Mamy jeszcze sporo do zaoferowania naszemu klientowi ;-)

    Jeżeli chciałbyś również zostać naszym klientem, zapraszamy Cię do kontaktu z nami