/**
  * @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'))));
 }
 /**
  * @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 SS_HTTPRequest $req
  * @return string
  */
 public function search_suggest(SS_HTTPRequest $req)
 {
     /** @var SS_HTTPResponse $response */
     $response = $this->owner->getResponse();
     $callback = $req->requestVar('callback');
     // convert the search results into usable json for search-as-you-type
     if (ShopSearch::config()->search_as_you_type_enabled) {
         $searchVars = $req->requestVars();
         $searchVars[ShopSearch::config()->qs_query] = $searchVars['term'];
         unset($searchVars['term']);
         $results = ShopSearch::inst()->suggestWithResults($searchVars);
     } else {
         $results = array('suggestions' => ShopSearch::inst()->suggest($req->requestVar('term')));
     }
     if ($callback) {
         $response->addHeader('Content-type', 'application/javascript');
         $response->setBody($callback . '(' . json_encode($results) . ');');
     } else {
         $response->addHeader('Content-type', 'application/json');
         $response->setBody(json_encode($results));
     }
     return $response;
 }
 /**
  * @param string $keywords
  * @param array  $filters
  * @return array
  */
 public function suggestWithResults($keywords, array $filters = array())
 {
     $limit = (int) ShopSearch::config()->sayt_limit;
     // process the keywords a bit
     $terms = preg_split('/\\s+/', trim(strtolower($keywords)));
     $lastTerm = count($terms) > 0 ? array_pop($terms) : '';
     $prefix = count($terms) > 0 ? implode(' ', $terms) . ' ' : '';
     //$terms[]    = $lastTerm;
     $terms[] = $lastTerm . '*';
     // this allows for partial words to still match
     // convert that to something solr adapater can handle
     $query = new SearchQuery();
     $query->search('+' . implode(' +', $terms));
     $params = array('sort' => 'score desc', 'facet' => 'true', 'facet.field' => '_autocomplete', 'facet.limit' => ShopSearch::config()->suggest_limit, 'facet.prefix' => $lastTerm);
     //		$facetSpec = array(
     //			'_autocomplete' => array(
     //				'Type'      => ShopSearch::FACET_TYPE_LINK,
     //				'Label'     => 'Suggestions',
     //				'Source'    => '_autocomplete',
     //			),
     //		);
     //
     //		Debug::dump($query);
     //
     //		$search     = $this->search($query, 0, $limit, $params, $facetSpec);
     //		Debug::dump($search);
     //		$prodList   = $search->Matches;
     //
     //		$suggestsion = array();
     ////		if ($)
     $service = $this->getService();
     SearchVariant::with(count($query->classes) == 1 ? $query->classes[0]['class'] : null)->call('alterQuery', $query, $this);
     $q = $terms;
     $fq = array();
     // Build the search itself
     //		foreach ($query->search as $search) {
     //			$text = $search['text'];
     //			preg_match_all('/"[^"]*"|\S+/', $text, $parts);
     //
     //			$fuzzy = $search['fuzzy'] ? '~' : '';
     //
     //			foreach ($parts[0] as $part) {
     //				$fields = (isset($search['fields'])) ? $search['fields'] : array();
     //				if(isset($search['boost'])) $fields = array_merge($fields, array_keys($search['boost']));
     //				if ($fields) {
     //					$searchq = array();
     //					foreach ($fields as $field) {
     //						$boost = (isset($search['boost'][$field])) ? '^' . $search['boost'][$field] : '';
     //						$searchq[] = "{$field}:".$part.$fuzzy.$boost;
     //					}
     //					$q[] = '+('.implode(' OR ', $searchq).')';
     //				}
     //				else {
     //					$q[] = '+'.$part.$fuzzy;
     //				}
     //			}
     //		}
     // Filter by class if requested
     $classq = array();
     foreach ($query->classes as $class) {
         if (!empty($class['includeSubclasses'])) {
             $classq[] = 'ClassHierarchy:' . $class['class'];
         } else {
             $classq[] = 'ClassName:' . $class['class'];
         }
     }
     if ($classq) {
         $fq[] = '+(' . implode(' ', $classq) . ')';
     }
     // Filter by filters
     foreach ($query->require as $field => $values) {
         $requireq = array();
         foreach ($values as $value) {
             if ($value === SearchQuery::$missing) {
                 $requireq[] = "(*:* -{$field}:[* TO *])";
             } elseif ($value === SearchQuery::$present) {
                 $requireq[] = "{$field}:[* TO *]";
             } elseif ($value instanceof SearchQuery_Range) {
                 $start = $value->start;
                 if ($start === null) {
                     $start = '*';
                 }
                 $end = $value->end;
                 if ($end === null) {
                     $end = '*';
                 }
                 $requireq[] = "{$field}:[{$start} TO {$end}]";
             } else {
                 $requireq[] = $field . ':"' . $value . '"';
             }
         }
         $fq[] = '+(' . implode(' ', $requireq) . ')';
     }
     foreach ($query->exclude as $field => $values) {
         $excludeq = array();
         $missing = false;
         foreach ($values as $value) {
             if ($value === SearchQuery::$missing) {
                 $missing = true;
             } elseif ($value === SearchQuery::$present) {
                 $excludeq[] = "{$field}:[* TO *]";
             } elseif ($value instanceof SearchQuery_Range) {
                 $start = $value->start;
                 if ($start === null) {
                     $start = '*';
                 }
                 $end = $value->end;
                 if ($end === null) {
                     $end = '*';
                 }
                 $excludeq[] = "{$field}:[{$start} TO {$end}]";
             } else {
                 $excludeq[] = $field . ':"' . $value . '"';
             }
         }
         $fq[] = ($missing ? "+{$field}:[* TO *] " : '') . '-(' . implode(' ', $excludeq) . ')';
     }
     //		if(!headers_sent()) {
     //			if ($q) header('X-Query: '.implode(' ', $q));
     //			if ($fq) header('X-Filters: "'.implode('", "', $fq).'"');
     //		}
     $params = array_merge($params, array('fq' => implode(' ', $fq)));
     $res = $service->search(implode(' ', $q), 0, $limit, $params, Apache_Solr_Service::METHOD_POST);
     $results = new ArrayList();
     if ($res->getHttpStatus() >= 200 && $res->getHttpStatus() < 300) {
         foreach ($res->response->docs as $doc) {
             $result = DataObject::get_by_id($doc->ClassName, $doc->ID);
             if ($result) {
                 $results->push($result);
             }
         }
         $numFound = $res->response->numFound;
     } else {
         $numFound = 0;
     }
     $ret = array();
     $ret['products'] = new PaginatedList($results);
     $ret['products']->setLimitItems(false);
     $ret['products']->setTotalItems($numFound);
     $ret['products']->setPageStart(0);
     $ret['products']->setPageLength($limit);
     // Facets (this is how we're doing suggestions for now...
     $ret['suggestions'] = array();
     if (isset($res->facet_counts->facet_fields->_autocomplete)) {
         foreach ($res->facet_counts->facet_fields->_autocomplete as $term => $count) {
             $ret['suggestions'][] = $prefix . $term;
         }
     }
     // Suggestions (requires custom setup, assumes spellcheck.collate=true)
     //		if(isset($res->spellcheck->suggestions->collation)) {
     //			$ret['Suggestion'] = $res->spellcheck->suggestions->collation;
     //		}
     return $ret;
 }
 /**
  * Returns an array that can be made into json and passed to the controller
  * containing both term suggestions and a few product matches.
  *
  * @param array $searchVars
  * @return array
  */
 public function suggestWithResults(array $searchVars)
 {
     $qs_q = $this->config()->get('qs_query');
     $qs_f = $this->config()->get('qs_filters');
     $keywords = !empty($searchVars[$qs_q]) ? $searchVars[$qs_q] : '';
     $filters = !empty($searchVars[$qs_f]) ? $searchVars[$qs_f] : array();
     $adapter = self::adapter();
     // get suggestions and product list from the adapter
     if ($adapter->hasMethod('suggestWithResults')) {
         $results = $adapter->suggestWithResults($keywords, $filters);
     } else {
         $limit = (int) ShopSearch::config()->sayt_limit;
         $search = self::adapter()->searchFromVars($keywords, $filters, array(), 0, $limit, 'Popularity DESC');
         //$search     = ShopSearch::inst()->search($searchVars, false, false, 0, $limit);
         $results = array('products' => $search->Matches, 'suggestions' => $this->suggest($keywords));
     }
     // the adapter just gave us a list of products, which we need to process a little further
     if (!empty($results['products'])) {
         // this gets encoded into the product links
         $searchVars['total'] = $results['products']->hasMethod('getTotalItems') ? $results['products']->getTotalItems() : $results['products']->count();
         $products = array();
         foreach ($results['products'] as $prod) {
             if (!$prod || !$prod->exists()) {
                 continue;
             }
             $img = $prod->hasMethod('ProductImage') ? $prod->ProductImage() : $prod->Image();
             $thumb = $img && $img->exists() ? $img->getThumbnail() : null;
             $json = array('link' => $prod->Link() . '?' . ShopSearch::config()->qs_source . '=' . urlencode(base64_encode(json_encode($searchVars))), 'title' => $prod->Title, 'desc' => $prod->obj('Content')->Summary(), 'thumb' => $thumb ? $thumb->Link() : '', 'price' => $prod->obj('Price')->Nice());
             if ($prod->hasExtension('HasPromotionalPricing') && $prod->hasValidPromotion()) {
                 $json['original_price'] = $prod->getOriginalPrice()->Nice();
             }
             $products[] = $json;
         }
         // replace the list of product objects with json
         $results['products'] = $products;
     }
     $this->extend('updateSuggestWithResults', $results, $keywords, $filters);
     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'));
 }
 public function testFilters()
 {
     VirtualFieldIndex::build('Product');
     // one filter
     $r = ShopSearch::inst()->search(array('f' => array('Model' => 'ABC')));
     $this->assertEquals(2, $r->TotalMatches, 'Should contain 2 products');
     $this->assertEquals('ABC', $r->Matches->first()->Model, 'Should actually match');
     // two filters
     $r = ShopSearch::inst()->search(array('f' => array('Model' => 'ABC', 'Price' => 10.5)));
     $this->assertEquals(1, $r->TotalMatches, 'Should contain 1 product');
     $this->assertEquals('ABC', $r->Matches->first()->Model, 'Should actually match');
     $this->assertEquals(10.5, $r->Matches->first()->sellingPrice(), 'Should actually match');
     // filter on category
     $r = ShopSearch::inst()->search(array('f' => array('Category' => $this->idFromFixture('ProductCategory', 'c3'))));
     $this->assertEquals(3, $r->TotalMatches, 'Should contain 3 products');
     // filter on multiple categories
     $r = ShopSearch::inst()->search(array('f' => array('Category' => array($this->idFromFixture('ProductCategory', 'c1'), $this->idFromFixture('ProductCategory', 'c3')))));
     $this->assertEquals(4, $r->TotalMatches, 'Should contain all products');
     // filter on multiple categories with comma separation
     $r = ShopSearch::inst()->search(array('f' => array('Category' => 'LIST~' . implode(',', array($this->idFromFixture('ProductCategory', 'c1'), $this->idFromFixture('ProductCategory', 'c3'))))));
     $this->assertEquals(4, $r->TotalMatches, 'Should contain all products');
     // filter on price range
     $r = ShopSearch::inst()->search(array('f' => array('Price' => 'RANGE~8~12')));
     $this->assertEquals(1, $r->TotalMatches, 'Should contain only 1 product');
     $this->assertEquals($this->idFromFixture('Product', 'p2'), $r->Matches->first()->ID, 'Match should be p2');
     $r = ShopSearch::inst()->search(array('f' => array('Price' => 'RANGE~-3~4')));
     $this->assertEquals(0, $r->TotalMatches, 'Empty matches work on the low end');
     $r = ShopSearch::inst()->search(array('f' => array('Price' => 'RANGE~5555~10000')));
     $this->assertEquals(0, $r->TotalMatches, 'Empty matches work on the high end');
     $r = ShopSearch::inst()->search(array('f' => array('Price' => 'RANGE~12~8')));
     $this->assertEquals(0, $r->TotalMatches, 'A flipped range does not cause error');
 }