/** * @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; }
/** * 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); }