Suzanne Favot
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # Recherche Dayuse ![Résultats de recherche](https://i.imgur.com/b3OMZx6.jpg) ## TLDR; ### Must have - [API] Supprimer l'utilisation du DTO `HotelSearch` qui fait doublon avec `HotelSearchOutput` - [API] Simplifier énormément la sérialisation d'HotelMarker (vu avec Thomas) - [FRONT] Ne pas récupérer les marqueurs en SSR mais seulement côté client, la map et les marqueurs peuvent être chargés en asynchrone - [FRONT] Ajouter des key sur les composants rendus dans des tableaux, notamment le composant `Card` ### Should have - [ES] Introduction d'un index pour les offres (qui embarque les infos de l'hôtel) pour faire des requêtes simplifiées - [ES] Index différents pour les offres par période de temps donné (mois / jour, dépend de la volumétrie) - [API] Supprimer les doubles appels aux listener Legacy Dayuse - [API] Désactiver le WriteListener d’APIP sur les endpoints de recherche (write: false dans la configuration des ressources) - [ES] Utiliser des `terms ou term query` au lieu des `range query`, plus gourmandes, quand c'est possible - [API] Retirer l'intégration avec Sentry qui fait trop d'appels - [FRONT] Ne pas récupérer les menus en SSR (cross-linking et top cities) mais seulement côté client. - [FRONT] Désactiver les re-fetch de react-query ### Nice to have - [API] Utiliser le `$context`dans les DataProvider pour récupérer les filtres (et l'interface `ContextAwareCollectionDataProviderInterface`) - [API] Utiliser un `input` pour peupler le DTO SearchHotelQueryDTO (https://api-platform.com/docs/core/dto/) - [API] Laisser le ValidateListener d'Api Platform valider les critères de recherche **ou** désactiver le ValidateListener (`validate : false` dans la configuration des ressources) - [ES] Analyser tous les champs traduits si on veut pouvoir faire de la recherche dessus un jour (actuellement en `keyword`) - [ES] Utiliser une valeur de préférence (id de session ou username) pour optimiser le cache sur les requêtes faites par le même utilisateur (souvent les mêmes requêtes) (https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-preference.html) - [ES] Identifier les critères souvent cherchés ensemble par les users (recherches types) pour identifier des presets de recherche et indexer des champs combinés (ex: wifi-9to5) (demande un travail d'analyse et des métriques fiables) - [FRONT] (Re)mettre le lazy load sur les photos des hôtels dans le carousel ## Analyse macro Api platform avec ES, 2 index : hôtel et POI - Poi => autocomplétion (pas exploré) - Hotel => recherche d’hôtels, d’offres et de marqueurs, multi-critères Résultat transformé directement en DTO (transformer) puis sérialisé par APIP Grosse volumétrie (chiffres exacts ?) En prod, recherche d'hôtels simple (peu de critères) => environ 200ms, recherche de marqueurs (même critères) => environ 1900ms. ## API ### Démarche suivie - Premier survol du code - Onglet "Event" du profiler Symfony pour vérifier quels listeners sont appelés - Premier aperçu des performances via le profiler de Symfony - Exploration plus poussée du code aux endroits suspectés d'être gourmands en performance - Profiling d’une recherche d’hôtels via Blackfire - Profiling d'une recherche de marqueurs via Blackfire ### Analyse #### Listeners API Platform : appel d’une suite de listeners pas tous utiles en fonction des routes - DayUse legacy validateUserAgent, validateRequestedMediaType, validateRequestedLanguage, validateRequestedCurrency) appelé deux fois - WriteListener inutilement appelé sur la recherche #### DataProvider Un DataProvider par type de recherche : - `HotelCollectionDataProvider`pour la recherche d'hôtels - `HotelMarkerCollectionDataProvider` pour la recherche des marqueurs Différentes opérations faites dans le DataProvider `HotelCollectionDataProvider` (le plus complexe) : - HotelCollectionDataProvider => récupération et validation des filtres depuis la requête alors que déjà fait par Api Platform et disponible dans un paramètre `$context` - SearchHotelQueryDTO peuplé dans le provider avec les filtres - Une requête Doctrine pour récupérer la page de recherche éventuellement - Une requête sur une agrégation en fonction du rayon de recherche pour trouver le rayon le plus approprié - La requête principale pour récupérer la liste des hôtels - Hydratation des résultats dans un DTO `Dayuse\Api\Elasticsearch\Model\HotelSearch` - Requête pour récupérer les agrégations (pour afficher le nombre d'hôtels sur chaque critère) - Hydratation des résultats de la recherche dans un DTO `HotelSearch` - Requête pour récupérer le nombre d'hôtels en fonction de chaque critère - Renvoie un `AggregatedPaginatorDTO` contenant les résultats et les agrégations Pourquoi passer par 2 DTOs (`HotelSearch`dans le DataProvider puis `HotelSearchOutput` pour API Platform) ? #### DTO `HotelSearchOutput` Utilisé par API Platform. Utilisation d’un DTO très bien (plus lisible, prévisible, typable). Propriétés publiques, sans getter ni setter => très léger gain de perf (très probablement négligeable, à mettre en balance avec les avantages d'une bonne encapsulation) Passage direct du DTO (pas de requête BDD) => :thumbsup: #### Profiling d'une recherche d'hôtels [Profile blackfire](https://blackfire.io/profiles/5dce5b7b-e31c-4d4c-8842-b2a7482c80be/graph) Recherche d'hôtels simple (ville, date), le profiler de Symfony donne les valeurs suivantes : ![Profiling recherche d'hôtels](https://i.imgur.com/7TIZgko.png) *:warning: Valeurs mesurées en environnement de developpement et en local, seuls les rapports entre les valeurs sont à considérer ici et pas les valeurs elles-mêmes.* 80% du temps total de la requête dans le ReadListener (on exclut le Profiler Listener). Avec Blackfire : *Pour ce profiling, le mode debug a été désactivé, ce qui donne des valeurs plus fiables, même si elles restent toujours plus grandes que celles qu'on observeraient en environnement de production.* ![](https://i.imgur.com/foowyfp.png) L'EventDispatcher occupe la quasi-totalité du temps de requête => attendu au vu du fonctionnement d'Api Platform. Rien à optimiser de ce côté à priori. Le graphique se divise ensuite en deux branches : ![](https://i.imgur.com/iIiLZpm.png) La première branche concerne la sérialisation. À première vue rien de choquant à ce niveau, en supposant que toutes les données sérialisées soient utilisées. ![](https://i.imgur.com/PyFky4O.png) La deuxième branche concerne l'`HotelCollectionDataProvider` (cohérent avec les données du profiler de Symfony puisque le DataProvider est appelé par le ReadListener). Les 3 méthodes au bout du graph sont celles qui nous intéressent. - L'appel au ProxyGenerator de Doctrine pour l'unique requête qui est faite pour récupérer la SearchPage prend 6,5% du temps total de la requête. Ce n'est pas normal puisque les proxies Doctrine sont censés être mis en cache en environnement de production. Puisque le profiling a été lancé en local et en environnement de développement il s'agit probablement d'un faux positif, mais c'est à vérifier. - Les 2 appels à `Elastica\Transport\Http:exec` correspondent aux 2 requêtes Elasticsearch qui sont faites : récupération de la liste des hôtels et des aggrégations. Ces requêtes représentent 14% du temps total de la requête. Dans le détail, la requête de recherche d'hôtels prend 129ms et celle des agrégations 21ms (sur un total de 748ms). On peut donc chercher à optimiser l'utilisation d'Elasticsearch sur ces requêtes, et particulièrement la recherche d'hôtels, c'est ce que nous verrons ensuite. - Enfin la méthode `HotelElasticSearchService:hydrateResultSet` appelée une seule fois, consomme 5,8% du temps de la requête. Beaucoup trop quand on sait qu'un deuxième transformer sera appelé ensuite. On retrouve le problème du DTO en double. #### Profiling de la recherche des marqueurs [Profile blackfire](https://blackfire.io/profiles/f0ba784c-b367-4676-8357-d192d3929e7d/graph) ![](https://i.imgur.com/HBlgpMf.png) Presque 6 fois plus long que la recherche d'hôtels (environ 4s contre 700ms). Même dynamique sur la production : 197ms pour la recherche d'hôtels et 1,5s pour la recherche de marqueurs. 2 points durs : la requête Elasticsearch en elle-même (34% du temps), que nous analyserons ensuite, et la sérialisation, en particulier la méthode `HotelMarkerNormalizer:normalize` (52% du temps). Ces 52% sont équitablement réparties dans deux méthodes : d'un côté `HotelElasticSearchService:processPricingInformation` et de l'autre `OjectNormalizer:normalize`. Concernant le normalizer, le profiling nous permet juste de voir qu'on appelle cette méthode de manière récursive (normal dans le cas d'entités imbriquées), mais surtout que l'appel le plus profond est fait plus de 24 000 fois, ce qui donne plus de 50 appels pour chacun des 434 hôtels remontés. Pas normal étant données le peu de données renvoyées pour chaque hôtel. Il s'agit donc de vérifier comment est sérialisé le DTO `HotelMarker`. Concernant la méthode `HotelElasticSearchService:processPricingInformation`, le profiling permet de remonter jusqu'à `MoneyConverter:convert` d'un côté et `FixedExchangeProvider:provideFixedRates` de l'autre. ![](https://i.imgur.com/yYeU5y9.png) ### Pistes #### Nettoyage des listeners - Corriger les doubles appels aux listener Legacy Dayuse - Désactiver le WriteListener d'APIP sur les endpoints de recherche (`write: false` dans la configuration des ressources) #### Améliorations sur le DataProvider - Utiliser le `$context`dans les DataProvider pour récupérer les filtres (et l'interface `ContextAwareCollectionDataProviderInterface`) - Utiliser un `input` pour peupler le DTO SearchHotelQueryDTO (https://api-platform.com/docs/core/dto/) - Laisser le ValidateListener d'Api Platform valider les critères de recherche **ou** désactiver le ValidateListener (`validate : false` dans la configuration des ressources) #### Doublon sur le DTO - Supprimer l'utilisation du DTO `HotelSearch` qui fait doublon avec `HotelSearchOutput`. #### Sentry L'integration avec Sentry ne semble pas être idéale (trop de calls faits). On dirait que Sentry intercepte des appels. Il serait plus judicieux de retirer l'intégration de Sentry, mais de garder uniquement le handler monolog qui pousse les logs dans Sentry. ![](https://i.imgur.com/YBAwMb2.png) #### Sérialisation d'HotelMarker - Sérialisation trop gourmande pour très peu de champs, vérifier et alléger la sérialisation. ## Elasticsearch ### Démarche suivie - Analyse du mapping et de la construction des requêtes - Profiling d’une recherche d’hôtels via Kibana - Profiling d'une recherche de marqueurs - Profiling d'une recherche d'offres (staycation) ### Analyse #### Index et mapping - Index trop volumineux - Champ nested pour offerPrices - Uniquement les champs recherchés sont indexés => :thumbsup: - Pas de champs dynamiques, y compris pour les traductions => :thumbsup: - Champs traduits indexés en `keyword` => recherche impossible #### Requêtes Construction de la requête principale pour récupérer la liste des hôtels : - Ajout de tous les filtres (principalement `term ` et `terms`) - Ajout du tri decay le cas échéant - Ajout du filtre nested pour les offres - Si l'utilisateur fournit sa position, ajout d'un script pour calculer sa distance à l'hôtel - Si on n'utilise pas le tri par défaut, ajout du tri demandé - Pour une recherche staycation, ajout d'une agrégation #### Profiling ![Recherche d'hôtels dans Kibana](https://i.imgur.com/DbAXasA.png) Profiling avec Kibana de : - une recherche d'hôtels - la requête d'aggrégations correspondante - la recherche de marquers correspondante Premier poste de dépense pour la recherche d'hôtels => JoinQuery pour récupérer les documents nested Idem pour la recherche de marqueurs Pour les aggrégations, à nouveau les filtres sur les documents nested sont ce qui coûte le plus cher. En seconde place on retrouve les aggrégations numériques (étoiles, équipements, addons) => facile à remplacer par des keywords. ![Aggrégations dans Kibana](https://i.imgur.com/qVyZAlK.png) ### Pistes #### Découpage des index - Introduction d'un index pour les offres (qui embarque les infos de l'hôtel) pour faire des requêtes simplifiées. - Index différents pour les offres par période de temps donné (mois / jour, dépend de la volumétrie) #### Mapping - Avoir des documents plus simples, sans nested => en faveur du découpage des index - Analyser tous les champs traduits si on veut pouvoir faire de la recherche dessus un jour (actuellement en `keyword`) #### Requêtes - Utiliser des `terms ou term query` au lieu des `range query`, plus gourmandes, quand c'est possible. - Par exemple, indexer `startHour`et `endHour` en tant que `keyword` et, pour chercher une offre qui commence à 8h, faire une query `terms` sur les valeurs `[6, 7, 8, 9]`(toutes les valeurs possibles avant 8h). - Alternativement, en plus d'indexer les heures de début et de fin d'une offre, indexer chaque heure active de l'offre dans un champ avec un mapping `keyword`, pour pouvoir utiliser des ` term query` sur ce champ. Par exemple pour une offre de 12h à 16h indexer un champ `activeHours: [12,13,14,15,16]` et pour chercher une offre ouverte à 8h il suffira de faire `term: {activeHours: 8}`. - Permet également d'utiliser un mapping `keyword` sur les champs qui sont actuellement en `numeric`. - Utiliser une valeur de préférence (id de session ou username) pour optimiser le cache sur les requêtes faites par le même utilisateur (souvent les mêmes requêtes) (https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-preference.html) - Identifier les critères souvent cherchés ensemble par les users (recherches types) pour identifier des presets de recherche et indexer des champs combinés (ex: wifi-9to5) (demande un travail d'analyse et des métriques fiables) #### Pistes non explorées (intéressant seulement quand tout le reste aura été bien optimisé) - Hardware - Découpage du cluster ## Front En React et TypeScript avec Next.js Premier rendu côté serveur Lag visible sur la page de résultats En prod 1600ms pour avoir la liste des hôtels et quasiment 5 secondes pour avoir la map et les marqueurs Même en prenant en compte la durée des requêtes API il y a matière à amélioration. ### Démarche - Désactiver le SSR pour pouvoir profiler le premier rendu - Profiling avec et sans SSR sur la page de résulltats - Parcours du code de la recherche ### Analyse Premier rendu côté serveur bien trop long (1600ms en prod) Multiples re-rendus, même avec le SSR, pour le premier chargement (25 commits) Les photos ne semblent pas lazy-loadées en prod (toutes les photos des carousels sont chargées immédiatement) Re-rendus inutiles déclenchés par des changements dans les hooks custom => on peut quasi systématiquement remonter à react-query ![](https://i.imgur.com/tQmUn1L.png) Profiling du premier commit Composant `SearchPage` à optimiser => principale dépense, traitements trop longs hors rendu des composants enfants. Idem pour `HotelListPage`, `FiltersMedium` et `Availabilities`. ### Pistes - Ne pas récupérer les marqueurs en SSR mais seulement côté client, la map et les marqueurs peuvent être chargés en asynchrone - Idem pour les menus (cross linking et top cities) - (Re)mettre le lazy load sur les photos des hôtels - Ajouter des key sur les composants rendus dans des tableaux, notamment le composant `Card` - Désactiver les re-fecth de react-query ## Requêtes profilées avec Blackfire ### Recherche d'hôtels ``` https://api-distribution.dayuse.test/website/search/hotels?page=1&stars[]=1&stars[]=2&stars[]=3&stars[]=4&stars[\]=5&checkinDate=2021-10-14&placeSlug=france%2File-de-france%2Fparis ``` ### Recherche de marqueurs ``` https://api-distribution.dayuse.test/website/search/hotels/markers?page=1&stars[]=1&stars[]=2&stars[]=3&stars[]=4&stars[]=5&checkinDate=2021-10-14&userCurrentTime=11:17&placeSlug=france%2File-de-france%2Fparis&selectedAddress=Paris ``` ## Notes Ligne inutile ? `HotelCollectionDataProvider:97` ```php $this->searchService->priceRangeFilter($userCurrency); ``` ## Littérature ### Elasticsearch - https://jolicode.com/blog/elasticsearch-the-right-way-in-symfony - https://www.gojek.io/blog/elasticsearch-the-trouble-with-nested-documents - https://www.elastic.co/guide/en/kibana/current/xpack-profiler.html - https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-search-speed.html - https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-preference.html ### APIP - https://api-platform.com/docs/core/dto/

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully