(JS/Vue) Filtres sur un Array : cumul, suppression et filtres de type Array

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je m’interroge sur la meilleure méthode pour appliquer des filtres sur un Array en termes d’efficacité et de rapidité.

A la base, j’ai un Array d’objets. En cliquant sur divers boutons, l’utilisateur peut réduire l’Array pour n’afficher que des résultats pertinents. Rien de révolutionnaire.

J’utilise Vue, mais bon sur le principe, c’est juste du JS, hein. Pour l’instant, je peux appliquer facilement un filtre. Ci-dessous par exemple le principe pour le filtre sur le prix :

//Dans computed:
activities() {
  return this.filteredActivities(this.filter);
},

//Dans les méthodes:
filterBy(type, value) {
  this.filter.type = type;
  this.filter.value = value;
  console.log(this.filter);     
},

//Dans le template
<v-btn-toggle v-model="price" mandatory @change="filterBy('price', price)">[etc.]

//Dans le store Vuex
filteredActivities: (state) => (filter) => {
      if (filter.type === '') {
        return state.orderedByDate;
      }
else {
        if (filter.type == 'price') {
          if (filter.value == 'free') {
            return state.orderedByDate.filter(activity => activity.price === '');
          }
          else if (filter.value == 'all') {
            return state.orderedByDate;
          }
          else if (filter.value == 'fee') {
            return state.orderedByDate.filter(activity => activity.price != '');
          }
        }

Donc tout ça fonctionne, c’est pas mal.

Mon souci maintenant c’est que sur certains filtres je dois retourner un Array, par exemple des catégories. Mon utilisateur clique sur des cases pour inclure des catégories qui l’intéressent ; ça construit un array qui ressemble à [’/api/category/1’, '/api/category/18’, [etc.]]. Il faut donc retourner la liste filtrée des activity qui ont dans leur array activity.categories la catégorie 1 OU la catégorie 18, OU la catégorie Bidule, etc. Et là je m’interroge sur la manière la plus économe en ressources pour effectuer cette opération. Des avis ?

Sinon, sur cette même fonction de filtres, j’ai deux autres interrogations.

1/ Quel serait le meilleur moyen de CUMULER des filtres ? Par exemple "gratuit" ET "categorie 18 ou categorie 1" ? Je pensais simplement à chaque application d’un filtre créer un array contenant les résultats de la recherche, puis réutiliser cet array comme base pour une nouvelle recherche, et donc modifier mon objet "this.filter" pour qu’il inclue le nombre de filtres en cours ? Y a-t-il mieux à faire ?

2/ Si on cumule des filtres, on doit pouvoir en ENLEVER. Et là, le plus simple n’est-il pas de recommencer chaque recherche à 0 avec les nouveaux filtres plutôt que de regarder comment rajouter dans la liste des résultats les Activity qu’il faudrait remettre ?

Bref je m’interroge.

Merci pour vos conseils.

Le plus simple serait de créer un tableau filtré en partant de la liste complète, d’y appliquer tes filtres un par un, pour enregistrer le résultat dans ton composant.

Par exemple :

this.listeFiltree = this.activities.filter((activity) => {
  let keep = true;
  this.filters.forEach((filter) => {
    if (!activity.matchedFilter(filter)) { // À toi d'implémenter tes filtres ici
      keep = false;
    }
  });
  return keep;
});

Ça pourrait encore être optimisé en arrêtant la boucle des filtres dès que l’un d’entre eux renvoie false d’ailleurs. Mais je fais ça de tête :)

Merci pour vos avis. Je teste cet après-midi diverses techniques.

J’ai un souci quasi permanent concernant la recherche d’optimisation et j’avoue sincèrement ne pas avoir les connaissances pour trancher. Je ne sais jamais quel est le "meilleur" moyen, il y a tellement de paramètres à prendre en compte, et j’ai horreur du principe de "bah on s’en fout, c’est le résultat qui compte, te prends pas la tête à optimiser la mémoire ou les requêtes".

Pour tous ces filtres, je m’embête avec JS pour filtrer une liste que je vais chercher via API Platform. Sincèrement, le plus efficace, ce ne serait pas de relancer une requête à chaque fois ? Parce que j’ai fini par comprendre API Platform et du coup, cumuler des filtres pour une requête ne me pose plus spécialement de problème. Mais est-ce le mieux pour l’appareil de l’utilisateur ? En termes de vitesse ? Et pour le serveur, ça ne risque pas de le faire craquer rapidement ? Sur toutes ces problématiques, je ne sais pas comment choisir :(

Auriez-vous un avis sur la question ?

Edit : Est-ce que ça aurait un sens d’utiliser les deux techniques ? Par exemple, pour les filtres simples type binaires, je laisse JS et l’appareil de l’utilisateur faire le taff. Pour les filtres plus complexes, comme les catégories ou les dates, je fais une requête sur laquelle j’applique les éventuels autres filtres binaires activés. Ce serait utile et intelligent ou juste une prise de tête supplémentaire ?

+0 -0

L’idée est surtout de savoir la quantité de données à filtrer et le besoin de temps réel : s’il y a énormément de données il vaudra mieux filtrer côté serveur, s’il y a plutôt besoin de temps réel ça peut se faire côté client. C’est un règle générale hein, pas un absolu : il faut prendre en compte tes utilisateurs et leurs capacités (si ton app est utilisée dans une zone de couverture faible, multiplier les requêtes sera compliqué par exemple).

Cumuler les deux techniques risque d’être compliqué, mais si ça peut permettre une meilleur expérience utilisateur ça peut-être un choix pertinent.

Avoir un résultat est prioritaire, mais rien n’empêche d’optimiser progressivement ensuite.

Tu peux également faire des benchmarks si tu veux comparer des méthodes : il existe plusieurs services en ligne qui vont faire tourner tes algos plusieurs (milliers ?) de fois pour comparer leurs performances, ça pourra peut-être t’aider.

J’ai encore un peu de mal à cerner le volume de données. Je table sur environ 200 objets concernant chaque utilisateur. Là sur mes tests j’ai créé 14 objets "fonctionnels", ramenés en 104ms (en moyenne, et hors attente) pour un poids de 34.95ko. Suis curieux de voir si le poids est complètement corrélé au nombre d’objets ou si sur les 35ko il y en a XX de pris par API Platform pour enrober toutes les données.

Et j’aurais tendance à dire que de nos jours, la rapidité de réponse est primordiale pour l’utilisateur, et ce d’autant que dans mon cas, l’utilisateur doit utiliser et rester sur mon app par "plaisir", rien ne l’oblige à le faire, ce n’est pas un employé devant de toute façon attendre, sans avoir le choix.

Ah mais si tu as du temps en trop, je suis tout à fait extrêmement beaucoup intéressé par ton point de vue sur la façon d’optimiser tout ça. :soleil:

Pour revenir sur mes filtres, ceci a l’air de fonctionner, et sans vouloir dire d’âneries, la fonction "some" s’arrête dès qu’elle trouve une occurence, ce qui me conviendrait farpaitement.

return state.orderedByDate.filter(activity => {               
    return filter.value.some((v) => {
        return activity.categories.includes(v);
    })   
});

Argh ma fonction précédente fonctionne SI activity.categories est un simple array contenant '/api/categories/1' etc. Si - comme c’est le cas en vrai, ce sont en fait des objets, ça ne fonctionne plus.

J’ai changé en ceci :

return state.orderedByDate.filter(activity => {               
  return filter.value.some((v) => {
    for (let cat of activity.categories) {
      return filter.value.indexOf(cat['@id']) > -1;
    }
  }) 
});

Ca a l’air de fonctionner aussi vu la cohérence des résultats. Maintenant, entre les fonctions fléchées et les "some", "filter" et les boucles, il est fort possible que j’ai créé un monstre. Ca doit pouvoir être simplifié/optimisé, non ? J’ai un sérieux doute sur l’utilité de la seconde ligne. Ma tête va exploser en cette fin de semaine ^^

Edit : ah en fait non, bug. Mff la suite demain.

+0 -0

Salut,

J’ai eu un projet où le cœur était une liste d’objets à afficher sous forme de tableau, graphe, etc. L’action qui sera bloquante la première est au chargement de la page, pour le parsing de la réponse JSON. La partie filtrage (arr.filter(...)) était immédiate tant que tous les filtres étaient combinés en un :

// ça
const conditions = x => x != 2 && x != 3 && x != 4
arr = arr.filter(cond1And2)

// plutôt que trois conditions...
const condition1 = x => x != 2
const condition2 = x => x != 3
const condition3 = x => x != 4
arr = arr.filter(condition1).filter(condition2).filter(condition3)

Autre chose, qui elle était non négociable : ne pas stocker le tableau complet dans la partie données de Vue. Le tableau et ses enfants objets sont tous modifiés pour notifier Vue en cas de modification et un filtre dessus est extrêmement lent (et le tableau prend beaucoup de place en mémoire). Le résultat était mis dans la partie Vue pour que Vue gère la partie rendu.

Avec ça, je pouvais filtrer instantanément de l’ordre de 250k à 1M d’entrées. Il y avait quelques secondes de chargement pour les données. De la pagination est bien entendue nécessaire parce que les modifications du DOM sont vites lentes : pour 1000 éléments par page on avait des ralentissements.

Bon j’ai encore des soucis avec cette saleté d’enchainement de filtres. Je n’arrive pas à faire ce que tleb dit, à savoir ".filter(toutes mes conditions)" plutôt que ".filter.filter.filter" etc. Quelles que soient les méthodes qu’on peut trouver sur Stack, ça revient toujours plus ou moins au même.

En m’inspirant de cet article j’ai pu obtenir ceci, qui m’a tout l’air fonctionnel, mais qui revient au final à ce que je devrais éviter:

filteredActivities: (state) => (filter) => {
   var Obj = {
      result : state.orderedByDate,
      filterByPrice: function(value) {
          this.result = this.result.filter(activity => activity.price != '');
          return this;
      },
      filterByPlace: function(value) {
          this.result = this.result.filter(activity => activity.isIndoor == false);
          return this;
      }
    }
    //console.log(Obj.filterByPrice().filterByPlace());
    return Obj.result;
    }      
}

Quand je change les valeurs pour ces deux filtres (pour l’instant à la main), ça me renvoie bien des résultats cohérents vu mes données. On m’a filé aussi un lien vers cet article mais même si j’arrive à le faire fonctionner, ça reviendra au final plus ou moins au même.

Ce que j’aimerais obtenir, c’est donc quelque chose comme:

filteredActivities: (state) => (filter) => {
      return state.orderedByDate.filter(activity => {
        return activity.price != '' 
        && activity.isIndoor === false 
        && activity.categories.some((cat) => { return ['/api/categories/8', '/api/categories/2'].includes(cat['@id']); })
      });
    },

Je ne sais pas comment rendre ce code dépendant des éventuels filtres actifs. Et est-ce réellement mieux niveau temps de calcul ? Sur quels sites je pourrais tester ?

+0 -0

Je ne comprends pas vraiment pourquoi tu mets en place un objet avec des propriétés pour faire des filtres. En supposant que state.orderedByDate est ta liste de valeurs à filter et que tu souhaites appliquer les filtres a.price != '' et !a.isIndoor (avec a un élément de ta liste), ça ressemblerait à quelque chose du genre :

// Notre fonction filteredActivities prend un variable state, qui définit
// apparemment l'état actuel. Au sein de cet état actuel, on utilise une seule
// valeur : state.orderedByDate. C'est la liste des éléments à filter.
filteredActivities: state => {
    // On retourne la liste filtré.
    return state.orderedByDate.filter(activity => {
        return activity.price != '' && !activity.isIndoor
    })
}

Cette fonction ne prend donc pas en compte les filtres actifs. Pour ça, il faut que tu commences par déterminer le format que tu vas utiliser pour représenter tes filtres. Par exemple, tu vas peut-être vouloir utiliser un objet qui a comme propriétés minPrice, maxPrice, indoor, outdoor, etc. Ou alors, le format de filter est déjà déterminé, basé sur le format que tu utilises niveau front pour les gérer. Ensuite, au sein de la fonction qui est exécuté sur chaque élément, et qui retourne un booléen si on le garde ou pas, tu utilises cette variable filter pour comparer ton activité à ce que tu recherches.

Ça peut simplifier ton code de faire plusieurs filtres à la suite. Je te conseille de faire ça, et de n’optimiser pour utiliser qu’un seul appel à array.filter() que si tu remarques que tu le nécessites. Ça ne devrait pas être le cas pour moins de quelques centaines de milliers de valeurs.

Au final (enfin…"final", on verra…), je me suis inspiré de ceci : article, et d’une conversation sur le forum de vue. J’aime bien le principe du résultat, et ça fonctionne. Ca reste du cumul de filtres mais relativement intelligent et efficace, et j’essaye de me rappeler que vu le volume de données, c’est pas la mort.

Merci à vous, utilisateurs de Zeste de Savoir, de me supporter…dans tous les sens du terme :)

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte