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