Zoekmachine implementeren met SQL binnen repository

Pagina: 1
Acties:

Onderwerpen

Vraag


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
Ik werk momenteel aan een redelijk groot PHP project gebouwd met Symfony 3, Doctrine en Twig en ik heb bij redelijk wat zaken het gevoel dat ik er een spaghettiboel van aan het maken ben door gebrek aan inzicht. Eigenlijk is dit een architecturele vraag die in het algemeen binnen MVC/DDD van toepassing is en niet zozeer Symfony/Doctrine op zich. Maar goed, hier is mijn probleemstelling:

Het belangrijkste en meest centrale deel van de applicatie is een "zoekmachine" die kan zoeken binnen een database van verzamelkaarten. Het gaat hier niet zozeer om tekst, maar om eigenschappen van de te zoeken kaart(en) in kwestie zoals "kaart types", "kaart effecten", "kaart aanvallen",... Vergelijk het met bijvoorbeeld coolblue waar ja een ganse lijst filters met checkboxes hebt. In totaal zijn er zo een 30 verschillende soorten filters, waarvan de meeste eigenlijk gewoon een array van IDs zijn.

Als entity voor kaarten heb ik een "Card" klasse, die gelinkt is met een "CardRepository" waarmee de kaartdata geraadpleegd kan worden, in mijn geval een MySQL database. Omdat de standaard repository methodes (find, findAll, findBy,...) niet volstaan voor een complexe zoekopdracht, heb ik een custom methode "search" in de repository aangemaakt. Deze methode heeft in totaal 5 parameters, zijnde:

Zoekcriteria

Voor de zoekcriteria heb ik een CardSearchCriteria klasse gemaakt met alle mogelijke 30 filtereigenschappen, waar getters en setters voor voorzien zijn.

Sorteeroptie

De sorteeroptie is is een string uit een lijst van 5 mogelijkheden, die als publieke constante (array) binnen de CardRepository gedefinieerd is.

Array van Card velden die prefetched moeten worden

Omdat het resultaat van Cards binnen een overzichtstabel getoond moet worden, moeten sommige velden van Card "eagerly" geprefetched worden samen omdat Doctrine anders voor elke kaart in de tabel hier een nieuwe query voor uitvoert (standaard lazy loading).

Limit en offset

Deze worden gebruikt om het resultaat te pagineren.

Wat de search methode zelf betreft, is deze echt gigantisch. Alle 30 filters worden afgelopen en conditioneel aan een queryBuilder toegevoegd. Bijvoorbeeld met dit stuk code pas ik de gegeven sorteeropties toe, wat eigenlijk maar een klein deel is:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
switch ($sortBy)
{
    case 'expansionsDesc':
        $queryBuilder
            ->addOrderBy('expansion.releasePeriodBegin', 'DESC')
            ->addOrderBy('expansion.id', 'DESC') // Order by ID in case multiple rows have same release date.
            ->addOrderBy('card.numberOrder', 'ASC');

        break;
    case 'expansionsAsc':
        $queryBuilder
            ->addOrderBy('expansion.releasePeriodBegin', 'ASC')
            ->addOrderBy('expansion.id', 'ASC') // Order by ID in case multiple rows have same release date.
            ->addOrderBy('card.numberOrder', 'ASC');
            
        break;
    case 'cardNameAsc':
        $queryBuilder->addOrderBy('COLLATE(card.name, UTF8_GENERAL_CI)', 'ASC');
        break;
    case 'cardNameDesc':
        $queryBuilder->addOrderBy('COLLATE(card.name, UTF8_GENERAL_CI)', 'DESC');
        break;
    default:
        throw new Exception(sprintf('Invalid sort by option "%s".', $sortBy));
}


Het resultaat van de search methode wordt teruggegeven als een "CardSearchResult" object dat oa een array van Cards bevat en het totaal aantal gevonden kaarten (getal voor paginatie).

Tot slot komt uiteindelijk de oorzaak waarom ik dit topic wou plaatsen. Ik zou namelijk uit de zoekmachine een lijst van actieve filters willen terugkrijgen, maar heb hierbij het gevoel dat het extreem complex en omslachtig begint te worden.

Ik wil dus aan de eindgebruiker een lijst van actieve filters tonen, en hiervoor zou ik binnen de search methode oa de entities moeten fetchen die overeenkomen met de IDs uit de criteria. Bij het genereren van de HTML kan ik dan de nodige getters van de entities aanspreken om de informatie te tonen (bv card type: ultra rare).

Mijn oorspronkelijke idee was om hier een "CardSearchActiveFilters" klasse te voorzien met alle nodige getters/setters, maar mijn frustratie met deze aanpak is dat ik binnen mijn view/html elke getter manueel moet checken. Het zou veel eenvoudiger zijn binnen de search methode een simpele array te bouwen die alle informatie heeft zodat ik deze eenvoudig kan oplijsten binnen een loop in de view. Om elk item binnen de array consistent te houden, ben ik praktisch verplicht logica die in de view zou moeten in mijn databaselaag onder te brengen (wat ik dus niet wil), dus dit lijkt ook niet echt een optie.

Ik vind het geheel gewoonweg enorm omslachtig/vuil aanvoelen, alleen al omdat bij elke kleine verandering aan de filters ik op 10 plaatsen aanpassingen moet maken (het parsen van de querystring naar het criteria object, het implementeren van de querybuilder zelf, het samenstellen van de actieve filters, het tonen binnen de view,...)

Ik werk op mezelf en mijn frustratie is dat ik geen flauw idee heb of ik fouten maak, of mijn manier van werken verbeterd kan worden. Mijn excuses voor deze lange post, maar ik zou graag wat feedback hebben van mensen met meer kennis over mijn manier van werken, want ik heb momenteel het gevoel dat ik er een grote spaghettiboel van aan het maken ben.

Alle reacties


Acties:
  • +4 Henk 'm!

Verwijderd

Klinkt alsof je het verkeerde gereedschap gebruikt. Ik zou eens gaan kijken naar een tool als Elasticsearch voor het verrichten van (complexe) zoekacties. Het zal er op neerkomen dat je alle complexe vragen aan Elasticsearch stelt (de case die je nu schetst, maar ook full-text searches met relevantie, etc) en alle simpele vragen (ophalen data op basis van de ID's die je terugkrijgt uit Elasticsearch) aan MySQL.

Acties:
  • 0 Henk 'm!

  • mathias82
  • Registratie: April 2017
  • Laatst online: 07-09 17:47
En als je het toch op jouw manier wilt doen kan je misschien een en ander vereenvoudigen met relfection.

Je zou bij het opbouwen van je query bijvoorbeeld kunnen itereren over alle properties van de CardSearchCriteria klasse en zo je query dynamisch opbouwen. Als er dan properties in die klasse bijkomen of verwijdenen moet je niet telkens je query aanpassen.

Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
Verwijderd schreef op vrijdag 14 juli 2017 @ 07:44:
Klinkt alsof je het verkeerde gereedschap gebruikt. Ik zou eens gaan kijken naar een tool als Elasticsearch voor het verrichten van (complexe) zoekacties. Het zal er op neerkomen dat je alle complexe vragen aan Elasticsearch stelt (de case die je nu schetst, maar ook full-text searches met relevantie, etc) en alle simpele vragen (ophalen data op basis van de ID's die je terugkrijgt uit Elasticsearch) aan MySQL.
Bedankt voor de suggestie! Heb even snel gekeken wat ElasticSearch doet, maar stel dat gebruikers kaarten aan hun verzameling moeten kunnen toevoegen, hoe kan ik het gebruikers/verzameling gedeelte van mijn MySQL database koppelen aan ElasticSearch? Bvb ik wil tellen hoeveel kaarten een gebruiker heeft van een bepaalde reeks.

Acties:
  • +1 Henk 'm!

  • HollowGamer
  • Registratie: Februari 2009
  • Niet online
gnoe93 schreef op vrijdag 14 juli 2017 @ 08:40:
[...]


Bedankt voor de suggestie! Heb even snel gekeken wat ElasticSearch doet, maar stel dat gebruikers kaarten aan hun verzameling moeten kunnen toevoegen, hoe kan ik het gebruikers/verzameling gedeelte van mijn MySQL database koppelen aan ElasticSearch? Bvb ik wil tellen hoeveel kaarten een gebruiker heeft van een bepaalde reeks.
Gewoon koppelen op basis van de ID's. Je gebruikt Elasticsearch puur voor het weergeven van het juiste resultaat (o.a. d.m.v. filters) en vervolgens sla je de ID's op in je MySQL database.

Het is niet de bedoeling (althans dat zou ik nooit doen) om userdata op te slaan in ES.

Acties:
  • +1 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
HollowGamer schreef op vrijdag 14 juli 2017 @ 08:45:
[...]

Gewoon koppelen op basis van de ID's. Je gebruikt Elasticsearch puur voor het weergeven van het juiste resultaat (o.a. d.m.v. filters) en vervolgens sla je de ID's op in je MySQL database.

Het is niet de bedoeling (althans dat zou ik nooit doen) om userdata op te slaan in ES.
Ook al zitten de kaart IDs gelinkt aan gebruikers in MySQL, stel dat dat ik wil filteren op kaarten die enkel in de collectie van de gebruiker zitten. Hoe ik het nu begrijp, is dat ik eerst alle kaart IDs (mogelijks duizenden) van de gebruiker uit MySQL moet halen, om vervolgens ElasticSearch te queryen met de filtergegevens + de mogelijks gigantische lijst van kaart IDs want in ElasticSearch zelf zit er niets van gebruikersgegevens. Is dit niet wat omslachtig of mis ik iets?

Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
mathias82 schreef op vrijdag 14 juli 2017 @ 08:21:
En als je het toch op jouw manier wilt doen kan je misschien een en ander vereenvoudigen met relfection.

Je zou bij het opbouwen van je query bijvoorbeeld kunnen itereren over alle properties van de CardSearchCriteria klasse en zo je query dynamisch opbouwen. Als er dan properties in die klasse bijkomen of verwijdenen moet je niet telkens je query aanpassen.
Het probleem is dat veel filters toegepast moeten worden op geneste properties binnen het Card object. Ik moet hier telkens manueel een "ketting" van de properties opgeven. Dit valt moeilijk met reflection op te lossen.

Acties:
  • +1 Henk 'm!

  • HollowGamer
  • Registratie: Februari 2009
  • Niet online
gnoe93 schreef op vrijdag 14 juli 2017 @ 09:03:
[...]


Ook al zitten de kaart IDs gelinkt aan gebruikers in MySQL, stel dat dat ik wil filteren op kaarten die enkel in de collectie van de gebruiker zitten. Hoe ik het nu begrijp, is dat ik eerst alle kaart IDs (mogelijks duizenden) van de gebruiker uit MySQL moet halen, om vervolgens ElasticSearch te queryen met de filtergegevens + de mogelijks gigantische lijst van kaart IDs want in ElasticSearch zelf zit er niets van gebruikersgegevens. Is dit niet wat omslachtig of mis ik iets?
Waarschijnlijk begrijp ik je niet zo goed, even aan de hand van hoe ik het bedoel. :)

Je gebruikt Elasticsearch puur als hulpmiddel voor het zoeken en filteren van de kaarten.
Stel dat ik als gebruiker het volgende uit de filters heb gekozen:
- Kleuren: rood
- Vorm: vierkant
- Tag: humor

Dan komen daar vervolgens weer kaarten uit die voldoen aan dat resultaat:
- 10012, 10051 hebben beide, behalve 10051 mist tag `humor`.
Gebruiker kiest 10012, die je al met alle eigenschappen gekoppeld hebt in je MySQL database.
Verder hoef je enkel in een andere tabel op te slaan wat de wensen voor de gebruiker precies zijn (toch een andere kleur, tekst, vorm, etc.).

Je gebruikt Elasticsearch vooral om het snel weergeven van data en hierbij eenvoudig(er) te filteren.
Een Elasticsearch database mag je inrichten zoals jij wilt, je hoeft hierin niet perse relaties te leggen.
Zo krijg je bijvoorbeeld dit als een (JSON-)record uit de tabel 'kaarten':
{ "id": 10012, "tags": ["vakantie","zomer", ".."], "prijs": 10.00 }

Je doet dus geen relaties leggen, gewoon puur alles als een record in een tabel.

Mocht je sterk zijn met JS dan kan je dit nog sneller maken d.m.v. een API, waar je niet alles in één ding gooit, maar puur per filter ID's ophaalt en deze samenvoegt tot één geheel.

[ Voor 15% gewijzigd door HollowGamer op 14-07-2017 09:21 ]


Acties:
  • 0 Henk 'm!

  • DJMaze
  • Registratie: Juni 2002
  • Niet online
Ik ben wel eens 3 dagen wezen stoeien met een gedrocht van een query. Uiteindelijk ging ik van seconden naar microseconden.
Het is niet onmogelijk zolang je de tijd krijgt en een goede DB Architectuur kan opzetten.

Maak je niet druk, dat doet de compressor maar


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
HollowGamer schreef op vrijdag 14 juli 2017 @ 09:15:
[...]

Waarschijnlijk begrijp ik je niet zo goed, even aan de hand van hoe ik het bedoel. :)

Je gebruikt Elasticsearch puur als hulpmiddel voor het zoeken en filteren van de kaarten.
Stel dat ik als gebruiker het volgende uit de filters heb gekozen:
- Kleuren: rood
- Vorm: vierkant
- Tag: humor

Dan komen daar vervolgens weer kaarten uit die voldoen aan dat resultaat:
- 10012, 10051 hebben beide, behalve 10051 mist tag `humor`.
Gebruiker kiest 10012, die je al met alle eigenschappen gekoppeld hebt in je MySQL database.
Verder hoef je enkel in een andere tabel op te slaan wat de wensen voor de gebruiker precies zijn (toch een andere kleur, tekst, vorm, etc.).

Je gebruikt Elasticsearch vooral om het snel weergeven van data en hierbij eenvoudig(er) te filteren.
Ahh, ik ging er vanuit dat het gehele kaartgeheelte in elasticsearch ging en dat ik enkel het gebruikersgedeelte zou overhouden in SQL :)

Maar ik begrijp nog niet echt hoe dit mijn probleem oplost. Ik kan inderdaad ElasticSearch gebruiken om op eigenschappen te filteren om zo de kaart IDs te krijgen, maar ik kan hier geen gebruikers ID bij meegegeven, want deze zit niet in ElasticSearch.

Ik moet het resultaat dat ik terugkrijg van ElasticSearch dus zelf nog eens vergelijken met de MySQL database om te kijken of de gebruiker deze kaarten heeft. Ik denk dan al verder aan paginatie, en stel dat ik 50 resultaten wou, blijven er mogelijks maar 2 van over omdat de gebruiker de overige 48 niet in zijn verzameling heeft.

Acties:
  • 0 Henk 'm!

  • CH4OS
  • Registratie: April 2002
  • Niet online

CH4OS

It's a kind of magic

Als de kaarten uniek zijn en altijd toegewezen zijn aan een gebruiker, kun je toch het user ID bij de betreffende kaart opslaan en dat standaard meenemen in je filter? :? Je hebt immers niet het gehele profiel van de gebruiker nodig, enkel de verwijzing naar de gebruiker. Meer hoeft vanuit DDD dan toch ook niet als referentie? :?

[ Voor 43% gewijzigd door CH4OS op 14-07-2017 09:32 ]


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
DJMaze schreef op vrijdag 14 juli 2017 @ 09:24:
Ik ben wel eens 3 dagen wezen stoeien met een gedrocht van een query. Uiteindelijk ging ik van seconden naar microseconden.
Het is niet onmogelijk zolang je de tijd krijgt en een goede DB Architectuur kan opzetten.
Performance is (voorlopig) nog geen probleem (< 50ms zonder caching op 20000 kaarten); mijn indexes zijn goed geplaatst en caching doet op zich ook veel. Meer dan 30000 zullen er ws nooit in de database geraken.
Het gaat hem in dit geval vooral over het zelf heruitvinden wat ElasticSearch reeds kan op een zeer omslachtige manier met SQL.

Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
CH40S schreef op vrijdag 14 juli 2017 @ 09:31:
Als de kaarten uniek zijn en altijd toegewezen zijn aan een gebruiker, kun je toch het user ID bij de betreffende kaart opslaan en dat standaard meenemen in je filter? :? Je hebt immers niet het gehele profiel van de gebruiker nodig, enkel de verwijzing naar de gebruiker. Meer hoeft vanuit DDD dan toch ook niet als referentie? :?
De kaartdatabase moet je zien als een apart geheel dat losstaat van gebruikers. Gebruikers die aangemeld zijn kunnen aanvinken welke kaarten ze in hun collectie hebben en dit wordt opgeslagen in een user_has_card tabel. Omdat de user_has_card tabel niet in ElasticSearch zit, kan ik hier niet direct naar queryen. Ik denk ook dat het niet verstandig is om dit soort data in een niet-acid database onder te brengen. Dus als ik tegelijk tegen bepaalde criteria wil zoeken en enkel de kaarten van een specifieke gebruiker wil, zit ik met een probleem.

[ Voor 7% gewijzigd door gnoe93 op 14-07-2017 09:43 ]


Acties:
  • 0 Henk 'm!

  • Richh
  • Registratie: Augustus 2009
  • Laatst online: 11-09 11:48
gnoe93 schreef op vrijdag 14 juli 2017 @ 09:39:
Ik denk ook dat het niet verstandig is om dit soort data in een niet-acid database onder te brengen. Dus als ik tegelijk tegen bepaalde criteria wil zoeken en enkel de kaarten van een specifieke gebruiker wil, zit ik met een probleem.
Waarom niet?

Sowieso, desnoods doe je na de uitkomst van je Elasticsearch query, dan nog een filter op wat de gebruiker aan mogelijke kaarten heeft. Lijkt me inefficiënter, maar met de grootte die jij noemt moet het toereikend zijn.

Ik ben zelf wel fan van Solr trouwens, dat doet hetzelfde als Elasticsearch maar dan met (imho) wat duidelijkere documentatie :P wat ik wil zeggen: er zijn talloze systemen die ditzelfde voor je kunnen doen; staar je niet blind op Elasticsearch.

☀️ 4500wp zuid | 🔋MT Venus 5kW | 🚗 Tesla Model 3 SR+ 2020 | ❄️ Daikin 3MXM 4kW


Acties:
  • 0 Henk 'm!

  • Morrar
  • Registratie: Juni 2002
  • Laatst online: 11-09 08:54
Het idee met ElasticSearch is dat je de kaarten database ook in ES stopt en daar *al* het zoekwerk op laat doen. Alle relationele zaken houd je in je RDBMS. De kaart info staat dus zowel in je RDBMS als in ES.

Als de gebruiker door zijn eigen kaarten wil bladeren, gebruik je het RDBMS om de kaarten op ID te koppelen. Als de gebruiker een kaart zoekt, zet je de query door naar ES. Als de gebruiker een gevonden kaart opslaat in zijn collectie, gebeurt dit weer in je RDBMS.

Als een gebruiker zijn eigen collectie wil doorzoeken, doe je die zoekquery eerst in ES. De geretourneerde kaart IDs leg je vervolgens in je RDMS langs de collectie van de gebruiker.

ES is een pure document store he, dus je kunt daarin in principe geen joins maken. Als je het puur ES wilt hebben, dan moet je van elke combinatie in je data model een apart document maken. In dit geval zou je dan dus User + Card als een document opslaan. Aangezien storage goedkoop is en je met een ES cluster ook goedkoop kunt opschalen, kun je de informatie over beiden dubbel opslaan. Of je zou een UserID + CardID document kunnen maken en twee queries op ES kunnen doen. Dus eerst CardID's zoeken op kenmerken en vervolgens UserID + CardID documenten doorzoeken voor de gevonden CardID's en filteren op het UserID.

[ Voor 43% gewijzigd door Morrar op 14-07-2017 09:59 ]


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
Ik neem mijn woorden deels terug, blijkbaar is ElasticSearch op document niveau wel acidic.
Het andere deel van mijn redenering is dat de kaartdata voor gebruikers constant geupdate wordt. Stel dat er 100 gebruikers tegelijk bezig zijn, kan dit snel oplopen tot 200 updates per seconde. Ik weet niet of dit de correcte use case is.
Richh schreef op vrijdag 14 juli 2017 @ 09:49:
Sowieso, desnoods doe je na de uitkomst van je Elasticsearch query, dan nog een filter op wat de gebruiker aan mogelijke kaarten heeft. Lijkt me inefficiënter, maar met de grootte die jij noemt moet het toereikend zijn.
Heb ik daarjuist al voorgesteld ;).
Dit lijkt me wel een probleem als de resultaten gepagineerd moeten worden. Ik vraag bijvoorbeeld de eerste 50 op via ElasticSearch, maar hou er potentieel 14 over omdat de overige 36 niet in de collectie van de gebruiker zitten.
Richh schreef op vrijdag 14 juli 2017 @ 09:49:
Ik ben zelf wel fan van Solr trouwens, dat doet hetzelfde als Elasticsearch maar dan met (imho) wat duidelijkere documentatie :P wat ik wil zeggen: er zijn talloze systemen die ditzelfde voor je kunnen doen; staar je niet blind op Elasticsearch.
Eens ik concrete plannen heb om over te stappen, zal ik wel eens alle beschikbare systemen vergelijken om te zien welke het best aan mijn eisen voldoet ;)
Morrar schreef op vrijdag 14 juli 2017 @ 09:51:
Het idee met ElasticSearch is dat je de kaarten database ook in ES stopt en daar *al* het zoekwerk op laat doen. Alle relationele zaken houd je in je RDBMS. De kaart info staat dus zowel in je RDBMS als in ES.

Als de gebruiker door zijn eigen kaarten wil bladeren, gebruik je het RDBMS om de kaarten op ID te koppelen. Als de gebruiker een kaart zoekt, zet je de query door naar ES. Als de gebruiker een gevonden kaart opslaat in zijn collectie, gebeurt dit weer in je RDBMS.

Als een gebruiker zijn eigen collectie wil doorzoeken, doe je die zoekquery eerst in ES. De geretourneerde kaart IDs leg je vervolgens in je RDMS langs de collectie van de gebruiker.

ES is een pure document store he, dus je kunt daarin in principe geen joins maken. Als je het puur ES wilt hebben, dan moet je van elke combinatie in je data model een apart document maken. In dit geval zou je dan dus User + Card als een document opslaan. Aangezien storage goedkoop is en je met een ES cluster ook goedkoop kunt opschalen, kun je de informatie over beiden dubbel opslaan. Of je zou een UserID + CardID document kunnen maken en twee queries op ES kunnen doen. Dus eerst CardID's zoeken op kenmerken en vervolgens UserID + CardID documenten doorzoeken voor de gevonden CardID's en filteren op het UserID.
Ik weet dat het gewoon een documentstore is, dat is juist de bedoeling; op gedenormaliseerde data zoeken voor betere performance/geen "join"-soep. Het is niet ofwel zoeken, ofwel de kaarten van de gebruiker bekijken; mijn probleem zit bij het combineren van deze 2 omdat ik in mijn vorige post zei dat ik de user_has_card gegevens niet in de documentstore wou steken (hoewel ik hier nog niet zeker van ben).

[ Voor 36% gewijzigd door gnoe93 op 14-07-2017 10:27 ]


Acties:
  • 0 Henk 'm!

  • q-enf0rcer.1
  • Registratie: Maart 2009
  • Laatst online: 11-09 18:49
@gnoe93, je kunt er eventueel ook voor kiezen alleen de noodzakelijke gegevens voor het zoeken naar ES te sturen. Je bent niet verplicht de tabellen volledig te mergen en in ES te schieten.

Acties:
  • 0 Henk 'm!

  • HollowGamer
  • Registratie: Februari 2009
  • Niet online
q-enf0rcer.1 schreef op vrijdag 14 juli 2017 @ 11:31:
@gnoe93, je kunt er eventueel ook voor kiezen alleen de noodzakelijke gegevens voor het zoeken naar ES te sturen. Je bent niet verplicht de tabellen volledig te mergen en in ES te schieten.
Je kunt ook prima syncen, genoeg packages die dat voor je kunnen doen.

Acties:
  • 0 Henk 'm!

  • incaz
  • Registratie: Augustus 2012
  • Laatst online: 15-11-2022
Eerste vraag die bij me opkwam: bedoel je met actieve filters de filters die de gebruiker heeft ingesteld? Zo ja, dan hoef je deze in principe niet af te leiden uit je zoekresultaten, maar kun je die rechtstreeks teruggeven uit wat de gebruiker via instellingen heeft gezet.

(Als het gaat om een subklassering van de filters, waarbij iemand bv wil zoeken op 'alles bijzonderder dan uncommon' en je wilt dan dus aangeven x rare en y ultra-rare, dan is het iets gecompliceerder.)

Om het vanuit het OO-perspectief te benaderen denk ik dat je wel degelijk een deel van je werk in de model-laag moet doen. Dat betekent overigens niet de database-laag: je kunt classes definieren zonder daar een database-opslag aan vast te hangen.

Ik heb het idee dat je dan bij een abstract class FilterType uitkomt, dat de belangrijkste methoden ondersteunt (inclusief bv ->addToQueryBuilder( $queryBuilder), getFilterDescription() en getFilterIdentifier())

Dan maak je je filtercriteria elk als apart object aan die de basis extends, waarbij je je template van de abstract class invult voor dat specifieke criterium. Ik denk dat het hier het eenvoudigst is om niet met aparte views per class te werken zolang je niet veel meer ingewikkelds doet dan een description tonen, maar ook custom templates kunnen op die manier gelokaliseerd worden gebouwd vziw.

Dan heb je nog steeds wel wat werk per criterium, maar het staat allemaal op 1 plek, in die ene specifieke class. (En een IDE moet helpen met het makkelijk implementeren van de template, dan hoef je dus alleen nog maar de juiste velden in te vullen.)

Vervolgens kun je vanaf het moment dat je gebruiker aan het zoeken begint gewoon een object maken met een lijst van al die geselecteerde filters. Dat doe je dan dus in de controller die de aanvraag van de gebruiker verwerkt, en daarna pass je dat object met filters gewoon steeds door, en laat je iedere volgende stap gewoon de methoden uitvoeren die nodig zijn.

In je datacontroller is dat bv iets als
PHP:
1
2
3
foreach( $searchCriteria as $key => $criterium ){ 
   $criterium->addToQueryBuilder( $this->queryBuilder ); 
}


en in je twig kun je dan met {{criterium.description}} de gewenste tekst weergeven.

Iets dergelijks werkt voor de sortoptions. Het principe is hetzelfde, en in plaats van een string als $sortBy mee te geven, geef je dan een instantie van het juiste sortBy-type mee. Binnen je queryBuilder kun je dan gewoon $sortBy->addToQueryBuilder aanroepen en heb je geen switch meer nodig.

Never explain with stupidity where malice is a better explanation


Acties:
  • 0 Henk 'm!

  • winkbrace
  • Registratie: Augustus 2008
  • Laatst online: 24-08 15:17
Mijns inziens denk je in de goede richting.

Ik zou een class maken die FilterCollection oid heet.

code:
1
2
3
4
5
FilterCollection of FilterBag ofzo als je niet wilt verwarren met Doctrine Collections
----------------------------
addFilter(Filter $filter)
addFilters(...Filter $filters)
getActiveFilters() : array van Filter objecten


Deze class managet alle filters voor je en vervangt de CardSearchCriteria class, zodat je alle logica maar op 1 plek hebt. Verder zijn value objects heel goed in het vereenvoudigen van code en Filter lijkt me een value object met $name, $options en $selected.

code:
1
2
3
4
5
6
7
abstract Filter
-----------
string $name
array $options (all available options)
array $selected (the selected option(s) )

abstract function makeCriteria() : Criteria


Ik zou zelf denk ik voor elk van de 30 filters een aparte class maken die abstract Filter extend, zodat ik niet telkens de $options hoef te specificeren als ik een Filter maak.

In je QueryBuilder methode zou het er dan ongeveer zo uit kunnen zien:

PHP:
1
2
3
foreach ($filters->getActiveFilters() as $filter) {
    $qb->addCriteria($filter->makeCriteria());
}


Bij je sortering zou ik trouwens het veld en de richting splitsen. Dat zorgt nu voor code duplication.

SQL is heel erg geschikt voor dit soort zoekwerk. Het is immers gestructureerde data. Het is erg veel werk om voor de eerste keer ElasticSearch te proberen aan de praat te krijgen en te gebruiken. Maar als je dit project wilt gebruiken om te leren, dan is dat natuurlijk een mooie kans.

Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
incaz schreef op vrijdag 14 juli 2017 @ 13:18:
Eerste vraag die bij me opkwam: bedoel je met actieve filters de filters die de gebruiker heeft ingesteld? Zo ja, dan hoef je deze in principe niet af te leiden uit je zoekresultaten, maar kun je die rechtstreeks teruggeven uit wat de gebruiker via instellingen heeft gezet.
Het gaat hem inderdaad over filters die de gebruiker heeft aangeklikt in bijvoorbeeld een overzicht van checkboxes met labels. Bedoel je dat ik vanuit de view de label meegeef naar de zoekmachine om achteraf te gebruiken in het overzicht van actieve filters? Wat als bepaalde filters geen labels hebben? Data attribuut? En hoe stuur ik dat naar de server? De querystring met zoekfilters moet bij voorkeur zichtbaar blijven, dus posten lijkt me niet mogelijk.
incaz schreef op vrijdag 14 juli 2017 @ 13:18:
(Als het gaat om een subklassering van de filters, waarbij iemand bv wil zoeken op 'alles bijzonderder dan uncommon' en je wilt dan dus aangeven x rare en y ultra-rare, dan is het iets gecompliceerder.)
Het zijn voornamelijk 4 soorten filters:
  • Lijst met checkboxes (elke checkbox word gezien als een OR operatie)
  • Knop bij de checkboxes om ze als AND operatie te interpreteren (meerdere eigenschappen per zelfde kaart)
  • Knop bij de checkboxes om ze als NOT operatie te interpreteren (toon niet de geselecteerde)
  • Op naam zoeken (tekst)
  • Range selecteren
Niet echt "custom" logica in de zin van "alles boven deze" zoals je bedoelt.
incaz schreef op vrijdag 14 juli 2017 @ 13:18:
Om het vanuit het OO-perspectief te benaderen denk ik dat je wel degelijk een deel van je werk in de model-laag moet doen. Dat betekent overigens niet de database-laag: je kunt classes definieren zonder daar een database-opslag aan vast te hangen.

Ik heb het idee dat je dan bij een abstract class FilterType uitkomt, dat de belangrijkste methoden ondersteunt (inclusief bv ->addToQueryBuilder( $queryBuilder), getFilterDescription() en getFilterIdentifier())

Dan maak je je filtercriteria elk als apart object aan die de basis extends, waarbij je je template van de abstract class invult voor dat specifieke criterium. Ik denk dat het hier het eenvoudigst is om niet met aparte views per class te werken zolang je niet veel meer ingewikkelds doet dan een description tonen, maar ook custom templates kunnen op die manier gelokaliseerd worden gebouwd vziw.

Dan heb je nog steeds wel wat werk per criterium, maar het staat allemaal op 1 plek, in die ene specifieke class. (En een IDE moet helpen met het makkelijk implementeren van de template, dan hoef je dus alleen nog maar de juiste velden in te vullen.)

Vervolgens kun je vanaf het moment dat je gebruiker aan het zoeken begint gewoon een object maken met een lijst van al die geselecteerde filters. Dat doe je dan dus in de controller die de aanvraag van de gebruiker verwerkt, en daarna pass je dat object met filters gewoon steeds door, en laat je iedere volgende stap gewoon de methoden uitvoeren die nodig zijn.

In je datacontroller is dat bv iets als
PHP:
1
2
3
foreach( $searchCriteria as $key => $criterium ){ 
   $criterium->addToQueryBuilder( $this->queryBuilder ); 
}


en in je twig kun je dan met {{criterium.description}} de gewenste tekst weergeven.

Iets dergelijks werkt voor de sortoptions. Het principe is hetzelfde, en in plaats van een string als $sortBy mee te geven, geef je dan een instantie van het juiste sortBy-type mee. Binnen je queryBuilder kun je dan gewoon $sortBy->addToQueryBuilder aanroepen en heb je geen switch meer nodig.
Bedoel je met datacontroller de CardRepository? Ik heb hier even op verdergezocht en ik denk dat je hetzelfde als deze manier van werken bedoelt, juist? http://blog.kevingomez.fr...trine-among-other-things/. Moest ik deze manier van werken toepassen, weet ik wel niet hoe ik de volgende situatie moet afhandelen:

Voor de filter "kaarttypes" zijn er 10 checkboxes met opties en 2 switches om AND en NOT aan/uit te zetten.
Om de kaarttypes filter toe te passen op de querybuilder, heb ik alle geselecteerde checkboxes (IDs) en de status van de AND/OF switches nodig. Al deze gegevens moeten dus aanwezig zijn binnen de "CardTypeFilterType" klasse als child class van de abstracte "FilterType" class om vervolgens "addToQueryBuilder" te kunnen oproepen.

Het probleem hierbij is dat ik elke geselecteerde checkbox alsook de AND/NOT opties als aparte actieve filters aan de gebruiker wil tonen. Stel bijvoorbeeld dat 4 van de 10 kaarttype opties zijn aangevinkt inclusief de AND switch, dan wil ik dat de gebruiker 5 knoppen ziet waarop hij individueel kan klikken om de actieve filter uit te zetten. Het probleem is dus dat er geen 1 op 1 relatie is tussen het filterobject en de te tonen actieve filters, wat het moeilijk maakt om eenvoudig door de filters te loopen in de view.

[ Voor 22% gewijzigd door gnoe93 op 15-07-2017 06:27 ]


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
winkbrace schreef op vrijdag 14 juli 2017 @ 13:21:
Mijns inziens denk je in de goede richting.

Ik zou een class maken die FilterCollection oid heet.

code:
1
2
3
4
5
FilterCollection of FilterBag ofzo als je niet wilt verwarren met Doctrine Collections
----------------------------
addFilter(Filter $filter)
addFilters(...Filter $filters)
getActiveFilters() : array van Filter objecten


Deze class managet alle filters voor je en vervangt de CardSearchCriteria class, zodat je alle logica maar op 1 plek hebt. Verder zijn value objects heel goed in het vereenvoudigen van code en Filter lijkt me een value object met $name, $options en $selected.

code:
1
2
3
4
5
6
7
abstract Filter
-----------
string $name
array $options (all available options)
array $selected (the selected option(s) )

abstract function makeCriteria() : Criteria


Ik zou zelf denk ik voor elk van de 30 filters een aparte class maken die abstract Filter extend, zodat ik niet telkens de $options hoef te specificeren als ik een Filter maak.

In je QueryBuilder methode zou het er dan ongeveer zo uit kunnen zien:

PHP:
1
2
3
foreach ($filters->getActiveFilters() as $filter) {
    $qb->addCriteria($filter->makeCriteria());
}


Bij je sortering zou ik trouwens het veld en de richting splitsen. Dat zorgt nu voor code duplication.

SQL is heel erg geschikt voor dit soort zoekwerk. Het is immers gestructureerde data. Het is erg veel werk om voor de eerste keer ElasticSearch te proberen aan de praat te krijgen en te gebruiken. Maar als je dit project wilt gebruiken om te leren, dan is dat natuurlijk een mooie kans.
Als ik me niet vergis lijkt dit dezelfde manier van werken te zijn die incaz voorstelt, juist? Wat ElasticSearch betreft is er momenteel niet echt een nood aan, maar het is altijd leuk eens alternatieve manieren van werken te leren. Als het evengoed met SQL kan, is me dat prima. Ik zocht eigenlijk vooral naar een betere manier om de "search engine" te organiseren binnen mijn applicatiearchitectuur.

Acties:
  • 0 Henk 'm!

  • incaz
  • Registratie: Augustus 2012
  • Laatst online: 15-11-2022
Ik heb het idee dat je een aantal dingen tegelijk wilt oplossen die los van elkaar staan: de user interface, namelijk hoe de user aangeeft waarop die wil zoeken (voornamelijk view in MVC), het versturen en interpreteren daarvan naar de server (de afhandeling maar ook een deel van de form-generator zit in de controller, hier zou ik dus een FilterController maken die een FilterCollection opbouwt) en het gebruiken van die FilterCollection om je resultaten op te halen (de CardRepository.)
(Daarna krijg je dan nog de view van de resultaten, maar dat lijkt me verder geen problemen op te leveren.)

Die dingen zou ik dus ook apart houden en apart uitwerken.
gnoe93 schreef op vrijdag 14 juli 2017 @ 22:37:
Het probleem hierbij is dat ik elke geselecteerde checkbox alsook de AND/NOT opties als aparte actieve filters aan de gebruiker wil tonen. Stel bijvoorbeeld dat 4 van de 10 kaarttype opties zijn aangevinkt inclusief de AND switch, dan wil ik dat de gebruiker 5 knoppen ziet waarop hij individueel kan klikken om de actieve filter uit te zetten. Het probleem is dus dat er geen 1 op 1 relatie is tussen het filterobject en de te tonen actieve filters, wat het moeilijk maakt om eenvoudig door de filters te loopen in de view.
Ik denk dat ik het niet helemaal goed begrijp, maar je zou dit kunnen oplossen door per kaarttype niet een checkbox maar een radiobutton (select, numeric input, invisible state, wat je maar leuk vindt) te genereren met 4 states per type ('irrelevant', 'sufficient', 'necessary', 'excluded') en die daarna met css / js te transformeren naar hoe je het er maar uit wilt laten zien.

En ook met 3 states en een master and/or kan het werken. Je kunt daarin in de FilterController de aansluiting maken tussen wat de gebruiker aan de voorkant ziet en wat jij aan de back-end nodig hebt, zonder dat die zaken in vorm exact gelijk moeten zijn.
(Oftewel, probeer niet een weergave te maken van de database-logica, want dat is niet de logica van de gebruiker. Geef de gebruiker de keuzes die de gebruiker nodig heeft, geef de QueryBuilder de zaken die de QueryBuilder nodig heeft, en maak de vertaalslag via je Filter-classes, je FilterCollection en je FilterController.)

Never explain with stupidity where malice is a better explanation


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
incaz schreef op zaterdag 15 juli 2017 @ 16:54:
Ik heb het idee dat je een aantal dingen tegelijk wilt oplossen die los van elkaar staan: de user interface, namelijk hoe de user aangeeft waarop die wil zoeken (voornamelijk view in MVC), het versturen en interpreteren daarvan naar de server (de afhandeling maar ook een deel van de form-generator zit in de controller, hier zou ik dus een FilterController maken die een FilterCollection opbouwt) en het gebruiken van die FilterCollection om je resultaten op te halen (de CardRepository.)
(Daarna krijg je dan nog de view van de resultaten, maar dat lijkt me verder geen problemen op te leveren.)

Die dingen zou ik dus ook apart houden en apart uitwerken.
Dit wordt momenteel ook apart uitgewerkt, maar ik dacht dat je zei in je vorige post gewoonweg een description per filter bij te houden en deze uit de gebruikersinput af te leiden, of heb ik dit verkeerd begrepen?
incaz schreef op zaterdag 15 juli 2017 @ 16:54:
Ik denk dat ik het niet helemaal goed begrijp, maar je zou dit kunnen oplossen door per kaarttype niet een checkbox maar een radiobutton (select, numeric input, invisible state, wat je maar leuk vindt) te genereren met 4 states per type ('irrelevant', 'sufficient', 'necessary', 'excluded') en die daarna met css / js te transformeren naar hoe je het er maar uit wilt laten zien.

En ook met 3 states en een master and/or kan het werken. Je kunt daarin in de FilterController de aansluiting maken tussen wat de gebruiker aan de voorkant ziet en wat jij aan de back-end nodig hebt, zonder dat die zaken in vorm exact gelijk moeten zijn.
(Oftewel, probeer niet een weergave te maken van de database-logica, want dat is niet de logica van de gebruiker. Geef de gebruiker de keuzes die de gebruiker nodig heeft, geef de QueryBuilder de zaken die de QueryBuilder nodig heeft, en maak de vertaalslag via je Filter-classes, je FilterCollection en je FilterController.)
Ik heb de indruk dat we wat naast elkaar aan het praten zijn, ik zal het best wat beter met wat concrete voorbeelden uitleggen. Als voorbeeld gebruik ik de "Card Type" filter: https://image.prntscr.com/image/RyZ03gmVTV2VlxAAjiZ_ug.png. Eén enkele kaart heeft altijd 1 of meer card types. Als de "multiple" (AND) switch uit staat, worden alle kaarten getoond die één (of meer) uit de aangevinkte opties hebben. Indien de "multiple" switch aan staat, worden enkel kaarten getoond die alle aangevinkte opties tegelijk hebben. de Exclude (NOT) switch geeft de optie het resultaat te inverteren.
Eigenlijk is dit hetzelfde systeem dat he hier oa op Tweakers terugvindt: https://image.prntscr.com/image/EfOT6EIMTs2pQhHVItWVxw.png.

In totaal zijn er dus 4 mogelijke queries:
  • OR
  • OR NOT
  • AND
  • AND NOT
Bijvoorbeeld de OR NOT query:

code:
1
2
3
$queryBuilder
    ->andWhere($queryBuilder->expr()->notIn( 'cardType.id', ':cardTypeIds'))
    ->setParameter('cardTypeIds', $ids);


Voor zo ver ik het begrijp, zou ik hiervan 4 afgeleide FilterType klasses kunnen maken (OrNotFilterType, AndFilterType, ...).

Wat de "actieve filters" betreft, wil ik het volgende aan de gebruiker tonen: https://image.prntscr.com/image/nlQ7hseMQmqWh-d8jwXQyQ.png. Op elk van deze knoppen moet de gebruiker kunnen klikken om deze te wissen. Hier heb ik dus een aantal problemen mee/vragen over.

1. Hoe vertaal ik het best de filter classes naar de actieve filters die ik aan de gebruiker wil tonen? Omdat een filter niet per se direct naar een actieve filter vertaald wordt (OrNotFilterType vertaalt naar 3 actieve filters in mijn bovenstaand voorbeeld), lijkt een eenvoudige beschrijving toevoegen aan de filter zoals je voorstelde geen oplossing.

Een mogelijke oplossing lijkt me een abstracte methode "getActiveFilters" toevoegen aan de abstracte FilterType klasse en dan vervolgens ook een "getActiveFilters" methode aan de FilterCollection class die op zich de "getActiveFilters" methodes van elke item oproept en aggregeert. Zo heb ik één enkele array die ik makkelijk kan overlopen binnen de view en blijft de logica binnen de FilterType/Collection classes.

2. Elke actieve filter heeft 2 eigenschappen nodig: een gebruiksvriendelijke naam en de querystring gegevens om hem te kunnen wissen met JavaScript. Binnen de afgeleide klassen van FilterType zijn enkel de IDs die de gebruiker geselecteerd heeft gekend, dus ik denk dat ik geen andere keuze heb dan de database te queryen naar de naamkolom adhv deze IDs voor de gebruiksvriendelijke beschrijving.

De querystring gegevens lijken mij een groter probleem: Ik zal deze dus op een of andere manier moeten toevoegen aan het FilterType object binnen de controller.

Acties:
  • 0 Henk 'm!

  • incaz
  • Registratie: Augustus 2012
  • Laatst online: 15-11-2022
gnoe93 schreef op zaterdag 15 juli 2017 @ 20:58:
[...]

De querystring gegevens lijken mij een groter probleem: Ik zal deze dus op een of andere manier moeten toevoegen aan het FilterType object binnen de controller.
Wat bedoel je met de querystring-gegevens? In principe heb je daar vrijwel niets mee te maken omdat Symfony dat allemaal regelt als je de FormBuilder gebruikt.

En verder denk ik dat als je begint met bouwen, je al doende merkt waar de problemen zitten (en waar niet.)

Overigens, als je inderdaad 8 cardtypes hebt, dan denk ik dat het goed is om cardtype een eigen entity in de db te maken (of die is het waarschijnlijk al) en dan kun je je informatie daaruit halen. Vooral ook omdat dat iets lijkt waar door de tijd heen makkelijk nieuwe cardtypes bijkomen.

Dat setje van 4 mogelijkheden (all, any, exclude all, exclude any) vind ik niet zo'n probleem om te hardcoden (als class-constants, denk ik), maar veranderlijke data hoort over het algemeen in de db.

Never explain with stupidity where malice is a better explanation


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
incaz schreef op zondag 16 juli 2017 @ 13:22:
Wat bedoel je met de querystring-gegevens? In principe heb je daar vrijwel niets mee te maken omdat Symfony dat allemaal regelt als je de FormBuilder gebruikt.
Ik gebruik de form component niet, ik lees manueel de querystring uit in de controller en bouw hier de criteria mee die ik naar de repository stuur. De requests worden via AJAX gedaan vanuit het JavaScript gedeelte.
incaz schreef op zondag 16 juli 2017 @ 13:22:
En verder denk ik dat als je begint met bouwen, je al doende merkt waar de problemen zitten (en waar niet.)

Overigens, als je inderdaad 8 cardtypes hebt, dan denk ik dat het goed is om cardtype een eigen entity in de db te maken (of die is het waarschijnlijk al) en dan kun je je informatie daaruit halen. Vooral ook omdat dat iets lijkt waar door de tijd heen makkelijk nieuwe cardtypes bijkomen.
Het is eigenlijk een bestaande werkende applicatie; ik ben een poging aan het doen om deze te refactoren omdat de card repository te veel gekoppeld is aan de view. Momenteel wordt de querystring als array meegegeven naar de search methode van de card repository, waarin deze array criterium per criterum verwerkt wordt naar de querybuilder. Deze search methode geeft dan vervolgens een een resultaatobject terug dat een lijst van gevonden kaarten, totaal aantal kaarten (getal) en een lijst met actieve filters bevat.

Acties:
  • 0 Henk 'm!

  • incaz
  • Registratie: Augustus 2012
  • Laatst online: 15-11-2022
gnoe93 schreef op zondag 16 juli 2017 @ 13:45:
[...]
Ik gebruik de form component niet, ik lees manueel de querystring uit in de controller en bouw hier de criteria mee die ik naar de repository stuur.
Ben benieuwd waarom? Symphony heeft juist zulke mooie opties om dat allemaal voor je te doen - bovendien kun je dan ook POST gebruiken wat stukken makkelijker is voor meer data.

(En het maken van ajax is ook niet al te gecompliceerd, omdat Symphony ook al mogelijkheden heeft om op je output op basis van je request type te bepalen. Dus het is heel makkelijk om iets te bouwen wat zowel in een volledige pagina als via ajax te laden is.)

Never explain with stupidity where malice is a better explanation


Acties:
  • 0 Henk 'm!

  • gnoe93
  • Registratie: September 2016
  • Laatst online: 08-04 13:00
incaz schreef op zondag 16 juli 2017 @ 13:54:
[...]


Ben benieuwd waarom? Symphony heeft juist zulke mooie opties om dat allemaal voor je te doen - bovendien kun je dan ook POST gebruiken wat stukken makkelijker is voor meer data.

(En het maken van ajax is ook niet al te gecompliceerd, omdat Symphony ook al mogelijkheden heeft om op je output op basis van je request type te bepalen. Dus het is heel makkelijk om iets te bouwen wat zowel in een volledige pagina als via ajax te laden is.)
Ik volg je even niet. Wat kan Symfony voor mij doen met de form component? Ik haal gewoon de data uit $request->query.

Update:

Je bedoelt mijn filters omvormen tot een HTML form en deze met de form component inlezen en telkens doorsturen na een verandering van de gebruiker? Ik zou dit kunnen doen maar dat is wel wat overkill voor wat ik doe. De querystring zelf uitlezen en omvormen tot de criteria voor de repository is heel wat eenvoudiger. Dit maakt trouwens geen verschil voor het probleem met de lijst van actieve filters.

[ Voor 23% gewijzigd door gnoe93 op 17-07-2017 13:18 ]

Pagina: 1