svergeylen / collector

Collector : site de gestion des BD, films, DVD, jeux de société, ...
2 stars 0 forks source link

Distincition des tags filtrants ou non filtrants #90

Closed svergeylen closed 6 years ago

svergeylen commented 6 years ago

Coucou, Tu peux m'aider sur la requête ? Il y a des tags qui filtrent les items (ex : BD, Thorgal, ...) mais aussi des tags qui ne servent "à rien", càd uniquement pour la navigation et la structure des tags... (ex : "thèmes", "séries", ...) Tout est déjà pret niveau database, vue, contrôleur et aussi la représentation graphique en gris clair des tags non filtrants (tag.filter_items = false). Il faut juste modifier la requete suivante : Model > Items > item.having_tags de façon à y inclure la condition "tag.filter_items = true"... De cette façon, les items ne seront filtrés qu'avec les tags filtrants, et pas les tags non filtrants :-) Merci :-) Eventuellement refaire entièrement la requete pour améliorer les performances (elle est un peu immonde et illisible)

svergeylen commented 6 years ago

Acutellement : les items n'apparaissent pas car les items ne possèdent pas les tags "thèmes" et "science-fiction" :

screenshot_2018-09-11 vergeylen eu

Idéalement, il faut arriver à obtenir cette liste d'item (ici, j'ai enlevé manuellement les deux tags gris) :

screenshot_2018-09-11 vergeylen eu 1

dvergeylen commented 6 years ago

Coucou,

Je ferais quelque chose comme ceci:

def self.having_tags(ar_tags)
  Item.includes(:tags).where(tags: {name: ar_tags, filter_items: false})
end

(si ar_tags contient bien les names des tags) :smiley:

svergeylen commented 6 years ago

Non, cela renvoie malheureusement aucun item en réponse... (avec ou sans les tags filtrant, j'ai donc essayé rien qu'avec un seul tag pour voir)

Started GET "/tags/785" for 127.0.0.1 at 2018-09-13 15:06:48 +0200
Processing by TagsController#show as HTML
  Parameters: {"id"=>"785"}
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Tag Load (0.1ms)  SELECT  "tags".* FROM "tags" WHERE "tags"."id" = ? LIMIT ?  [["id", 785], ["LIMIT", 1]]
  Tag Load (0.2ms)  SELECT "tags".* FROM "tags" WHERE "tags"."id" IN (776, 778, 779, 785)
  Tag Exists (0.2ms)  SELECT  1 AS one FROM "tags" INNER JOIN "ownertags" ON "tags"."id" = "ownertags"."tag_id" WHERE "ownertags"."owner_id" = ? AND "ownertags"."owner_type" = ? LIMIT ?  [["owner_id", 785], ["owner_type", "Tag"], ["LIMIT", 1]]
  Rendering tags/show.html.erb within layouts/application
  Rendered search/_form_tags.html.erb (0.9ms)
  Tag Load (0.3ms)  SELECT "tags".* FROM "tags" INNER JOIN "ownertags" ON "tags"."id" = "ownertags"."tag_id" WHERE "ownertags"."owner_id" = ? AND "ownertags"."owner_type" = ? ORDER BY "tags"."name" ASC  [["owner_id", 785], ["owner_type", "Tag"]]
  SQL (4.1ms)  SELECT "items"."id" AS t0_r0, "items"."name" AS t0_r1, "items"."series_id" AS t0_r2, "items"."created_at" AS t0_r3, "items"."updated_at" AS t0_r4, "items"."adder_id" AS t0_r5, "items"."number" AS t0_r6, "items"."description" AS t0_r7, "items"."rails_view" AS t0_r8, "tags"."id" AS t1_r0, "tags"."name" AS t1_r1, "tags"."root_tag" AS t1_r2, "tags"."default_view" AS t1_r3, "tags"."letter" AS t1_r4, "tags"."view_alphabet" AS t1_r5, "tags"."filter_items" AS t1_r6 FROM "items" LEFT OUTER JOIN "ownertags" ON "ownertags"."owner_id" = "items"."id" AND "ownertags"."owner_type" = ? LEFT OUTER JOIN "tags" ON "tags"."id" = "ownertags"."tag_id" WHERE "tags"."name" IN ('776', '778', '779', '785') AND "tags"."filter_items" = ?  [["owner_type", "Item"], ["filter_items", "f"]]
  Rendered tags/show.html.erb within layouts/application (12.6ms)
  Rendered shared/_debugging.html.erb (0.7ms)
Completed 200 OK in 67ms (Views: 56.4ms | ActiveRecord: 5.2ms)

Ce serait bien que cette requete fonctionne, parce que c'est vraiment l'essence de l'idée des tags que tu avais proposée.... ("sélection de plusieurs tags et affichage des items qui y sont reliés, sauf pour certains tags qui ne servent qu'à la navigation" ;-) )

dvergeylen commented 6 years ago

Hum...

C'est bizarre, car en testant ça fonctione:

cd /tmp
git clone https://github.com/svergeylen/collector.git
cd collector/
bundle install
bundle exec rake db:setup
rails console
# Creating Tags: a,b,c,d,e
a = Tag.create(name: "a", root_tag: true, filter_items:false)
b = Tag.create(name: "b", root_tag: false, filter_items:true)
c = Tag.create(name: "c", root_tag: false, filter_items:true)
d = Tag.create(name: "d", root_tag: false, filter_items:false)

# Associating Tags: a → b → c → d (optional)
a.tags = [b]
b.tags = [c]
c.tags = [d]

# Creating Item
item = Item.create(name: "Bonjour", adder_id: User.first.id)
item.tags = [a,d] # Tags non being 'filter_items'

# Query
ar_tags= ["a", "d"] # Tags names, not ids
Item.includes(:tags).where(tags: {name: ar_tags, filter_items: false})

# => #<ActiveRecord::Relation [#<Item id: 1, name: "Bonjour", series_id: nil, created_at: "2018-09-14 08:00:52", updated_at: "2018-09-14 08:00:52", adder_id: 1, number: nil, description: nil, rails_view: "general">]> 

Même en ajoutant d'autres tags à Item, ça ne perturbe pas l'output. Peux-tu reproduire et me faire un retour?

dvergeylen commented 6 years ago

Alalaa, mais je te demande si ar_tags contient des name ou pas...

(si ar_tags contient bien les names des tags) :smiley:

Dans app/controllers/tags_controller.rb > show, tu utilises session[:active_tags] qui semble stocker des ids, donc la requête est juste modifiée par id: au lieu de name::

def self.having_tags(ar_tags)
  Item.includes(:tags).where(tags: {id: ar_tags, filter_items: false})
end

En daar is de werk!

svergeylen commented 6 years ago

ok, je vais regarder (stp laisse ouvert les taches sinon je pense qu'il n'y a plus d'action à faire dans le code, ce n'est pas un projet public donc on peut surement s'écarter de la stricte utilisation de github)

svergeylen commented 6 years ago

Voilà, comme tu me le suggères, je regarde plus en détail les points sur lesquels tu m'aides, au lieu de copier coller ...

La solution a ce problème est la suivante :

applicable_tag_ids = Tag.where(id: ar_tags).where(filter_items: true).pluck(:id)
        ownertags = Ownertag.where(tag_id: applicable_tag_ids, owner_type: "Item").group(:owner_id).count.select{|owner_id, value| value >= applicable_tag_ids.size }
        Item.where(id: ownertags.keys)

Malheureusement, la solution plus courte que tu donnes en fermant l'issue ne fonctionne pas, pour la raison suivante :

tags: {id: ar_tags} renvoie tous les ownertags qui contiennent n'importe lequel des tag_ids donnés (OR) , et ne renvoie pas les ownertags qui contiennent tous les tag_ids donnés (AND).

Comme toutes les BD ont le tag "BD", cette requête renvoie toutes les bd d'office, peu importe les autre tag_ids qui sont donnés dans l'array et n'est donc pas directement utilisable. (>2700 records à chaque requête)

A moins de pouvoir modifier cette requête courte pour qu'elle sélectionne des records qui possèdent tous les tag_ids donnés (ce qui me parait impossible en une seule étape vu que le SQL ne peut faire des AND que sur des attributs d'une même ligne et pas entre différentes lignes), il faut conserver la requête originale que j'ai simplement modifiée en enlevant les tag_ids qui ne sont pas filtrants (filter_items: false). La seconde ligne n'utilise donc que les tags filtrants dans la requete.

Voilà, je n'essaie pas de faire le malin, mais simplement te dire que la requête donnée ne fonctionnait pas et ce n'est pas grave. On peut probablement encore la simplifier, mais cela fonctionne a priori :

Avec le tag série "non filtrant", tous les items s'affichent (identique avec ou sans le tag "séries", en fait, ce qui est logique) : image

dvergeylen commented 6 years ago

Ok je comprends!

Effectivement, les Tags généraux comme BD vont renvoyer plein de résultats, bien vu. Ceci n'était pas visible dans mon banc de test.

Dans ce cas je propose soit de le faire en trois requêtes comme tu fais, soit directement sur base de OwnerTag (qui contient toutes les infos, tu peux encapsuler la restriction des Tags dans la requête ownertag) et là deux requêtes suffisent:

Impossible de réduire à une requête parce que Owner est polymorphique (et Rails renvoie ActiveRecord::EagerLoadPolymorphicError), autrement ça marcherait !

def self.having_tags(ar_tags)
  item_ids = Ownertag.joins(:tag).where({tags: {filter_items: false, id: ar_tags}}).where(owner_type: "Item").group_by(&:owner_id).select{|item_id, tags| tags.size == ar_tags.size}.keys
  Item.where(id: item_ids)
end

Attention c'est bien joins(:tag) (singulier) mais .where({tags: {...}}) (pluriel), parce que le premier donne l'indication à Rails de comment est nommée l'association (ici belongs_to donc tag au singulier) et le deuxième la syntaxe des inner joins, toujours au pluriel dans SQL (ceci est visible dans la requête faite par Rails).

A mon sens on ne sait effectivement pas faire mieux à ce stade :smiley:

Pourrait être d'intérêt: Ajouter un scope aux Ownertag pour filtrer sur ceux ayant un owner item ou un owner tag (toujours utile en association polymorphique):

# Dans app/models/ownertag.rb
scope :where_owner_is_an_item, -> {where owner_type: "Item"}
scope :where_owner_is_a_tag,   -> {where owner_type: "Tag"}

# Et les requêtes deviennent:
Ownertag.where_owner_is_an_item.joins(:tag).where(...)

Doc

svergeylen commented 6 years ago

J'ai tenté mais cela ne renvoie aucun item de mon coté. Voici la requete :

Started GET "/tags/798" for 127.0.0.1 at 2018-09-21 11:51:11 +0200
Processing by TagsController#show as HTML
  Parameters: {"id"=>"798"}
  User Load (1.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Tag Load (0.1ms)  SELECT  "tags".* FROM "tags" WHERE "tags"."id" = ? LIMIT ?  [["id", 798], ["LIMIT", 1]]
  Tag Load (0.2ms)  SELECT "tags".* FROM "tags" WHERE "tags"."id" IN (789, 797, 798)
  Tag Load (0.2ms)  SELECT "tags".* FROM "tags" INNER JOIN "ownertags" ON "tags"."id" = "ownertags"."tag_id" WHERE "ownertags"."owner_id" = ? AND "ownertags"."owner_type" = ?  [["owner_id", 798], ["owner_type", "Tag"]]
  Ownertag Load (9.0ms)  SELECT "ownertags".* FROM "ownertags" INNER JOIN "tags" ON "tags"."id" = "ownertags"."tag_id" WHERE "tags"."filter_items" = ? AND "tags"."id" IN (789, 797, 798) AND "ownertags"."owner_type" = ?  [["filter_items", "t"], ["owner_type", "Item"]]
  Rendering tags/show.html.erb within layouts/application
  Rendered search/_form_tags.html.erb (1.2ms)
  Item Load (0.2ms)  SELECT "items".* FROM "items" WHERE 1=0
  Rendered tags/show.html.erb within layouts/application (6.7ms)
  Rendered shared/_debugging.html.erb (0.8ms)
Completed 200 OK in 166ms (Views: 49.1ms | ActiveRecord: 15.4ms)

J'ai tenté avec true au lieu de false, parce que filter_items = true pour les tags filtrants

image

Du coup, avec un peu de commentaires, ca donnerait ceci au final :

# On sélectionne dans les tags donnés uniquement ceux qui doivent filtrer les items
    applicable_tag_ids = Tag.where(id: ar_tags).where(filter_items: true).pluck(:id)
    # On sélectionne les items qui correspondent à ces tags filtrants en comptant si chaque item est repris autant de fois que le nombre de tags filtrants donné
    # Si il y a deux tags filtrants donnés, il faut que ownertags contiennent 2 lignes pour cet item (une ligne pour chaque tag différent)
    ownertags = Ownertag.where(tag_id: applicable_tag_ids, owner_type: "Item").group(:owner_id).count.select{|owner_id, value| value >= applicable_tag_ids.size }
    # On charge les items correspondants aux lignes trouvées dans ownertags
    Item.where(id: ownertags.keys)  
dvergeylen commented 6 years ago

Il faut effectivement mettre true et non false. Je ne parviens pas à reproduire le problème, en dumpant data.yml de la DB en production ça renvoie bien des Items, mais je ne parviens pas à reproduire ton exemple car le Tag "Bandes dessinées" n'existe pas pour le moment semble-t-il.

Si tu savais me lister des fixtures pour reproduire, ça serait cool. :wink:

Entretemps, en prenant les tags du premier Item en DB production :

ar_tags = Item.first.tag_ids
#  => [1, 4]

item_ids = Ownertag.joins(:tag).where({tags: {filter_items: true, id: ar_tags}}).where(owner_type: "Item").group_by(&:owner_id).select{|item_id, tags| tags.size == ar_tags.size}.keys
# => [1] → Correct, l'Item.first.id est bien 1

Item.where(id: item_ids)
# => #<ActiveRecord::Relation [#<Item id: 1, name: "La Magicienne trahie", series_id: 2, created_at: "2007-12-24 08:51:55", updated_at: "2017-10-23 16:11:31", adder_id: nil, number: 1.0, description: nil, rails_view: "general">]>

Loupe-je un truc? 😇

Edit : sinon c'est déjà bien suffisant il me semble hein (mais juste pr savoir)

svergeylen commented 6 years ago

COucou, Pour les fixtures, rien de plus simple, il suffit d'importer la database :

rake db:reset
rake db:data:load

Après, il faut migrer toutes les données vers les tags avec les taches rake db:convert_xxxxx ..

Mais si tu veux, je peux recréer un nouveau fichier data.yml directement au bon format avec les tags et tout et le commit dans le prochain commit... comme ca, tu n'auras qu'a faire les 2 lignes de codes ci-dessous... Ca te tentes ? Si non, je peux essayer de décoder les 3 lignes ci-dessus (mais bon, comme ca marche acutellement, autant se concentrer sur les virtual attributes, non ? cela marche, mais pas avec des tags imbriqués et pas avec selectize.js qui veut absolument un ... ... rien n'est jamais simple)

svergeylen commented 6 years ago

Voilà, je t'i envoyé par email le fichier data.yml déjà migré en "ownertags", comme ca, tu dois juste faire le reset et le load 😎