Site Web de la boutique en ligne ECommerce: découverte de produits similaires par programmation

Je développe une application web visortingne. Lorsqu'un client potentiel consulte un produit sur le site Web, je voudrais proposer automatiquement un set de produits similaires à partir de la database (ou requestr à un user de saisir explicitement datatables / mappages de similarité de produit).

En fait, quand on y pense, la plupart des bases de données sur les visortingnes ont déjà beaucoup de données de similarité disponibles. Dans mon cas, les Products pourraient être:

  • mappé à un Manufacturer (aka Brand ),
  • mappé à une ou plusieurs Categories , et
  • mappé à un ou plusieurs Tags (aka Keywords ).

Exemple de Datamodel Storefront

En comptant le nombre d'attributes partagés entre un produit et tous les autres, vous pouvez calculer un "SimilarityScore" pour comparer d'autres produits par rapport à celui qui est vu par le client. Voici mon implémentation de prototype initiale:

 ;WITH ProductsRelatedByTags (ProductId, NumberOfRelations) AS ( SELECT t2.ProductId, COUNT(t2.TagId) FROM ProductTagMappings AS t1 INNER JOIN ProductTagMappings AS t2 ON t1.TagId = t2.TagId AND t2.ProductId != t1.ProductId WHERE t1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' GROUP BY t2.ProductId ), ProductsRelatedByCategories (ProductId, NumberOfRelations) AS ( SELECT t2.ProductId, COUNT(t2.CategoryId) FROM ProductCategoryMappings AS t1 INNER JOIN ProductCategoryMappings AS t2 ON t1.CategoryId = t2.CategoryId AND t2.ProductId != t1.ProductId WHERE t1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' GROUP BY t2.ProductId ) SELECT prbt.ProductId AS ProductId ,IsNull(prbt.NumberOfRelations, 0) AS TagsInCommon ,IsNull(prbc.NumberOfRelations, 0) AS CategoriesInCommon ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId THEN 1 ELSE 0 END as SameManufacturer ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId THEN IsNull(prbt.NumberOfRelations, 0) + IsNull(prbc.NumberOfRelations, 0) + 1 ELSE IsNull(prbt.NumberOfRelations, 0) + IsNull(prbc.NumberOfRelations, 0) END as SimilarityScore FROM Products AS SourceProduct, Products AS SimilarProduct INNER JOIN ProductsRelatedByTags prbt ON prbt.ProductId = SimilarProduct.Id FULL OUTER JOIN ProductsRelatedByCategories prbc ON prbt.ProductId = prbc.ProductId WHERE SourceProduct.Id = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' 

ce qui entraîne des données comme celle-ci:

 ProductId TagsInCommon CategoriesInCommon SameManufacturer SimilarityScore ------------------------------------ ------------ ------------------ ---------------- --------------- 6416C19D-BA4F-4AE6-AB75-A25A0138B3A5 1 0 0 1 77B2ECC0-E2EB-4C1B-A1E1-A25A0138BA19 1 0 0 1 2D83276E-40CC-44D0-9DDF-A25A0138BE14 2 1 1 4 E036BFE0-BBB5-450C-858C-A25A0138C21C 3 0 0 3 

Je ne suis pas un gourou de la performance SQL, donc j'ai les questions suivantes pour vous gourous SQL:

  • Les expressions de table commune (CTE) sont-elles appropriées / optimales dans ce cas d'utilisation? (Ils semblent sûr de rendre plus facile à lire / suivre le SQL). Y at-il un moyen d'save une jointure là-bas n'importe où, étant donné le model présenté ci-dessus?

et

  • Serait-ce un bon candidat pour une vue indexée (pour la persistance) ou cela appendait-il un coût excessif aux changements dans datatables sources? Dans ce cas, je vais faire une procédure stockée qui met à jour une table SimilarProductMappings physique pour un produit donné.

Vous posez beaucoup de questions. Je vais essayer d'aborder chacun d'eux sans trop entrer dans les détails.

  1. Les CTE vs. les tables dérivées sont des sucres syntaxiques. Cela ne fait aucune différence en termes de performances. Le seul avantage de les utiliser est que vous pouvez les réutiliser au lieu de copyr / coller / dactylographier à nouveau une table dérivée. Cependant, vous ne les réutilisez pas dans ce cas, c'est à vous de décider.

  2. Vues indexées: Gardez à l'esprit que les index sur les vues agissent comme des index sur la ou les tables avec peu d'exceptions. Imaginez-le comme une autre table est créée pour votre requête / vue spécifique et stockée sur le disque pour une récupération plus rapide. Lorsque datatables sous-jacentes changent, ces index doivent être mis à jour. Oui, cela peut créer un énorme impact sur les ressources. En général, je préfère voir quelqu'un écrire une requête qui utilise les index sur la table de base, et s'ils ont besoin de plus d'index dans un but spécifique, alors regardez cela en détail plutôt que de façon holistique sur une vue avec plusieurs tables. C'est beaucoup plus facile à maintenir et beaucoup plus facile de comprendre pourquoi votre CRUD prend plus de time que prévu. Il n'y a rien de nécessairement faux avec une vue indexée. Mais, soyez très prudent avec l'ajout de ceci sur un model de database d'application comme ceci en raison de la complexité des tables mises à jour / insérées / supprimées. La plupart des utilisations les plus appropriées pour une vue indexée se trouvent dans un entrepôt de données de rapports. Peu importe, ne mettez pas un index sur une vue sans comprendre ce qu'il fera aux tables pour les opérations CRUD (créer, lire, mettre à jour, supprimer). Et dans une database de type CRM ou support d'application, je restrais loin d'eux pour la plupart, sauf s'il y a un besoin statique et que cela n'a pas vraiment d'impact sur les performances.

Lisez cet article: http://technet.microsoft.com/en-us/library/ms187864(v=sql.105).aspx

Notez à peu près les 3/4 de la page, il est question de ne pas en utiliser un et je pense que votre cas correspond à 4/5 des scénarios où vous ne devriez pas l'utiliser.

  1. En ce qui concerne la sauvegarde des jointures … gardez à l'esprit les jointures complètes sont l'un des pires contrevenants pour l'efficacité. Il me semble que la seule raison pour laquelle vous l'avez, c'est parce que vous n'incluez pas le fabricant dans vos CTE. Vous pouvez simplement l'inclure dans vos CTE et ensuite agréger le nombre de correspondances par cat / tag dans la requête finale qui le rassemble pour get votre score. De cette façon, vous n'avez que deux jointures externes gauche (une à chaque CTE), puis additionnez les deux comptages set et regroupez par le même fabricant (déclaration de cas), productId etc.

  2. Finalement … Je considérerais placer tout cela dans une table dé-normalisée ou peut-être même un cube où il est précalculé. Considérons plusieurs choses à propos de votre exigence: a. Est-ce que le score de corrélation pour les produits doit être vivant? Si oui, pourquoi? Ceci n'est pas essentiel lorsque de nouveaux produits sont ajoutés / supprimés. Quiconque dit que cela doit être vivant ne le pense probablement pas vraiment. b. Vitesse de récupération. Je pourrais réécrire votre requête en utilisant des tables temporaires, m'assurer que les index sont corrects etc. et arriver à une requête raisonnablement plus rapide dans une procédure stockée. Mais, je suis toujours en train d'agréger datatables de la database pour les afficher partout dans mon magasin à chaque chargement de la page. Si datatables sont précalculées et stockées dans une table séparée de productIds et de scores pour chaque produit et indexées par productId, la récupération serait très rapide. Vous pouvez troncer et recharger la table dans un ETL tous les soirs, toutes les heures et ne vous inquiétez pas de la maintenance des index reconstruits à chaque fois. Bien sûr, si le front de votre magasin est 24/7/365, vous devrez écrire du code de database pour vous préoccuper de la gestion des versions afin que votre application n'ait jamais à attendre si la database est en cours de recalcul.

Aussi, assurez-vous au less de mettre en cache ces informations sur le server Web / application si rien d'autre. Une chose est sûre, si vous allez avec votre solution ci-dessus, alors vous aurez besoin de build quelque chose sur votre site afin qu'il n'attend pas que datatables reviennent et met en cache à la place.

J'espère que tout cela aidera.

Que diriez-vous d'une approche quelque peu différente?

 ;WITH ProductFindings (ProductId, NbrTags, NbrCategories) AS ( SELECT t2.ProductId, COUNT(t2.TagId), 0 FROM ProductTagMappings AS t1 INNER JOIN ProductTagMappings AS t2 ON t1.TagId = t2.TagId AND t1.ProductId != t2.ProductId WHERE t1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' GROUP BY t2.ProductId UNION ALL SELECT c2.ProductId, 0, COUNT(c2.CategoryId) FROM ProductCategoryMappings AS c1 INNER JOIN ProductCategoryMappings AS c2 ON c1.CategoryId = c2.CategoryId AND c1.ProductId != c2.ProductId WHERE c1.ProductId = '22D6059C-D981-4A97-8F7B-A25A0138B3F4' GROUP BY c2.ProductId ), ProductTally (ProductId, TotTags, TotCategories) as ( SELECT ProductID, sum(NbrTags), sum(NbrCategories) FROM ProductFindings GROUP BY ProductID ) SELECT Tot.ProductId AS ProductId ,Tot.TotTags AS TagsInCommon ,Tot.TotCategories AS CategoriesInCommon ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId THEN 1 ELSE 0 END as SameManufacturer ,CASE WHEN SimilarProduct.ManufacturerId = SourceProduct.ManufacturerId THEN 1 ELSE 0 END + Tot.TotTags + Tot.TotCategories as SimilarityScore FROM ProductTally as Tot INNER JOIN Products AS SimilarProduct ON Tot.ProductID = SimilarProduct.Id INNER JOIN Products AS SourceProduct ON SourceProduct.Id = '22D6059C-D981-4A97-8F7B-A25A0138B3F4'