diff --git a/lib/AlgoliaSearch/Client.php b/lib/AlgoliaSearch/Client.php index 496d21b6..53f74293 100644 --- a/lib/AlgoliaSearch/Client.php +++ b/lib/AlgoliaSearch/Client.php @@ -37,6 +37,7 @@ class Client const CAINFO = 'cainfo'; const CURLOPT = 'curloptions'; const PLACES_ENABLED = 'placesEnabled'; + const FAILING_HOSTS_CACHE = 'failingHostsCache'; /** * @var ClientContext @@ -94,12 +95,18 @@ public function __construct($applicationID, $apiKey, $hostsArray = null, $option case self::PLACES_ENABLED: $this->placesEnabled = (bool) $value; break; + case self::FAILING_HOSTS_CACHE: + if (! $value instanceof FailingHostsCache) { + throw new \InvalidArgumentException('failingHostsCache must be an instance of \AlgoliaSearch\FailingHostsCache.'); + } + break; default: throw new \Exception('Unknown option: '.$option); } } - $this->context = new ClientContext($applicationID, $apiKey, $hostsArray, $this->placesEnabled); + $failingHostsCache = isset($options[self::FAILING_HOSTS_CACHE]) ? $options[self::FAILING_HOSTS_CACHE] : null; + $this->context = new ClientContext($applicationID, $apiKey, $hostsArray, $this->placesEnabled, $failingHostsCache); } /** @@ -408,13 +415,13 @@ public function initIndex($indexName) } /** - * List all existing user keys with their associated ACLs. + * List all existing API keys with their associated ACLs. * * @return mixed * * @throws AlgoliaException */ - public function listUserKeys() + public function listApiKeys() { return $this->request( $this->context, @@ -429,13 +436,22 @@ public function listUserKeys() } /** - * Get ACL of a user key. + * @return mixed + * @deprecated use listApiKeys instead + */ + public function listUserKeys() + { + return $this->listApiKeys(); + } + + /** + * Get ACL of a API key. * * @param string $key * * @return mixed */ - public function getUserKeyACL($key) + public function getApiKey($key) { return $this->request( $this->context, @@ -450,13 +466,23 @@ public function getUserKeyACL($key) } /** - * Delete an existing user key. + * @param $key + * @return mixed + * @deprecated use getApiKey instead + */ + public function getUserKeyACL($key) + { + return $this->getApiKey($key); + } + + /** + * Delete an existing API key. * * @param string $key * * @return mixed */ - public function deleteUserKey($key) + public function deleteApiKey($key) { return $this->request( $this->context, @@ -471,7 +497,17 @@ public function deleteUserKey($key) } /** - * Create a new user key. + * @param $key + * @return mixed + * @deprecated use deleteApiKey instead + */ + public function deleteUserKey($key) + { + return $this->deleteApiKey($key); + } + + /** + * Create a new API key. * * @param array $obj can be two different parameters: * The list of parameters for this key. Defined by an array that @@ -504,7 +540,7 @@ public function deleteUserKey($key) * * @throws AlgoliaException */ - public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) + public function addApiKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) { if ($obj !== array_values($obj)) { // is dict of value $params = $obj; @@ -537,7 +573,21 @@ public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $ma } /** - * Update a user key. + * @param $obj + * @param int $validity + * @param int $maxQueriesPerIPPerHour + * @param int $maxHitsPerQuery + * @param null $indexes + * @return mixed + * @deprecated use addApiKey instead + */ + public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0, $indexes = null) + { + return $this->addApiKey($obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery, $indexes); + } + + /** + * Update an API key. * * @param string $key * @param array $obj can be two different parameters: @@ -571,7 +621,7 @@ public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $ma * * @throws AlgoliaException */ - public function updateUserKey( + public function updateApiKey( $key, $obj, $validity = 0, @@ -608,6 +658,27 @@ public function updateUserKey( ); } + /** + * @param $key + * @param $obj + * @param int $validity + * @param int $maxQueriesPerIPPerHour + * @param int $maxHitsPerQuery + * @param null $indexes + * @return mixed + * @deprecated use updateApiKey instead + */ + public function updateUserKey( + $key, + $obj, + $validity = 0, + $maxQueriesPerIPPerHour = 0, + $maxHitsPerQuery = 0, + $indexes = null + ) { + return $this->updateApiKey($key, $obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery, $indexes); + } + /** * Send a batch request targeting multiple indices. * @@ -918,7 +989,7 @@ public function doRequest( curl_close($curlHandle); if (intval($http_status / 100) == 4) { - throw new AlgoliaException(isset($answer['message']) ? $answer['message'] : $http_status . ' error'); + throw new AlgoliaException(isset($answer['message']) ? $answer['message'] : $http_status.' error'); } elseif (intval($http_status / 100) != 2) { throw new \Exception($http_status.': '.$response); } diff --git a/lib/AlgoliaSearch/ClientContext.php b/lib/AlgoliaSearch/ClientContext.php index ec86dd13..55adeac6 100644 --- a/lib/AlgoliaSearch/ClientContext.php +++ b/lib/AlgoliaSearch/ClientContext.php @@ -77,21 +77,20 @@ class ClientContext public $connectTimeout; /** - * @var array + * @var FailingHostsCache */ - private static $failingHosts = array(); + private $failingHostsCache; /** - * ClientContext constructor. - * - * @param string $applicationID - * @param string $apiKey - * @param array $hostsArray - * @param bool $placesEnabled + * @param string $applicationID + * @param string $apiKey + * @param array $hostsArray + * @param bool $placesEnabled + * @param FailingHostsCache $failingHostsCache * * @throws Exception */ - public function __construct($applicationID, $apiKey, $hostsArray, $placesEnabled = false) + public function __construct($applicationID, $apiKey, $hostsArray, $placesEnabled = false, FailingHostsCache $failingHostsCache = null) { // connect timeout of 1s by default $this->connectTimeout = 1; @@ -113,8 +112,6 @@ public function __construct($applicationID, $apiKey, $hostsArray, $placesEnabled $this->writeHostsArray = $this->getDefaultWriteHosts(); } - $this->rotateHosts(); - if ($this->applicationID == null || mb_strlen($this->applicationID) == 0) { throw new Exception('AlgoliaSearch requires an applicationID.'); } @@ -129,6 +126,14 @@ public function __construct($applicationID, $apiKey, $hostsArray, $placesEnabled $this->algoliaUserToken = null; $this->rateLimitAPIKey = null; $this->headers = array(); + + if ($failingHostsCache === null) { + $this->failingHostsCache = new InMemoryFailingHostsCache(); + } else { + $this->failingHostsCache = $failingHostsCache; + } + + $this->rotateHosts(); } /** @@ -182,7 +187,7 @@ private function getDefaultWriteHosts() */ public function __destruct() { - if ($this->curlMHandle != null) { + if (is_resource($this->curlMHandle)) { curl_multi_close($this->curlMHandle); } } @@ -194,7 +199,7 @@ public function __destruct() */ public function getMHandle($curlHandle) { - if ($this->curlMHandle == null) { + if (!is_resource($this->curlMHandle)) { $this->curlMHandle = curl_multi_init(); } curl_multi_add_handle($this->curlMHandle, $curlHandle); @@ -258,15 +263,20 @@ public function setExtraHeader($key, $value) } /** - * @param $host + * @param string $host */ - public static function addFailingHost($host) + public function addFailingHost($host) { - if (! in_array($host, self::$failingHosts)) { - self::$failingHosts[] = $host; - } + $this->failingHostsCache->addFailingHost($host); } + /** + * @return FailingHostsCache + */ + public function getFailingHostsCache() + { + return $this->failingHostsCache; + } /** * This method is called to pass on failing hosts. * If the host is first either in the failingHosts array, we @@ -276,14 +286,15 @@ public static function addFailingHost($host) */ public function rotateHosts() { + $failingHosts = $this->failingHostsCache->getFailingHosts(); $i = 0; - while ($i <= count($this->readHostsArray) && in_array($this->readHostsArray[0], self::$failingHosts)) { + while ($i <= count($this->readHostsArray) && in_array($this->readHostsArray[0], $failingHosts)) { $i++; $this->readHostsArray[] = array_shift($this->readHostsArray); } $i = 0; - while ($i <= count($this->writeHostsArray) && in_array($this->writeHostsArray[0], self::$failingHosts)) { + while ($i <= count($this->writeHostsArray) && in_array($this->writeHostsArray[0], $failingHosts)) { $i++; $this->writeHostsArray[] = array_shift($this->writeHostsArray); } diff --git a/lib/AlgoliaSearch/FailingHostsCache.php b/lib/AlgoliaSearch/FailingHostsCache.php new file mode 100644 index 00000000..17ab336f --- /dev/null +++ b/lib/AlgoliaSearch/FailingHostsCache.php @@ -0,0 +1,23 @@ +failingHostsCacheFile = $this->getDefaultCacheFile(); + } else { + $this->failingHostsCacheFile = (string) $file; + } + + $this->assertCacheFileIsValid($this->failingHostsCacheFile); + + if ($ttl === null) { + $ttl = 60 * 5; // 5 minutes + } + + $this->ttl = (int) $ttl; + } + + /** + * @return int + */ + public function getTtl() + { + return $this->ttl; + } + + /** + * @param $file + */ + private function assertCacheFileIsValid($file) + { + $fileDirectory = dirname($file); + + if (! is_writable($fileDirectory)) { + throw new \RuntimeException(sprintf('Cache file directory "%s" is not writable.', $fileDirectory)); + } + + if (! file_exists($file)) { + // The dir being writable, the file will be created when needed. + return; + } + + if (! is_readable($file)) { + throw new \RuntimeException(sprintf('Cache file "%s" is not readable.', $file)); + } + + if (! is_writable($file)) { + throw new \RuntimeException(sprintf('Cache file "%s" is not writable.', $file)); + } + } + + /** + * @return string + */ + private function getDefaultCacheFile() + { + return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'algolia-failing-hosts'; + } + + /** + * @param string $host + */ + public function addFailingHost($host) + { + $cache = $this->loadFailingHostsCacheFromDisk(); + + if (isset($cache[self::TIMESTAMP]) && isset($cache[self::FAILING_HOSTS])) { + // Update failing hosts cache. + // Here we don't take care of invalidating. We do that on retrieval. + if (!in_array($host, $cache[self::FAILING_HOSTS])) { + $cache[self::FAILING_HOSTS][] = $host; + $this->writeFailingHostsCacheFile($cache); + } + } else { + $cache[self::TIMESTAMP] = time(); + $cache[self::FAILING_HOSTS] = array($host); + $this->writeFailingHostsCacheFile($cache); + } + } + + /** + * Get failing hosts from cache. This method should also handle cache invalidation if required. + * The TTL of the failed hosts cache should be 5mins. + * + * @return array + */ + public function getFailingHosts() + { + $cache = $this->loadFailingHostsCacheFromDisk(); + + return isset($cache[self::FAILING_HOSTS]) ? $cache[self::FAILING_HOSTS] : array(); + } + + /** + * Removes the file storing the failing hosts. + */ + public function flushFailingHostsCache() + { + if (file_exists($this->failingHostsCacheFile)) { + unlink($this->failingHostsCacheFile); + } + } + + /** + * @return array + */ + private function loadFailingHostsCacheFromDisk() + { + if (! file_exists($this->failingHostsCacheFile)) { + return array(); + } + + $json = file_get_contents($this->failingHostsCacheFile); + if ($json === false) { + return array(); + } + + $data = json_decode($json, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + return array(); + } + + // Some basic checks. + if ( + !isset($data[self::TIMESTAMP]) + || !isset($data[self::FAILING_HOSTS]) + || !is_int($data[self::TIMESTAMP]) + || !is_array($data[self::FAILING_HOSTS]) + ) { + return array(); + } + + // Validate the hosts array. + foreach ($data[self::FAILING_HOSTS] as $host) { + if (!is_string($host)) { + return array(); + } + } + + $elapsed = time() - $data[self::TIMESTAMP]; // Number of seconds elapsed. + + if ($elapsed > $this->ttl) { + $this->flushFailingHostsCache(); + + return array(); + } + + return $data; + } + + /** + * @param array $data + */ + private function writeFailingHostsCacheFile(array $data) + { + $json = json_encode($data); + if ($json !== false) { + file_put_contents($this->failingHostsCacheFile, $json); + } + } +} diff --git a/lib/AlgoliaSearch/InMemoryFailingHostsCache.php b/lib/AlgoliaSearch/InMemoryFailingHostsCache.php new file mode 100644 index 00000000..a4cea21c --- /dev/null +++ b/lib/AlgoliaSearch/InMemoryFailingHostsCache.php @@ -0,0 +1,76 @@ +ttl = (int) $ttl; + } + + + /** + * @param string $host + */ + public function addFailingHost($host) + { + if (! in_array($host, self::$failingHosts)) { + // Keep a local cache of failed hosts in case the file based strategy doesn't work out. + self::$failingHosts[] = $host; + + if (self::$timestamp === null) { + self::$timestamp = time(); + } + } + } + + /** + * Get failing hosts from cache. This method should also handle cache invalidation if required. + * The TTL of the failed hosts cache should be 5mins. + * + * @return array + */ + public function getFailingHosts() + { + if (self::$timestamp === null) { + return self::$failingHosts; + } + + $elapsed = time() - self::$timestamp; + if ($elapsed > $this->ttl) { + $this->flushFailingHostsCache(); + } + + return self::$failingHosts; + } + + public function flushFailingHostsCache() + { + self::$failingHosts = array(); + self::$timestamp = null; + } +} diff --git a/lib/AlgoliaSearch/Index.php b/lib/AlgoliaSearch/Index.php index 3be615e4..82d76612 100644 --- a/lib/AlgoliaSearch/Index.php +++ b/lib/AlgoliaSearch/Index.php @@ -172,9 +172,8 @@ public function addObjects($objects, $objectIDKey = 'objectID') /** * Get an object from this index. * - * @param $objectID the unique identifier of the object to retrieve - * @param $attributesToRetrieve (optional) if set, contains the list of attributes to retrieve as a string - * separated by "," + * @param string $objectID the unique identifier of the object to retrieve + * @param string[] $attributesToRetrieve (optional) if set, contains the list of attributes to retrieve * * @return mixed */ @@ -194,6 +193,10 @@ public function getObject($objectID, $attributesToRetrieve = null) ); } + if (is_array($attributesToRetrieve)) { + $attributesToRetrieve = implode(',', $attributesToRetrieve); + } + return $this->client->request( $this->context, 'GET', @@ -209,13 +212,14 @@ public function getObject($objectID, $attributesToRetrieve = null) /** * Get several objects from this index. * - * @param array $objectIDs the array of unique identifier of objects to retrieve + * @param array $objectIDs the array of unique identifier of objects to retrieve + * @param string[] $attributesToRetrieve (optional) if set, contains the list of attributes to retrieve * * @return mixed * * @throws \Exception */ - public function getObjects($objectIDs) + public function getObjects($objectIDs, $attributesToRetrieve = null) { if ($objectIDs == null) { throw new \Exception('No list of objectID provided'); @@ -224,6 +228,15 @@ public function getObjects($objectIDs) $requests = array(); foreach ($objectIDs as $object) { $req = array('indexName' => $this->indexName, 'objectID' => $object); + + if ($attributesToRetrieve) { + if (is_array($attributesToRetrieve)) { + $attributesToRetrieve = implode(',', $attributesToRetrieve); + } + + $req['attributesToRetrieve'] = $attributesToRetrieve; + } + array_push($requests, $req); } @@ -412,7 +425,7 @@ public function deleteByQuery($query, $args = array(), $waitLastCall = true) * Search inside the index. * * @param string $query the full text query - * @param mixed $args (optional) if set, contains an associative array with query parameters: + * @param mixed $args (optional) if set, contains an associative array with query parameters: * - page: (integer) Pagination parameter used to select the page to retrieve. * Page is zero-based and defaults to 0. Thus, to retrieve the 10th page you need to set page=9 * - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. @@ -488,8 +501,8 @@ public function deleteByQuery($query, $args = array(), $waitLastCall = true) * duplicate value for the attributeForDistinct attribute are removed from results. For example, * if the chosen attribute is show_name and several hits have the same value for show_name, then * only the best one is kept and others are removed. - * * @return mixed + * @throws AlgoliaException */ public function search($query, $args = null) { @@ -498,6 +511,10 @@ public function search($query, $args = null) } $args['query'] = $query; + if (isset($args['disjunctiveFacets'])) { + return $this->searchWithDisjunctiveFaceting($query, $args); + } + return $this->client->request( $this->context, 'POST', @@ -510,15 +527,148 @@ public function search($query, $args = null) ); } + /** + * @param $query + * @param $args + * @return mixed + * @throws AlgoliaException + */ + private function searchWithDisjunctiveFaceting($query, $args) + { + if (! is_array($args['disjunctiveFacets']) || count($args['disjunctiveFacets']) <= 0) { + throw new \InvalidArgumentException('disjunctiveFacets needs to be an non empty array'); + } + + if (isset($args['filters'])) { + throw new \InvalidArgumentException('You can not use disjunctive faceting and the filters parameter'); + } + + /** + * Prepare queries + */ + // Get the list of disjunctive queries to do: 1 per disjunctive facet + $disjunctiveQueries = $this->getDisjunctiveQueries($args); + + // Format disjunctive queries for multipleQueries call + foreach ($disjunctiveQueries as &$disjunctiveQuery) { + $disjunctiveQuery['indexName'] = $this->indexName; + $disjunctiveQuery['query'] = $query; + unset($disjunctiveQuery['disjunctiveFacets']); + } + + // Merge facets and disjunctiveFacets for the hits query + $facets = isset($args['facets']) ? $args['facets'] : array(); + $facets = array_merge($facets, $args['disjunctiveFacets']); + unset($args['disjunctiveFacets']); + + // format the hits query for multipleQueries call + $args['query'] = $query; + $args['indexName'] = $this->indexName; + $args['facets'] = $facets; + + // Put the hit query first + array_unshift($disjunctiveQueries, $args); + + /** + * Do all queries in one call + */ + $results = $this->client->multipleQueries(array_values($disjunctiveQueries)); + $results = $results['results']; + + /** + * Merge facets from disjunctive queries with facets from the hits query + */ + + // The first query is the hits query that the one we'll return to the user + $queryResults = array_shift($results); + + // To be able to add facets from disjunctive query we create 'facets' key in case we only have disjunctive facets + if (false === isset($queryResults['facets'])) { + $queryResults['facets'] = array(); + } + + foreach ($results as $disjunctiveResults) { + if (isset($disjunctiveResults['facets'])) { + foreach ($disjunctiveResults['facets'] as $facetName => $facetValues) { + $queryResults['facets'][$facetName] = $facetValues; + } + } + } + + return $queryResults; + } + + /** + * @param $queryParams + * @return array + */ + private function getDisjunctiveQueries($queryParams) + { + $queriesParams = array(); + + foreach ($queryParams['disjunctiveFacets'] as $facetName) { + $params = $queryParams; + $params['facets'] = array($facetName); + $facetFilters = isset($params['facetFilters']) ? $params['facetFilters']: array(); + $numericFilters = isset($params['numericFilters']) ? $params['numericFilters']: array(); + + $additionalParams = array( + 'hitsPerPage' => 1, + 'page' => 0, + 'attributesToRetrieve' => array(), + 'attributesToHighlight' => array(), + 'attributesToSnippet' => array() + ); + + $additionalParams['facetFilters'] = $this->getAlgoliaFiltersArrayWithoutCurrentRefinement($facetFilters, $facetName . ':'); + $additionalParams['numericFilters'] = $this->getAlgoliaFiltersArrayWithoutCurrentRefinement($numericFilters, $facetName); + + $queriesParams[$facetName] = array_merge($params, $additionalParams); + } + + return $queriesParams; + } + + /** + * @param $filters + * @param $needle + * @return array + */ + private function getAlgoliaFiltersArrayWithoutCurrentRefinement($filters, $needle) + { + // iterate on each filters which can be string or array and filter out every refinement matching the needle + for ($i = 0; $i < count($filters); $i++) { + if (is_array($filters[$i])) { + foreach ($filters[$i] as $filter) { + if (mb_substr($filter, 0, mb_strlen($needle)) === $needle) { + unset($filters[$i]); + $filters = array_values($filters); + $i--; + break; + } + } + } else { + if (mb_substr($filters[$i], 0, mb_strlen($needle)) === $needle) { + unset($filters[$i]); + $filters = array_values($filters); + $i--; + } + } + } + + return $filters; + } + /** * Perform a search inside facets. * * @param $facetName * @param $facetQuery * @param array $query + * * @return mixed */ - public function searchFacet($facetName, $facetQuery, $query = array()) + public function searchForFacetValues($facetName, $facetQuery, $query = array()) { $query['facetQuery'] = $facetQuery; @@ -547,6 +697,7 @@ public function searchFacet($facetName, $facetQuery, $query = array()) * * @throws AlgoliaException * @throws \Exception + * @deprecated you should use $index->search($query, ['disjunctiveFacets' => $disjunctive_facets]]); instead */ public function searchDisjunctiveFaceting($query, $disjunctive_facets, $params = array(), $refinements = array()) { @@ -752,69 +903,70 @@ public function clearIndex() /** * Set settings for this index. * - * @param mixed $settings the settings object that can contains : - * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = - * 3). - * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default - * = 7). - * - hitsPerPage: (integer) the number of hits per page (default = 10). - * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. - * If set to null, all attributes are retrieved. - * - attributesToHighlight: (array of strings) default list of attributes to highlight. - * If set to null, all indexed attributes are highlighted. - * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the - * number of words to return (syntax is attributeName:nbWords). By default no snippet is computed. - * If set to null, no snippet is computed. - * - searchableAttributes (formerly named attributesToIndex): (array of strings) the list of fields you want to index. - * If set to null, all textual and numerical attributes of your objects are indexed, but you - * should update it to get optimal results. This parameter has two important uses: - * - Limit the attributes to index: For example if you store a binary image in base64, you want to - * store it and be able to retrieve it but you don't want to search in the base64 string. - * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in - * attributes at the beginning of the list will be considered more important than matches in - * attributes further down the list. In one attribute, matching text at the beginning of the - * attribute will be considered more important than text after, you can disable this behavior if - * you add your attribute inside `unordered(AttributeName)`, for example searchableAttributes: - * ["title", "unordered(text)"]. - * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. - * All strings in the attribute selected for faceting are extracted and added as a facet. If set - * to null, no attribute is used for faceting. - * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature - * is similar to the SQL "distinct" keyword: when enabled in query with the distinct=1 parameter, - * all hits containing a duplicate value for this attribute are removed from results. For example, - * if the chosen attribute is show_name and several hits have the same value for show_name, then - * only the best one is kept and others are removed. - * - ranking: (array of strings) controls the way results are sorted. - * We have six available criteria: - * - typo: sort according to number of typos, - * - geo: sort according to decreassing distance when performing a geo-location based search, - * - proximity: sort according to the proximity of query words in hits, - * - attribute: sort according to the order of attributes defined by searchableAttributes, - * - exact: - * - if the user query contains one word: sort objects having an attribute that is exactly the - * query word before others. For example if you search for the "V" TV show, you want to find it - * with the "V" query and avoid to have all popular TV show starting by the v letter before it. - * - if the user query contains multiple words: sort according to the number of words that matched - * exactly (and not as a prefix). - * - custom: sort according to a user defined formula set in **customRanking** attribute. - * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] - * - customRanking: (array of strings) lets you specify part of the ranking. - * The syntax of this condition is an array of strings containing attributes prefixed by asc - * (ascending order) or desc (descending order) operator. For example `"customRanking" => - * ["desc(population)", "asc(name)"]` - * - queryType: Select how the query words are interpreted, it can be one of the following value: - * - prefixAll: all query words are interpreted as prefixes, - * - prefixLast: only the last word is interpreted as a prefix (default behavior), - * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. - * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in - * the query result (default to ""). - * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in - * the query result (default to ""). - * - optionalWords: (array of strings) Specify a list of words that should be considered as - * optional when found in the query. - * - * @param bool $forwardToReplicas + * @param mixed $settings the settings object that can contains : + * - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = + * 3). + * - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default + * = 7). + * - hitsPerPage: (integer) the number of hits per page (default = 10). + * - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. + * If set to null, all attributes are retrieved. + * - attributesToHighlight: (array of strings) default list of attributes to highlight. + * If set to null, all indexed attributes are highlighted. + * - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the + * number of words to return (syntax is attributeName:nbWords). By default no snippet is computed. + * If set to null, no snippet is computed. + * - searchableAttributes (formerly named attributesToIndex): (array of strings) the list of fields you want to index. + * If set to null, all textual and numerical attributes of your objects are indexed, but you + * should update it to get optimal results. This parameter has two important uses: + * - Limit the attributes to index: For example if you store a binary image in base64, you want to + * store it and be able to retrieve it but you don't want to search in the base64 string. + * - Control part of the ranking*: (see the ranking parameter for full explanation) Matches in + * attributes at the beginning of the list will be considered more important than matches in + * attributes further down the list. In one attribute, matching text at the beginning of the + * attribute will be considered more important than text after, you can disable this behavior if + * you add your attribute inside `unordered(AttributeName)`, for example searchableAttributes: + * ["title", "unordered(text)"]. + * - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. + * All strings in the attribute selected for faceting are extracted and added as a facet. If set + * to null, no attribute is used for faceting. + * - attributeForDistinct: (string) The attribute name used for the Distinct feature. This feature + * is similar to the SQL "distinct" keyword: when enabled in query with the distinct=1 parameter, + * all hits containing a duplicate value for this attribute are removed from results. For example, + * if the chosen attribute is show_name and several hits have the same value for show_name, then + * only the best one is kept and others are removed. + * - ranking: (array of strings) controls the way results are sorted. + * We have six available criteria: + * - typo: sort according to number of typos, + * - geo: sort according to decreasing distance when performing a geo-location based search, + * - proximity: sort according to the proximity of query words in hits, + * - attribute: sort according to the order of attributes defined by searchableAttributes, + * - exact: + * - if the user query contains one word: sort objects having an attribute that is exactly the + * query word before others. For example if you search for the "V" TV show, you want to find it + * with the "V" query and avoid to have all popular TV show starting by the v letter before it. + * - if the user query contains multiple words: sort according to the number of words that matched + * exactly (and not as a prefix). + * - custom: sort according to a user defined formula set in **customRanking** attribute. + * The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] + * - customRanking: (array of strings) lets you specify part of the ranking. + * The syntax of this condition is an array of strings containing attributes prefixed by asc + * (ascending order) or desc (descending order) operator. For example `"customRanking" => + * ["desc(population)", "asc(name)"]` + * - queryType: Select how the query words are interpreted, it can be one of the following value: + * - prefixAll: all query words are interpreted as prefixes, + * - prefixLast: only the last word is interpreted as a prefix (default behavior), + * - prefixNone: no query word is interpreted as a prefix. This option is not recommended. + * - highlightPreTag: (string) Specify the string that is inserted before the highlighted parts in + * the query result (default to ""). + * - highlightPostTag: (string) Specify the string that is inserted after the highlighted parts in + * the query result (default to ""). + * - optionalWords: (array of strings) Specify a list of words that should be considered as + * optional when found in the query. + * @param bool $forwardToReplicas + * * @return mixed + * * @throws AlgoliaException */ public function setSettings($settings, $forwardToReplicas = false) @@ -838,13 +990,13 @@ public function setSettings($settings, $forwardToReplicas = false) } /** - * List all existing user keys associated to this index with their associated ACLs. + * List all existing API keys associated to this index with their associated ACLs. * * @return mixed * * @throws AlgoliaException */ - public function listUserKeys() + public function listApiKeys() { return $this->client->request( $this->context, @@ -859,7 +1011,26 @@ public function listUserKeys() } /** - * Get ACL of a user key associated to this index. + * @deprecated use listApiKeys instead + * @return mixed + */ + public function listUserKeys() + { + return $this->listApiKeys(); + } + + /** + * @deprecated use getApiKey in + * @param $key + * @return mixed + */ + public function getUserKeyACL($key) + { + return $this->getApiKey($key); + } + + /** + * Get ACL of a API key associated to this index. * * @param string $key * @@ -867,7 +1038,7 @@ public function listUserKeys() * * @throws AlgoliaException */ - public function getUserKeyACL($key) + public function getApiKey($key) { return $this->client->request( $this->context, @@ -881,8 +1052,9 @@ public function getUserKeyACL($key) ); } + /** - * Delete an existing user key associated to this index. + * Delete an existing API key associated to this index. * * @param string $key * @@ -890,7 +1062,7 @@ public function getUserKeyACL($key) * * @throws AlgoliaException */ - public function deleteUserKey($key) + public function deleteApiKey($key) { return $this->client->request( $this->context, @@ -905,7 +1077,17 @@ public function deleteUserKey($key) } /** - * Create a new user key associated to this index. + * @param $key + * @return mixed + * @deprecated use deleteApiKey instead + */ + public function deleteUserKey($key) + { + return $this->deleteApiKey($key); + } + + /** + * Create a new API key associated to this index. * * @param array $obj can be two different parameters: * The list of parameters for this key. Defined by a array that @@ -937,7 +1119,7 @@ public function deleteUserKey($key) * * @throws AlgoliaException */ - public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) + public function addApiKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) { // is dict of value if ($obj !== array_values($obj)) { @@ -967,7 +1149,21 @@ public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $ma } /** - * Update a user key associated to this index. + * @param $obj + * @param int $validity + * @param int $maxQueriesPerIPPerHour + * @param int $maxHitsPerQuery + * @return mixed + * @deprecated use addApiKey instead + */ + public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) + { + return $this->addApiKey($obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery); + } + + + /** + * Update an API key associated to this index. * * @param string $key * @param array $obj can be two different parameters: @@ -1000,7 +1196,7 @@ public function addUserKey($obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $ma * * @throws AlgoliaException */ - public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) + public function updateApiKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) { // is dict of value if ($obj !== array_values($obj)) { @@ -1029,6 +1225,20 @@ public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour ); } + /** + * @param $key + * @param $obj + * @param int $validity + * @param int $maxQueriesPerIPPerHour + * @param int $maxHitsPerQuery + * @return mixed + * @deprecated use updateApiKey instead + */ + public function updateUserKey($key, $obj, $validity = 0, $maxQueriesPerIPPerHour = 0, $maxHitsPerQuery = 0) + { + return $this->updateApiKey($key, $obj, $validity, $maxQueriesPerIPPerHour, $maxHitsPerQuery); + } + /** * Send a batch request. * @@ -1283,6 +1493,18 @@ public function saveSynonym($objectID, $content, $forwardToReplicas = false) ); } + /** + * @deprecated Please use searchForFacetValues instead + * @param $facetName + * @param $facetQuery + * @param array $query + * @return mixed + */ + public function searchFacet($facetName, $facetQuery, $query = array()) + { + return $this->searchForFacetValues($facetName, $facetQuery, $query); + } + /** * @param string $name * @param array $arguments @@ -1291,14 +1513,14 @@ public function saveSynonym($objectID, $content, $forwardToReplicas = false) */ public function __call($name, $arguments) { - if ($name !== 'browse') { - return; - } + if ($name === 'browse') { + if (count($arguments) >= 1 && is_string($arguments[0])) { + return call_user_func_array(array($this, 'doBrowse'), $arguments); + } - if (count($arguments) >= 1 && is_string($arguments[0])) { - return call_user_func_array(array($this, 'doBrowse'), $arguments); + return call_user_func_array(array($this, 'doBcBrowse'), $arguments); } - return call_user_func_array(array($this, 'doBcBrowse'), $arguments); + return; } } diff --git a/lib/AlgoliaSearch/Json.php b/lib/AlgoliaSearch/Json.php index 5b02d500..f86819e3 100644 --- a/lib/AlgoliaSearch/Json.php +++ b/lib/AlgoliaSearch/Json.php @@ -3,11 +3,7 @@ namespace AlgoliaSearch; /** - * Class Json - * @package AlgoliaSearch - * - * Helper class to simplify work with JSON - * + * Class Json. */ class Json { diff --git a/lib/AlgoliaSearch/Version.php b/lib/AlgoliaSearch/Version.php index a8e1c592..18b9c605 100644 --- a/lib/AlgoliaSearch/Version.php +++ b/lib/AlgoliaSearch/Version.php @@ -29,7 +29,7 @@ class Version { - const VALUE = '1.12.1'; + const VALUE = '1.17.0'; public static $custom_value = ''; @@ -39,12 +39,12 @@ class Version // Method untouched to keep backward compatibility public static function get() { - return self::VALUE . static::$custom_value; + return self::VALUE.static::$custom_value; } public static function getUserAgent() { - $userAgent = self::$prefixUserAgentSegments . 'Algolia for PHP ('.self::VALUE.')' . static::$suffixUserAgentSegments; + $userAgent = self::$prefixUserAgentSegments.'Algolia for PHP ('.self::VALUE.')'.static::$suffixUserAgentSegments; // Keep backward compatibility $userAgent .= static::$custom_value; @@ -54,11 +54,25 @@ public static function getUserAgent() public static function addPrefixUserAgentSegment($segment, $version) { - self::$prefixUserAgentSegments = $segment.' ('.$version.'); '.self::$prefixUserAgentSegments; + $prefix = $segment.' ('.$version.'); '; + + if (false === mb_strpos(self::getUserAgent(), $prefix)) { + self::$prefixUserAgentSegments = $prefix . self::$prefixUserAgentSegments; + } } public static function addSuffixUserAgentSegment($segment, $version) { - self::$suffixUserAgentSegments .= '; '.$segment.' ('.$version.')'; + $suffix = '; '.$segment.' ('.$version.')'; + + if (false === mb_strpos(self::getUserAgent(), $suffix)) { + self::$suffixUserAgentSegments .= $suffix; + } + } + + public static function clearUserAgentSuffixesAndPrefixes() + { + self::$suffixUserAgentSegments = ''; + self::$prefixUserAgentSegments = ''; } } diff --git a/lib/AlgoliaSearch/loader.php b/lib/AlgoliaSearch/loader.php index 0fecfc7f..994f95a0 100644 --- a/lib/AlgoliaSearch/loader.php +++ b/lib/AlgoliaSearch/loader.php @@ -33,3 +33,6 @@ require_once __DIR__.'/SynonymType.php'; require_once __DIR__.'/Version.php'; require_once __DIR__.'/Json.php'; +require_once __DIR__.'/FailingHostsCache.php'; +require_once __DIR__.'/FileFailingHostsCache.php'; +require_once __DIR__.'/InMemoryFailingHostsCache.php';