Dejsa Cocan Dejsa Cocan - 6 months ago 39
Ajax Question

SilverStripe - Custom Faceted Search Navigation

I'm working on page for a SilverStripe that will allow users to sort through portfolio pieces based on the selected facets.

Here are the key points/requirements:


  • I have 2 facet categories they can search by: Media Type (i.e. Ads,
    Posters, TV, Web) and Industry (Entertainment, Finance, Healthcare,
    Sport, etc).

  • Users should be allowed to search multiple facets at once and also
    across media type and industry at once.

  • In the SilverStripe admin, since content managers need to be able
    to maintain the facet namesfor Media Type and Industry, I made it so
    that there are 2 admin models where the names can be entered:
    MediaTypeTagAdmin and IndustryTagAdmin. Here are the data object
    classes for MediaTypeTag and IndustryTag that are used by the admin
    models:



MediaTypeTag class

<?php
class MediaTypeTag extends DataObject {

private static $db = array(
'Name' => 'varchar(250)',
);

private static $summary_fields = array(
'Name' => 'Title',
);

private static $field_labels = array(
'Name'
);

private static $belongs_many_many = array(
'PortfolioItemPages' => 'PortfolioItemPage'
);

// tidy up the CMS by not showing these fields
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeByName("PortfolioItemPages");

return $fields;
}

static $default_sort = "Name ASC";
}


IndustryTag class

<?php
class IndustryTag extends DataObject {

private static $db = array(
'Name' => 'varchar(250)',
);

private static $summary_fields = array(
'Name' => 'Title',
);

private static $field_labels = array(
'Name'
);

private static $belongs_many_many = array(
'PortfolioItemPages' => 'PortfolioItemPage'
);

// tidy up the CMS by not showing these fields
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeByName("PortfolioItemPages");

return $fields;
}


static $default_sort = "Name ASC";
}



  • There needs to be a page for each Portfolio Item, so I made a PortfolioItemPage type, which has 2 tabs: one for Media Type and one for Industry Type. This is so content managers can associated whatever tags they want with each Portfolio Item by checking the appropriate boxes:



PortfolioItemPage.php file:



private static $db = array(
'Excerpt' => 'Text',
);

private static $has_one = array(
'Thumbnail' => 'Image',
'Logo' => 'Image'
);

private static $has_many = array(
'PortfolioChildItems' => 'PortfolioChildItem'
);

private static $many_many = array(
'MediaTypeTags' => 'MediaTypeTag',
'IndustryTags' => 'IndustryTag'
);

public function getCMSFields() {
$fields = parent::getCMSFields();

if ($this->ID) {
$fields->addFieldToTab('Root.Media Type Tags', CheckboxSetField::create(
'MediaTypeTags',
'Media Type Tags',
MediaTypeTag::get()->map()
));
}

if ($this->ID) {
$fields->addFieldToTab('Root.Industry Tags', CheckboxSetField::create(
'IndustryTags',
'Industry Tags',
IndustryTag::get()->map()
));
}


$gridFieldConfig = GridFieldConfig_RecordEditor::create();

$gridFieldConfig->addComponent(new GridFieldBulkImageUpload());

$gridFieldConfig->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
'EmbedURL' => 'YouTube or SoundCloud Embed Code',
'Thumb' => 'Thumb (135px x 135px)',
));

$gridfield = new GridField(
"ChildItems",
"Child Items",
$this->PortfolioChildItems(),
$gridFieldConfig
);

$fields->addFieldToTab('Root.Child Items', $gridfield);

$fields->addFieldToTab("Root.Main", new TextareaField("Excerpt"), "Content");
$fields->addFieldToTab("Root.Main", new UploadField('Thumbnail', "Thumbnail (400x x 400px)"), "Content");
$fields->addFieldToTab("Root.Main", new UploadField('Logo', "Logo"), "Content");

return $fields;
}

}
class PortfolioItemPage_Controller extends Page_Controller {

private static $allowed_actions = array (
);

public function init() {
parent::init();
}
}


What I thought might be a good approach would be to use jQuery and AJAX to send the ids of the selected facets to the server:

(function($) {

$(document).ready(function() {
var industry = $('.industry');
var media = $('.media');
var tag = $('.tag');
var selectedTags = "";

tag.each(function(e) {
$(this).bind('click', function(e) {
e.preventDefault();

$(this).addClass('selectedTag');

if(selectedTags.indexOf($(this).text()) < 0){
if($(this).hasClass('media')){
selectedTags += + $(this).attr("id") + "," +"media;";
}
else{
selectedTags += + $(this).attr("id") + "," +"industry;";
}
}
sendTag(selectedTags);

}.bind($(this)));
});

function sendTag(TagList){
$.ajax({
type: "POST",
url: "/home/getPortfolioItemsByTags/",
data: { tags: TagList },
dataType: "json"
}).done(function(response) {
var div = $('.portfolioItems');
div.empty();
for (var i=0; i<response.length; i++){
div.append(response[i].name + "<br />");
//return portfolio data here
}

})
.fail(function() {
alert("There was a problem processing the request.");
});
}
});

}(jQuery));


Then on Page.php, I loop through the ids and get the corresponding PortfolioItemPage information based on the facet ids:

public function getPortfolioItemsByTags(){
//remove the last comma from the list of tag ids

$IDs = $this->getRequest()->postVar('tags');
$IDSplit = substr($IDs, 0, -1);

//put the tag ids and their tag names (media or industry) into an array
$IDListPartial = explode(";",$IDSplit);

//This will hold the associative array of ids to types (i.e. 34 => media)
$IDListFinal = array();
array_walk($IDListPartial, function($val, $key) use(&$IDListFinal){
list($key, $value) = explode(',', $val);
$IDListFinal[$key] = $value;
});

//get Portfolio Items based on the tag ids and tag type
foreach($IDListFinal as $x => $x_value) {
if($x_value=='media'){
$tag = MediaTypeTag::get()->byId($x);
$portfolioItems = $tag->PortfolioItemPages();
}
else{
$tag = IndustryTag::get()->byId($x);
$portfolioItems = $tag->PortfolioItemPages();
}

$return = array();

foreach($portfolioItems as $portfolioItem){
$return[] = array(
'thumbnail' => $portfolioItem->Thumbnail()->Link(),
'name' => $portfolioItem->H1,
'logo' => $portfolioItem->Logo()->Link(),
'excerpt' => $portfolioItem->Excerpt,
'id' => $portfolioItem->ID
);
}
return json_encode($return);
}
}


However, this is where I am getting stuck. While I have found some decent examples of building a PHP/MySQL faceted search outside of a CMS, I am not sure what I can modify in order to make the search work inside a CMS. That, and the examples put the facets in one table in the MySQL database whereas I have 2 (As much as I want to have just one MySQL table for both the Media Type and Industry facets, I am not sure if this is a good idea since content managers want to maintain the facet names themselves).

Are there any tutorials out there that might provide further assistance, or possibly a plugin that I have not found yet? If there is a better way to set this faceted search up, by all means, please suggest ideas. This is pretty new to me.

Answer

The most efficient way to do this is to filter based on the tag/media type IDs in one query (your example is doing one database query per tag/type, then appending the results).

You should be able to do something like this:

<?php

public function getPortfolioItemsByTags(){
    $tagString = $this->getRequest()->postVar('tags');

    // remove the last comma from the list of tag ids
    $tagString = substr($tagString, 0, -1);

    //put the tag ids and their tag names (media or industry) into an array
    $tags = explode(";", $tagString);

    //This will hold the associative array of ids to types (i.e. 34 => media)
    $filters = array(
        'media' => array(),
        'industry' => array()
    );
    array_walk($tags, function($val, $key) use(&$filters) {
        list($id, $type) = explode(',', $val);
        $filters[$type][] = $id;
    });

    $portfolioItems = PortfolioItemPage::get()->filterAny(array(
        'MediaTypeTags.ID' => $filters['media'],
        'IndustryTags.ID' => $filters['industry']
    ));

    $return = array();
    foreach($portfolioItems as $portfolioItem){
        $return[] = array(
            'thumbnail' => $portfolioItem->Thumbnail()->Link(),
            'name' => $portfolioItem->H1,
            'logo' => $portfolioItem->Logo()->Link(),
            'excerpt' => $portfolioItem->Excerpt,
            'id' => $portfolioItem->ID
        );
    }

    return json_encode($return);
}
Comments