/**
  * @param string $keywords
  * @param array $filters [optional]
  * @param array $facetSpec [optional]
  * @param int $start [optional]
  * @param int $limit [optional]
  * @param string $sort [optional]
  * @return ArrayData
  */
 function searchFromVars($keywords, array $filters = array(), array $facetSpec = array(), $start = -1, $limit = -1, $sort = '')
 {
     $searchable = ShopSearch::get_searchable_classes();
     $matches = new ArrayList();
     foreach ($searchable as $className) {
         $list = DataObject::get($className);
         // get searchable fields
         $keywordFields = $this->scaffoldSearchFields($className);
         // convert that list into something we can pass to Datalist::filter
         $keywordFilter = array();
         if (!empty($keywords)) {
             foreach ($keywordFields as $searchField) {
                 $name = strpos($searchField, ':') !== FALSE ? $searchField : "{$searchField}:PartialMatch";
                 $keywordFilter[$name] = $keywords;
             }
         }
         if (count($keywordFilter) > 0) {
             $list = $list->filterAny($keywordFilter);
         }
         // add in any other filters
         $list = FacetHelper::inst()->addFiltersToDataList($list, $filters);
         // add any matches to the big list
         $matches->merge($list);
     }
     return new ArrayData(array('Matches' => $matches, 'Facets' => FacetHelper::inst()->buildFacets($matches, $facetSpec, (bool) Config::inst()->get('ShopSearch', 'auto_facet_attributes'))));
 }
 /**
  * @param string $keywords
  * @param array $filters [optional]
  * @param array $facetSpec [optional]
  * @param int $start [optional]
  * @param int $limit [optional]
  * @param string $sort [optional]
  * @return ArrayData
  */
 function searchFromVars($keywords, array $filters = array(), array $facetSpec = array(), $start = -1, $limit = -1, $sort = '')
 {
     $searchable = ShopSearch::get_searchable_classes();
     $matches = new ArrayList();
     foreach ($searchable as $className) {
         $list = DataObject::get($className);
         // get searchable fields
         $keywordFields = $this->getSearchFields($className);
         // build the filter
         $filter = array();
         // Use parametrized query if SilverStripe >= 3.2
         if (SHOP_SEARCH_IS_SS32) {
             foreach ($keywordFields as $indexFields) {
                 $filter[] = array("MATCH ({$indexFields}) AGAINST (?)" => $keywords);
             }
             $list = $list->whereAny($filter);
         } else {
             foreach ($keywordFields as $indexFields) {
                 $filter[] = sprintf("MATCH ({$indexFields}) AGAINST ('%s')", Convert::raw2sql($keywords));
             }
             // join all the filters with an "OR" statement
             $list = $list->where(implode(' OR ', $filter));
         }
         // add in any other filters
         $list = FacetHelper::inst()->addFiltersToDataList($list, $filters);
         // add any matches to the big list
         $matches->merge($list);
     }
     return new ArrayData(array('Matches' => $matches, 'Facets' => FacetHelper::inst()->buildFacets($matches, $facetSpec, (bool) Config::inst()->get('ShopSearch', 'auto_facet_attributes'))));
 }
 /**
  * This is an intermediary to bridge the search form input
  * and the SearchQuery class. It allows us to have other
  * drivers that may not use the FullTextSearch module.
  *
  * @param string $keywords
  * @param array $filters [optional]
  * @param array $facetSpec [optional]
  * @param int $start [optional]
  * @param int $limit [optional]
  * @param string $sort [optional]
  * @return ArrayData
  */
 public function searchFromVars($keywords, array $filters = array(), array $facetSpec = array(), $start = -1, $limit = -1, $sort = 'score desc')
 {
     $query = new SearchQuery();
     $params = array('sort' => $sort);
     // swap out title search
     if ($params['sort'] == 'SiteTree_Title') {
         $params['sort'] = '_titleSort';
     }
     // search by keywords
     $query->search(empty($keywords) ? '*:*' : $keywords);
     // search by filter
     foreach ($filters as $k => $v) {
         if (isset($this->fieldMap[$k])) {
             if (is_string($v) && preg_match('/^RANGE\\~(.+)\\~(.+)$/', $v, $m)) {
                 // Is it a range value?
                 $range = new SearchQuery_Range($m[1], $m[2]);
                 $query->filter($this->fieldMap[$k], $range);
             } else {
                 // Or a normal scalar value
                 $query->filter($this->fieldMap[$k], $v);
             }
         }
     }
     // add facets
     $facetSpec = FacetHelper::inst()->expandFacetSpec($facetSpec);
     $params += $this->buildFacetParams($facetSpec);
     // TODO: add spellcheck
     return $this->search($query, $start, $limit, $params, $facetSpec);
 }
 /**
  * @return ArrayList
  */
 public function Facets()
 {
     $spec = $this->getFacetSpec();
     if (empty($spec)) {
         return new ArrayList();
     }
     // remove any disabled facets
     foreach ($this->getDisabledFacetsArray() as $disabled) {
         if (isset($spec[$disabled])) {
             unset($spec[$disabled]);
         }
     }
     $request = $this->getController()->getRequest();
     $baseLink = $request->getURL(false);
     $filters = $this->getFilters();
     $baseParams = array_merge($request->requestVars(), array());
     unset($baseParams['url']);
     $products = $this->owner->hasMethod('ProductsForFaceting') ? $this->owner->ProductsForFaceting() : $this->FilteredProducts();
     $facets = FacetHelper::inst()->buildFacets($products, $spec, (bool) Config::inst()->get('FacetedCategory', 'auto_facet_attributes'));
     $facets = FacetHelper::inst()->transformHierarchies($facets);
     $facets = FacetHelper::inst()->updateFacetState($facets, $filters);
     $facets = FacetHelper::inst()->insertFacetLinks($facets, $baseParams, $baseLink);
     return $facets;
 }
Ejemplo n.º 5
0
 /**
  * The result will contain at least the following:
  *      Matches - SS_List of results
  *      TotalMatches - total # of results, unlimited
  *      Query - query string
  * Also saves a log record.
  *
  * @param array $vars
  * @param bool $logSearch [optional]
  * @param bool $useFacets [optional]
  * @param int $start [optional]
  * @param int $limit [optional]
  * @return ArrayData
  */
 public function search(array $vars, $logSearch = true, $useFacets = true, $start = -1, $limit = -1)
 {
     $qs_q = $this->config()->get('qs_query');
     $qs_f = $this->config()->get('qs_filters');
     $qs_ps = $this->config()->get('qs_parent_search');
     $qs_t = $this->config()->get('qs_title');
     $qs_sort = $this->config()->get('qs_sort');
     if ($limit < 0) {
         $limit = $this->config()->get('page_size');
     }
     if ($start < 0) {
         $start = !empty($vars['start']) ? (int) $vars['start'] : 0;
     }
     // as far as i can see, fulltextsearch hard codes 'start'
     $facets = $useFacets ? $this->config()->get('facets') : array();
     if (!is_array($facets)) {
         $facets = array();
     }
     if (empty($limit)) {
         $limit = -1;
     }
     // figure out and scrub the sort
     $sortOptions = $this->config()->get('sort_options');
     $sort = !empty($vars[$qs_sort]) ? $vars[$qs_sort] : '';
     if (!isset($sortOptions[$sort])) {
         $sort = current(array_keys($sortOptions));
     }
     // figure out and scrub the filters
     $filters = !empty($vars[$qs_f]) ? FacetHelper::inst()->scrubFilters($vars[$qs_f]) : array();
     // do the search
     $keywords = !empty($vars[$qs_q]) ? $vars[$qs_q] : '';
     if ($keywordRegex = $this->config()->get('keyword_filter_regex')) {
         $keywords = preg_replace($keywordRegex, '', $keywords);
     }
     $results = self::adapter()->searchFromVars($keywords, $filters, $facets, $start, $limit, $sort);
     // massage the results a bit
     if (!empty($keywords) && !$results->hasValue('Query')) {
         $results->Query = $keywords;
     }
     if (!empty($filters) && !$results->hasValue('Filters')) {
         $results->Filters = new ArrayData($filters);
     }
     if (!$results->hasValue('Sort')) {
         $results->Sort = $sort;
     }
     if (!$results->hasValue('TotalMatches')) {
         $results->TotalMatches = $results->Matches->hasMethod('getTotalItems') ? $results->Matches->getTotalItems() : $results->Matches->count();
     }
     // for some types of facets, update the state
     if ($results->hasValue('Facets')) {
         FacetHelper::inst()->transformHierarchies($results->Facets);
         FacetHelper::inst()->updateFacetState($results->Facets, $filters);
     }
     // make a hash of the search so we can know if we've already logged it this session
     $loggedFilters = !empty($filters) ? json_encode($filters) : null;
     $loggedQuery = strtolower($results->Query);
     //		$searchHash    = md5($loggedFilters . $loggedQuery);
     //		$sessSearches  = Session::get('loggedSearches');
     //		if (!is_array($sessSearches)) $sessSearches = array();
     //		Debug::dump($searchHash, $sessSearches);
     // save the log record
     if ($start == 0 && $logSearch && (!empty($keywords) || !empty($filters))) {
         // && !in_array($searchHash, $sessSearches)) {
         $log = SearchLog::create(array('Query' => $loggedQuery, 'Title' => !empty($vars[$qs_t]) ? $vars[$qs_t] : '', 'Link' => Controller::curr()->getRequest()->getURL(true), 'NumResults' => $results->TotalMatches, 'MemberID' => Member::currentUserID(), 'Filters' => $loggedFilters, 'ParentSearchID' => !empty($vars[$qs_ps]) ? $vars[$qs_ps] : 0));
         $log->write();
         $results->SearchLogID = $log->ID;
         $results->SearchBreadcrumbs = $log->getBreadcrumbs();
         //			$sessSearches[] = $searchHash;
         //			Session::set('loggedSearches', $sessSearches);
     }
     return $results;
 }
 /**
  * @param array $data
  * @return mixed
  */
 public function results(array $data)
 {
     // do the search
     $results = ShopSearch::inst()->search($data);
     $request = $this->controller->getRequest();
     $baseLink = $request->getURL(false);
     // if there was only one category filter, remember it for the category dropdown to retain it's value
     if (!ShopSearchForm::config()->disable_category_dropdown) {
         $qs_filters = (string) Config::inst()->get('ShopSearch', 'qs_filters');
         $categoryKey = (string) ShopSearchForm::config()->category_field;
         if (preg_match('/\\[(.+)\\]/', $categoryKey, $matches)) {
             // get right of the f[] around the actual key if present
             $categoryKey = $matches[1];
         }
         if (!empty($data[$qs_filters][$categoryKey])) {
             $categoryID = $data[$qs_filters][$categoryKey];
             if (is_numeric($categoryID)) {
                 // If it's set in the dropdown it will just be a number
                 // If it's set from the checkboxes it will be something like LIST~1,2,3,4
                 // We only want to remember the value in the former case
                 Session::set('LastSearchCatID', $categoryID);
             }
         } else {
             // If they unchecked every value, then clear the dropdown as well
             Session::clear('LastSearchCatID');
         }
     }
     // add links for any facets
     if ($results->Facets && $results->Facets->count()) {
         $qs_ps = (string) Config::inst()->get('ShopSearch', 'qs_parent_search');
         $baseParams = array_merge($data, array($qs_ps => $results->SearchLogID));
         unset($baseParams['url']);
         $results->Facets = FacetHelper::inst()->insertFacetLinks($results->Facets, $baseParams, $baseLink);
     }
     // add a dropdown for sorting
     $qs_sort = (string) Config::inst()->get('ShopSearch', 'qs_sort');
     $options = Config::inst()->get('ShopSearch', 'sort_options');
     $sortParams = array_merge($data, array($qs_sort => 'NEWSORTVALUE'));
     unset($sortParams['url']);
     $results->SortControl = DropdownField::create($qs_sort, ShopSearch::config()->sort_label, $options, $results->Sort)->setAttribute('data-url', $baseLink . '?' . http_build_query($sortParams));
     // a little more output management
     $results->Title = "Search Results";
     $results->Results = $results->Matches;
     // this makes us compatible with the default search template
     // Give a hook for the parent controller to format the results, for example,
     // interpreting filters in a specific way to affect the title or content
     // when no results are returned. Since this is domain-specific we just leave
     // it up to the host app.
     if ($this->controller->hasMethod('onBeforeSearchDisplay')) {
         $this->controller->onBeforeSearchDisplay($results);
     }
     // give a hook for processing ajax requests through a different template (i.e. for returning only fragments)
     $tpl = Config::inst()->get('ShopSearch', 'ajax_results_template');
     if (!empty($tpl) && Director::is_ajax()) {
         return $this->controller->customise($results)->renderWith($tpl);
     }
     // Give a hook for modifying the search responses
     $this->controller->extend('updateSearchResultsResponse', $request, $response, $results, $data);
     return $response ?: $this->controller->customise($results)->renderWith(array('ShopSearch_results', 'Page_results', 'Page'));
 }
 /**
  * For checkbox and range facets, this updates the state (checked and min/max)
  * based on current filter values.
  *
  * @param ArrayList $facets
  * @param array     $filters
  * @return ArrayList
  */
 public function updateFacetState(ArrayList $facets, array $filters)
 {
     foreach ($facets as $facet) {
         if ($facet->Type == ShopSearch::FACET_TYPE_CHECKBOX) {
             if (empty($filters[$facet->Source])) {
                 // If the filter is not being used at all, we count
                 // all values as active.
                 foreach ($facet->Values as $value) {
                     $value->Active = (bool) FacetHelper::config()->default_checkbox_state;
                 }
             } else {
                 $filterVals = $filters[$facet->Source];
                 if (!is_array($filterVals)) {
                     $filterVals = array($filterVals);
                 }
                 $this->updateCheckboxFacetState(!empty($facet->NestedValues) ? $facet->NestedValues : $facet->Values, $filterVals, !empty($facet->FilterOnlyLeaves));
             }
         } elseif ($facet->Type == ShopSearch::FACET_TYPE_RANGE) {
             if (!empty($filters[$facet->Source]) && preg_match('/^RANGE\\~(.+)\\~(.+)$/', $filters[$facet->Source], $m)) {
                 $facet->MinValue = $m[1];
                 $facet->MaxValue = $m[2];
             }
         }
     }
     return $facets;
 }
 public function testStaticAttributes()
 {
     VirtualFieldIndex::build('Product');
     foreach (Product::get() as $p) {
         $p->publish('Stage', 'Live');
     }
     $c = $this->objFromFixture('ProductCategory', 'c3');
     $c->publish('Stage', 'Live');
     // set up some attributes
     $p1 = $this->objFromFixture('Product', 'p1');
     $p2 = $this->objFromFixture('Product', 'p2');
     $pat1 = $this->objFromFixture('ProductAttributeType', 'pat1');
     $pat1v1 = $this->objFromFixture('ProductAttributeValue', 'pat1v1');
     $pat1v2 = $this->objFromFixture('ProductAttributeValue', 'pat1v2');
     $p1->StaticAttributeTypes()->add($pat1);
     $p1->StaticAttributeValues()->add($pat1v1);
     $p1->StaticAttributeValues()->add($pat1v2);
     $p2->StaticAttributeTypes()->add($pat1);
     $p2->StaticAttributeValues()->add($pat1v1);
     // Should be able to filter by an attribute
     $attkey = 'ATT' . $pat1->ID;
     $prods = FacetHelper::inst()->addFiltersToDataList($c->ProductsShowable(), array($attkey => $pat1v1->ID));
     $this->assertEquals(2, $prods->count(), 'Should be 2 products for v1');
     $prods = FacetHelper::inst()->addFiltersToDataList($c->ProductsShowable(), array($attkey => $pat1v2->ID));
     $this->assertEquals(1, $prods->count(), 'Should be 1 product for v2');
     // Should be able to facet by ATT1 explicitly
     $facets = FacetHelper::inst()->buildFacets($c->ProductsShowable(), array($attkey => array('Label' => 'By Color', 'Type' => ShopSearch::FACET_TYPE_LINK)));
     $this->assertEquals(1, $facets->count(), 'Should be 1 facet');
     $f1 = $facets->First();
     $this->assertEquals(2, $f1->Values->count(), 'Should be 2 values');
     $this->assertEquals('Red', $f1->Values->First()->Label);
     $this->assertEquals(2, $f1->Values->First()->Count);
     $this->assertEquals('Green', $f1->Values->Last()->Label);
     $this->assertEquals(1, $f1->Values->Last()->Count);
     // Should be able to facet by auto_facet_attributes
     $facets = FacetHelper::inst()->buildFacets($c->ProductsShowable(), array(), true);
     $this->assertEquals(1, $facets->count(), 'Should be 1 facet');
     $f1 = $facets->First();
     $this->assertEquals(2, $f1->Values->count(), 'Should be 2 values');
     $this->assertEquals('Red', $f1->Values->First()->Label);
     $this->assertEquals(2, $f1->Values->First()->Count);
     $this->assertEquals('Green', $f1->Values->Last()->Label);
     $this->assertEquals(1, $f1->Values->Last()->Count);
 }