/**
  * Get singleton instance for filter
  * @param string $filterID
  * @return eZFindExtendedAttributeFilterInterface|false
  */
 public static function getInstance($filterID)
 {
     if (!isset(self::$instances[$filterID])) {
         try {
             if (!self::$filtersList) {
                 $ini = eZINI::instance('ezfind.ini');
                 self::$filtersList = $ini->variable('ExtendedAttributeFilters', 'FiltersList');
             }
             if (!isset(self::$filtersList[$filterID])) {
                 throw new Exception($filterID . ' extended attribute filter is not defined');
             }
             $className = self::$filtersList[$filterID];
             if (!class_exists($className)) {
                 throw new Exception('Could not find class ' . $className);
             }
             $instance = new $className();
             if (!$instance instanceof eZFindExtendedAttributeFilterInterface) {
                 throw new Exception($className . ' is not a valid eZFindExtendedAttributeFilterInterface');
             }
             self::$instances[$filterID] = $instance;
         } catch (Exception $e) {
             eZDebug::writeWarning($e->getMessage(), __METHOD__);
             self::$instances[$filterID] = false;
         }
     }
     return self::$instances[$filterID];
 }
 /**
  * Search on the Solr search server
  *
  * @param string search term
  * @param array parameters.
  *      Example:
  * <code>
  * array( 'SearchOffset' => <offset>,
  *        'SearchLimit' => <limit>,
  *        'SearchSubTreeArray' => array( <node ID1>[, <node ID2>]... ),
  *        'SearchContentClassID' => array( <class ID1>[, <class ID2>]... ),
  *        'SearchContentClassAttributeID' => <class attribute ID>,
  *        'Facet' => array( array( 'field' => <class identifier>/<attribute identifier>[/<option>], ... ) ) ),
  *        'Filter' => array( <base_name> => <value>, <base_name2> => <value2> ),
  *        'SortBy' => array( <field> => <asc|desc> [, <field2> => <asc|desc> [,...]] ) |
  *                    array( array( <field> => <asc|desc> )[, array( <field2> => <asc|desc> )[,...]] ),
  *        'BoostFunctions' => array( 'fields' => array(
  *                                               'article/title' => 2,
  *                                               'modified:5'
  *                                                    ),
  *                                   'functions' => array( 'rord(meta_modified_dt)^10' )
  *                                  ),
  *        'ForceElevation' => false,
  *        'EnableElevation' => true
  *        'DistributedSearch" => array ( 'shards', array( 'shard1', 'shard2' , ... )
  *                                        'searchfields', array ( 'myfield1, 'myfield2', ... )
  *                                        'returnfields', array ( 'myfield1, 'myfield2', ... )
  *                                        'rawfilterlist, array ( 'foreignfield:a', '(foreignfield:b AND otherfield:c)', ... )
  *                                      )
  *      );
  * </code>
  * For full facet description, see facets design document.
  * For full description about 'ForceElevation', see elevate support design document ( elevate_support.rst.txt )
  *
  * the rawFilterList in distributed search is appended to the policyfilterlist with an 'OR' for each entry, as the policy list will
  * in general not be applicable to foreign indexes. To be used with care!
  *
  * @param array Search types. Reserved.
  *
  * @return array Solr query results.
  *
  * @see ezfeZPSolrQueryBuilder::buildBoostFunctions()
  */
 public function buildSearch($searchText, $params = array(), $searchTypes = array())
 {
     eZDebugSetting::writeDebug('extension-ezfind-query', $params, 'search params');
     $searchCount = 0;
     $eZFindIni = eZINI::instance('ezfind.ini');
     $solrIni = eZINI::instance('solr.ini');
     $siteIni = eZINI::instance('site.ini');
     $offset = isset($params['SearchOffset']) ? $params['SearchOffset'] : 0;
     $limit = isset($params['SearchLimit']) ? $params['SearchLimit'] : 10;
     $subtrees = isset($params['SearchSubTreeArray']) ? $params['SearchSubTreeArray'] : array();
     $contentClassID = isset($params['SearchContentClassID']) && $params['SearchContentClassID'] != -1 ? $params['SearchContentClassID'] : false;
     $contentClassAttributeID = isset($params['SearchContentClassAttributeID']) && $params['SearchContentClassAttributeID'] != -1 ? $params['SearchContentClassAttributeID'] : false;
     $sectionID = isset($params['SearchSectionID']) && $params['SearchSectionID'] > 0 ? $params['SearchSectionID'] : false;
     $dateFilter = isset($params['SearchDate']) && $params['SearchDate'] > 0 ? $params['SearchDate'] : false;
     $asObjects = isset($params['AsObjects']) ? $params['AsObjects'] : true;
     $spellCheck = isset($params['SpellCheck']) && $params['SpellCheck'] > 0 ? $params['SpellCheck'] : array();
     $queryHandler = isset($params['QueryHandler']) ? $params['QueryHandler'] : $eZFindIni->variable('SearchHandler', 'DefaultSearchHandler');
     // eZFInd 2.3: check ini setting and take it as a default instead of false
     $visibilityDefaultSetting = $siteIni->variable('SiteAccessSettings', 'ShowHiddenNodes');
     $visibilityDefault = $visibilityDefaultSetting === 'true' ? true : false;
     $ignoreVisibility = isset($params['IgnoreVisibility']) ? $params['IgnoreVisibility'] : $visibilityDefault;
     $this->searchPluginInstance->postSearchProcessingData['ignore_visibility'] = $ignoreVisibility;
     $limitation = isset($params['Limitation']) ? $params['Limitation'] : null;
     $boostFunctions = isset($params['BoostFunctions']) ? $params['BoostFunctions'] : null;
     $forceElevation = isset($params['ForceElevation']) ? $params['ForceElevation'] : false;
     $enableElevation = isset($params['EnableElevation']) ? $params['EnableElevation'] : true;
     $distributedSearch = isset($params['DistributedSearch']) ? $params['DistributedSearch'] : false;
     $fieldsToReturn = isset($params['FieldsToReturn']) ? $params['FieldsToReturn'] : array();
     $highlightParams = isset($params['HighLightParams']) ? $params['HighLightParams'] : array();
     $searchResultClusterParams = isset($params['SearchResultClustering']) ? $params['SearchResultClustering'] : array();
     $extendedAttributeFilter = isset($params['ExtendedAttributeFilter']) ? $params['ExtendedAttributeFilter'] : array();
     // distributed search option
     // @since ezfind 2.2
     $extraFieldsToSearch = array();
     $extraFieldsToReturn = array();
     $shardURLs = array();
     $iniShards = $solrIni->variable('SolrBase', 'Shards');
     $shardQuery = NULL;
     $shardFilterQuery = array();
     if (isset($distributedSearch['shards'])) {
         foreach ($distributedSearch['shards'] as $shard) {
             $shardURLs[] = $iniShards[$shard];
         }
         $shardQuery = implode(',', $shardURLs);
     }
     if (isset($distributedSearch['searchfields'])) {
         $extraFieldsToSearch = $distributedSearch['searchfields'];
     }
     if (isset($distributedSearch['returnfields'])) {
         $extraFieldsToReturn = $distributedSearch['returnfields'];
     }
     if (isset($distributedSearch['rawfilterlist'])) {
         $shardFilterQuery = $distributedSearch['rawfilterlist'];
     }
     // check if filter parameter is indeed an array, and set it otherwise
     if (isset($params['Filter']) && !is_array($params['Filter'])) {
         $params['Filter'] = array($params['Filter']);
     }
     $pathFieldName = eZSolr::getMetaFieldName($ignoreVisibility ? 'path' : 'visible_path');
     $filterQuery = array();
     // Add subtree query filter
     if (!empty($subtrees)) {
         $this->searchPluginInstance->postSearchProcessingData['subtree_array'] = $subtrees;
         $subtreeQueryParts = array();
         foreach ($subtrees as $subtreeNodeID) {
             $subtreeQueryParts[] = $pathFieldName . ':' . $subtreeNodeID;
         }
         $filterQuery[] = implode(' OR ', $subtreeQueryParts);
     }
     // Add policy limitation query filter
     $policyLimitationFilterQuery = $this->policyLimitationFilterQuery($limitation, $ignoreVisibility);
     if ($policyLimitationFilterQuery !== false) {
         $filterQuery[] = $policyLimitationFilterQuery;
     }
     // Add time/date query filter
     if ($dateFilter > 0) {
         switch ($dateFilter) {
             // last day
             case 1:
                 $searchTimestamp = strtotime('-1 day');
                 break;
                 // last week
             // last week
             case 2:
                 $searchTimestamp = strtotime('-1 week');
                 break;
                 // last month
             // last month
             case 3:
                 $searchTimestamp = strtotime('-1 month');
                 break;
                 // last three month
             // last three month
             case 4:
                 $searchTimestamp = strtotime('-3 month');
                 break;
                 // last year
             // last year
             case 5:
                 $searchTimestamp = strtotime('-1 year');
                 break;
         }
         $filterQuery[] = eZSolr::getMetaFieldName('published') . ':[' . ezfSolrDocumentFieldBase::preProcessValue($searchTimestamp, 'date') . '/DAY TO *]';
     }
     if ((!eZContentObjectTreeNode::showInvisibleNodes() || !$ignoreVisibility) && $eZFindIni->variable('SearchFilters', 'FilterHiddenFromDB') == 'enabled') {
         $db = eZDB::instance();
         $invisibleNodeIDArray = $db->arrayQuery('SELECT node_id FROM ezcontentobject_tree WHERE ezcontentobject_tree.is_invisible = 1', array('column' => 0));
         $hiddenNodesQueryText = 'meta_main_node_id_si:[* TO *] -meta_main_node_id_si:(';
         foreach ($invisibleNodeIDArray as $element) {
             $hiddenNodesQueryText = $hiddenNodesQueryText . $element['node_id'] . ' ';
         }
         $hiddenNodesQueryText = $hiddenNodesQueryText . ')';
         // only add filter if there are hidden nodes after all
         if ($invisibleNodeIDArray) {
             $filterQuery[] = $hiddenNodesQueryText;
         }
     }
     // Add content class query filter
     $classLimitationFilter = $this->getContentClassFilterQuery($contentClassID);
     if ($classLimitationFilter !== null) {
         $filterQuery[] = $classLimitationFilter;
     }
     // Add section to query filter.
     if ($sectionID) {
         $filterQuery[] = eZSolr::getMetaFieldName('section_id') . ':' . $sectionID;
     }
     $languageFilterQuery = $this->buildLanguageFilterQuery();
     if ($languageFilterQuery) {
         $filterQuery[] = $languageFilterQuery;
     }
     $paramFilterQuery = $this->getParamFilterQuery($params);
     if (!empty($paramFilterQuery)) {
         $filterQuery = array_merge($filterQuery, $paramFilterQuery);
     }
     //add raw filters
     if ($eZFindIni->hasVariable('SearchFilters', 'RawFilterList')) {
         $rawFilters = $eZFindIni->variable('SearchFilters', 'RawFilterList');
         if (is_array($rawFilters)) {
             $filterQuery = array_merge($filterQuery, $rawFilters);
         }
     }
     // Build and get facet query prameters.
     $facetQueryParamList = $this->buildFacetQueryParamList($params);
     // search only text type declared fields
     $fieldTypeExcludeList = $this->fieldTypeExludeList(NULL);
     // Create sort parameters based on the parameters.
     $sortParameter = $this->buildSortParameter($params);
     //the array_unique below is necessary because attribute identifiers are not unique .. and we get as
     //much highlight snippets as there are duplicate attribute identifiers
     //these are also in the list of query fields (dismax, ezpublish) request handlers
     $queryFields = array_unique($this->getClassAttributes($contentClassID, $contentClassAttributeID, $fieldTypeExcludeList));
     //highlighting only in the attributes, otherwise the object name is repeated in the highlight, which is already
     //partly true as it is mostly composed of one or more attributes.
     //maybe we should add meta data to the index to filter them out.
     $highLightFields = $queryFields;
     //@since eZ Find 2.3
     //when dedicated attributes are searched for, don't add meta-fields to the $queryfields list
     if (!$contentClassAttributeID) {
         $queryFields[] = eZSolr::getMetaFieldName('name');
         if (!$eZFindIni->hasVariable('SearchFilters', 'ExcludeOwnerName') || $eZFindIni->variable('SearchFilters', 'ExcludeOwnerName') !== 'enabled') {
             $queryFields[] = eZSolr::getMetaFieldName('owner_name');
         }
     }
     $spellCheckParamList = array();
     // @param $spellCheck expects array (true|false, dictionary identifier, ...)
     if (isset($spellCheck[0]) and $spellCheck[0] or $eZFindIni->variable('SpellCheck', 'SpellCheck') == 'enabled' and (isset($spellCheck[0]) and !$spellCheck[0])) {
         $dictionary = isset($spellCheck[1]) ? $spellCheck[1] : $eZFindIni->variable('SpellCheck', 'DefaultDictionary');
         $spellCheckParamList = array('spellcheck' => 'true', 'spellcheck.q' => $searchText, 'spellcheck.dictionary' => $dictionary, 'spellcheck.collate' => 'true', 'spellcheck.extendedResults' => 'true', 'spellcheck.onlyMorePopular' => 'true', 'spellcheck.count' => 1);
     }
     // Create the Elevate-related parameters here :
     $elevateParamList = eZFindElevateConfiguration::getRuntimeQueryParameters($forceElevation, $enableElevation, $searchText);
     // process query handler: standard, simplestandard, ezpublish, heuristic
     // first determine which implemented handler to use when heuristic is specified
     if (strtolower($queryHandler) === 'heuristic') {
         // @todo: this code will evolve of course
         if (preg_match('/[\\^\\*\\~]|AND|OR/', $searchText) > 0) {
             $queryHandler = 'simplestandard';
         } else {
             $queryHandler = 'ezpublish';
         }
     }
     $handlerParameters = array();
     $queryHandler = strtolower($queryHandler);
     switch ($queryHandler) {
         case 'standard':
             // @todo: this is more complicated
             // build the query against all "text" like fields
             // should take into account all the filter fields and class filters to shorten the query
             // need to build: Solr q
             if (array_key_exists('fields', $boostFunctions)) {
                 $handlerParameters = array('q' => $this->buildMultiFieldQuery($searchText, array_merge($queryFields, $extraFieldsToSearch), $boostFunctions['fields']), 'qt' => 'standard');
             } else {
                 $handlerParameters = array('q' => $this->buildMultiFieldQuery($searchText, array_merge($queryFields, $extraFieldsToSearch)), 'qt' => 'standard');
             }
             break;
         case 'simplestandard':
             // not to do much, searching is against the default aggregated field
             // only highlightfields
             $highLightFields = array('ezf_df_text');
             $handlerParameters = array('q' => $searchText, 'qt' => 'standard', 'hl.usePhraseHighlighter' => 'true', 'hl.highlightMultiTerm' => 'true');
             break;
         case 'ezpublish':
             // the dismax based handler, just keywordss input, most useful for ordinary queries by users
             // need to build: Solr q, qf, dismax specific parameters
         // the dismax based handler, just keywordss input, most useful for ordinary queries by users
         // need to build: Solr q, qf, dismax specific parameters
         default:
             // ezpublish of course, this to not break BC and is the most "general"
             // if another value is specified, it is supposed to be a dismax like handler
             // with possible other tuning variables then the stock provided 'ezpublish' in solrconfi.xml
             // remark it should be lowercase in solrconfig.xml!
             $boostQueryString = $this->boostQuery();
             $rawBoostQueries = $eZFindIni->variable('QueryBoost', 'RawBoostQueries');
             if (is_array($rawBoostQueries) && !empty($rawBoostQueries)) {
                 $boostQueryString .= ' ' . implode(' ', $rawBoostQueries);
             }
             $handlerParameters = array('q' => $searchText, 'bq' => $boostQueryString, 'qf' => implode(' ', array_merge($queryFields, $extraFieldsToSearch)), 'qt' => $queryHandler);
     }
     // Handle boost functions :
     $boostFunctionsParamList = $this->buildBoostFunctions($boostFunctions, $handlerParameters);
     // special handling of filters in the case of distributed search filters
     // incorporate distributed search filters if defined with an OR expression, and AND-ing all others
     // need to do this as multiple fq elements are otherwise AND-ed by the Solr backend
     // when using this to search across a dedicated set of languages, it will still be valid with the ezp permission
     // scheme
     if (!empty($shardFilterQuery)) {
         $fqString = '((' . implode(') AND (', $filterQuery) . ')) OR ((' . implode(') OR (', $shardFilterQuery) . '))';
         // modify the filterQuery array with this single string as the only element
         $filterQuery = array($fqString);
     }
     // Document transformer fields since eZ Find 5.4
     $docTransformerFields = array('[elevated]');
     $fieldsToReturnString = eZSolr::getMetaFieldName('guid') . ' ' . eZSolr::getMetaFieldName('installation_id') . ' ' . eZSolr::getMetaFieldName('main_url_alias') . ' ' . eZSolr::getMetaFieldName('installation_url') . ' ' . eZSolr::getMetaFieldName('id') . ' ' . eZSolr::getMetaFieldName('main_node_id') . ' ' . eZSolr::getMetaFieldName('language_code') . ' ' . eZSolr::getMetaFieldName('name') . ' score ' . eZSolr::getMetaFieldName('published') . ' ' . eZSolr::getMetaFieldName('path_string') . ' ' . eZSolr::getMetaFieldName('main_path_string') . ' ' . eZSolr::getMetaFieldName('is_invisible') . ' ' . implode(' ', $docTransformerFields) . ' ' . implode(' ', $extraFieldsToReturn);
     if (!$asObjects) {
         if (empty($fieldsToReturn)) {
             // @todo: needs to be refined with Solr supporting globbing in fl argument, otherwise requests will be to heavy for large fields as for example binary file content
             $fieldsToReturnString = 'score, *';
         } else {
             $fieldsToReturnString .= ' ' . implode(',', $fieldsToReturn);
         }
     }
     $searchResultClusterParamList = array('clustering' => 'true');
     $searchResultClusterParamList = $this->buildSearchResultClusterQuery($searchResultClusterParams);
     eZDebugSetting::writeDebug('extension-ezfind-query', $searchResultClusterParamList, 'Cluster params');
     $queryParams = array_merge($handlerParameters, array('start' => $offset, 'rows' => $limit, 'sort' => $sortParameter, 'indent' => 'on', 'version' => '2.2', 'fl' => $fieldsToReturnString, 'fq' => $filterQuery, 'hl' => $eZFindIni->variable('HighLighting', 'Enabled'), 'hl.fl' => implode(' ', $highLightFields), 'hl.snippets' => $eZFindIni->variable('HighLighting', 'SnippetsPerField'), 'hl.fragsize' => $eZFindIni->variable('HighLighting', 'FragmentSize'), 'hl.requireFieldMatch' => $eZFindIni->variable('HighLighting', 'RequireFieldMatch'), 'hl.simple.pre' => $eZFindIni->variable('HighLighting', 'SimplePre'), 'hl.simple.post' => $eZFindIni->variable('HighLighting', 'SimplePost'), 'wt' => 'php'), $facetQueryParamList, $spellCheckParamList, $boostFunctionsParamList, $elevateParamList, $searchResultClusterParamList);
     if (isset($extendedAttributeFilter['id']) && isset($extendedAttributeFilter['params'])) {
         //single filter
         $extendedAttributeFilter = array($extendedAttributeFilter);
     }
     foreach ($extendedAttributeFilter as $filterDefinition) {
         if (isset($filterDefinition['id'])) {
             $filter = eZFindExtendedAttributeFilterFactory::getInstance($filterDefinition['id']);
             if ($filter) {
                 $filterParams = isset($filterDefinition['params']) ? $filterDefinition['params'] : array();
                 $queryParams = $filter->filterQueryParams($queryParams, $filterParams);
             }
         }
     }
     return $queryParams;
 }