optimisation de mon moteur de recherche pourrite

optimisation de mon moteur de recherche pourrite - SQL/NoSQL - Programmation

Marsh Posté le 09-05-2007 à 23:10:44    

Bonswère,
 
J'ai fait un mini moteur de recherche pour mon site (gallerie), et il serait interessant de le booster pour accroître sa pertinence. La merde que j'ai pondue va bien si l'utilisateur stipule un seul mot dans le champ de saisi, mais si il en met plusieurs... ça marche plus ou moins, voyons pourquoi: [:dawa]
 
en gros, si on résume mon moteur, c'est ça:

Code :
  1. $reponse = mysql_query('SELECT * FROM '.$table.' WHERE alt LIKE "%'.$motcle.'%"');


 
dans la table, il y a un champ qui s'appelle "alt" et qui contient des keywords en rapport avec une photo de la gallerie, par exemple:


id   | alt
-----------------------------------------------
1    | renault clio rouge sport vitres vitre electrique éléctrique éléctriques electriques ETC
-----------------------------------------------
2    | bmw m3 sport noir noire tuning spoiler faible kilometrage vitres première main premiere ETC
-----------------------------------------------
3    | ETC  


 
1.donc si l'utiliseur tape UN SEUL MOT dans le input de recherche, par exemple "sport" ça va lui ressortir les 2 entrées. impeccable.
2.maintenant, si il tape "sport vitres" ça lui ressort la première entrée SI ET SEULEMENT il les saisi dans cet ordre. ce qui est infiniment probable. et en plus ça ne va pas lui retrouner l'entrée numéro 2 (la bmw) qui pourtant contient ces 2 mots.
3.pire, si il tape "sport clio" ça ne lui retournera AUCUN résultat! on voit bien pourquoi.
 
 
Voilà, donc l'idéal serait de faire un explode sur la saisie avec le caractère "espace" puis de traiter chacun de ces mots afin qu'il n'y ai pas cette contrainte d'ORDRE consécutif des mots pour que ça retourne un résultat.
 
Je ne sais pas comment faire ça proprement parce que la requete peut vite devenir malpropre et pas optimisée.
est-ce qu'une ébauche telle que la suivante est susceptible de fonctionner, et, surtout, est-ce optimisable?
 

Code :
  1. $mots = explode(" ", $motcle); //$motcle correspond à la saisie du visiteur. ie: "clio premiere main"
  2. $reponse='';
  3. for($i=0; $i<count($mots); $i++){
  4. $reponse .= mysql_query('SELECT * FROM '.$table.' WHERE alt LIKE "%'.$mots["$i"].'%"');
  5. }
  6. $datacar = mysql_fetch_array($reponse);
  7. // la suite est simple


 
 
en vous remerciant.  :jap:


Message édité par pimsa le 10-05-2007 à 00:08:39
Reply

Marsh Posté le 09-05-2007 à 23:10:44   

Reply

Marsh Posté le 10-05-2007 à 00:08:15    

:??:

Reply

Marsh Posté le 10-05-2007 à 00:11:14    

Fait une table séparée avec des champs "id" et "alt", et dans le champ alt tu ne met qu'un seul mot.
Une requète la dessus + un join pour avoir le reste des infos et hop, tu n'aura plus ce genre de problèmes ;)


---------------
Me: Django Localization, Yogo Puzzle, Chrome Grapher, C++ Signals, Brainf*ck.
Reply

Marsh Posté le 10-05-2007 à 00:33:05    

0x90 a écrit :

Fait une table séparée avec des champs "id" et "alt", et dans le champ alt tu ne met qu'un seul mot.
Une requète la dessus + un join pour avoir le reste des infos et hop, tu n'aura plus ce genre de problèmes ;)


oulah ça me semble bien bien bien crados ce que tu me proposes là. :/
en gros ce que tu me conseille c'est une entrée=un mot. non merci.  :pt1cable:  
 
Il doit être possible de faire celà plus proprement.

Reply

Marsh Posté le 10-05-2007 à 00:49:38    

pimsa a écrit :

oulah ça me semble bien bien bien crados ce que tu me proposes là. :/
en gros ce que tu me conseille c'est une entrée=un mot. non merci.  :pt1cable:  
 
Il doit être possible de faire celà plus proprement.


 
Pourquoi est-ce que tu trouves ça crado exactement ?


---------------
Me: Django Localization, Yogo Puzzle, Chrome Grapher, C++ Signals, Brainf*ck.
Reply

Marsh Posté le 10-05-2007 à 01:12:45    

parce que je vais me retrouver avec 20 fois plus d'entrées et une requête encore plus lourde (jointure), alors que je cherche à compacter au maximum. ta méthode marchera mais c'est du bidouillage.
Et puis il est trop tard, j'ai tout bâti sur ce modèle, et y'a pas moins de 1500 entrées actuellement. si j'employais ta méthode, sachant qu'il doit y avoir +-20 mots-clés par articles: 1500x20=30000 entrées  :lol:  
 
merci quand même pour la suggestion.  :jap:

Reply

Marsh Posté le 10-05-2007 à 01:36:44    

T'aura 20x plus d'entrées mais tes entrées seront 20x plus petites.
Tu as une jointure pour avoir les infos autre que l'id mais en contrepartie tu ne fait qu'une seule requète.
 
Pour la conversion, il te faut un script qui lit l'ancienne table, tu explode le alt de chaque entrée, et tu insère chaque mot séparément dans la nouvelle, pas compliqué ...


---------------
Me: Django Localization, Yogo Puzzle, Chrome Grapher, C++ Signals, Brainf*ck.
Reply

Marsh Posté le 10-05-2007 à 01:44:41    

Bien que je pense que la solution de 0x90 reste la meilleur et la plus logique, je donne quand même ce qu'il cherche. Du moins quelque chose qui s'en rapproche :

Code :
  1. $mots = explode(" ", $motcle); //$motcle correspond à la saisie du visiteur. ie: "clio premiere main"
  2. $sql = '';
  3. foreach($mots as $mot)
  4. {
  5.   if(!empty($sql)) $sql .= ' OR alt';
  6.   $sql.= ' LIKE "%'.$mot.'%"';
  7. }
  8. $sql = 'SELECT * FROM '.$table.' WHERE alt '.$sql;
  9. $datacar = mysql_fetch_array($sql);
  10. // la suite est simple : oui!
 

Le OR pouvant être remplacé par un AND selon besoins.
Et attention aux injections de SQL.

 

Edit : Franchement tu devrais prendre la solution de 0x90. Le code que je viens de donner CA c'est de la bidouille. Comme il l'a dit, transférer les données c'est pas compliqué et si tu évite les doublons tu dois pouvoir réduire le nombre d'enregistrement.


Message édité par dwogsi le 10-05-2007 à 01:49:12

---------------
-- Debian -- Le système d'exploitation universel | Le gras c'est la vie! | /(bb|[^b]{2})/
Reply

Marsh Posté le 10-05-2007 à 02:11:11    

merci à vous.  
tu as oublié le mysql_query() avant le mysql_fetch_array().  :p  
 
Vraiment, ça m'ennuierai de reprendre ce point. J'ai developpé un panneau admin qui repose également sur ça, ainsi que ma gallerie, donc il faudrait revoir des sacrés blocs de codes si je me re-fonde sur votre architecture. Et puis ce champ alt doit absolument contenir plusieurs mots en même temps, car je les récupère +-100 fois dans une même page, dans un autre but. avec une jointure je vous garantie que la requête serait énorme.  :sweat:  
Bref, vous auriez surement raison dans le cadre d'un moteur de recherche, mais ce champ 'alt' est repris également dans un autre contexte sur le site, donc dans ce cadre là la méthode que je cherche à employer est impeccable croyez moi.  :)  
 
merci encore.

Reply

Marsh Posté le 10-05-2007 à 02:27:30    

pimsa a écrit :

merci à vous.  
tu as oublié le mysql_query() avant le mysql_fetch_array().  :p


Exacte, pas fait attention  :p  

pimsa a écrit :

Vraiment, ça m'ennuierai de reprendre ce point. J'ai developpé un panneau admin qui repose également sur ça, ainsi que ma gallerie, donc il faudrait revoir des sacrés blocs de codes si je me re-fonde sur votre architecture. Et puis ce champ alt doit absolument contenir plusieurs mots en même temps, car je les récupère +-100 fois dans une même page, dans un autre but. avec une jointure je vous garantie que la requête serait énorme.  :sweat:  
Bref, vous auriez surement raison dans le cadre d'un moteur de recherche, mais ce champ 'alt' est repris également dans un autre contexte sur le site, donc dans ce cadre là la méthode que je cherche à employer est impeccable croyez moi.  :)  
 
merci encore.


Pas sûr ce soit normal ça... Enfin bon, tu fais comme tu veux.


---------------
-- Debian -- Le système d'exploitation universel | Le gras c'est la vie! | /(bb|[^b]{2})/
Reply

Marsh Posté le 10-05-2007 à 02:27:30   

Reply

Marsh Posté le 10-05-2007 à 11:43:54    

parcours tout tes textes, splitte tout les mots d'apres certains characteres (, " ' ! - .) retire les accents, converti en minuscule, apres insere tout ses mots dans une table...
 
attention tu n'insere les mots qu'une fois...
il te faut egalement un table qui fait le lien entre un mot et une page de ton site
apres tu fait plus de like juste un = sur une seule colonne.... Il y a plein d'autre avantages, tu peut rajouter une colonne de points pour favoriser certains mots (ex : si c dans le titre de la page) et tu peut egalement parcourir la table des mots pour "proposer" un mot clé si lutilisateur n'a pas mit le mot clé avec la bonne orthographe...
 
en gros ca donne ca
 

14 auto  
15 clio
16 phare  


 
puis l'autre table
 

14 8
14 7
14 2
15 1


 

8 auto.php
7 vente.php
etc....


Message édité par red faction le 10-05-2007 à 11:46:16
Reply

Marsh Posté le 10-05-2007 à 12:18:36    

Je suis en train de faire un bench sur les deux méthodes avec SQL Server 2005 (jeu de test de 1 000 000 lignes avec en moyenne 12.5 mots clés par ligne)
 
Je mise très largement sur le coup des jointures.
 
(par contre, ça prends du temps à fabriquer un tel jeu de test, y'aura pas de résultat avant cet après-midi :D)
 
PS : MySQL supporte aujourd'hui le PL/SQL (enfin, un dérivé) donc ce que j'arrive à faire avec SQL Server 2005 est parfaitement adaptable. Deplus, MySQL est réputé plus rapide que SQL Server, sans parler du fait que je tourne sur un ordinateur portable, donc pas forcément dans les meilleurs conditions pour un serveur de base de données ;) En gros, si les jointures explosent (comme elles devraient) la grosse merde infâme qu'est la succession de mots clés sur ma config, y'a pas moyen de moyenner que t'arrive pas à un résultat similaire en MySQL.


Message édité par MagicBuzz le 10-05-2007 à 12:22:29
Reply

Marsh Posté le 10-05-2007 à 12:26:21    

ah c'est chouette, je suis le premier interessé. tiens nous au courant MagicBuzz.  :hello:  
j'adore faire ce genre de tests moi aussi, on est tout de suite fixé.
 
 
j'espère que la succession de keywords fait mieux que les jointures parce que si je ne retouche pas mon architecture, je dormirais pas l'esprit tranquille, si je la retouche, c'est du boulot.

Reply

Marsh Posté le 10-05-2007 à 13:51:24    

Bon, j'ai merdé le script de génération du jeu de test. Là il tourne, mais ça va pas vite du tout pour le remplir :D
 
Reste ensuite à créer les index...
 
Et je pourrai enfin donner le résultat.
 
A savoir que de toute façon, même sans faire le test, il est "évident" qu'avec les mots-clés ça sera plus rapide dans le mesure où on ne fait plus de like mais des =, ce qui permet d'entrée de jeu d'utiliser une colonne indexée, et nécessite bien moins de traîtements.

Reply

Marsh Posté le 10-05-2007 à 13:54:22    

quel feinte , vous etiez tous persuadés que google faisait un like sur le contenu des pages? [:tinostar]

Message cité 1 fois
Message édité par red faction le 10-05-2007 à 13:54:30
Reply

Marsh Posté le 10-05-2007 à 14:44:56    

C'est long à initialiser :sleep:
 
En attendant, voici un début...
 
Script de la structure de la base :

Code :
  1. CREATE TABLE alt1 (
  2.     id numeric(18, 0) NOT NULL,
  3.     alt varchar(8000) NULL,
  4. CONSTRAINT PK_alt1 PRIMARY KEY CLUSTERED
  5. (
  6.     id ASC
  7. )
  8. )
  9. GO
  10.  
  11. CREATE TABLE alt2 (
  12.     id numeric(18, 0) NOT NULL,
  13. CONSTRAINT PK_alt2 PRIMARY KEY CLUSTERED
  14. (
  15.     id ASC
  16. )
  17. )
  18.  
  19. CREATE TABLE alt2keys (
  20.     id numeric(18, 0) NOT NULL,
  21.     id2 numeric(18, 0) NOT NULL,
  22.     keyword varchar(50) NOT NULL,
  23. CONSTRAINT PK_alt2keys PRIMARY KEY CLUSTERED
  24. (
  25.     id ASC,
  26.     id2 ASC
  27. )
  28. )
  29. GO
  30. ALTER TABLE alt2keys WITH CHECK ADD  CONSTRAINT FK_alt2keys_alt2 FOREIGN KEY(id2)
  31. REFERENCES alt2 (id)
  32. GO
  33. ALTER TABLE alt2keys CHECK CONSTRAINT FK_alt2keys_alt2
  34. GO
  35.  
  36. -- Cette table est inutile, elle ne sert qu'à créer le jeu de test. Elle contient 25 mots clés
  37. CREATE TABLE keywords(
  38.     id numeric(18, 0) IDENTITY(1,1) NOT NULL,
  39.     keyword varchar(50) NOT NULL,
  40. CONSTRAINT PK_keywords PRIMARY KEY CLUSTERED
  41. (
  42.     id ASC
  43. )
  44. )
  45. GO
  46. -- Ca va faire boum parceque à la base, un index ça peut pas dépasser 900 octets de profondeur
  47. CREATE INDEX ix_keys1 ON alt1 (alt)
  48. go
  49. -- Et vu que ça dit ça
  50. -- Warning! The maximum key length is 900 bytes. The index
  51. -- 'ix_keys1' has maximum length of 8000 bytes. For some
  52. -- combination of large values, the insert/update operation will
  53. -- fail.
  54. -- Du coup, on le dégage...
  55. DROP INDEX ix_keys1 ON alt1
  56. go
  57. CREATE INDEX ix_keys2 ON alt2keys (keyword)
  58. go


 
Nota : On voit ici que je ne me base pas sur un dictionnaire, ceci afin de rester le plus proche possible de ta solution actuelle. Effectivement, entre la réécriture complète de ton truc, et les soucis de gestions d'un dictionnaire, je ne suis pas sûr du gain réel de passer par un dictionnaire. J'utilise donc une table alt2keys bourrée de mots en double, mais tant pis.
 
 
Ma petite fonction SPLIT() bien utile puisque dans SQL Server elle n'existe pas en natif :

Code :
  1. CREATE FUNCTION split(@keywords AS varchar(8000))
  2. returns @tmp TABLE (keyword varchar(50))
  3. AS
  4. begin
  5.    while charindex(' ', @keywords, 1) > 0
  6.    begin
  7.       INSERT INTO @tmp VALUES (rtrim(LEFT(@keywords, charindex(' ', @keywords, 1))));
  8.       SET @keywords = rtrim(RIGHT(@keywords, len(@keywords) - charindex(' ', @keywords, 1)))
  9.    end
  10.    INSERT INTO @tmp VALUES (@keywords);
  11.    RETURN;
  12. end
  13. go;


 
 
Le script de création du jeu de test :

Code :
  1. declare @i AS int;
  2. declare @j AS int;
  3. declare @m AS int;
  4. declare @n AS int;
  5. declare @str AS varchar(8000);
  6. declare @KEY AS varchar(50);
  7.  
  8. SET @i = 0;
  9. SELECT @m = count(*) FROM keywords;
  10.  
  11. while @i < 1000000
  12. begin
  13.  SET @str = '';
  14.  SELECT @n = cast(rand() * @m AS int);
  15.  SET @j = 0;
  16.  INSERT INTO alt2 (id) VALUES (@i);
  17.  while @j < @n
  18.  begin
  19.    SELECT @KEY = keyword FROM keywords WHERE id = cast(rand() * @m AS int);
  20.    SELECT @str = @str + @KEY + ' ';
  21.    INSERT INTO alt2keys (id, id2, keyword) VALUES (@j, @i, @KEY);
  22.    SELECT @j = @j + 1;
  23.  end
  24.  INSERT INTO alt1 (id, alt) VALUES (@i, rtrim(@str));
  25.  SELECT @i = @i + 1;
  26. end


 
Voici les requêtes de test :

Code :
  1. SELECT count(*)
  2. FROM alt1 a INNER JOIN split('clio sport coupé') s ON a.alt LIKE '%' + s.keyword + '%'
  3.  
  4. SELECT count(*)
  5. FROM alt2 a INNER JOIN alt2keys ak ON ak.id2 = a.id INNER JOIN split('clio sport coupé') s ON ak.keyword = s.keyword


 
=> Logiquement, je devrais avoir le même résultat pour les deux (ce serait cool :D) et on va pouvoir comparer le temps d'exécution.


Message édité par MagicBuzz le 10-05-2007 à 16:25:34
Reply

Marsh Posté le 10-05-2007 à 14:46:40    

red faction a écrit :

quel feinte , vous etiez tous persuadés que google faisait un like sur le contenu des pages? [:tinostar]


meuh non :o
 
tout le monde sait qu'à chaque recherche sur google, il ca interroger l'ensemble des pages de tous les sites de la planète pour voir s'il trouve le mot dedans :o

Reply

Marsh Posté le 10-05-2007 à 14:53:27    

-- Bon, le script à la con est en train de retourner...
 
-- Grrr, y'a un bug :o
 
-- Ah non, pas de bug, juste que j'ai pas rechargé la table des keywords après avoir recréé la base :ange:
 
-- Bon ben dans deux heures je pourrai tester :D (en espérant que ça foire pas ce coup-ci :o)
 
-- ben ça monte pas vite... 150 000 lignes :sleep:
 
-- tout pété. chuis en train de vider la base (pigé pkoi ct aussi lent, j'ai une table qui... grossissait de façon exponentielle, alors je vous laisse imaginer ce que ça donnait autour de 300k lignes :D) - et du coup c'est très long à vider :sleep:
 
-- bon, ça re-rempli...


Message édité par MagicBuzz le 10-05-2007 à 16:47:21
Reply

Marsh Posté le 10-05-2007 à 15:43:37    

Bon, ça me gave, j'ai arrêté le truc au bout de 201 092 lignes (trop lent, fait chier :o)
 
Alors :

Code :
  1. SELECT a.id, count(s.keyword) rank
  2. FROM alt1 a INNER JOIN split('clio sport coupé') s ON a.alt LIKE '%' + s.keyword + '%'
  3. GROUP BY a.id
  4. ORDER BY rank DESC, a.id


Déjà, à cause du like, le "rank" ne permettra d'identifier que si 3, 2 ou 1 mot clés parmis les mots clés saisis sont présents. Ainsi, le rang de pertinance n'est pas très bon, mais y'a déjà quelquechose, c'est pas mal :D


13 secondes
137 017 lignes


 
 

Code :
  1. SELECT a.id, count(ak.id) rank
  2. FROM alt2 a INNER JOIN alt2keys ak ON ak.id2 = a.id INNER JOIN split('clio sport coupé') s ON ak.keyword = s.keyword
  3. GROUP BY a.id
  4. ORDER BY rank DESC, a.id



5 secondes
137 017 lignes


Ici, le problème du rank est inverse : il donne le nombre total de mots trouvés, y compris si le même mot est trouvé plusieurs fois (logiquement, dans ton cas c'est pas possible, mais on sait jamais)
 
Note : alt2keys contient 2 413 280 lignes
 
On a donc une requête 2,5 fois plus rapide en se basant sur une table de mots clés plutôt que sur une série de like, malgré l'utilisation d'une jointure supplémentaire.
Ensuite, d'un point de vue programmatique, une chose importante est à noter : j'ai des lignes qui commencent à s'afficher presque immédiatement avec la première requête tandis que la seconde demande bien plus de temps (qu'on ne me demande pas pourquoi :D)
 
J'aimerais confronter les résultats à un fulltext index, mais j'arrive pas à en créer un avec cette saleté de SQL Server 2005 :o
Je trouve plus où on les crée (je les ai activé, mais pas moyen de les créer, il me dis que je ne peux pas en spécifier un sur un champ si j'en ai pas déjà un et rendu actif... oui mais où ? :o)


Message édité par MagicBuzz le 10-05-2007 à 17:07:34
Reply

Marsh Posté le 10-05-2007 à 17:15:57    

Bon, je le crée à la main puisque c'est comme ça :o
 

Code :
  1. CREATE FULLTEXT CATALOG ft AS DEFAULT;
  2. CREATE FULLTEXT INDEX ON alt1 (alt) KEY INDEX pk_alt1;


 
Interrogation de la base avec le catalogue :

Code :
  1. SELECT a.id, f.rank
  2. FROM alt1 a INNER JOIN freetexttable(alt1, alt, 'clio sport coupé') f
  3. ON a.id = f.[KEY]
  4. ORDER BY f.rank, a.id



137 017 lignes
2 secondes


 
En bref, quand un SGBD propose des outils adaptés, autant les utiliser :spamafote:
 
 
PS : MySQL dispose d'un fulltext search tout comme SQL Server.
 
 
 
PS² : Maintenant que tous les index ont bien reconstruits, je viens de retester les 3 requête.
 
Like : 9 secondes
Keywords : 1 seconde
Fulltext : 1 seconde
 
Je n'observe aucune différence entre les deux dernières requêtes d'un point de vue temps d'exécution.


Message édité par MagicBuzz le 10-05-2007 à 17:20:21
Reply

Marsh Posté le 10-05-2007 à 17:41:06    

bon, en attendant, tu peux applause quelques instants quand même...
 
je viens de passer là journée à écrire un putain de bench de merde qui s'écrit normalement en 5 minutes :D (normal, je suis payé à l'heure par le client pour faire autrechose :ange:)
si ça c'est pas une performance... :o

Reply

Marsh Posté le 10-05-2007 à 18:08:10    

whoohoo j'ai la loose.  \o/
 
Bon faudra que je reprenne ça du coup. merci pour le benchmark chef, c'est bon à savoir.

Reply

Marsh Posté le 10-05-2007 à 18:47:48    

Style c'est des photos de bagnole ton truc  [:delarue3].
En fait ton truc de alt là, c'est un peu comme un système de tags (c.f Dotclear 2) non ?

Reply

Marsh Posté le 10-05-2007 à 19:06:59    

mais de quoi jme mêle puceau. [:delarue3]
 
 
 
 

Spoiler :


Nikos [:dawa]

Reply

Marsh Posté le 10-05-2007 à 19:38:02    

J'oubliais un truc...
 
Certes, entre la table de mot-clé et le fulltext search, il ne semble pas y avoir de différence notable d'un point de vue performances...
Mais à côté de ça, il apporte de très grandes améliorations :
- Un RANK très fiable (à la sauce Google on va dire)
- Une recherche "naturelle" dans le sens où, selon les modules complémentaires et les fonctionnalités de la base, il peut se base sur des synonymes, traductions, champ lexical, etc.
- Permet de rechercher différentes formes d'un mot : tu recherches "habiller", il va trouvers aussi "habit", "habillage", etc. mais pas "habitat".
- Permet des recherches très rapides dans des descriptions : adieu les mot-clés, et bonjour les vrais textes.
- Permet des fonctionnalités telles que des expression entières "3 portes", des conditions "confiserie et artisanal ou patisserie" ou simplement d'affecter un "poids" à des mots afin d'influencer directement le rank, ou imposser que deux mots soient plus ou moins proches, etc.
 
Bref, ceci permet des fonctionnalités très diverses et extrêment puissantes (le seul produit que je connaisse qui sâche faire tout ça, pour le moment, c'est Oracle, et moyennant des packages supplémentaires payants, mais bon, c'est tout de même à noter).
 
Mais enfin, surtout, comme tu peux voir dans mon exemple, sans changer du tout la structure de tes tables, et en modifiant à peine ta requête, tu peux exploiter la fonction "freetext" (supportée par MySQL) afin d'obtenir déjà d'excellents résultats.

Reply

Marsh Posté le 10-05-2007 à 19:51:34    

Un article intéressant (car même si une grande partie est spécifique à SQL Server 2005, les notions abordées sont généralisables).
T'as le droit de pas tout comprendre ou de pas tout lire, moi j'ai callé arrivé au tiers :D
http://msdn2.microsoft.com/en-us/library/ms345119.aspx

Reply

Marsh Posté le    

Reply

Sujets relatifs:

Leave a Replay

Make sure you enter the(*)required information where indicate.HTML code is not allowed